问题概述
测试环境系统启动了两个不同业务的消费者,但由于使用不当,将两者都归到同一消费者组。导致消费者一直无法订阅上相关的主题,无法接收到消息。本文主要记录了问题的发生和排查过程,通过分析源码查清问题的根源从而避免相同的问题再次发生。
系统发布后通过RocketMQ管理页面查看到目标Topic没有消费者订阅,但是通过日志追踪确认消费者已启动完成,Topic设置也无错误。查询结果如下:
考虑到该功能已经上线,并且线上运行无问题,所以首先考虑到是测试环境的配置问题。查看测试环境配置发现这两个消费者的消费者组相同,随后将两个消费者的组区分开重新发布系统,结果功能正常使用。
问题虽然得到解决,但当时还存在一个疑问。那就是根据RocketMQ的负载均衡原理,消费者所在的组中消费者个数是两个,理论上说至少可以订阅到一半的队列,可是目前的问题是一条队列都没订阅到。又或者说其实消费者已经订阅了那一半队列,只是因为某个机制这一半队列又被删除了。于是决定把这个问题弄清楚。
问题排查
首先在项目里面RocketMQ负载均衡相关功能处打上断点,然后启动项目。通过调试发现消费者一开始的时候确实成功订阅了一半队列,也就是说负载均衡功能是起作用的。通过管理控制台查看到的信息也佐证了这一点。
该主题设置了16条队列,消费者订阅了后8条队列。因为是本地调试,执行到断点后所有线程都处于暂停状态,所以我们可以在管理控制台看到订阅了一半队列的中间状态。因此基本可以确认是某一个机制周期性地将这部分订阅信息删除了,只是这个周期很短,我们很难觉察到。
如果对RocketMQ的源码没有足够的熟悉,很难一下子定位到是什么机制周期性的删除订阅信息。只能通过跟踪管理控制台展示的数据的源头在哪里,找到了源头就可以对它进行Debug观察数据的变动过程。
因此,经过跟踪管理控制台后端的调用接口,定位到数据源头是一个叫subscriptionTable的哈希表。它是ConsumerGroupInfo类的一个成员变量。看类名就知道该类记录的是消费者组相关的信息,而subscriptionTable则是负责组内订阅信息的维护。
ConsumerGruopInfo类里面有个updateSubscription方法。该方法负责维护subscriptionTable,跟踪该方法的调用链发现Consumer向Broker上报心跳时,Broker接收到心跳包后会调用。追查到这里基本上可以明确问题是Consumer在周期性的上报心跳信息导致的。因为Consumer上报心跳时会带上它订阅的主题信息,Broker收到这部分信息后会拿subscriptionTable与Consumer上报的主题对比,如果不在Consumer上报的主题中就会删除该主题信息。
而在本次事例中,因为两个消费者同属一个消费者组,而目标主题只有其中一个消费者订阅了,这就导致未订阅目标主题的消费者上报心跳信息时不会带上目标主题的信息,Broker收到心跳后就会将目标主题从subsriptionTable移除。以下是updateSubscription方法的源码:
public boolean updateSubscription(final Set<SubscriptionData> subList) {
boolean updated = false;
......省略部分代码................
Iterator<Entry<String, SubscriptionData>> it = this.subscriptionTable.entrySet().iterator();
//开始遍历subscriptionTable
while (it.hasNext()) {
Entry<String, SubscriptionData> next = it.next();
String oldTopic = next.getKey();
/**
* subList为Consumer上报的订阅主题。oldTopic是现存在subscriptionTable的主题,
* 循环判断oldTopic是否在subList中
*/
boolean exist = false;
for (SubscriptionData sub : subList) {
if (sub.getTopic().equals(oldTopic)) {
exist = true;
break;
}
}
if (!exist) {
log.warn("subscription changed, group: {} remove topic {} {}",
this.groupName,
oldTopic,
next.getValue().toString()
);
/**
* 如果当前oldTopic不存在subList,把它从subscriptionTable移除
*/
it.remove();
updated = true;
}
}
this.lastUpdateTimestamp = System.currentTimeMillis();
return updated;
}
至此,该问题排查过程结束。