kafka如何保证消息不丢失

Kafka介绍

大家都知道 Kafka 的整个架构非常简洁,是分布式的架构,主要由 Producer、Broker、Consumer 三部分组成,后面剖析丢失场景会从这三部分入手来剖析。

消息传传递语义(producer和consumer)

所谓的消息传递语义是 Kafka 提供的 Producer 和 Consumer 之间的消息传递过程中消息传递的保证性。主要分为三种, 如下图所示:

image.png

Producer端

从Producer的角度来看,At most once意味着Producer发送完一条消息后,不会确认消息是否成功送达。这样从Producer的角度来看,消息仅仅被发送一次,也就存在者丢失的可能性。

从Producer的角度来看,At least once意味着Producer发送完一条消息后,会确认消息是否发送成功。如果Producer没有收到Broker的ack确认消息,那么会不断重试发送消息。这样就意味着消息可能被发送不止一次,也就存在这消息重复的可能性。

从Producer的角度来看,Exactly once意味着Producer消息的发送是幂等的。这意味着不论消息重发多少遍,最终Broker上记录的只有一条不重复的数据。

Producer At least once配置

Kafka默认的Producer消息送达语义就是At least once,这意味着我们不用做任何配置就能够实现At least once消息语义。

原因是Kafka中默认`acks=1`并且`retries=2147483647`复制代码

Producer At most once配置

  • acks=0acks配置项表示Producer期望的Broker的确认数。默认值为1。可选项:[0,1,all]。如果设置为0,表示Producer发送完消息后不会等待任何Broker的确认;设置为1表示Producer会等待Broker集群中的leader的确认写入消息;设置为all表示Producer需要等待Broker集群中leader和其所有follower的确认写入消息。

  • retries=0retires配置项表示当消息发送失败时,Producer重发消息的次数。默认值为2147483647。当配置了acks=0时,retries配置项就失去了作用,因此这儿可以不用配置。

Producer Exactly once配置

Exactly once是Kafka从版本0.11之后提供的高级特性。我们可以通过配置Producer的以下配置项来实现Exactly once语义:

  • enable.idempotence=trueenable.idempotence配置项表示是否使用幂等性。当enable.idempotence配置为true时,acks必须配置为all。并且建议max.in.flight.requests.per.connection的值小于5。

  • acks=all

如何实现幂等后边在理解。

Consumer端

从Consumer的角度来看,At most once意味着Consumer对一条消息最多消费一次,因此有可能存在消息消费失败依旧提交offset的情况。考虑下面的情况:Consumer首先读取消息,然后提交offset,最后处理这条消息。在处理消息时,Consumer宕机了,此时offset已经提交,下一次读取消息时读到的是下一条消息了,这就是At most once消费。

从Consumer的角度来看,At least once意味着Consumer对一条消息可能消费多次。考虑下面的情况:Consumer首先读取消息,然后处理这条消息,最后提交offset。在处理消息时成功后,Consumer宕机了,此时offset还未提交,下一次读取消息时依旧是这条消息,那么处理消息的逻辑又将被执行一遍,这就是At least once消费。

从Consumer的角度来看,Exactly once意味着消息的消费处理逻辑和offset的提交是原子性的,即消息消费成功后offset改变,消息消费失败offset也能回滚

Consumer At least once配置

  • enable.auto.commit=false。禁止后台自动提交offset。
  • 手动调用consumer.commitSync()来提交offset。手动调用保证了offset即时更新。

通过手动提交offset,就可以实现Consumer At least once语义。

Consumer At most once配置

  • enable.auto.commit=true。后台定时提交offset。
  • auto.commit.interval.ms配置为一个很小的数值。auto.commit.interval.ms表示后台提交offset的时间间隔。

通过自动提交offset,并且将定时提交时间间隔设置的很小,就可以实现Consumer At most once语义。

Consumer Exactly once配置

  • isolation.level=read_committedisolation.level表示何种类型的message对Consumer可见。

也即事务机制,后面在理解。

总结

默认 Kafka 提供「at least once」语义的消息传递,允许用户通过在处理消息之前保存 Offset的方式提供 「at most once」 语义。如果我们可以自己实现消费幂等,理想情况下这个系统的消息传递就是严格的「exactly once」, 也就是保证不丢失、且只会被精确的处理一次,但是这样是很难做到的。 kafka 默认acks为1,retries=2147483647,即at least once,enable.auto.commit=false默认为true,即at mostonce。

