深入分析Kafka的可靠性保证

2,923 阅读14分钟

1 Kafka可靠性分析

1.1 副本剖析

Kafka 从 0.8 版本开始为分区引入了多副本机制,通过增加副本数量来提升数据容灾能力。 同时, Kafka 通过多副本机制实现故障自动转移。

  • 副本是相对于分区而言的,即副本是特定分区的副本
  • 一个分区中包含一个或多个副本,其中一个为leader副本,其余为follower副本,各个副本位于不同的broker节点中。只有leader副本对外提供服务, follower副本只负责数据同步
  • LEO标识每个分区中最后一条消息的下一个位置,分区的每个副本都有自己的LEO, ISR中最小的LEO即为HW,俗称高水位,消费者只能拉取到HW之前的消息

Kafka根据副本同步的情况,分成了3个集合

  • AR(Assigned Replicas):包括ISR和OSR
  • ISR(In-sync Replicas):和leader副本保持同步的副本集合,可以被认为是可靠的数据
  • OSR(Out-Sync Replicas):和Leader副本同步失效的副本集合

从生产者发出的一条消息首先会被写入分区的leader副本,还需要等待ISR集合中的所有follower副本都同步完之后才能被认为已经提交,之后才会更新分区的HW,进而消费者可以消费到这条消息。

1.1.1 失效副本

在ISR集合之外,也就是处于同步失效功能失效的副本统称为失效副本,失效副本对应的分区也就称为同步失效分区 ,即under-replicated分区。

查看under-replicated分区

正常情况分区的所有副本都处于ISR集合中,查询返回返回空

bin/kafka topics.sh --zookeeper localhost: 2181/kafka -describe --topic topic partitions under-replicated- partitions

同步失效判断

通过broker端参数判断,对broker中的所有主题都生效

  • replica.lag.time.max.ms:默认值为10000,当follower滞后leader的时间超过此参数,需要将此 follower剔除出 ISR 集合
  • replica.lag.max.messages:默认值4000,当follower滞后leader的消息数超过此参数

注意replica.lag.max.messages若设置得太大,没有太多意义,若设置得太小则会让 follower反复处于同步、未同步、同步的死循环中,进而又造成ISR集合的频繁伸缩。所以从0.9.x版本开始, Kafka移除了这个参数。

image.png

副本失效的场景

  • follower 副本进程卡住,在一段时间内根本没有向leader副本发起同步请求,比如频繁 的 Full GC
  • follower 副本进程同步过慢,在一段时间内都无法追赶上leader副本,比如 1/0 开销过 大。
  • 通过工具增加了副本,新增加的副本在赶上leader副本之前也都是处于失效状态的

1.1.2 ISR的伸缩

Kafka在启动的时候会开启两个与ISR 相关的定时任务,名称分别为isr-expirationisr-change-propagationisr-expiration任务会周期性地检测每个分区是否需要缩减其ISR集合。这个周期是replica.lag.time.max.ms参数值的一半, 默认值为 5000ms。当检测到 ISR集合中有失效副本时,就会收缩 ISR集合。 当检测到分区的 ISR 集合发生变化时,还需要检查以下两个条件:

  • 上一次 ISR 集合发生变化距离现在己经超过5s
  • 上一次写入ZooKeeper的时间距离现在已经超过60s

满足以上两个条件之一才可以将ISR集合的变化写入。

1.1.3 LEO与HW

在Kafka中,分区有多个副本。整个消息追加的过程可以概括如下:

  • 生产者发送消息至leader副本中
  • 消息被迫加到leader的本地日志,并且会更新日志的偏移量
  • follower向leader请求同步数据,在拉取的请求中会带有自身的LEO信息
  • leader收到follower的FetchRequest请求,其中带有follower的LEO,选取其中的最小值作为新的HW
  • leader所在的服务器将拉取结果,连同HW一起返回给follower
  • follower接收leader返回的FetchResponse,将消息追加到本地日志中,更新日志偏移量LEO,并且更新自己的HW为min(LE0,HW)

image.png

1.1.4 Leader Epoch 的介入

在 0.11.0.0版本之前,Kafka使用的是基于HW的同步机制,但这样有可能出现数据丢失或leader副本和follower副本数据不一致的问题。

