kafka 高级应用

302 阅读7分钟

除了正常的消息发送和消费,在使用Kafka的过程中难免会遇到一些其他高级应用类的需求,比如消费回溯,这个可以通过原生Kafka提供的KafkaConsumer.seek()方法来实现,然而类似延时队列、消息轨迹等应用需求在原生Kafka 中就没有提供了。我们在使用其他消息中间件时,比如RabbitMQ,使用到了延时队列、消息轨迹的功能,如果我们将应用直接切换到Kafka中,那么只能选择舍弃它们。但这也不是绝对的,我们可以通过一定的手段来扩展 Kafka。

过期时间

队列是存储消息的载体,延时队列存储的对象是延时消息。所谓的“延时消息”是指消息被发送以后,并不想让消费者立刻获取,而是等待特定的时间后,消费者才能获取这个消息进行消费,延时队列一般也被称为“延迟队列”。注意延时与TTL的区别,延时的消息达到目标延时时间后才能被消费,而TTL的消息达到目标超时时间后会被丢弃。 延时队列的使用场景有很多,比如:

  • 在订单系统中,一个用户下单之后通常有30分钟的时间进行支付,如果30分钟之内没有支付成功,那么这个订单将进行异常处理,这时就可以使用延时队列来处理这些订单了。
  • 订单完成1小时后通知用户进行评价。
  • 用户希望通过手机远程遥控家里的智能设备在指定时间进行工作。这时就可以将用户指令发送到延时队列,当指令设定的时间到了之后再将它推送到智能设备。

在 Kafka 的原生概念中并没有“队列”的影子,Kafka 中存储消息的载体是主题(更加确切地说是分区),我们可以把存储延时消息的主题称为“延时主题”,不过这种称谓太过于生僻。在其他消息中间件(比如 RabbitMQ)中大多采用“延时队列”的称谓,为了不让 Kafka过于生分,我们这里还是习惯性地沿用“延时队列”的称谓来表示 Kafka中用于存储延时消息的载体。 原生的 Kafka 并不具备延时队列的功能,不过我们可以对其进行改造来实现。Kafka 实现延时队列的方式也有很多种,在11.1节中我们通过消费者客户端拦截器来实现消息的TTL,延时队列也可以使用这种方式实现。 不过使用拦截器的方式来实现延时的功能具有很大的局限性,某一批拉取到的消息集中有一条消息的延时时间很长,其他的消息延时时间很短而很快被消费,那么这时该如何处理呢?下面考虑以下这几种情况:

(1)如果这时提交消费位移,那么延时时间很长的那条消息会丢失。

(2)如果这时不继续拉取消息而等待这条延时时间很长的消息到达延时时间,这样又会导致消费滞后很多,而且如果位于这条消息后面的很多消息的延时时间很短,那么也会被这条消息无端地拉长延时时间,从而大大地降低了延时的精度。

(3)如果这个时候不提交消费位移而继续拉取消息,等待这条延时时间很长的消息满足条件之后再提交消费位移,那么在此期间这条消息需要驻留在内存中,而且需要一个定时机制来定时检测是否满足被消费的条件,当这类消息很多时必定会引起内存的暴涨,另一方面当消费很大一部分消息之后这条消息还是没有能够被消费,此时如果发生异常,则会由于长时间的未提交消费位移而引起大量的重复消费。

延时队列

死信队列和重试队列

由于某些原因消息无法被正确地投递,为了确保消息不会被无故地丢弃,一般将其置于一个特殊角色的队列,这个队列一般称为死信队列。后续分析程序可以通过消费这个死信队列中的内容来分析当时遇到的异常情况,进而可以改善和优化系统。

与死信队列对应的还有一个“回退队列”的概念,如果消费者在消费时发生了异常,那么就不会对这一次消费进行确认,进而发生回滚消息的操作之后,消息始终会放在队列的顶部,然后不断被处理和回滚,导致队列陷入死循环。为了解决这个问题,可以为每个队列设置一个回退队列,它和死信队列都是为异常处理提供的一种机制保障。实际情况下,回退队列的角色可以由死信队列和重试队列来扮演。

无论RabbitMQ 中的队列,还是Kafka 中的主题,其实质上都是消息的载体,换种角度看待问题可以让我们找到彼此的共通性。我们依然可以把Kafka中的主题看作“队列”,那么重试队列、死信队列的称谓就可以同延时队列一样沿用下来。

理解死信队列,关键是要理解死信。死信可以看作消费者不能处理收到的消息,也可以看作消费者不想处理收到的消息,还可以看作不符合处理要求的消息。比如消息内包含的消息内容无法被消费者解析,为了确保消息的可靠性而不被随意丢弃,故将其投递到死信队列中,这里的死信就可以看作消费者不能处理的消息。再比如超过既定的重试次数之后将消息投入死信队列,这里就可以将死信看作不符合处理要求的消息。

至于死信队列到底怎么用,是从broker端存入死信队列,还是从消费端存入死信队列,需要先思考两个问题:死信有什么用?为什么用?从而引发怎么用。在 RabbitMQ 中,死信一般通过broker端存入,而在Kafka中原本并无死信的概念,所以当需要封装这一层概念的时候,就可以脱离既定思维的束缚,根据应用情况选择合适的实现方式,理解死信的本质进而懂得如何去实现死信队列的功能。

重试队列其实可以看作一种回退队列,具体指消费端消费消息失败时,为了防止消息无故丢失而重新将消息回滚到broker中。与回退队列不同的是,重试队列一般分成多个重试等级,每个重试等级一般也会设置重新投递延时,重试次数越多投递延时就越大。举个例子:消息第一次消费失败入重试队列Q1,Q1的重新投递延时为5s,5s过后重新投递该消息;如果消息再次消费失败则入重试队列Q2,Q2的重新投递延时为10s,10s过后再次投递该消息。以此类推,重试越多次重新投递的时间就越久,为此还需要设置一个上限,超过投递次数就进入死信队列。重试队列与延时队列有相同的地方,都“都需要设置延时级别。它们的区别是:延时队列动作由内部触发,重试队列动作由外部消费端触发;延时队列作用一次,而重试队列的作用范围会向后传递。

消息路由

消息轨迹

消息审计

消息代理

消息中间件选型

参考

  • 深入理解Kafka:核心设计与实践原理 2019 朱忠华 此材料可能受版权保护。

如果这篇文章帮助到了你,欢迎评论、点赞、转发。