一种Kafka消息传输可靠性及容错性方案

在实际项目中,几乎都会用到消息队列。就像微服务一样。对于消息队列的选型,需要根据不同消息队列特定能否满足项目需求而定。在最近几年,Kafka 大幅增加其市场份额。除了微服务和消息队列,还有一种已经比较流行的架构模式,即 Event Sourcing(事件溯源)。Event Sourcing 有几个特点:

  1. 整个系统以事件为驱动,所有业务都由事件驱动来完成。
  2. 事件是一等公民,系统的数据以事件为基础,事件序列需要保存在某种存储介质上。
  3. 业务数据只是一些由事件产生的视图,不一定要保存到数据库中。

Kafka 所具有的特性很好地满足了这种模式。例如,如果 Event Sourcing 中的事件为系统状态的审计日志,那么通过重演事件就可以重建任何时刻的系统状态。当然,对于架构模式,都有利弊,由于消息队列异步通信特性,使得异常处理变得异常困难。

假设有这样一个简单场景:

  1. 新用户注册账号触发了一次 API 调用;
  2. API 调用同步请求到API网管;
  3. API 网管将调用同步分发到下游的微服务,下游微服务将用户信息保存到 MySQL;
  4. MySQL 在10分钟内无法连接;
  5. 最后,错误返回给用户;
    在这里插入图片描述

这这种情况下,用户马上察觉到注册不成功,通知管理员解决问题。
但是,如果我们使用消息队列通信的话,会发生什么呢?

  1. 新用户注册账号触发了一次 API 调用;
  2. API 调用同步请求到API网管;
  3. API 网管将用户信息发送到 Kafka 消息队列并立刻返回用户注册成功;
  4. 同时,下游微服务异步从 Kafka 接受注册信息,并开始注册;
  5. MySQL 在10分钟内无法连接;
  6. 异常被屏蔽,用户不能感知注册失败

在这里插入图片描述

消息未处理完时服务器宕机

除了上面这种情况,使用Kafka消息队列还存在其他问题。假设微服务正常处理用户注册信息,并且MySQL满负荷运行。当微服务正在处理消息时,服务由于内存不足宕机。这时发人员还来不及处理,微服务已经使用自动位移提交(auto-commit)提交了从Kafka读到消息的位移,这部分消息将会丢失。
什么是auto-commit呢? 对于消费者而言,有位移(offset)的概念,消费者使用位移来标识消费到分区中某个消息所在的位置。为了保证在每次调用拉取消息时返回的是还没有被消费过的消息集,需要记录上一次消费时的消费位移。并且这个消费位移必须做持久化保存,而不是单单保存在内存中,否则消费者重启之后就无法知晓之前的消费位移。在旧消费者客户端中,消费位移是存储在 ZooKeeper 中的。而在新消费者客户端中,消费位移存储在 Kafka 内部的主题__consumer_offsets 中。这里把将消费位移存储起来(持久化)的动作称为“提交”,消费者在消费完消息之后需要执行消费位移的提交。

在 Kafka 中默认的消费位移的提交方式是自动提交。这个由消费者客户端参数 enable.auto.commit 配置,默认值为 true。当然这个默认的自动提交不是每消费一条消息就提交一次,而是定期提交,这个定期的周期时间由客户端参数 auto.commit.interval.ms 配置,默认值为5秒,此参数生效的前提是 enable.auto.commit 参数为 true。

假设应用Kafka客户端成功拉取到消息,但是在处理消息时需要进行比较耗时的操作,比如远程调用、读数据库等等,这时服务器宕机,由于位移已经自动提交了,客户端下次再次消费时不会将这部分消息消费到,造成消息丢失。

传递保障

Kafka提供的一个关键特性是传递保障。在Kafka中,消息传递保障是由客户端决定的,而不是Kafka安装配置。
Kafka提供三种传递保障:

  1. At-most once delivery — Messages may be lost but are never redelivered.
    最多一次 — 消息可能丢失,但绝不会重发。
  2. At-least once delivery —Messages are never lost but may be redelivered.
    至少一次 — 消息绝不会丢失,但有可能重新发送。
  3. Exactly once delivery —t his is what people actually want, each message is delivered once and only once.
    正好一次 — 这是人们真正想要的,每个消息传递一次且仅一次。

最多一次传递是指当某主题出现新消息时,消费者要么能收到消息并成功处理,要么消费不到这调消息,实际上,这是大多数Kafka消费端框架默认传递保障。 启用自动提交模式后,客户端最多消费一次。

手动提交

解决方案也很简单,当应用程序正在消费消息时,不要立刻提交已经读取到消息的位移,而是等到消息处理完成后再手动提交位移。 采用这种方法可以确保仅在应用程序消息处理逻辑完成后才认为消息已被消费。

但是,还有一件事需要考虑,当应用程序重启时,它将重新消费这部分消息,所以需要保证消息处理逻辑的幂等性。想象,如果系统正在处理用户的注册信息,正在向数据库插入数据时,系统宕机了,此时位移还没有提交。当系统重启时,将会重新消费到该用户的注册信息,再向数据库中插入一行相同数据。显然,这是系统中的一个小故障。 这就是为什么保证消息处理逻辑幂等性很重要。

使用手动提交时,还要考虑业务代码处理消息时发生错误时这种场景。比如,还是以用户注册为例,如果应用处理用户注册失败信息后不提交位移,那么每次都会拉到这条消息,直到能成功处理这条消息。
有很多方法可以解决,一种选择是在应用程序中使用通用错误处理器,该处理器将捕获应用程序代码抛出的异常,并简单地记录一条日志。 它可以工作,但是我们可以做得更好。

错误恢复

回到示例。 从这种这种错误也很容易。 只需等待一段时间,再尝试使用数据库。 您可以准备应用程序代码以进行智能重试。可以使用Spring或者Guava封装好的重试工具,但是这里打算利用Kafka实现此功能,假设在Kafka中还有另外一个主题,这个主题的消费者仅仅是将消费到的消息在发送到原始主题。
有一种处理失败消息的行业标准,称为死信队列(Dead-letter queue,DLQ), Kafka中另外那个主题即为DLQ。

DLQ消费者可以重新发送消息到原始主题,但是我们无限无限重试的为题。 因此,我们可以引入retryCount的概念,该概念告诉DLQ重试。 如果达到阈值(例如5),它将停止重试该消息。 达到阈值后,可以用另外的方式处理这部分消息,比如人工干预等。另外,我们可将将SLQ中的消息通过Logstash,Fluentd等方式同步到ElasticSearch,然后在Kibana中,可用很容易统计重试失败的消息。
另外使用Kafka主题作为DLQ可以低侵入原有代码,像retryCount和原始主题可以记录在消息的Heather中,无需重新设计数据结构。

参考文档

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值