五. 消息中间件的高可用性
5.1 消息中间件的高可用性
Kafka 实现高可用性的方式是进行 replication。对于 kafka,如果没有提供高可用性机制,一旦一个或多个 Broker 宕机,则宕机期间其上所有 Partition 都无法继续提供服务。若该 Broker永远不能再恢复,那么所有的数据也就将丢失,这是不可容忍的。所以 kafka 高可用性的设计也是进行 Replication。
Replica 的分布:为了尽量做好负载均衡和容错能力,需要将同一个 Partition 的 Replica 尽量分散到不同的机器。
Replica 的同步:当有很多 Replica 的时候,一般来说,对于这种情况有两个处理方法:
- 同步复制:当 producer 向所有的 Replica 写入成功消息后才返回。一致性得到保障,但是延迟太高,吞吐率降低。
- 异步复制:所有的 Replica 选取一个一个 leader,producer 向 leader 写入成功即返回(即生产者参数 acks = 1)。leader 负责将消息同步给其他的所有 Replica。但是消息同步一致性得不到保证,但是保证了快速的响应。
而 kafka 选取了一个折中的方式:ISR (in-sync replicas)。producer 每次发送消息,将消息发送给 leader,leader 将消息同步给他“信任”的“小弟们”就算成功,巧妙的均衡了确保数据不丢失以及吞吐率。具体步骤:
- 在所有的 Replica 中,leader 会维护一个与其基本保持同步的 Replica 列表,该列表称为ISR (in-sync Replica);每个 Partition 都会有一个 ISR,而且是由 leader 动态维护。
- 如果一个 replica 落后 leader 太多,leader 会将其剔除。如果另外的 replica 跟上脚步,leader 会将其加入。
- 同步:leader 向 ISR 中的所有 replica 同步消息,当收到所有 ISR 中 replica 的 ack 之后,leader 才 commit。
- 异步:收到同步消息的 ISR 中的 replica,异步将消息同步给 ISR 集合外的 replica。
- 问题 1:使用 Kafka 的时候,你们怎么保证投递出去的消息一定不会丢失?
- 问题 2:你们怎么保证投递出去的消息只有一条且仅仅一条,不会出现重复的数据?
上述问题换一种问法,可以翻译为**如何保证消息队列的幂等性?**这个问题可以认为是消息队列领域的基本问题。这个问题的回答可以根据具体的业务场景来答,没有固定的答案。
无论是哪种消息队列,造成重复消费原因其实都是类似的。正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。只是不同的消息队列发出的确认消息形式不同(例如 RabbitMQ 是发送一个 ACK 确认消息,RocketMQ 是返回一个 CONSUME_SUCCESS 成功标志),kafka 是通过**提交 offset 的方式**让消息队列知道自己已经消费过了。
造成重复消费的原因,就是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
注:关于重复消费,还有部分与第七章 Kafka 再均衡相关的内容。
参考地址:《记一次线上kafka一直rebalance故障》
如何解决?这个问题针对业务场景来答,分以下三种情况:
- 实际使用方案:准备一个第三方介质,来做消费记录
- 以 redis 为例,给消息分配一个全局 ID,只要消费过该消息,将 <id,message>以 K-V 形式写入 redis。消费者开始消费前,先去 redis 中查询有没有消费记录即可。
- 数据库主键:拿到这个消息做数据库的 insert 操作,那就容易了,给这个消息做一个唯一的主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
- Redis Set 操作:拿到这个消息做 redis 的 set 的操作,那就容易了,不用解决,set 操作无论几次结果都是一样的,因为 set 操作本来就是幂等操作。
5.2 Kafka 消息投递语义
kafka 有三种消息投递语义:
- At most Once:最多一次;消息不会重复,但可能丢失;
- At least Once:最少一次;消息不会丢失,但可能重复;
- Exactly Once:最佳情况,只且消费一次;消息不会重复,也不会丢失;
整体的消息投递语义由生产者、消费者两端同时保证。
5.3 Producer 生产者端
Producer 端保证消息投递重复性,是通过 Producer 的 acks 参数与 Broker 端的 min.insync.replicas 参数决定的。
Producer 端的 acks 参数值信息如下:
- acks = 0:不等待任何响应的发送消息;
- acks = 1:leader 分片写消息成功,就返回响应给生产者;
- acks = -1(all):要求 ISR 集合至少两个 Replica,而且必须全部 Replica 都写入成功,才返回响应给 Producer;
- 无论 ISR 少于两个 Replica,或者不是全部 Replica 写入成功,都会抛出异常;
前面 Producer 的 acks = 1 可以保证写入 Leader 副本,对大部分情况没有问题。但如果刚刚一条消息写入 Leader,还没有把这条消息同步给其他 Replica,Leader 就挂了,那么这条消息也就丢失了。所以如果保证消息的完全投递,还是需要令 acks = all;
5.4 Broker 节点端
首先上面说到,为了配合 Producer acks 参数为 all,需要令 Broker 端参数 min.insync.replicas = 2;
min.insync.replicas 参数是用来配合 Producer acks 参数的。因为如果 acks 设置为 all,但某个 Topic 只有 leader 一个 Replica(或者某个 Kafka 集群中由于同步很慢,导致所有 follower 全部被剔除 ISR 集合),这样 acks = -1 就演变成了 acks = 1。
所以需要 Broker 端设置 min.insync.replicas 参数:当参数值为 2 时,如果副本数小于 2 个,会抛出异常。
注:然而在笔者的使用环境中,订阅是 Kafka 主要的使用场景之一,方式是对于想要订阅的某个 Topic,每个用户创建并独享一个不会重复的消费组。所以这样的情况下,环境下的 min.insync.replicas 只能等于 1;
除此之外,broker 端还有一个需要注意的参数 unclean.leader.election.enable。该参数为 true 的时候,表示在 leader 下线的时候,可以从非 ISR 集合中选举出新的 Leader。这样的话可能会造成数据的丢失。所以如果需要在 Broker 端的 unclean.leader.election.enable 设置为 false。
5.5 Consumer 消费者端
Consumer 端比较麻烦,原因是需要考虑到某个 Consumer 宕机后,同 Consumer Group 会发生负载均衡,同 Group 其他的 Consumer 会重新接管并继续消费。
假设两种场景:
第一个场景,Consumer 先提交 offset,再处理消息。代码如下:
List<String> messages = consumer.poll();
consumer.commitOffset();
processMsg(messages);
这种情形下,提交 offset 成功,但处理消息失败,同时当前 Consumer 宕机,这时候发生负载均衡,其他 Consumer 从已经提交的 offset 之后继续消费。这样的情况保证了 at most once 的消费语义,当然也可能会丢消息。
第二个场景,Consumer 先处理消息,再提交 offset。代码如下:
List<String> messages = consumer.poll();
processMsg(messages);
consumer.commitOffset();
这种情形下,消息处理成功,提交 offset 失败,同时当前 Consumer 宕机,这时候发生负载均衡,其他 Consumer 依旧从同样的 offset 拉取消息消费。这样的情况保证了 at least once 的消费语义,可能会重复消费消息。
上述机制的保证都不是直接一个配置可以解决的,而是 Consumer 端代码的处理先后顺序问题完成的。
注:关于 Kafka 解耦作用的思考:
注册中心可以将服务于服务之间解耦,但 Kafka 也可以通过相同的 topic 的消息传递实现业务的解耦。这两种形式都可以实现解耦,但笔者个人理解:
- 注册中心通过请求 -> 响应的模式,等待其他服务处理结果完毕之后的响应;
- Kafka 的将消息从生产者投递,消费者接收,但消费者的消费结果通常生产者并不需要的,生产者只需要确保将消息投递到 Kafka Broker 节点即可。