怎么在Kafka上支持延迟消息?
延迟列队和延迟消息
- 延迟列队是一种特殊队列,里面每个元素都有过期时间,如果没过期去拿元素会被阻塞,元素过期时就会拿到这个元素。可以说拿到的永远是最先过期的元素
- 延迟消息指基于消息队列的延迟队列。消息不是被立刻消费,一段时间后才会被消费。到时间前,消息一直都被存储在消息队列的服务器上
支持延迟消息的消息队列
- RabbitMQ:通过启动插件rabbitmq_delayed_message_exchange实现延迟消息。就是实现了一个exchange,exchange控制消息什么时候被投递到队列里
案例
利用定时任务调度
- 开发一个定时任务平台,相当于业务发送者注册了一个任务,这个任务就是30分钟后发送一条消息到kafka上。之后消费者就能消费了
- 缺点:定时任务中间件支撑不了高并发
分区设置不同延迟时间
-
关键角色是delay_topic、延迟消费组
- deleay_topic的分区被用来接收不同延迟时间的消息
- 延迟消费组按照分区数量创建消费者,每个消费者一个分区。消费者每次读取一个消息,等延迟时间到后转发给biz_topic
介绍
- 我们的方案比较简单,创建一个delay_topic,这个topic有N个分区,每个分区设置不同延迟时间。创建一个消费组消费这个delay_topic,消费者读取到消息后,根据延迟时间等待。等待完后发送到真正的topic上
亮点1
- rebalance问题:消费者睡眠了,睡眠期间不消费消息,kafka会判定消费者崩溃,触发rebalance。rebalance之后,等消费者恢复过来,会被随机分配到别的分区,等于白睡了
- 解决方案:利用kafka的暂停功能确保不触发rebalance,睡眠结束后再恢复。kafka的暂停功能相当于拉去0条数据,不是不拉数据。还是会发出poll调用,让kafka认为消费者还活着
亮点2
- 一致性问题:实际就是再问biz_topic是先提交还是先转发,要抓住关键字后提交来回答
- 一致性问题解决起来需要业务方配合,我们逻辑是到了延迟时间,就先转发biz_topic,然后再提交。也就是说转发biz_topic后,提交失败,下次还可以重试,biz_topic可能收到两条同样的消息。这种场景下只能要求消费者做到幂等
优缺点
-
优点:足够简单,容易实现
-
缺点:延迟时间必须预先设定好、分区之间负载不均匀
- 延迟时间必须预先设定好:不支持随机延迟时间
- 分区之间负载不均匀:很多业务只需要延迟3min。1min、10min数据就很少,可能造成消息积压问题
基于MySQL的亮点方案
实践中最好放弃随机延迟时间,绝大多数时间都用不上随机延迟时间。可以通过调整业务来适配固定的延迟时间
介绍
方案关键点是创建一个delay_topic ,业务发送者把消息发到这个topic,消息带上延迟的时间。然后有一个延迟消费者 ,消费delay_topic中的消息,转储到数据库 。还有一个延迟发送者 ,会轮询 数据库中的消息,把到时间的消息转发到真正的biz_topic中,完成后延迟发送者把数据库状态更新成已发送,最后业务消费者消费biz_topic
怎么支撑住高并发
-
分区表:根据并发量选择按月份、周分、天分。历史分区直接清理
-
表交替:准备两个表交替查写,交替查询。比如今天用tab0,明天用tab1,用tab1时就清空tab0,但延迟不能超过一天
-
分库分表:并发非常高时考虑,只需要按照biz_topic的名字来分库分表,还可以叠加分区表和表交替提高性能
- 隐患:不同topic并发度不一样,导致不同库不同表的压力差异很大
- 解决方案:不考虑消息有序性,可以考虑轮询插入。查找只会按照发送时间来找,所以随机插入都没问题
- 消息有序性:要保持消息有序性可以在分库分表时,确保同一个biz_topic的消息在同一张表,并且delay_topic也是按照biz_topic区分的,那么就可以保证延迟消息转发到biz上跟被发送到delay是一样的
批量操作
- 还可以利用批量操作减轻MySQL的压力。延迟消费者可以消费了一批数据再批量插入到数据库,然后再提交这一批消息。延迟发送者可以发送一批数据后,再批量把这些消息更新为已发送
- 批量操作可能会使数据一致性问题更严重。只要消费者可以做到幂等就可以了,比如用唯一索引