1. 业务场景
在使用Canal订阅binlog的过程中,我们观察到一个现象:尽管有大量的Topic,消息却默认发送到MQ的第一个队列。这导致当消费者服务启动并查询动态注册的消费者时,只有最先启动的消费者能够注册大量消费者,进而造成单点性能问题。
2. 解决方案探索
首先想到的是从消费者端进行负载均衡
思路1:
原来的消费者服务作为代理服务,负责转发消息到新的Topic,然后新增一个消费者服务,消费新的Topic
逻辑处理清晰易懂,但是需要新增一个服务
思路2:在原来的消费者服务基础上,进行负载均衡,按照表名进行Hash。不同的服务消费对应的Topic
目测可行,由于下面找到问题的根本原因,所以没试
3. 深入Canal源码
在我开始阅读Canal的源码时,我们发现在初始化canalMQProducer.init时,会构建一个线程池,这个线程池管理着对不同RocketMQ队列的消息发送任务。进一步的研究让我们找到了partitionsNum和partitionHash这两个关键的参数配置
下面贴出我们生产上当时的配置
canal.mq.dynamicTopic=bond_ext.abnormal_price,bond_ext_etl.com_default_course
canal.mq.partition=0
# hash partition config
# canal.mq.partitionsNum=4
# canal.mq.partitionHash=$[PARTITION_HASH]
canal.mq.partition=0,这个参数的意思就是消息只会发送到Topic的0号队列
我当时脑子立刻浮现的想法是:这不就是问题所在嘛,但是我们的业务场景是单个表的变更都在同一个队列即可,而不是固定发送到0号队列,应该根据表名去做Hash
其实到这里,不继续追踪源码,也能猜出来问题在这里,于是我就改配置测试下
# canal.mq.partition=0
canal.mq.partitionsNum=4
canal.mq.partitionHash=etl_test_rocsea.abs_com_relation:id
但是测试结果如下,我的预期是让它随机选一个队列,但是这样单个表的变更发到4个队列,不能做到局部有序了。我直接解释下:这里其实会根据ID的值去Hash
这时候我脑子灵机一动,那么如果我不设置ID呢?改成这样呢,是不是就会根据表名做hash
# canal.mq.partition=0
canal.mq.partitionsNum=4
canal.mq.partitionHash=etl_test_rocsea.abs_com_relation,etl_test_rocsea.com_default_course
于是我拿了几张表做测试,果然达到我的预期结果,同一个表的变更只会发到一个队列,并且不同的表会随机发到不同的队列,再也不是永远只会发送到第一个队列啦!!!!
4. 彩蛋后续(发现坑)
如果你有上述的类似问题,看到这里心情一定非常愉悦了,感觉可以直接开干了,但是当我看完 canal完整的源码,发现里面还是有坑的。这里先贴出我当时看到的关键代码
这里直接贴出我画的源码流程图,里面包含我对messagePartition方法详细的解剖,对应图中圈出来的红色部分。
那么我就来测试验证我发现的坑,这个坑就是"如果是ddl就会固定发到0号队列",这里直接贴出我测试的结果,其中1号队列的2026条消息是增删改,而0号队列的2条消息,分别是我操作的TRUNCATE操作以及修改字段名。
0号队列",这里直接贴出我测试的结果,其中1号队列的2026条消息是增删改,而0号队列的2条消息,分别是我操作的TRUNCATE操作以及修改字段名。