一、前言
在上一篇文章里我们探讨了kafka的工作流程、存储机制、分区策略,理清楚了生产者生产的数据是怎么存储的以及怎么根据offset去查询数据这类问题:Kafka(一):工作流程、存储机制、分区策略
那么kafka是怎么保证数据可靠性的呢?怎么保证exactly once的呢?在分布式的环境下又是如何进行故障处理的呢?本篇文章我们就来分析这个问题。
二、数据可靠性
首先我们要知道kafka发送数据的机制:Kafka为了保证producer发送的数据,能可靠的发送到指定的topic,因此topic的每个partition收到producer发送的数据后,都需要向producer发送ack信息(acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
2.1、副本数据同步策略
我们知道kafka的partition是主从结构的,因此当一个topic对应多个partiton时,为了保证leader挂掉之后,能在follower中选举出新的leader且不丢失数据,就需要确保follower与leader同步完成之后,leader再发送ack。
大致图示如下:
这时会产生一个问题也就是副本数据同步策略:多少个follower同步完成之后才发送ack呢?
有两个方案对比如下:
- 半数以上完成同步,就发送ack(优点:延迟低;缺点:选举新的leader时,容忍n台节点的故障,需要2n+1个副本)
- 全部完成同步,才发送ack(优点:选举新的leader时,容忍n台节点的故障,需要n+1个副本;缺点:延迟高)
我们知道kafka采用零拷贝技术优化数据传输,因此网络延迟对kafka的影响较小。但是由于kafka一般都是处理海量数据,在同样为了容忍n台节点故障的前提下,第一种方案需要2n+1个副本,而第二种方案只需要n+1个副本,而Kafka的每个分区都有大量的数据,第一种方案会造成大量数据的冗余,因此kafka采用了第二种方案:全部完成同步,才发送ack。
2.2、ISR
kafka选用第二种发案来同步副本数据后,可能会出现一个问题:比如leader收到数据,然后开始向所有的follower同步数据,但是有那么一个或多个follower因为挂掉了之类的原因出现了故障,不能和leader进行同步,那leader要一直等下去吗?当然不可以,为了解决这个问题,引入了ISR的概念。
ISR是一个动态的in-sync replica set数据集,代表了和leader保持同步的follower集合。
当ISR中的follower完成数据的同步之后,leader就会给follower发送ack。如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阈值由replica.lag.time.max.ms参数设定。Leader发生故障之后,就会从ISR中选举新的leader。
相当于leader只要和ISR里的follower进行数据同步就可以了,出现故障的会被ISR移出去,恢复之后并经过处理还会加入进来。那移出去的follower要经过怎样的处理才能重新加入ISR呢?可以先思考一下,后面故障处理部分会进行分析。
2.3、ack应答机制
由于数据的重要程度是不一样的,有些可以少量允许丢失,希望快一点处理;有些不允许,希望稳妥一点处理,所以没必要所有的数据处理的时候都等ISR中的follower全部接收成功。因此kafka处理数据时为了更加灵活,给用户提供了三种可靠性级别,用户可以通过调节acks参数来选择合适的可靠性和延迟。
acks的参数分别可以配置为:0,1,-1。
它们的作用分别是:
- 配置为0:producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据;
- 配置为1:producer等待broker的ack,partition的leader写入磁盘成功后返回ack,但是如果在follower同步成功之前leader故障,那么将会丢失数据;
- 配置为-1:producer等待broker的ack,partition的leader和follower全部写入磁盘成功后才返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,此时会选举新的leader,但是新的leader已经有了数据,但是由于没有之前的ack,producer会再次发送数据,那么就会造成数据重复。
三、Exactly Once
3.1、幂等性机制
Kafka在0.11版本之后,引入了幂等性机制(idempotent),指的是当发送同一条消息时,数据在 Server 端只会被持久化一次,数据不丟不重,但是这里的幂等性是有条件的:
- 只能保证 Producer 在单个会话内不丟不重,如果 Producer 出现意外挂掉再重启是 无法保证的。因为幂等性情况下,是无法获取之前的状态信息,因此是无法做到跨会话级别的不丢不重。
- 幂等性不能跨多个 Topic-Partition,只能保证单个 Partition 内的幂等性,当涉及多个Topic-Partition 时,这中间的状态并没有同步。
3.2、实现exactly once
一般对于重要的数据,我们需要实现数据的精确一致性,对于kafka也就是保证每条消息被发送且仅被发送一次,不能重复,这就是exactly once。我们通过上篇文章已经知道当acks = -1时,kafka可以实现at least once语义,这时候的数据会被至少发送一次。再配合前面介绍的幂等性机制保证数据不重复,那合在一起就可以实现producer到broker的exactly once语义。
它们的关系可以写成一个公式:idempotent + at least once = exactly once
那怎么配置kafka以实现exactly once呢?
很简单,只需将enable.idempotence属性设置为true,kafka会自动将acks属性设为-1。
四、故障处理
在分析故障处理之前,我们需要先知道几个概念:
- LEO:全称Log End Offset,代表每个副本的最后一条消息的offset
- HW:全称High Watermark,代表一个分区中所有副本最小的offset,用来判定副本的备份进度,HW以外的消息不被消费者可见。leader持有的HW即为分区的HW,同时leader所在broker还保存了所有follower副本的LEO。
如下图,是一个topic下的某一个partition里的副本的LEO和HW关系:
注意:只有HW之前的数据才对Consumer可见,也就是只有同一个分区下所有的副本都备份完成,才会让Consumer消费。
它们之间的关系:leader的LEO >= follower的LEO >= leader保存的follower的Leo >= leader的HW >= follower的HW
由于partition是实际的存储数据的结构,因此kafka的故障主要分为两类:follower故障和leader故障。
4.1、follower故障
这部分可以回答前面2.2节最后提到的问题:移出去的follower要经过怎样的处理才能重新加入ISR呢?
通过前面我们已经知道follower发生故障后会被临时踢出ISR,其实待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO大于等于该分区的HW(leader的HW),即follower追上leader之后(追上不代表相等),就可以重新加入ISR了。
4.2、leader故障
leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。
注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
那怎么解决故障恢复后,数据丢失和重复的问题呢?
kafka在0.11版本引入了Lead Epoch来解决HW进行数据恢复时可能存在的数据丢失和重复的问题。
4.3、引入Lead Epoch
leader epoch实际是一对值(epoch, offset),epoch表示leader版本号,offset为对应版本leader的LEO,它在Leader Broker上单独开辟了一组缓存,来记录(epoch, offset)这组键值对数据,这个键值对会被定期写入一个检查点文件。Leader每发生一次变更epoch的值就会加1,offset就代表该epoch版本的Leader写入的第一条日志的位移。当Leader首次写底层日志时,会在缓存中增加一个条目,否则不做更新。这样就解决了之前版本使用HW进行数据恢复时可能存在的数据丢失和重复的问题
这就有点像HashMap源码里面的modCount,用来记录整体的更新变化。