从 Kafka 整体架构图我们可以得出有三次消息传递的过程:

1)Producer 端发送消息给 Kafka Broker 端。

2)Kafka Broker 将消息进行同步并持久化数据。

3)Consumer 端从Kafka Broker 将消息拉取并进行消费。

在以上这三步中每一步都可能会出现丢失数据的情况, 那么 Kafka 到底在什么情况下才能保证消息不丢失呢?

一句话概括:Kafka 只对「已提交」的消息做「最大限度的持久化保证不丢失

这句话里面有两个核心要素,(已提交的消息和有限度的持久化保证)

第一个核心要素是已提交的消息。什么是已提交的消息?当 Kafka 的若干个 Broker 成功地接收到一条消息并写入到日志文件后,它们会告诉生产者程序这条消息已成功提交。此时,这条消息在 Kafka 看来就正式变为“已提交”消息了。

那为什么是若干个 Broker 呢?这取决于你对“已提交”的定义。你可以选择只要有一个 Broker 成功保存该消息就算是已提交,也可以是令所有 Broker 都成功保存该消息才算是已提交。不论哪种情况,Kafka 只对已提交的消息做持久化保证这件事情是不变的。

第二个核心要素就是有限度的持久化保证,也就是说 Kafka 不可能保证在任何情况下都做到不丢失消息。举个极端点的例子,如果地球都不存在了,Kafka 还能保存任何消息吗?显然不能!倘若这种情况下你依然还想要 Kafka 不丢消息,那么只能在别的星球部署 Kafka Broker 服务器了。现在你应该能够稍微体会出这里的“有限度”的含义了吧,其实就是说 Kafka 不丢消息是有前提条件的。假如你的消息保存在 N 个 Kafka Broker 上,那么这个前提条件就是这 N 个 Broker 中至少有 1 个存活。只要这个条件成立,Kafka 就能保证你的这条消息永远不会丢失。

也就是说 Kafka 是能做到不丢失数据的,只不过这些消息必须是 「已提交」的消息,且还要满足一定的条件才可以。

了解了 Kafka 消息传递语义以及什么情况下可以保证不丢失数据,下面我们来详细剖析每个环节为什么会丢数据,以及如何最大限度的避免丢失数据。

消息丢失场景剖析

Producer 端丢失场景剖析

消息的发送流程:

1)首先我们要知道一点就是Producer 端是直接与 Broker 中的 Leader Partition 交互的,所以在 Producer 端初始化中就需要通过 Partitioner 分区器从 Kafka 集群中获取到相关 Topic 对应的 Leader Partition 的元数据。
2)待获取到 Leader Partition 的元数据后直接将消息发送过去。
3)Kafka Broker 对应的 Leader Partition 收到消息会先写入 Page Cache,定时刷盘进行持久化(顺序写入磁盘)。
4) Follower Partition 拉取 Leader Partition 的消息并保持同 Leader Partition 数据一致,待消息拉取完毕后需要给 Leader Partition 回复 ACK 确认消息。
5)待 Kafka Leader 与 Follower Partition 同步完数据并收到所有 ISR 中的 Replica 副本的 ACK 后,Leader Partition 会给 Producer 回复 ACK 确认消息。
复制代码

Producer 端为了提升发送效率,减少IO操作,发送数据的时候是将多个请求合并成一个个 RecordBatch,并将其封装转换成 Request 请求「异步」将数据发送出去(也可以按时间间隔方式,达到时间间隔自动发送),所以 Producer 端消息丢失更多是因为消息根本就没有发送到 Kafka Broker 端

导致 Producer 端消息没有发送成功有以下原因:

  • 网络原因: 由于网络抖动导致数据根本就没发送到 Broker 端。
  • 数据原因: 消息体太大超出 Broker 承受范围而导致 Broker 拒收消息。

通过配置来确认消息是否生产成功:即 acks = 0 ,1 ,-1 or all

