副本还有一个重要的机制,就是数据同步过程,它需要解决
- 怎么传播消息
- 在向消息发送端返回 ack 之前需要保证多少个 Replica 已经接收到这个消息
一、 副本的结构
深红色部分表示 test_replica 分区的 leader 副本,另外两个节点上浅色部分表示 follower 副本
二、 数据的处理过程
Producer 在发布消息到某个 Partition 时:
- 先通过 ZooKeeper 找到该 Partition 的Leader
get /brokers/topics/partitions/2/state
,然后无论该 Topic 的 Replication Factor 为多 少(也即该Partition 有多少个 Replica),Producer 只将该消息发送到该 Partition 的 Leader。 - Leader 会将该消息写入其本地 Log。每个 Follower 都从 Leader pull 数据。这种方式上,Follower 存储的数据顺序与 Leader 保持一致。
- Follower 在收到该消息并写入其 Log 后,向 Leader 发送 ACK。
- 一旦 Leader 收到了 ISR 中的所有 Replica 的 ACK,该消息就被认为已经commit了,Leader 将增加 HW(HighWatermark) 并且向 Producer 发送 ACK。
三、 LEO和HW
LEO:即日志末端位移(log end offset),记录了该副本底层日志(log)中下一条消息的位移值。注意是下 一条消息!也就是说,如果LEO=10,那么表示该副本保存了10条消息,位移值范围是[0, 9]。另外, leader LEO和follower LEO的更新是有区别的。
HW:即上面提到的水位值(Hight Water)。对于同一个副本对象而言,其HW值不会大于LEO值。小于等于HW值的所有消息都被认为是“已备份”的(replicated)。同理,leader副本和follower副本的 HW更新是有区别的。
通过下面这幅图来表达LEO、HW的含义,随着 follower 副本不断和 leader 副本进行数据同步,follower 副本的 LEO 会主键后移并且追赶到 leader 副本,这个追赶上的判断标准是当前副本的 LEO 是否大于或者等于 leader 副本的 HW,这个追赶上也会使得被踢出的 follower 副本重新加入到 ISR 集合中。
假如说下图中的最右侧的 follower 副本被踢出 ISR 集合,也会导致这个分区的 HW 发生变化,变成了3。
四、 副本状态分析
4.1 初始状态下
leader 和 follower 的 HW 和 LEO 都是0,leader 副本会保存 remote LEO,表示所有 follower LEO,也会被初始化为0,这个时候,producer没有发送消息。follower会不断地个 leade r发送 FETCH 请求,但是因为没有数据,这个请求会被 leader 寄存,当在指定的时间之后会强制完成请求,这个时间配置是(replica.fetch.wait.max.ms),如果在指定时间内 producer 有消息发送过来,那么 kafka 会唤醒 fetch 请求,让 leader 继续处理
数据的同步处理会分两种情况,这两种情况下处理方式是不一样的
- 第一种是 leader 处理完 producer 请求之后,follower 发送一个 fetch 请求过来
- 第二种是 follower 阻塞在 leader 指定时间之内,leader 副本收到 producer 的请求。
4.2 第一种情况
生产者发送一条消息
leader 处理完 producer 请求之后,follower 发送一个fetch请求过来 。状态图如下:
leader 副本收到请求以后,会做几件事情:
- 把消息追加到log文件,同时更新leader副本的LEO
- 尝试更新 leader HW 值。这个时候由于follower 副本还没有发送 fetch 请求,那么 leader 的 remote LEO 仍然是0。leader 会比较自己的 LEO 以及 remote LEO的值发现最小值是0,与 HW 的值相同,所 以不会更新 HW
follower fetch 消息
follower 发送 fetch 请求,leader 副本的处理逻辑是:
- 读取 log 数据、更新 remote LEO=0 ( follower 还没有写入这条消息,这个值是根据 follower 的 fetch 请求中的 offset 来确定的)
- 尝试更新 HW,因为这个时候 LEO 和 remoteLEO 还是不一致,所以仍然是 HW=0
- 把消息内容和当前分区的 HW 值发送给 follower 副本
follower副本收到response以后:
- 将消息写入到本地 log,同时更新 follower 的 LEO
- 更新 follower HW,本地的 LEO 和 leader 返回的 HW 进行比较取小的值,所以仍然是0
第一次交互结束以后,HW 仍然还是0,这个值会在下一次 follower 发起 fetch 请求时被更新
follower 发第二次 fetch 请求,leader 收到请求以后:
- 读取 log 数据
- 更新 remote LEO=1, 因为这次 fetch 携带的 offset 是1.
- 更新当前分区的 HW,这个时候 leader LEO 和 remote LEO都是1,所以 HW 的值也更新为1
- 把数据和当前分区的 HW 值返回给 follower 副本,这个时候如果没有数据,则返回为空
follower 副本收到 response 以后:
- 如果有数据则写本地日志,并且更新 LEO
- 更新 follower 的 HW 值
到目前为止,数据的同步就完成了,意味着消费端能够消费 offset=1 这条消息。
4.3 第二种情况
前面说过,由于 leader 副本暂时没有数据过来,所以 follower 的 fetch 会被阻塞,直到等待超时或者 leader 接收到新的数据。
当 leader 收到请求以后会唤醒处于阻塞的 fetch 请求。处理过程基本上和前面说的一致
- leader 将消息写入本地日志,更新 Leader 的 LEO
- 唤醒 follower 的 fetch 请求
- 更新 HW
kafka 使用 HW 和 LEO的方式来实现副本数据的同步,本身是一个好的设计,但是在这个地方会存在一个数据丢失的问题,当然这个丢失只出现在特定的背景下。我们回想一下,HW 的值是在新的一轮FETCH 中才会被更新。我们分析下这个过程为什么会出现数据丢失。
五、 数据丢失的问题
前提:min.insync.replicas=1
//设定 ISR
中 的最小副本数是多少,默认值为1
(在server.properties
中配 置), 并且acks
参数设置为-1
(表示需要所有副本确认)时,此参数才生效。
表达的含义是,至少需要多少个副本同步才能表示消息是提交的, 所以,当 min.insync.replicas=1 的时候,一旦消息被写入 leader 端 log 即被认为是“已提交”,而延迟一轮 FETCH RPC 更新 HW 值的设计使得 follower HW 值是异步延迟更新的,倘若在这个过程中 leader 发生变更,那么成为新 leader 的 follower的 HW 值就有可能是过期的,使得 clients 端认为是成功提交的消息被删除。
producer 的 ack:
acks 配置表示 producer 发送消息到 broker上 以后的确认值。有三个可选项
- 0:表示 producer 不需要等待 broke r的消息确认。这个选项时延最小但同时风险最大(因为当server宕 机时,数据将会丢失)。
- 1:表示 producer 只需要获得 kafka 集群中的 leader 节点确认即可,这个选择时延较小同时确保了 leader 节点确认接收成功。
- all(-1):需要 ISR 中所有的 Replica 给予接收确认,速度最慢,安全性最高,但是由于 ISR 可能会缩小到仅包含一个 Replica,所以设置参数为 all 并不能一定避免数据丢失,
六、 数据丢失的解决方案
在 kafka0.11.0.0 版本之后,引入了一个 leader epoch 来解决这个问题,所谓的 leader epoch 实际上是 一对值(epoch,offset),epoch代表leader的版本号,从0开始递增,当 leader 发生过变更,epoch 就+1,而 offset 则是对应这个 epoch 版本的 leader 写入第一条消息的 offset。
比如 (0,0), (1,50) ,表示第一个leader从offset=0开始写消息,一共写了50条。第二个leader版本号是1,从 offset=50开始写,这个信息会持久化在对应的分区的本地磁盘上,文件名是 /tmp/kafka-log/topic/leader-epoch-checkpoint
。
leader broker 中会保存这样一个缓存,并且定期写入到 checkpoint 文件中
当 leader 写 log 时它会尝试更新整个缓存: 如果这个 leader 首次写消息,则会在缓存中增加一个条目;否则就不做更新。而每次副本重新成为 leader 时会查询这部分缓存,获取出对应 leader 版本的 offset。
我们基于同样的情况来分析,follower 宕机并且恢复之后,有两种情况,如果这个时候 leader 副本没有挂,也就是意味着没有发生 leader 选举,那么 follower 恢复之后并不会去截断自己的日志,而是先发送 一个 OffsetsForLeaderEpochRequest
请求给到 leader 副本,leader 副本收到请求之后返回当前的 LEO。
如果 follower 副本的 leaderEpoch 和 leader 副本的 epoch 相同, leader 的 LEO 只可能大于或者等于 follower副本的 LEO 值,所以这个时候不会发生截断。
如果 follower 副本和 leader 副本的 epoch 值不同,那么 leader 副本会查找 follower 副本传过来的 epoch+1 在本地文件中存储的 StartOffset 返回给 follower 副本,也就是新 leader 副本的 LEO。这样也避免了数据丢失的问题。
如果 leader 副本宕机了重新选举新的 leader,那么原本的 follower 副本就会变成 leader,意味着 epoch 从0变成1,使得原本 followe r副本中 LEO 的值的到了保留。
七、 Leader 副本的选举过程
- KafkaController 会监听 ZooKeeper 的 /brokers/ids 节点路径,一旦发现有 broker 挂了,执行下面的逻辑。这里暂时先不考虑 KafkaController 所在 broker 挂了的情况,KafkaController 挂了,各个 broker 会重新 leader 选举出新的 KafkaController
- leader 副本在该 broker 上的分区就要重新进行 leader 选举,目前的选举策略是:
① 优先从 isr 列表中选出第一个作为 leader 副本,这个叫优先副本,理想情况下有限副本就是该分区的 leader 副本
② 如果 isr 列表为空,则查看该 topic 的unclean.leader.election.enable
配置。 unclean.leader.election.enable
:为 true 则代表允许选用非 isr 列表的副本作为 leader,那么此时就意味着数据可能丢失,为 false的话,则表示不允许,直接抛出 NoReplicaOnlineException 异常,造成 leader 副本选举失败。
able配置。
unclean.leader.election.enable`:为 true 则代表允许选用非 isr 列表的副本作为 leader,那么此时就意味着数据可能丢失,为 false的话,则表示不允许,直接抛出 NoReplicaOnlineException 异常,造成 leader 副本选举失败。
③ 如果上述配置为 true,则从其他副本中选出一个作为 leader 副本,并且 isr 列表只包含该 leader 副本。一旦选举成功,则将选举后的 leader 和 isr 和其他副本信息写入到该分区的对应的 zk 路径上。