Troubleshooting a MetaQ Consumer Backlog
Note: MetaQ is an internal Alibaba product whose open-source version is called RocketMQ.
Symptoms
For special reasons, one application in the business I owned needed its messaging configuration changed from Spring Cloud Stream to native MetaQ. The configurations before and after the change were:
spring.cloud.stream.bindings.consumerA.group=CID_CONSUMER_A
spring.cloud.stream.bindings.consumerA.contentType=text/plain
spring.cloud.stream.bindings.consumerA.destination=CONSUMER_A_TOPIC
spring.cloud.stream.metaq.bindings.consumerA.consumer.tags=CONSUMER_A_TOPIC_TAG
spring.cloud.stream.bindings.consumerB.group=CID_CONSUMER_A
spring.cloud.stream.bindings.consumerB.contentType=text/plain
spring.cloud.stream.bindings.consumerB.destination=CONSUMER_B_TOPIC
spring.cloud.stream.metaq.bindings.consumerB.consumer.tags=CONSUMER_B_TOPIC_TAG
spring.cloud.stream.bindings.consumerC.group=CID_CONSUMER_A
spring.cloud.stream.bindings.consumerC.contentType=text/plain
spring.cloud.stream.bindings.consumerC.destination=CONSUMER_C_TOPIC
spring.cloud.stream.metaq.bindings.consumerC.consumer.tags=CONSUMER_C_TOPIC_TAG
spring.metaq.consumers[0].consumer-group=CID_CONSUMER_A
spring.metaq.consumers[0].topic=CONSUMER_A_TOPIC
spring.metaq.consumers[0].sub-expression=CONSUMER_A_TOPIC_TAG
spring.metaq.consumers[0].message-listener-ref=consumerAListener
spring.cloud.stream.bindings.consumerB.group=CID_CONSUMER_A
spring.cloud.stream.bindings.consumerB.contentType=text/plain
spring.cloud.stream.bindings.consumerB.destination=CONSUMER_B_TOPIC
spring.cloud.stream.metaq.bindings.consumerB.consumer.tags=CONSUMER_B_TOPIC_TAG
spring.cloud.stream.bindings.consumerC.group=CID_CONSUMER_A
spring.cloud.stream.bindings.consumerC.contentType=text/plain
spring.cloud.stream.bindings.consumerC.destination=CONSUMER_C_TOPIC
spring.cloud.stream.metaq.bindings.consumerC.consumer.tags=CONSUMER_C_TOPIC_TAG
When half the machines had been deployed and we began observing the canary rollout, messages started accumulating:
Cause
Inconsistent Message Subscriptions
Based on past experience, I suspected that machines in the consumer group had inconsistent subscription relationships. Some subscribed to A and others to B, so MetaQ could not determine whether to consume the messages and left them accumulating on the broker. The MetaQ console confirmed that inconsistent subscriptions had caused the backlog.
The subscription on the deployed machine was:
The undeployed machine had the following subscriptions, clearly more than the deployed machine:
Spring Cloud Stream and Native MetaQ
This raised another question: why did the subscriptions for the same ConsumerId change after replacing Spring Cloud Stream with native MetaQ?
More simply, when MetaQ and Spring Cloud Stream used the same ConsumerId, why did MetaQ’s subscriptions overwrite Spring Cloud Stream’s subscriptions?
Note: one
consumerIdcan subscribe to multiple topics.
The only way to answer this was to inspect the startup source code of Spring Cloud Stream and MetaQ.
MetaQ
The MetaQ client’s class diagram is shown below:
MQConsumerInnerrecords how the currentconsumerGroupinteracts with the server and maps topics to tags. Its default implementation isDefaultMQPushConsumerImpl, with a one-to-one relationship toconsumerGroup.MQClientInstancecentrally manages reusable objects such as network connections. A map maintains the relationship betweenConsumerGroupIdandMQConsumerInner. Put simply, oneConsumerGroupcan correspond to only oneMQConsumerInner, as shown below:
Spring Cloud Stream
Spring Cloud Stream is a glue layer between Spring and middleware. When it starts, it also registers a ConsumerGroup, as shown below:
Root Cause
At this point, the cause is clear. During startup, Spring Cloud Stream creates its own MetaPushConsumer, which is effectively a new MQConsumerInner. One ConsumerGroup therefore has two MQConsumerInner instances, violating MetaQ’s required one-to-one mapping. By default, MetaQ replaces the old mapping with the new one. The native MetaQ consumer consequently replaces Spring Cloud Stream’s consumer.
That is why, for ConsumerA on deployed machines, only the native MetaQ subscription remained.
Solution
Change the consumer ID:
spring.metaq.consumers[0].consumer-group=CID_CONSUMER_A
spring.metaq.consumers[0].topic=CONSUMER_A_TOPIC
spring.metaq.consumers[0].sub-expression=CONSUMER_A_TOPIC_TAG
spring.metaq.consumers[0].message-listener-ref=consumerAListener
spring.cloud.stream.bindings.consumerB.group=CID_CONSUMER_B
spring.cloud.stream.bindings.consumerB.contentType=text/plain
spring.cloud.stream.bindings.consumerB.destination=CONSUMER_B_TOPIC
spring.cloud.stream.metaq.bindings.consumerB.consumer.tags=CONSUMER_B_TOPIC_TAG
spring.cloud.stream.bindings.consumerC.group=CID_CONSUMER_B
spring.cloud.stream.bindings.consumerC.contentType=text/plain
spring.cloud.stream.bindings.consumerC.destination=CONSUMER_C_TOPIC
spring.cloud.stream.metaq.bindings.consumerC.consumer.tags=CONSUMER_C_TOPIC_TAG
Reflections and Summary
- The cause was not complicated, but many people might stop at the first layer—an inconsistent subscription relationship caused the backlog. We still need the curiosity to investigate more deeply.
- Avoid maintaining two sets of configuration in production wherever possible, because doing so adds cognitive overhead.