ディレクトリ
沿って、本明細書に「RocketMQメッセージトラック-デザインの記事は」アイデアは、以下の三つの側面からそのソースを解釈します:
- メッセージ・トラックを送信
- メッセージ・トラック・フォーマット
- 軌跡データメッセージを保存します
@(このセクションのディレクトリ)
1、プロセスは、メッセージトレースを送信します
まず、次のようにメッセージで送信者、サンプルコードをトレースメッセージを有効にする方法を見てみましょう。
public class TraceProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",true); // @1
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
for (int i = 0; i < 10; i++)
try {
{
Message msg = new Message("TopicTest",
"TagA",
"OrderID188",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
} catch (Exception e) {
e.printStackTrace();
}
producer.shutdown();
}
}
その上のコードポイントがDefaultMQProducerを作成するときに、メッセージの追跡を有効化キーを指定していることがわかるから。我々としてもDefaultMQProducerを見て、コンストラクタは、関連ニュースを追跡可能にするかもしれません。
public DefaultMQProducer(final String producerGroup, boolean enableMsgTrace)
public DefaultMQProducer(final String producerGroup, boolean enableMsgTrace, final String customizedTraceTopic)
パラメータは次のとおりです。
- 文字列producerGroupの
プロデューサーは、グループ名に属しています。 - ブールenableMsgTrace
オープントレースメッセージ軌道に、デフォルトはfalseであるかどうか。 - 文字列customizedTraceTopicは
あなたが追跡がデータが属するメッセージを格納するサブジェクト名を追跡するために使用されたメッセージを開くと、デフォルトは次のとおりです。RMQ_SYS_TRACE_TOPIC。
1.1 DefaultMQProducerコンストラクタ
public DefaultMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace,final String customizedTraceTopic) { // @1
this.producerGroup = producerGroup;
defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
//if client open the message trace feature
if (enableMsgTrace) { // @2
try {
AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(customizedTraceTopic, rpcHook);
dispatcher.setHostProducer(this.getDefaultMQProducerImpl());
traceDispatcher = dispatcher;
this.getDefaultMQProducerImpl().registerSendMessageHook(
new SendMessageTraceHookImpl(traceDispatcher)); // @3
} catch (Throwable e) {
log.error("system mqtrace hook init failed ,maybe can't send msg trace data");
}
}
}
1 @コード:まず自分のローカル変数について教えてください。
- 文字列producerGroupは、
生産者のグループを所有しています。 - RPCHook rpcHookの
プロデューサーは、フック関数を送ります。 - ブールenableMsgTrace
メッセージ追跡を開くかどうか。 - 文字列customizedTraceTopicの
データストレージトラックのカスタムメッセージ。
2 @コード:非同期転送ニュース軌跡データ、後に焦点を当てる:、AsyncTraceDispatcherを構築し、その名前を参照するために使用。
コード@ 3:建設SendMessageTraceHookImplオブジェクトと非同期転送AsyncTraceDispatcherため。
1.2 SendMessageTraceHookImplフック関数
1.2.1 SendMessageTraceHookImpl类图
- SendMessageHookの
メッセージを送信する前にメッセージフック関数、メッセージがトラックドット記録最良拡張され、送信後、いくつかのビジネスロジックを実行します。 - TraceDispatcher
メッセージ転送プロセッサ軌道、デフォルト実装クラスAsyncTraceDispatcher、非同期実装軌跡データメッセージの送信。簡単な紹介を行うため、そのプロパティ下:- int型QUEUESIZE
非同期転送、キューの長さは、デフォルトは2048である、現在のバージョンを変更することはできません。 - INTバッチサイズの
メッセージのデータ片ロット番号の個数は、メッセージ送信要求メッセージは、トラックを備え、デフォルトは現在のバージョンを変更することができない、100です。 - INT maxMsgSize
最初のトラックによって送信されたメッセージの最大メッセージサイズは、デフォルトは128Kである、現在のバージョンを変更することはできません。 - DefaultMQProducer traceProducer
メッセージトレースを送信するメッセージの送信者のために。 - ThreadPoolExecutor traceExecuterの
非同期メッセージ送信を実行するスレッドプール。 - AtomicLong discardCountの
ドロップ記録されるメッセージの数。 - ワーカースレッド
wokerスレッドは、スレッドプールの実装に提出し、追加のトラックデータキューから送信されるメッセージの数を取得する責任があります。 - ArrayBlockingQueue< TraceContext> traceContextQueue
消息轨迹TraceContext队列,用来存放待发送到服务端的消息。 - ArrayBlockingQueue< Runnable> appenderQueue
线程池内部队列,默认长度1024。 - DefaultMQPushConsumerImpl hostConsumer
消费者信息,记录消息消费时的轨迹信息。 - String traceTopicName
用于跟踪消息轨迹的topic名称。
- int型QUEUESIZE
1.2.2 源码分析SendMessageTraceHookImpl
1.2.2.1 sendMessageBefore
public void sendMessageBefore(SendMessageContext context) {
//if it is message trace data,then it doesn't recorded
if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName())) { // @1
return;
}
//build the context content of TuxeTraceContext
TraceContext tuxeContext = new TraceContext();
tuxeContext.setTraceBeans(new ArrayList<TraceBean>(1));
context.setMqTraceContext(tuxeContext);
tuxeContext.setTraceType(TraceType.Pub);
tuxeContext.setGroupName(context.getProducerGroup()); // @2
//build the data bean object of message trace
TraceBean traceBean = new TraceBean(); // @3
traceBean.setTopic(context.getMessage().getTopic());
traceBean.setTags(context.getMessage().getTags());
traceBean.setKeys(context.getMessage().getKeys());
traceBean.setStoreHost(context.getBrokerAddr());
traceBean.setBodyLength(context.getMessage().getBody().length);
traceBean.setMsgType(context.getMsgType());
tuxeContext.getTraceBeans().add(traceBean);
}
代码@1:如果topic主题为消息轨迹的Topic,直接返回。
代码@2:在消息发送上下文中,设置用来跟踪消息轨迹的上下环境,里面主要包含一个TraceBean集合、追踪类型(TraceType.Pub)与生产者所属的组。
代码@3:构建一条跟踪消息,用TraceBean来表示,记录原消息的topic、tags、keys、发送到broker地址、消息体长度等消息。
从上文看出,sendMessageBefore主要的用途就是在消息发送的时候,先准备一部分消息跟踪日志,存储在发送上下文环境中,此时并不会发送消息轨迹数据。
1.2.2.2 sendMessageAfter
public void sendMessageAfter(SendMessageContext context) {
//if it is message trace data,then it doesn't recorded
if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName()) // @1
|| context.getMqTraceContext() == null) {
return;
}
if (context.getSendResult() == null) {
return;
}
if (context.getSendResult().getRegionId() == null
|| !context.getSendResult().isTraceOn()) {
// if switch is false,skip it
return;
}
TraceContext tuxeContext = (TraceContext) context.getMqTraceContext();
TraceBean traceBean = tuxeContext.getTraceBeans().get(0); // @2
int costTime = (int) ((System.currentTimeMillis() - tuxeContext.getTimeStamp()) / tuxeContext.getTraceBeans().size()); // @3
tuxeContext.setCostTime(costTime); // @4
if (context.getSendResult().getSendStatus().equals(SendStatus.SEND_OK)) {
tuxeContext.setSuccess(true);
} else {
tuxeContext.setSuccess(false);
}
tuxeContext.setRegionId(context.getSendResult().getRegionId());
traceBean.setMsgId(context.getSendResult().getMsgId());
traceBean.setOffsetMsgId(context.getSendResult().getOffsetMsgId());
traceBean.setStoreTime(tuxeContext.getTimeStamp() + costTime / 2);
localDispatcher.append(tuxeContext); // @5
}
代码@1:如果topic主题为消息轨迹的Topic,直接返回。
代码@2:从MqTraceContext中获取跟踪的TraceBean,虽然设计成List结构体,但在消息发送场景,这里的数据永远只有一条,及时是批量发送也不例外。
代码@3:获取消息发送到收到响应结果的耗时。
代码@4:设置costTime(耗时)、success(是否发送成功)、regionId(发送到broker所在的分区)、msgId(消息ID,全局唯一)、offsetMsgId(消息物理偏移量,如果是批量消息,则是最后一条消息的物理偏移量)、storeTime,这里使用的是(客户端发送时间 + 二分之一的耗时)来表示消息的存储时间,这里是一个估值。
代码@5:将需要跟踪的信息通过TraceDispatcher转发到Broker服务器。其代码如下:
public boolean append(final Object ctx) {
boolean result = traceContextQueue.offer((TraceContext) ctx);
if (!result) {
log.info("buffer full" + discardCount.incrementAndGet() + " ,context is " + ctx);
}
return result;
}
这里一个非常关键的点是offer方法的使用,当队列无法容纳新的元素时会立即返回false,并不会阻塞。
接下来将目光转向TraceDispatcher的实现。
1.3 TraceDispatcher实现原理
TraceDispatcher,用于客户端消息轨迹数据转发到Broker,其默认实现类:AsyncTraceDispatcher。
1.3.1 TraceDispatcher构造函数
public AsyncTraceDispatcher(String traceTopicName, RPCHook rpcHook) throws MQClientException {
// queueSize is greater than or equal to the n power of 2 of value
this.queueSize = 2048;
this.batchSize = 100;
this.maxMsgSize = 128000;
this.discardCount = new AtomicLong(0L);
this.traceContextQueue = new ArrayBlockingQueue<TraceContext>(1024);
this.appenderQueue = new ArrayBlockingQueue<Runnable>(queueSize);
if (!UtilAll.isBlank(traceTopicName)) {
this.traceTopicName = traceTopicName;
} else {
this.traceTopicName = MixAll.RMQ_SYS_TRACE_TOPIC;
} // @1
this.traceExecuter = new ThreadPoolExecutor(// :
10, //
20, //
1000 * 60, //
TimeUnit.MILLISECONDS, //
this.appenderQueue, //
new ThreadFactoryImpl("MQTraceSendThread_"));
traceProducer = getAndCreateTraceProducer(rpcHook); // @2
}
代码@1:初始化核心属性,该版本这些值都是“固化”的,用户无法修改。
- queueSize
队列长度,默认为2048,异步线程池能够积压的消息轨迹数量。 - batchSize
一次向Broker批量发送的消息条数,默认为100. - maxMsgSize
向Broker汇报消息轨迹时,消息体的总大小不能超过该值,默认为128k。 - discardCount
整个运行过程中,丢弃的消息轨迹数据,这里要说明一点的是,如果消息TPS发送过大,异步转发线程处理不过来时,会主动丢弃消息轨迹数据。 - traceContextQueue
traceContext积压队列,客户端(消息发送、消息消费者)在收到处理结果后,将消息轨迹提交到噶队列中,则会立即返回。 - appenderQueue
提交到Broker线程池中队列。 - traceTopicName
用于接收消息轨迹的Topic,默认为RMQ_SYS_TRANS_HALF_TOPIC。 - traceExecuter
用于发送到Broker服务的异步线程池,核心线程数默认为10,最大线程池为20,队列堆积长度2048,线程名称:MQTraceSendThread_。、 - traceProducer
发送消息轨迹的Producer。
代码@2:调用getAndCreateTraceProducer方法创建用于发送消息轨迹的Producer(消息发送者),下面详细介绍一下其实现。
1.3.2 getAndCreateTraceProducer详解
private DefaultMQProducer getAndCreateTraceProducer(RPCHook rpcHook) {
DefaultMQProducer traceProducerInstance = this.traceProducer;
if (traceProducerInstance == null) { //@1
traceProducerInstance = new DefaultMQProducer(rpcHook);
traceProducerInstance.setProducerGroup(TraceConstants.GROUP_NAME);
traceProducerInstance.setSendMsgTimeout(5000);
traceProducerInstance.setVipChannelEnabled(false);
// The max size of message is 128K
traceProducerInstance.setMaxMessageSize(maxMsgSize - 10 * 1000);
}
return traceProducerInstance;
}
代码@1:如果还未建立发送者,则创建用于发送消息轨迹的消息发送者,其GroupName为:_INNER_TRACE_PRODUCER,消息发送超时时间5s,最大允许发送消息大小118K。
1.3.3 start
public void start(String nameSrvAddr) throws MQClientException {
if (isStarted.compareAndSet(false, true)) { // @1
traceProducer.setNamesrvAddr(nameSrvAddr);
traceProducer.setInstanceName(TRACE_INSTANCE_NAME + "_" + nameSrvAddr);
traceProducer.start();
}
this.worker = new Thread(new AsyncRunnable(), "MQ-AsyncTraceDispatcher-Thread-" + dispatcherId); // @2
this.worker.setDaemon(true);
this.worker.start();
this.registerShutDownHook();
}
开始启动,其调用的时机为启动DefaultMQProducer时,如果启用跟踪消息轨迹,则调用之。
代码@1:如果用于发送消息轨迹的发送者没有启动,则设置nameserver地址,并启动着。
代码@2:启动一个线程,用于执行AsyncRunnable任务,接下来将重点介绍。
1.3.4 AsyncRunnable
class AsyncRunnable implements Runnable {
private boolean stopped;
public void run() {
while (!stopped) {
List<TraceContext> contexts = new ArrayList<TraceContext>(batchSize); // @1
for (int i = 0; i < batchSize; i++) {
TraceContext context = null;
try {
//get trace data element from blocking Queue — traceContextQueue
context = traceContextQueue.poll(5, TimeUnit.MILLISECONDS); // @2
} catch (InterruptedException e) {
}
if (context != null) {
contexts.add(context);
} else {
break;
}
}
if (contexts.size() > 0) { :
AsyncAppenderRequest request = new AsyncAppenderRequest(contexts); // @3
traceExecuter.submit(request);
} else if (AsyncTraceDispatcher.this.stopped) {
this.stopped = true;
}
}
}
}
代码@1:构建待提交消息跟踪Bean,每次最多发送batchSize,默认为100条。
代码@2:从traceContextQueue中取出一个待提交的TraceContext,设置超时时间为5s,即如何该队列中没有待提交的TraceContext,则最多等待5s。
代码@3:向线程池中提交任务AsyncAppenderRequest。
1.3.5 AsyncAppenderRequest#sendTraceData
public void sendTraceData(List<TraceContext> contextList) {
Map<String, List<TraceTransferBean>> transBeanMap = new HashMap<String, List<TraceTransferBean>>();
for (TraceContext context : contextList) { //@1
if (context.getTraceBeans().isEmpty()) {
continue;
}
// Topic value corresponding to original message entity content
String topic = context.getTraceBeans().get(0).getTopic(); // @2
// Use original message entity's topic as key
String key = topic;
List<TraceTransferBean> transBeanList = transBeanMap.get(key);
if (transBeanList == null) {
transBeanList = new ArrayList<TraceTransferBean>();
transBeanMap.put(key, transBeanList);
}
TraceTransferBean traceData = TraceDataEncoder.encoderFromContextBean(context); // @3
transBeanList.add(traceData);
}
for (Map.Entry<String, List<TraceTransferBean>> entry : transBeanMap.entrySet()) { // @4
flushData(entry.getValue());
}
}
代码@1:遍历收集的消息轨迹数据。
代码@2:获取存储消息轨迹的Topic。
代码@3:对TraceContext进行编码,这里是消息轨迹的传输数据,稍后对其详细看一下,了解其上传的格式。
代码@4:将编码后的数据发送到Broker服务器。
1.3.6 TraceDataEncoder#encoderFromContextBean
根据消息轨迹跟踪类型,其格式会有一些不一样,下面分别来介绍其合适。
1.3.6.1 PUB(消息发送)
case Pub: {
TraceBean bean = ctx.getTraceBeans().get(0);
//append the content of context and traceBean to transferBean's TransData
sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getTopic()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getTags()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getStoreHost()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getBodyLength()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getCostTime()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getMsgType().ordinal()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getOffsetMsgId()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.isSuccess()).append(TraceConstants.FIELD_SPLITOR);
}
消息轨迹数据的协议使用字符串拼接,字段的分隔符号为1,整个数据以2结尾,感觉这个设计还是有点“不可思议”,为什么不直接使用json协议呢?
1.3.6.2 SubBefore(消息消费之前)
for (TraceBean bean : ctx.getTraceBeans()) {
sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getRequestId()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getRetryTimes()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getKeys()).append(TraceConstants.FIELD_SPLITOR);//
}
}
轨迹就是按照上述顺序拼接而成,各个字段使用1分隔,每一条记录使用2结尾。
1.3.2.3 SubAfter(消息消费后)
case SubAfter: {
for (TraceBean bean : ctx.getTraceBeans()) {
sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getRequestId()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getCostTime()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.isSuccess()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)//
.append(ctx.getContextCode()).append(TraceConstants.FIELD_SPLITOR);
}
}
}
格式编码一样,就不重复多说。
经过上面的源码跟踪,消息发送端的消息轨迹跟踪流程、消息轨迹数据编码协议就清晰了,接下来我们使用一张序列图来结束本部分的讲解。
其实行文至此,只关注了消息发送的消息轨迹跟踪,消息消费的轨迹跟踪又是如何呢?其实现原理其实是一样的,就是在消息消费前后执行特定的钩子函数,其实现类为ConsumeMessageTraceHookImpl,由于其实现与消息发送的思路类似,故就不详细介绍了。
2、 消息轨迹数据如何存储
其实从上面的分析,我们已经得知,RocketMQ的消息轨迹数据存储在到Broker上,那消息轨迹的主题名如何指定?其路由信息又怎么分配才好呢?是每台Broker上都创建还是只在其中某台上创建呢?RocketMQ支持系统默认与自定义消息轨迹的主题。
2.1 使用系统默认的主题名称
RocketMQ默认的消息轨迹主题为:RMQ_SYS_TRACE_TOPIC,那该Topic需要手工创建吗?其路由信息呢?
{
if (this.brokerController.getBrokerConfig().isTraceTopicEnable()) { // @1
String topic = this.brokerController.getBrokerConfig().getMsgTraceTopicName();
TopicConfig topicConfig = new TopicConfig(topic);
this.systemTopicList.add(topic);
topicConfig.setReadQueueNums(1); // @2
topicConfig.setWriteQueueNums(1);
this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);
}
}
TopicConfigManagerコンストラクタからコード情報トピックのルーティングを管理するために使用topicConfigManagerブローカが開始オブジェクトを作成します。
1 @コード:メッセージ追跡は、ブローカー(traceTopicEnable = true)を有効にした場合は、自動的にデフォルトのメッセージトピック経路情報トラックを作成し、読み取りおよび書き込みキューの数は、1に注意してください。
2.2ユーザー定義メッセージ軌道テーマ
あなたがメッセージの送信者を作成するときにトピックトラックが、メッセージの消費者をメッセージを指定し、例えば、表示されることがあります。
public DefaultMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace,final String customizedTraceTopic)
public DefaultMQPushConsumer(final String consumerGroup, RPCHook rpcHook,
AllocateMessageQueueStrategy allocateMessageQueueStrategy, boolean enableMsgTrace, final String customizedTraceTopic)
トピックメッセージは、軌道customizedTraceTopicで指定されました。
ヒント:通常、本番環境では、事前に良いトピックを作成するRocketMQ操作や保守管理を必要とする、自動的にオンのテーマを作成することはありません。
さて、この記事では、ここでご紹介します、紙は次の実装原則RocktMQニュース軌道を説明し、我々は複数のコピーの調査に入ります。
著者:
丁偉、「RocketMQ技術インサイダー」の著者、RocketMQコミュニティ説教、公共数:ミドルウェア関心サークル擁護、Javaソースコード解析のセット、Javaと契約(JUC)が公開されている、ネッティー、Mycat、ダボ、 RocketMQ、MyBatisのと他のソース列。