为了解决上述两种问题,Kafka从0.11.0.0 开始引入了leader epoch的概念,在需要截断数据的时候使用leader epoch作为参考依据而不是原本的HW。
leader epoch 代表 leader 的纪元信息( epoch),初始值为0。每当leader变更一次, leader epoch 的值就会加1。
与此同时,每个副本中还会增设一个矢量<LeaderEpoch=> StartOffset>,其中 StartOffset表示当前 LeaderEpoch下写入的第一条消息的偏移量。每个副本的Log下都有一个 leader-epoch-checkpoint文件,在发生leader epoch变更时,会将对应的矢量对追加到这个文件中。

1.1.4.1 数据丢失

Replica B 是当前的leader 副本(用L标记),ReplicaA是follower副本:

  • T1: B中有2条消息ml和m2, A从B中同步了这两条消息,此时A和B的LEO都为2, 同时HW都为l;

  • T2: A再向B中发送请求以拉取消息,FetchRequest请求中带上了A的LEO信息, B在收到请求之后更新了自己的HW为2, 延时一段时间之后(T3) 返回 FetchResponse,并在其中包含了 HW 信息;

  • T4: A根据FetchResponse中的HW信息更新自己的HW为2。

image.png

发生数据丢失

假如T3T4之间,A岩机了,那么在A重启之后会根据之前HW位置(会存入本地的复制点文件replication-offset-checkpoint)进行日志截断,这样便会将m2这条消息删除。
此时若B再宕机,那么A就会被选举为新的leader。B恢复之后会成为follower,由于follower的HW不能比leader的高,所以还会做一次日志截断,因此
将HW调整为1,这样m2这条消息就丢失了。

解决

同样T3T4之间A发生重启时:

  • A不是先忙着截断日志而是先发送 OffsetsForLeaderEpochRequest请求 (请求中包含A当前的LeaderEpoch值)到B;
  • B会查找LeaderEpoch对应的LEO: OffsetsForLeaderEpochRequest的请求看作用来查找follower副本当前LeaderEpoch的LEO;
  • A在收到之后发现和目前的LEO相同,也就不需要截断日志了。

1.1.4.2 数据不一致

当前leader副本为A,follower副本为B,A中有2条消息m1和m2,并且HW和LEO都为2,B中有1条消息m1,井且HW和LEO都为1。

image.png

  • T1: 假设A和B同时“挂掉”,然后B第一个恢复过来并成为leader

image.png

  • T2: 之后B写入消息m3,并将LEO和HW更新至2
  • T3: A也恢复过来了,需要根据HW截断日志及发送FetchRequest至B,不过此时A的HW好也为2, 那么就不做任何调整了

image.png

发生数据不一致

如此一来A中保留了m2而B中没有,B中新增了m3而A也同步不到,这样A和B就出现了数据不一致的情形。

解决

T3时刻,A恢复过来成为follower并向B发送 OffsetsForLeaderEpochRequest请求,B根据A的LeaderEpoch查询到对应的offset为1井返回给A, A就截断日志并删除了消息m2。

1.2 可靠性参数

1.2.1 Broker端参数

replication.factor参数

replication.factor >= 3,即副本数至少是3个。
越多的副本数越能够保证数据的可靠性。置副本数为 3 即可满足绝大多数场景对可靠性的要求。国内部分银行在使用 Kafka时就会设置副本数为5。

min.insync.replicas参数

2 <= min.insync.replicas <= replication.factor

min.insync.replicas指定了Broker所要求的ISR最小长度,默认值为1。也即ISR可以只包含Leader。但此时如果Leader宕机,则该Partition不可用,可用性得不到保证。

unclean.leader.election.enable参数

unclean.leader.election.enable = false

leader的选举时,如果ISR为空,Broker端提供了参数unclean.leader.election控制是否允许从非ISR集合副本中选举,默认为false。

  • true,表示可以参与选举,由于非ISR集合副本的消息较为滞后,就有可能发生数据丢失和数据不一致的情况,Kafka的可靠性就会降低;
  • false,此时如果ISR列表为空,会一直等待旧leader恢复,Kafka的可用性就会降低

log.flush.interval.messages 和 log.flush.interval.ms 参数

broker端还有两个参数log.flush.interval.messageslog.flush.interval.ms控制同步刷盘策略。
同步刷盘是增强一个组件可靠性的有效方式,Kafka 也不例外,但大多数情景下,一个组件(尤其是大数据量的组件)的可靠性不应该由同步刷盘这种极其损耗性能的操作来保障,而应该采用多副本的机制来保障。

