背景
网上很多教程提到kafka消费都有提到一点是手动提交偏移量,确保每一条消息被正确消费后提交偏移量。实际上,这个说法是针对直接操作kafka相关组件的时候才这么用的,这里我以go micro v4的开源组件“github.com/go-micro/plugins/v4/broker/kafka” 来分析消费是否必须手动提交偏移量。
在组件目录下的options.go文件我们可以搜到ConsumeClaim方法:
eh := h.kopts.ErrorHandler
...
if err == nil && h.subopts.AutoAck {
sess.MarkMessage(msg, "")
} else if err != nil {
p.err = err
if eh != nil {
eh(p)
} else {
log.Errorf("[kafka]: subscriber error: %v", err)
}
}
真相揭秘
eh指的是ErrorHandler(错误处理器)用于处理消费失败的数据的方法,开发时需要自己从外部注入这个方法。从代码中我们可以看到如果是自动确认消息那么就会标记这条消息为已消费,假设我们设置为自动提交偏移量,那么它将在指定时间间隔提交偏移量。消费失败则调用ErrorHandler来处理,ErrorHandler的结构如下:
func(event broker.Event) error
broker.Event是个接口包含Ack()方法用于确认消息,对应的就是sess.MarkMessage()。所以这样我们可以得出结论,在该机制下几乎不需要手动提交偏移量,插件会帮我们处理标记消息,处理完消息会自动提交偏移量。对于消费失败的数据通常会放入死信队列(DLQ),这个操作可以由ErrorHandler来完成,推入死信队列后原主题的消息由死信主题接管,在ErrorHandler的最后一步操作调用Ack()确认消息。所以一定要确保消息能够推入死信队列。
关键点剖析
如果你的应用需要和底层组件打交道,手动提交偏移量是最安全的做法,但从系统架构的角度上分析其实不太妥当,因为具体的组件和业务逻辑代码强耦合不易替换,像go micro这类框架已经进行二次封装,底层组件的实现逻辑被隐藏起来,对外只保留broker接口,手动提交偏移量显得比较多余。
另一种更高级的做法是异步手动提交偏移量,这需要自行实现broker.Broker接口或者框架,自研组件/框架可以定制化需求但需要团队自己长期维护,选择自研组件必须确保团队能够持续维护。
无论偏移量是手动提交还是自动提交都要确保幂等性,最后的底线一定要守住。即使是手动提交偏移量,万一在标记消息前服务挂掉,重启后同样会重复拉取和消费,所以幂等性一定要做,比如新建一张表记录事件ID,发布端和订阅端都要做幂等性处理防止重复消费造成结果异常。