丢失原因:

  • acks =0: 由于发送后就自认为发送成功,这时如果发生网络抖动, Producer 端并不会校验 ACK 自然也就丢了,且无法重试。

  • acks = 1: 消息发送 Leader Parition 接收成功就表示发送成功,这时只要 Leader Partition 不 Crash 掉,就可以保证 Leader Partition 不丢数据,但是如果 Leader Partition 异常 Crash 掉了, Follower Partition 还未同步完数据且没有 ACK,这时就会丢数据。

  • acks = -1 或者 all: 消息发送需要等待 ISR 中 Leader Partition 和 所有的 Follower Partition 都确认收到消息才算发送成功, 可靠性最高, 但也不能保证不丢数据,比如当 ISR 中只剩下 Leader Partition 了, 这样就变成 acks = 1 的情况了。

Broker 端丢失场景剖析

KafkaBroker 集群接收到数据后会将数据进行持久化存储到磁盘,为了提高吞吐量和性能,采用的是「异步批量刷盘的策略」,也就是说按照一定的消息量和间隔时间进行刷盘。首先会将数据存储到 「PageCache」 中,至于什么时候将 Cache 中的数据刷盘是由「操作系统」根据自己的策略决定或者调用 fsync 命令进行强制刷盘,如果此时 Broker 宕机 Crash 掉,且选举了一个落后 Leader Partition 很多的 Follower Partition 成为新的 Leader Partition,那么落后的消息数据就会丢失。

既然 Broker 端消息存储是通过异步批量刷盘的,那么这里就可能会丢数据的 !!!

  • 由于 Kafka 中并没有提供「同步刷盘」的方式,所以说从单个 Broker 来看还是很有可能丢失数据的。
  • kafka 通过「**多 Partition (分区)多 Replica(副本)机制」**已经可以最大限度的保证数据不丢失,如果数据已经写入 PageCache 中但是还没来得及刷写到磁盘,此时如果所在 Broker 突然宕机挂掉或者停电,极端情况还是会造成数据丢失。

Consumer 端丢失场景剖析

消费流程:

1)Consumer 拉取数据之前跟Producer 发送数据一样, 需要通过订阅关系获取到集群元数据,找到相关 Topic 对应的 Leader Partition 的元数据。

2)然后 Consumer 通过 Pull 模式主动的去 Kafka 集群中拉取消息。

3)在这个过程中,有个消费者组的概念,多个 Consumer 可以组成一个消费者组即 Consumer Group,每个消费者组都有一个Group-Id。同一个 Consumer Group 中的 Consumer 可以消费同一个 Topic 下不同分区的数据,但是不会出现多个 Consumer 去消费同一个分区的数据。

4)拉取到消息后进行业务逻辑处理,待处理完成后,会进行 ACK 确认,即提交 Offset 消费位移进度记录。

5)最后 Offset 会被保存到 Kafka Broker 集群中的  __consumer_offsets 这个 Topic 中,且每个 Consumer 保存自己的 Offset 进度。
复制代码

消费主要分为两个阶段:

  • 获取元数据并从 Kafka Broker 集群拉取数据。
  • 处理消息,并标记消息已经被消费,提交 Offset 记录。

既然 Consumer拉取后消息最终是要提交 Offset, 那么这里就可能会丢数据的 !!!

  • 可能使用的「自动提交 Offset 方式
  • 拉取消息后「先提交 Offset,后处理消息」,如果此时处理消息的时候异常宕机,由于 Offset 已经提交了, 待 Consumer 重启后,会从之前已提交的 Offset 下一个位置重新开始消费, 之前未处理完成的消息不会被再次处理,对于该 Consumer 来说消息就丢失了。
  • 拉取消息后「先处理消息,在进行提交 Offset」, 如果此时在提交之前发生异常宕机,由于没有提交成功 Offset, 待下次 Consumer 重启后还会从上次的 Offset 重新拉取消息,不会出现消息丢失的情况, 但是会出现重复消费的情况,这里只能业务自己保证幂等性。

实践处理及解决方案

Producer端解决方案

在剖析Producer端丢失场景的时候, 我们得出其是通过「异步」方式进行发送的,所以如果此时是使用「发后即焚」的方式发送,即调用 Producer.send(msg) 会立即返回,由于没有回调,可能因网络原因导致 Broker 并没有收到消息,此时就丢失了。

因此我们可以从以下几方面进行解决 Producer 端消息丢失问题:

更换调用方式:

不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。记住,一定要使用带有回调通知的 send 方法。这样一旦发现发送失败, 就可以做针对性处理。 解决 (1)网络抖动导致消息丢失,Producer 端可以进行重试。 (2)消息大小不合格,可以进行适当调整,符合 Broker 承受范围再发送。