1.2.2 Producer参数

ack参数

acks = all (-1)

  • acks = all (或者-1)的情形,它要求ISR中所有的副本都收到相关的消息之后才能够告知生产者己经成功提交。即使此时leader副本宕机,消息也不会丢失
  • acks = 1 的配置,生产者将消息发送到leader副本。此时leader 宕机,消息丢失
  • acks = 0 不需要任何服务器的确认

retries参数

retries > 0
有些发送异常属于可重试异常,比如NetworkException,客户端内部本身提供了重试机制来应对这种类型的异常。retries参数设置为0,即不进行重试,对于高可靠性要求的场景,需要设置retries > 0

retry.backoff.ms参数,设定两次重试之间的时间间隔,以此避免无效的频繁重试。 在配置 retriesretry.backoff.ms 之前,最好先估算一下可能的异常恢复时间。

注意:如果retries > 0,则可能引起一些负面的影响。由于默认的 max.in.flight. requests.per.connection = 5,这样可能会影响消息的顺序性。

1.2.3 Consumer端参数

enable.auto.commit参数 enable.auto.commit = false
消费端参数enable.auto.commit默认true,会有重复消费和消息丢失问题。
对于高可靠性要求的应用来说需要将参数设置为 false来执行手动位移提交, 且遵循一个原则: 如果消息没有被成功消费,那么就不能提交所对应的消费位移。

  • 对于高可靠要求的应用来说,宁愿重复消费也不应该因为消费异常而导致消息丢失。
  • 对于一直不能够成功被消费的消息,可以暂存到死信队列中,以便后续的故障排除

2 Kafka Partition Leader选举机制

2.1 日志同步机制

在分布式系统中,日志同步机制既要保证数据的一致性,也要保证数据的顺序性

最简单高效的方式是从集群中选出一个leader来负责处理数据写入的顺序性。follower只需按照leader中的写入顺序来 进行同步即可。

在Kafka中,Producer只将该消息发送到Partition的Leader。Leader会将该消息写入其本地Log。每个Follower都从Leader pull数据。这种方式上,Follower存储的数据顺序与Leader保持一致。Follower在收到该消息并写入其Log后,向Leader发送ACK。一旦Leader收到了ISR中的所有Replica的ACK,该消息就被认为已经commit了,Leader将增加HW并且向Producer发送ACK。

当leader岩机时,要从follower中选举出一个新的leader。日志同步机制的一个基本原则就是: 如果告知客户端已经成功提交了某条消息,那么即使leader岩机,也要保证新选举出来的 leader中能够包含这条消息。

这里就有一个需要权衡(tradeoff) 的地方,如果leader在消息被提交前需要等待更多的follower确认,那么就可以有更多的follower替代它,不过这也会造成性能的下降。

2.1.1 Majority Vote模型

一种常用的Leader Election的方式是Majority Vote,即少数服从多数。如果有2f+1个副本,在commit之前至少要保证有f+1个副本完成日志同步。为了保证能正确选举出新的leader,失败的副本不能超过f个。因为在剩下f+1个副本中,至少有一个副本能够包含己提交的全部消息,这个副本的日志拥有最全的消息,因此会有资格被选举为新的leader来对外提供服务。

优势

系统的延迟取决于最快的几个节点。

劣势

需要较高的冗余度:为了保证leader选举的正常进行,能容忍的失败副本数比较少。要容忍n个失败,需要2n+1个副本。
而大量的副本又会在大数据量下导致性能的急剧下降。 因此这种Quorum模型常被用作共享集群配置(比如ZooKeeper),而很少用于主流的数据存储中。

相关的一致性协议

比如Zab、Raft和Viewstamped Replication等。

2.2.2 ISR 模型

Kafka使用的更像是微软的PacificA算法。
在Kafka中动态维护着一个ISR集合,处于ISR集合内的节点保持与leader相同的高水位HW,只有位列其中的副本(unclean.leader.electtion.enable = false)才有资格被选为新leader。
写入消息时只有等到所有ISR中的副本都确认收到后才能被认为已经提交。ISR中的副本节点都有资格成为leader,选举过程简单、开销低,这也是Kafka选用此模型的重要因素。
Kafka中包含大量的分区,leader副本的均衡保障了整体负载的均衡,所以这一因素也极大地影响Kafka的性能指标。

