kakfa用offset来记录某消费者消费到的位置,由于kafka是个分布式结构,数据被存放在多个partition上,那么要为每个partition单独记录一个offset,该offset保存在一个叫__consumer_offsets 的Topic里,与此同时,kafka规定在同一消费者组里,同一时刻一个partition只能有一个消费者,这样的规定优势是每个consumer不用都跟大量的broker通信,减少通信开销,同时也降低了分配难度,实现也更简单,另外,因为同一个partition里的数据是有序的,这种设计可以保证每个partition里的数据是有序被消费。
那么,在一个partition用一个offset记录的设计下,假设有三个有序的消息(m1,m2,m3),消费者顺序消费,若是m1、m2消费了但是不提交offset,消费m3的时候提交offset,那么此时kafka记录的offset是m1的位置(第一次未commit的位置)还是m3的位置(最新commit的位置)呢
经过实验(实验版本kakfa 0.10.0, Spring kafka api),kafka记录的是最新commit的位置作为offset,那么未提交的消息m1、m2也被kafka认为是已经消费过了的,不会再重复消费:
设想这样一个常见的场景,若程序消费消息后处理并存库,若存库失败则不commit offset,即用at least once设置,期望存库失败的消息能被再次消费,那么问题就来了,若是消费者消费下一条消息成功并commit了offset,那么存库失败的消息显然不能被重新消费了,它的offset已经被覆盖,kafka已经认为它被消费了,这样就达不到预期的效果了,实验后结果也确实如此
难道消费者有一个消息没有commit的话会一直重试去消费这个消息吗,那岂不是很容易死锁,经实验,在不触发rebalance的情况下,消费者并不会去重新消费之前没有commit的消息,而是继续往下消费,那么没有commit的消息妥妥的就丢了
再考虑触发rebalance的情况,如下图,任意broker或者是消费者的变化(重启、移除、加入新节点)的情况都会触发,也就是说,在一个partition 上,一个消息没有被commit,在对应的消费者还没有commit后续的消息之前,若是触发了rebalance,这种情况下,新分配给这个partition的消费者会重新消费这条消息。
经实验,将消费者重启即可重新消费,此外,实验过程中发现,一旦一个消息未被消费者commit,且一段时间内(很短)没有后续消息的话,会触发rebalance,于此对照的是:正常消费并commit的消费者,尽管后续没有消息来,依然不会触发rebalance。
对于以上这种情况,我认为消费者端会存放一个值用来记录消费到了哪里,这个值可能与partition上的offset不一致(消息未提交),于是每隔一段时间消费者与broker同步时,若发现这两个值不一致,就会触发rebalance,这一段就属于猜想了,没有找到文档,下一步再仔细看吧。
分析这么多,得出一个结论。单纯想通过不commit offset来想达到再次消费消息的目的是不太靠谱的,考虑额外的措施,如:一旦消费消息后存库失败,那么就不commit offset,并将该消费者重启以触发rebalance,这样的弊端是会一直重复消费这个消息,容易死锁;要么就把处理失败的消息再投递,丢回kafka里;要么就打个log,然后commit 得了。