最大限度的保证消息发送成功

ack确认机制

该参数代表了对 "已提交" 消息的定义。

需要将 request.required.acks设置为 -1/ all,-1/all 表示有多少个副本 Broker 全部收到消息,才认为是消息提交成功的标识。

分两种情况

1)数据发送到 Leader Partition, 且所有的 ISR 成员全部同步完数据, 此时,LeaderPartition异常 Crash 掉,那么会选举新的 Leader Partition,数据不会丢失

2)数据发送到Leader Partition,部分 ISR 成员同步完成,此时 Leader Partition 异常 Crash,剩下的 Follower Partition 都可能被选举成新的 Leader Partition,会给 Producer 端发送失败标识, 后续会重新发送数据,数据可能会重复。

但是在此操作中需要保证 replication.factor >= 2 min.insync.replicas> 1

重试次数 retries:

该参数表示 Producer 端发送消息的重试次数。

需要将 retries设置为大于0的数, 在 Kafka 2.4 版本中默认设置为Integer.MAX_VALUE。另外如果需要保证发送消息的顺序性,配置如下:

retries = Integer.MAX_VALUE max.in.flight.requests.per.connection = 1

这样 Producer 端就会一直进行重试直到 Broker 端返回 ACK 标识,同时只有一个连接向 Broker 发送数据保证了消息的顺序性。

重试时间 retry.backoff.ms:

该参数表示消息发送超时后两次重试之间的间隔时间,避免无效的频繁重试,默认值为100ms, 推荐设置为300ms

Broker 端解决方案

unclean.leader.election.enable

该参数表示有哪些 Follower 可以有资格被选举为 Leader , 如果一个 Follower 的数据落后 Leader 太多,那么一旦它被选举为新的 Leader, 数据就会丢失,因此我们要将其设置为false,防止此类情况发生。

replication.factor

该参数表示分区副本的个数。建议设置replication.factor >=3, 这样如果 Leader 副本异常 Crash 掉,Follower 副本会被选举为新的 Leader 副本继续提供服务。

min.insync.replicas

该参数表示消息至少要被写入成功到 ISR 多少个副本才算 "已提交", 建议设置min.insync.replicas > 1, 这样才可以提升消息持久性,保证数据不丢失。

另外我们还需要确保一下replication.factor > min.insync.replicas, 如果相等,只要有一个副本异常 Crash 掉,整个分区就无法正常工作了,因此推荐设置成:replication.factor =min.insync.replicas +1, 最大限度保证系统可用性。

Consumer 端解决方案

在剖析 Consumer 端丢失场景的时候,我们得出其拉取完消息后是需要提交 Offset 位移信息的,因此为了不丢数据,正确的做法是:拉取数据、 业务逻辑处理、 提交消费 Offset 位移信息。

我们还需要设置参数enable.auto.commit = false, 采用手动提交位移的方式。

另外对于消费消息重复的情况,业务自己保证幂等性, 保证只成功消费一次即可

总结

针对上面分析,我们做一下kafka无丢失配置。

1.不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。记住,一定要使用带有回调通知的 send 方法。

2.设置 acks = all。acks 是 Producer 的一个参数,代表了你对“已提交”消息的定义。如果设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”。这是最高等级的“已提交”定义。

3.设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。

4.设置 unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。

5.设置 replication.factor >= 3。这也是 Broker 端的参数。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。

6.设置 min.insync.replicas > 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。

7.确保 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1。

8.确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式。

解答

  1. min.insync.replicas只有在ack=-1时才生效

2.关于 第二条的ack=all与第六条的min.insync.replicas的理解,acks=all表示消息要写入所有ISR副本,但没要求ISR副本有多少个。但是min.insync.replicas要求的是最少写入的副本数,也就是写入消息的下限。 举例:min.insync.replicas=2, replication.factor =3,这是只要写入成功2个就算写入成功了。如果挂掉一个,此时min.insync.replicas = replication.factor,此时说明消息要全部写入副本才算成功,如果在挂掉一个,就不能工作了,因为,副本数就一个,producer的消息必须阻塞等broker的isr数量达到min.insync.replicas才提交成功,显然成功不了,最终producer请求会超时。

猜你喜欢

转载自juejin.im/post/7102243362471673892