优势

采用ISR模型,f+1个副本数的配置下,能够容忍最多f个节点失败。 所需要的副本总数变少,复制带来的集群开销也就更低。

劣势

它不能像少数服从多数,可以绕开最慢副本的确认信息,降低提交的延迟。

2.2 Kafka Leader的选举

如果某个分区的Leader不可用,Kafka就会从ISR集合中选择一个副本作为新的Leader。

2.2.1 所有follower参与注册zookeeper节点的选举方法

简单方案

由于Kafka集群依赖zookeeper集群,最简单最直观的方案是,所有Follower都在ZooKeeper上设置一个Watch,一旦Leader宕机,其对应的ephemeral znode会自动删除,此时所有Follower都尝试创建该节点,而创建成功者(ZooKeeper保证只有一个能创建成功)即是新的Leader,其它Replica即为Follower。

问题

  • 脑裂问题(split-brain) 这是由Zookeeper的特性引起的,虽然Zookeeper能保证所有Watch按顺序触发,但并不能保证同一时刻所有Replica“看”到的状态是一样的,这就可能造成不同Replica的响应不一致
  • 羊群效应 (herd effect) 如果宕机的那个Broker上的Partition比较多,会造成多个Watch被触发,造成集群内大量的调整
  • Zookeeper负载过重 每个Replica都要为此在Zookeeper上注册一个Watch,当集群规模增加到几千个Partition时Zookeeper负载会过重。

2.2.2 Kafka Partition选主机制

2.2.2.1 Controller Leader 选举

Kafka集群中多个broker,有一个的Controller会被选举为Controller Leader,负责管理整个集群中分区和副本的状态和 Leader选举等工作。 Controller的选举和信息同步依赖于Zookeeper。

选举流程

kafka每启动一个节点就会Zookeeper中注册一个节点信息,每一个broker节点都有对应的Controller,他们会争先抢占Controller的注册,谁先注册谁会被选举为Controller Leader

一旦有Broker宕机,其在Zookeeper对应的znode会自动被删除,Zookeeper会fire Controller注册的watch,Controller读取最新的幸存的Broker作为Controller Leader

选举出来的Controller Leader会监听 brokers节点变化,决定Leader 的选举,将节点信息上传到Zookeeper,其他Contorller就会从Zookeeper同步相关信息

2.2.2.2 Partition Leader 选举

Controller Leader确定后,所有Partition的Leader选举都由Controller Leader决定。 一旦有Broker宕机(包括但不限于断电,网络不可用,GC导致的Stop The World,进程crash等), Controller Leader就会监听到节点变化, 然后获取到ISR,选举新的Leader。

Partition Leader选举步骤如下

步骤1:Controller在Zookeeper注册了Watch,一旦有Broker宕机,宕机Broker在Zookeeper对应的znode会自动被删除,Zookeeper会fire Controller注册的watch;

步骤2:Controller决定set_p,该集合包含了宕机的Broker上的所有Partition;

步骤3: 对set_p中的每一个Partition:

步骤3.1 : 从/brokers/topics/[topic]/partitions/[partition]/state读取该Partition当前的ISR;

步骤3.2 : 决定该Partition的新Leader;

  • 调用配置的分区选择算法选择分区的leader(一般是PreferredReplica分区算法)

步骤3.3 :将新的Leader,ISR和新的leader_epoch及controller_epoch写入/brokers/topics/[topic]/partitions/[partition]/state

步骤4 : 直接通过RPC向set_p相关的Broker发送LeaderAndISRRequest命令。Controller可以在一个RPC操作中发送多个命令从而提高效率。

image.png

2.2.2.3 ISR中的副本都失效

在ISR中至少有一个follower时,Kafka可以确保已经commit的数据不丢失,但如果某个Partition的所有副本都宕机了。这种情况下有两种方案:

  • 等待ISR中的任一个Replica“活”过来,并且选它作为Leader

  • 选择第一个“活”过来的Replica(不一定是ISR中的)作为Leader

这就需要在可用性和一致性当中作出一个简单的折衷。由参数unclean.leader.election.enable控制。 unclean.leader.election.enable = false表示选择第一种方案,可以保证数据不丢失,但牺牲了可用性。