Kafka是一个知名的流处理框架,同时也可以作为消息队列使用,但至少在官方描述中,Kafka不被承认是消息队列服务。
一开始我对官方这个描述是无感知的,因为Kafka太优秀了,性能优越、低延迟、真正的分布式服务,理论上是可以胜任消息队列服务的,但是后来我发现我错了。
起源是在一个高并发项目中使用Kafka,因为并发量比较大,所以使用Kafka作为削峰作用。至此我仍然看不出所谓流处理跟消息队列有什么区别,因为这部分的任务都非常简单,纯纯是对数据库的插入操作,可以在极短的时间内完成,这部分Kafka跑得很好。
但是后面我有一些长时间的任务,需要到消息队列,出于程序员惯有的技术惰性,我也懒于再增加一个类似Rabbitmq的经典消息队列服务,认为Kafka也能胜任这个任务,但事实证明我错了,我付出了比选型Rabbitmq百倍的代价,才将Kafka调教得勉强能作为消息队列使用,而且并没有像Rabbitmq那样好用。
一切的悲剧在于我忽视了官方那句话:“Kafka并不是消息队列服务”。
分析
我总结了一下Kafka不适合作为长时间任务的消息队列服务的原因:
-
超时问题
作为真正的分布式服务,Kafka不但保证服务端是高可用的,并且保证消费者也是高可用的,因此就需要有一定手段来检测客户端是否存活,而检测方案是通过心跳检测来实现的,一旦心跳间隔大于预设的超时时间,即可判断消费者是失活状态,从而将它暂时踢出消费者组。
但是让我感到无比震惊的是,Kafka居然是直接用任务本身作为心跳机制。什么意思呢?就是接收任务和完成任务ACK作为心跳,并且没有任何的配置可以新开一个线程来做心跳存活。
这样会给长时间任务带来一个矛盾点:
如果超时时间设置得太短,任务还没跑完,Kafka服务端会认定心跳检测不通过,从而认为消费者存在故障,于是将任务重新分配给其他消费者,这将导致任务被重复消费,这个过程会一直轮回,最终导致所有消费者都卡在同一任务上不停重复执行;
如果超时时间设置得太长,那任务是可以正常跑完,但是心跳的机制就完全失效,更加可怕的是,消费者要是故障退出,那服务端也需要等待漫长的超时时间才能认为消费者是异常退出了,即使你把消费者重新拉起来,也没用,因为服务端会认为之前的任务还在运行中,需要等待超时时间过去了,任务才能重新被消费。
我查阅了大量资料,也没有找到解决办法,因为,确实没有任何的配置可以新开一个线程来做心跳存活,那怎么整都是白搭。
-
分片机制
不同于Rabbitmq的抢占式消费,Kafka的消费能力完全取决于topic的分片数量,体现有两点:
每个分片必须对应一个消费者,换言之,消费者组里面的消费者数量比分片少的时候,将会有部分分片没法被消费。
当Kafka消费者组中的消费者数量大于topic的分片(分区)数量时,多余的消费者会处于备用状态,不会消费任何消息。每个分区只能分配给一个消费者,所以多余的消费者会空闲等待,跟CPU上的一核有难多核围观差不多。
这就导致消费者无法像Rabbitmq那样无限水平伸缩,并且Kafak的分片只能伸不能缩,要是你前期伸得太多了,那就只能哭了,祈求领导给你分配的消费者资源足够多吧。
-
没有优先级
优先级是消息队列非常经典的功能,而Kafka是没有的,毕竟它并非传统意义的上消息队列。偏要在Kafka上实现优先级的话,那得按优先级设置多个topic和多组消费者,属实不方便。
为了解决上述1和2两个问题,我想出了一个尚且能解决问题的办法,那就是在大循环里面,按照“消费者连接服务端->获取消息->消费者退出->处理消息”这样循环,这样在一定程度上能缓解症状,但却丧失了ACK的作用,并且显得与正常的Kafka消费方式格格不入。