ご存知のとおり、RocketMQ の消費モードには PULL モードと PUSH モードがありますが、本質的にはいずれも PULL モードであり、実際の使用では PUSH モードが一般的に使用されます。
ただし、RocketMQ の PUSH モードには明らかな欠陥があり、主に次の点に反映されます。
-
ニュースが滞っており、消費者を追加しても必ずしも解決するとは限りません。PUSH モードは次のとおりです。
上の図では、コンシューマ グループの各コンシューマが 2 つの MessageQueue を消費しますが、この場合、コンシューマを増やすことで消費容量を増やすことができます。
ただし、以下の図では、各コンシューマが MessageQueue を消費します。同じ MessageQueue は、同じ消費グループ内の 1 つのコンシューマによってのみ消費できるため、コンシューマを追加しても消費容量は増加しません。
-
クライアント側には、負荷分散、オフセット管理、消費失敗後の処理 (失敗メッセージをブローカーに送り返すなど) など、多くの処理ロジックがあり、これらはすべてクライアント側にあります。
-
他の言語をサポートすると、クライアントはどんどん重くなります。
-
次の図に示すように、コンシューマ マシンがハングし、メッセージのバックログが発生する可能性があります。
クライアントはバランシングを担当します。MessageQueue0 キューは、排他的に消費するために Consumer0 に割り当てられます。Consumer0 がハングアップしてもサービスはハングアップしていない場合、Consumer0 によってプルされたメッセージは消費できないため、Consumer0 はネーム サーバーからオフラインになることはできません。オフセットを更新するリクエストをブローカーに送信することは不可能であり、最終的にはメッセージのバックログが発生します。この場合、手動で Consumer0 をオフラインにするか、Consumer0 を再起動することしかできません。
RocketMQ 5.0 では、上記の PUSH Consumer の問題を解決するために、POP Consumer を導入しました。
1 POP クライアント
POP モード クライアントの導入の背景は RocketMQ 5.0 です。クラウド ネイティブをよりよく受け入れるためには、クライアントをステートレスな軽量クライアントに変換する必要があります。RocketMQ 4.x のクライアントには、負荷分散、権限管理、消費管理が備わっています。 . およびその他の機能はクライアントからプロキシに移動されました。
POP 消費モードは次のとおりです。
4 つのコンシューマはすべて、Broker1 と Broker2 のすべてのキューを消費できるため、特定のコンシューマがハングした場合でも、他のコンシューマはメッセージのバックログを発生させることなく消費できます。
同時に、上図からわかるように、POP クライアントには、コンシューマの数を増やすことで消費容量を向上させることができ、MessageQueue の数やコンシューマの数に制限されないという利点もあります。
PUSH モードと比較すると、POP モードはメッセージをプルした後、POP_CK 属性を設定します。コードは次のとおりです。
//MQClientAPIImpl.java
if (requestHeader instanceof PopMessageRequestHeader) {
if (startOffsetInfo == null) {
// we should set the check point info to extraInfo field , if the command is popMsg
// find pop ck offset
String key = messageExt.getTopic() + messageExt.getQueueId();
if (!map.containsKey(messageExt.getTopic() + messageExt.getQueueId())) {
map.put(key, ExtraInfoUtil.buildExtraInfo(messageExt.getQueueOffset(), responseHeader.getPopTime(), responseHeader.getInvisibleTime(), responseHeader.getReviveQid(),
messageExt.getTopic(), brokerName, messageExt.getQueueId()));
}
messageExt.getProperties().put(MessageConst.PROPERTY_POP_CK, map.get(key) + MessageConst.KEY_SEPARATOR + messageExt.getQueueOffset());
} else {
String queueIdKey = ExtraInfoUtil.getStartOffsetInfoMapKey(messageExt.getTopic(), messageExt.getQueueId());
String queueOffsetKey = ExtraInfoUtil.getQueueOffsetMapKey(messageExt.getTopic(), messageExt.getQueueId(), messageExt.getQueueOffset());
int index = sortMap.get(queueIdKey).indexOf(messageExt.getQueueOffset());
Long msgQueueOffset = msgOffsetInfo.get(queueIdKey).get(index);
messageExt.getProperties().put(MessageConst.PROPERTY_POP_CK,
ExtraInfoUtil.buildExtraInfo(startOffsetInfo.get(queueIdKey), responseHeader.getPopTime(), responseHeader.getInvisibleTime(),
responseHeader.getReviveQid(), messageExt.getTopic(), brokerName, messageExt.getQueueId(), msgQueueOffset)
);
//...
}
}
POP_CK 属性には、brokerName、Topic、QueueId、offset などのパラメータが含まれており、これによってメッセージを一意に識別できることがわかります。
上記のコードからも、responseHeader に InvisibleTime 属性があることがわかります。この属性の機能は、コンシューマーが POP モードを通じてメッセージをプルした後、この期間 (invisibleTime) の間、メッセージはブローカー側で非表示になることです。 . 、消費者が再度プルしても、繰り返しプルすることはありません。ただし、この時間が経過してもコンシューマーがブローカーに ACK を返さなかった場合、このメッセージは表示されるようになり、コンシューマーによって再びプルされます。
消費が完了したら、ブローカーに ACK メッセージを送信します。以下のコードを参照してください。
public void ackMessageAsync(
final String addr,
final long timeOut,
final AckCallback ackCallback,
final AckMessageRequestHeader requestHeader //
) throws RemotingException, MQBrokerException, InterruptedException {
final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader);
this.remotingClient.invokeAsync(addr, request, timeOut, new BaseInvokeCallback(MQClientAPIImpl.this) {
@Override
public void onComplete(ResponseFuture responseFuture) {
RemotingCommand response = responseFuture.getResponseCommand();
if (response != null) {
try {
AckResult ackResult = new AckResult();
if (ResponseCode.SUCCESS == response.getCode()) {
ackResult.setStatus(AckStatus.OK);
} //...
assert ackResult != null;
ackCallback.onSuccess(ackResult);
} //...
} else {
//...
}
}
});
}
2. ブローカー
上記の紹介から、各コンシューマーが Broker のすべての MessageQueue からメッセージをプルできることがわかります。そのため、複数のコンシューマーが 1 つの MessageQueue からメッセージをプルした場合、繰り返し消費することは可能でしょうか。
ブローカーがメッセージ プル リクエストを受信すると、メッセージ ストアからメッセージをプルするときに、最初に MessageQueue がロックされます。ロックが成功した後、メッセージがプルされます。これは、他のクライアントがメッセージをプルするときにロックに失敗するためです。 。
//PopMessageProcessor.java
String lockKey = topic + PopAckConstants.SPLIT + requestHeader.getConsumerGroup() + PopAckConstants.SPLIT + queueId;
long offset = getPopOffset(topic, requestHeader, queueId, false, lockKey);
if (!queueLockManager.tryLock(lockKey)) {
restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum;
return restNum;
}
ブローカーはメッセージストアからメッセージを取得した後、チェックポイントを定義してキャッシュに置きます。コードは次のとおりです。
//PopMessageProcessor.java
private long popMsgFromQueue(boolean isRetry, GetMessageResult getMessageResult,
PopMessageRequestHeader requestHeader, int queueId, long restNum, int reviveQid,
Channel channel, long popTime,
ExpressionMessageFilter messageFilter, StringBuilder startOffsetInfo,
StringBuilder msgOffsetInfo, StringBuilder orderCountInfo) {
String topic = isRetry ? KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(),
requestHeader.getConsumerGroup()) : requestHeader.getTopic();
String lockKey =
topic + PopAckConstants.SPLIT + requestHeader.getConsumerGroup() + PopAckConstants.SPLIT + queueId;
//...
offset = getPopOffset(topic, requestHeader, queueId, true, lockKey);
GetMessageResult getMessageTmpResult = null;
try {
//...
restNum = getMessageTmpResult.getMaxOffset() - getMessageTmpResult.getNextBeginOffset() + restNum;
if (!getMessageTmpResult.getMessageMapedList().isEmpty()) {
if (isOrder) {
//...
} else {
appendCheckPoint(requestHeader, topic, reviveQid, queueId, offset, getMessageTmpResult, popTime, this.brokerController.getBrokerConfig().getBrokerName());
}
} //...
} //...
return restNum;
}
ブローカーはコンシューマーから ACK を受信した後、キャッシュからチェックポイントを削除します。
ブローカーが ACK を受信していない場合、ブローカーはチェックポイントをキャッシュから削除し、同時にチェックポイントをメッセージストアに送信し、メッセージストアはそれを再試行キューに送信します。コードは以下のように表示されます:
boolean removeCk = !this.serving;
// ck will be timeout
if (point.getReviveTime() - now < brokerController.getBrokerConfig().getPopCkStayBufferTimeOut()) {
removeCk = true;
}
// the time stayed is too long
if (now - point.getPopTime() > brokerController.getBrokerConfig().getPopCkStayBufferTime()) {
removeCk = true;
}
// double check
if (removeCk) {
// put buffer ak to store
if (pointWrapper.getReviveQueueOffset() < 0) {
putCkToStore(pointWrapper, false);
}
}
}
3 まとめ
POP クライアントには、次のような多くの利点があります。
-
ステートレスであり、クラウド ネイティブをより適切に採用します。
-
計算関連の関数は、より軽量なプロキシに移動されます。
-
消費容量の拡大は、MessageQueue の数によって制限されません。
-
コンシューマは電話を切るため、メッセージのバックログが発生することはありません。