一、前言
消息轨迹 在不少情况是很重要的,主要指的是一条消息从生产方发出到消费方消费处理,整个过程中的各个相关节点的时间地点等数据汇聚而成的完整链路信息。比如消息到底有没有发送出去,消息存储在哪个Broker,消息被哪些消费者消费、消费时间、消费耗时等等。
RocketMQ中的一条消息的完整链路包含消息生产方、Broker服务方、消息消费方三个角色,这其中每个部分处理消息的过程中都会在轨迹链路中增加相关的信息,将这些信息汇聚即可获取任意消息的当前状态,从而为生产环境中的问题排查提供强有力的数据支持。
注:RocketMQ 4.4.0版本开始支持消息轨迹。
下图是使用RocketMQ-console查看消息轨迹的数据:
二、设计原理
RocketMQ消息轨迹,主要跟踪消息发送、消息消费的轨迹,即详细记录消息各个处理环节的日志,从设计上至少需要解决如下三个核心问题:
- 消息轨迹数据格式
- 记录消息轨迹(消息日志)
- 消息轨迹数据存储在哪?
1、消息轨迹数据格式
public class TraceContext implements Comparable<TraceContext> {
//跟踪类型
private TraceType traceType;
private long timeStamp = System.currentTimeMillis();
//broker所在的区域ID,取自BrokerConfig#regionId
private String regionId = "";
private String regionName = "";
private String groupName = "";
private int costTime = 0;
private boolean isSuccess = true;
private String requestId = MessageClientIDSetter.createUniqID();
private int contextCode = 0;
private List<TraceBean> traceBeans;
......省略get、set方法
}
public class TraceBean {
private static final String LOCAL_ADDRESS = UtilAll.ipToIPv4Str(UtilAll.getIP());
private String topic = "";
private String msgId = "";
private String offsetMsgId = "";
private String tags = "";
private String keys = "";
private String storeHost = LOCAL_ADDRESS;
private String clientHost = LOCAL_ADDRESS;
private long storeTime;
private int retryTimes;
private int bodyLength;
private MessageType msgType;
......省略get、set方法
}
2、记录消息轨迹
消息中间件的两大核心主题:消息发送、消息消费,其核心载体就是消息,消息轨迹(消息的流转)主要是记录消息是何时发送到哪台Broker,发送耗时多少时间,在什么是被哪个消费者消费。
记录消息的轨迹主要是集中在消息发送前后、消息消费前后,可以通过RokcetMQ的Hook机制。通过如下两个接口来定义钩子函数。
通过实行上述两个接口,可以实现在消息发送、消息消费前后记录消息轨迹,为了不明显增加消息发送与消息消费的时延,记录消息轨迹最好使用异步发送模式。
3、如何存储消息轨迹数据
消息轨迹需要存储什么消息以及在什么时候记录消息轨迹的问题都以及解决,那接下来就得思考将消息轨迹存储在哪里?存储在数据库中或其他媒介中,都会加重消息中间件,使其依赖外部组件,最佳的选择还是存储在Broker服务器中,将消息轨迹数据也当成一条消息存储到Broker服务器。
既然把消息轨迹当成消息存储在Broker服务器,那存储消息轨迹的Topic如何确定呢?RocketMQ提供了两种方法来定义消息轨迹的Topic。
- 系统默认Topic
如果Broker的traceTopicEnable配置设置为true,表示在该Broker上创建topic名为:RMQ_SYS_TRACE_TOPIC,队列个数为1,默认该值为false,表示该Broker不承载系统自定义用于存储消息轨迹的topic。 - 自定义Topic
在创建消息生产者或消息消费者时,可以通过参数自定义用于记录消息轨迹的Topic名称,不过要注意的是,rokcetmq控制台(rocketmq-console)中只支持配置一个消息轨迹Topic,故自定义Topic,在目前这个阶段或许还不是一个最佳实践,建议使用系统默认的Topic即可。
注:通常为了避免消息轨迹的数据与正常的业务数据混合在一起,官方建议,在Broker集群中,新增加一台机器,只在这台机器上开启消息轨迹跟踪,这样该集群内的消息轨迹数据只会发送到这一台Broker服务器上,并不会增加集群内原先业务Broker的负载压力。
三、源码实现
1、Broker服务方
Broker配置
属性 | 默认值 | 描述 |
---|---|---|
traceTopicEnable | false | 是否开启消息轨迹Topic |
msgTraceTopicName | RMQ_SYS_TRACE_TOPIC | 消息轨迹Topic名 |
public class TopicConfigManager extends ConfigManager {
public TopicConfigManager(BrokerController brokerController) {
{
if (this.brokerController.getBrokerConfig().isTraceTopicEnable()) {
String topic = this.brokerController.getBrokerConfig().getMsgTraceTopicName();
TopicConfig topicConfig = new TopicConfig(topic);
this.systemTopicList.add(topic);
topicConfig.setReadQueueNums(1);
topicConfig.setWriteQueueNums(1);
this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);
}
}
}
}
在一个集群中可以配置一台机器专门负责消息轨迹的收集工作,该台机器上配置traceTopicEnable = true,broker启动的时候自动创建默认轨迹topic。
2、消息生产方
public DefaultMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace,
final String customizedTraceTopic) {
this.producerGroup = producerGroup;
defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
//if client open the message trace feature
if (enableMsgTrace) {
try {
AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(customizedTraceTopic, rpcHook);
dispatcher.setHostProducer(this.defaultMQProducerImpl);
traceDispatcher = dispatcher;
//为消息轨迹注册hook,在消息发送前后执行
this.defaultMQProducerImpl.registerSendMessageHook(
new SendMessageTraceHookImpl(traceDispatcher));
} catch (Throwable e) {
log.error("system mqtrace hook init failed ,maybe can't send msg trace data");
}
}
}
SendMessageTraceHookImpl 实现了SendMessageHook接口,在消息发送前后会被调用
AsyncTraceDispatcher 主要负责消息的发送工作。
private SendResult sendKernelImpl(final Message msg,
final MessageQueue mq,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final TopicPublishInfo topicPublishInfo,
final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
//省略部分代码。。。
if (this.hasSendMessageHook()) {
context = new SendMessageContext();
this.executeSendMessageHookBefore(context);
}
if (this.hasSendMessageHook()) {
context.setSendResult(sendResult);
this.executeSendMessageHookAfter(context);
}
}
在后置钩子中,会把 TraceContext 追加在 TraceDispatcher 分发器中。
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;
}
TraceDispatcher,用于客户端消息轨迹数据转发到Broker,其默认实现类:AsyncTraceDispatcher。
public void start(String nameSrvAddr, AccessChannel accessChannel) throws MQClientException {
if (isStarted.compareAndSet(false, true)) {
traceProducer.setNamesrvAddr(nameSrvAddr);
traceProducer.setInstanceName(TRACE_INSTANCE_NAME + "_" + nameSrvAddr);
traceProducer.start();
}
this.accessChannel = accessChannel;
this.worker = new Thread(new AsyncRunnable(), "MQ-AsyncTraceDispatcher-Thread-" + dispatcherId);
this.worker.setDaemon(true);
this.worker.start();
this.registerShutDownHook();
}
开始启动,其调用的时机为启动DefaultMQProducer时,如果启用跟踪消息轨迹,则调用之。
代码@1:如果用于发送消息轨迹的发送者没有启动,则设置nameserver地址,并启动着。
代码@2:启动一个线程,用于执行AsyncRunnable任务,接下来将重点介绍。
class AsyncRunnable implements Runnable {
private boolean stopped;
@Override
public void run() {
while (!stopped) {
List<TraceContext> contexts = new ArrayList<TraceContext>(batchSize);
for (int i = 0; i < batchSize; i++) {
TraceContext context = null;
try {
//从阻塞队列中,获取跟踪数据
context = traceContextQueue.poll(5, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
if (context != null) {
contexts.add(context);
} else {
break;
}
}
if (contexts.size() > 0) {
//若队列中不为空,TraceContext 列表有数据,封装异步追加请求,丢入异步线程池处理
AsyncAppenderRequest request = new AsyncAppenderRequest(contexts);
traceExecutor.submit(request);
} else if (AsyncTraceDispatcher.this.stopped) {
this.stopped = true;
}
}
}
}
private void sendTraceDataByMQ(Set<String> keySet, final String data, String dataTopic, String regionId) {
String traceTopic = traceTopicName;
if (AccessChannel.CLOUD == accessChannel) {
traceTopic = TraceConstants.TRACE_TOPIC_PREFIX + regionId;
}
final Message message = new Message(traceTopic, data.getBytes());
// Keyset of message trace includes msgId of or original message
message.setKeys(keySet);
try {
Set<String> traceBrokerSet = tryGetMessageQueueBrokerSet(traceProducer.getDefaultMQProducerImpl(), traceTopic);
SendCallback callback = new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {}
@Override
public void onException(Throwable e) {
log.info("send trace data ,the traceData is " + data);
}
};
if (traceBrokerSet.isEmpty()) {
// No cross set
traceProducer.send(message, callback, 5000);
} else {
traceProducer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Set<String> brokerSet = (Set<String>) arg;
List<MessageQueue> filterMqs = new ArrayList<MessageQueue>();
for (MessageQueue queue : mqs) {
if (brokerSet.contains(queue.getBrokerName())) {
filterMqs.add(queue);
}
}
int index = sendWhichQueue.getAndIncrement();
int pos = Math.abs(index) % filterMqs.size();
if (pos < 0) {
pos = 0;
}
return filterMqs.get(pos);
}
}, traceBrokerSet, callback);
}
} catch (Exception e) {
log.info("send trace data,the traceData is" + data);
}
}
代码@1:遍历收集的消息轨迹数据。
代码@2:获取存储消息轨迹的Topic。
代码@3:对TraceContext进行编码,这里是消息轨迹的传输数据。
代码@4:将编码后的数据发送到Broker服务器。
3、消息消费方
public DefaultMQPushConsumer(final String namespace, final String consumerGroup, RPCHook rpcHook,
AllocateMessageQueueStrategy allocateMessageQueueStrategy, boolean enableMsgTrace, final String customizedTraceTopic) {
this.consumerGroup = consumerGroup;
this.namespace = namespace;
this.allocateMessageQueueStrategy = allocateMessageQueueStrategy;
defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook);
if (enableMsgTrace) {
try {
AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(customizedTraceTopic, rpcHook);
dispatcher.setHostConsumer(this.getDefaultMQPushConsumerImpl());
traceDispatcher = dispatcher;
this.getDefaultMQPushConsumerImpl().registerConsumeMessageHook(
new ConsumeMessageTraceHookImpl(traceDispatcher));
} catch (Throwable e) {
log.error("system mqtrace hook init failed ,maybe can't send msg trace data");
}
}
}
ConsumeMessageTraceHookImpl 实现了ConsumeMessageHook接口,在消息消费前后会被调用AsyncTraceDispatcher 主要负责消息的消费记录工作。
public void start() throws MQClientException {
setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
this.defaultMQPushConsumerImpl.start();
if (null != traceDispatcher) {
try {
traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
} catch (MQClientException e) {
log.warn("trace dispatcher start failed ", e);
}
}
}
开始启动,其调用的时机为启动DefaultMQPushConsumer时,如果启用跟踪消息轨迹,则调用之。
4、小结
- Broker 开启消息轨迹记录,创建消息轨迹 Topic;
- Producer 开启消息轨迹,注册消息发送钩子,在消息发送后,把 TraceContext 追加在 TraceDispatcher 分发器中,由其发送到 Broker;
- Consumer 开启消息轨迹,注册消息消费钩子,在消息消费后,把 TraceContext 追加在 TraceDispatcher 分发器中,由其发送到 Broker;
引用