探究Kafka原理-6.CAP理论实践(上):https://developer.aliyun.com/article/1413725
解决“消费者所见不一致” (消费者只允许看到 HW 以下的 message)
解决“分区副本数据最终不一致” (follower 数据按HW截断)
high watermark
代表数据在多副本备份的进度
hw就代表offset < hw的数据已经在所有isr副本间全部备份完毕。
所以,offset < hw的数据,让消费者可见,是相对安全的
HW 方案的天生缺陷
如前所述,看似 HW 解决了“分区数据最终不一致”的问题,以及“消费者所见不一致”的问题,但其实,这里面存在一个巨大的隐患,导致:
“分区数据最终不一致”的问题依然存在
producer 设置 acks=all 后,依然有可能丢失数据的问题
产生如上结果的根源是:HW 高水位线的更新,与数据同步的进度,存在迟滞!
Step 1:leader 和 follower 副本处于初始化值,follower 副本发送 fetch 请求,由于 leader 副本没有数据,因此不会进行同步操作;
Step 2:生产者发送了消息 m1 到分区 leader 副本,写入该条消息后 leader 更新 LEO = 1;
Step 3:follower 发送 fetch 请求,携带当前最新的 offset = 0,leader 处理 fetch 请求时,更新 remote LEO = 0,对比 LEO 值最小为 0,所以 HW = 0,leader 副本响应消息数据及 leader HW = 0 给follower,follower 写入消息后,更新 LEO 值,同时对比 leader HW 值,取最小的作为新的 HW 值,此时 follower HW = 0,这也意味着,follower HW 是不会超过 leader HW 值的。
Step 4:follower 发送第二轮 fetch 请求,携带当前最新的 offset = 1,leader 处理 fetch 请求时,更新 remote LEO = 1,对比 LEO 值最小为 1,所以 HW = 1,此时 leader 没有新的消息数据,所以直接返回 leader HW = 1 给 follower,follower 对比当前最新的 LEO 值 与 leader HW 值,取最小的作为新的 HW 值,此时 follower HW = 1。
从以上步骤可看出,leader 中保存的 remote LEO 值的更新(也即 HW 的更新)总是需要额外一轮fetch RPC 请求才能完成,这意味着在 leader 切换过程中,会存在数据丢失以及数据不一致的问题!
HW 会产生数据丢失和副本最终不一致问题
数据丢失的问题(即使 produce 设置 acks=all,依然会发生)
注意回顾:leader 中的 HW 值是在 follower 下一轮 fetch RPC 请求中完成更新的
如上图所示:
- 状态起始: B 为 leader,A 为 follower; 最新消息 m2 已同步,但 B 的 HW 比 A 的HW 大
- A 在此时崩溃(即 follower 没能通过下一轮请求来更新 HW 值)
- A 重启时,会自动将 LEO 值调整到之前的 HW 值,即会进行日志截断
- B 重启后,会从 向 A 发送 fetch 请求,收到 fetch 响应后,拿到 HW 值,并更新本地 HW 值,这时 B 会做日志截断,因此,offsets = 1 的消息被永久地删除了。
副本间数据最终不一致的问题(即使 produce 设置 acks=all,依然会发生)
如上图所示:
- 状态起始: A 为 leader,B 为 follower; 最新消息 m2 已同步,但 B 的 HW 比 A 的 HW 小
- A 在此时崩溃(即 follower 没能通过下一轮请求来更新 HW 值)
- B 先重启,会自动将 LEO 值调整到之前的 HW 值,即会进行日志截断,并在此刻接收了新的消息 m3,HW 随之上升为 2
- 然后,A 重启上线,会从 向 B 发送 fetch 请求,收到 fetch 响应后,拿到 HW 值,并更新本地 HW 值,发现不需要截断,从而已经产生了“副本间数据最终不一致”!
只要新一届 leader 在老 leader 重启上线前,接收了新的数据,就可能发生上图中的场景,根源也在于HW 的更新落后于数据同步进度
Leader-Epoch 机制的引入
为了解决 HW 更新时机是异步延迟的,而 HW 又是决定日志是否备份成功的标志,从而造成数据丢失和数据不一致的现象,Kafka 引入了 leader epoch 机制;
在每个副本日志目录下都创建一个 leader-epoch-checkpoint 文件,用于保存 leader 的 epoch 信息
leader-epoch 的含义
如下,leader epoch 长这样:
它的格式为 (epoch offset),epoch 指的是 leader 版本,它是一个单调递增的一个正整数值,每次 leader变更,epoch 版本都会 +1,offset 是每一代 leader 写入的第一条消息的位移值,比如:
(0,0) (1,300)
以上第 2 个版本是从位移 300 开始写入消息,意味着第一个版本写入了 0-299 的消息。
这里面记录信息的本质试:从哪个offset开始的数据,是从那届leader写入的。
leader epoch 具体的工作机制:
- 当副本成为 leader 时:
这时,如果此时生产者有新消息发送过来,会首先更新 leader epoch 以及 LEO ,并添加到leader-epoch-checkpoint 文件中
- 当副本变成 follower 时
发送 LeaderEpochRequest 请求给 leader 副本,该请求包括了 follower 中最新的 epoch 版本;leader 返回给 follower 的响应中包含了一个 LastOffset,如果 follower last epoch = leader last epoch(纪元相同),则 LastOffset = leader LEO,否则取 大于 follower last epoch 中最小的 leader epoch 的 start offset值;
举个例子:假设 follower last epoch = 1,此时 leader 有 (1, 20) (2, 80) (3, 120),则 LastOffset = 80;
follower 拿到 LastOffset 之后,会对比当前 LEO 值是否大于 LastOffset,如果当前 LEO 大于LastOffset,则从 LastOffset 截断日志;follower 开始发送 fetch 请求给 leader 保持消息同步。
leader epoch 如何解决 HW 的备份缺陷
解决数据丢失:
如上图所示:
A 重启之后,发送 LeaderEpochRequest 请求给 B,由于 B 还没追加消息,此时 epoch = request epoch = 0,因此返回 LastOffset = leader LEO = 2 给 A
A 拿到 LastOffset 之后,发现等于当前 LEO 值,故不用进行日志截断。就在这时 B 宕机了,A 成为 leader,在 B 启动回来后,会重复 A 的动作,同样不需要进行日志截断,数据没有丢失。
解决数据最终不一致问题:
如上图所示:
- A 和 B 同时宕机后,B 先重启回来成为分区 leader,这时候生产者发送了一条消息过来,leader epoch 更新到 1
- 此时 A 启动回来后,发送 LeaderEpochReques(t follower epoch = 0)给 B,B 判断 follower epoch不等于最新的 epoch,于是找到大于 follower epoch 最小的 epoch = 1,即 LastOffset = epoch start offset = 1
- A 拿到 LastOffset 后,判断小于当前 LEO 值,于是从 LastOffset 位置进行日志截断,接着开始发送 fetch 请求给 B 开始同步消息,避免了消息不一致/离散的问题。
HW的时候,计算producer把acks = all,依然会丢数据,因为它是依赖HW来进行数据截断的。
而HW的更新是相对于数据同步进度落后一轮请求的。
而现在,acks = all依赖于 leader epoch的话,不会再有数据丢失发生了,也不会再有消息不一致情况了。
LEO/HW/LSO 等相关术语速查
LEO:(last end offset)就是该副本中消息的最大偏移量的值+1
HW:(high watermark)各副本中 LEO 的最小值。这个值规定了消费者仅能消费 HW 之前的数据
LEO 与 HW 与数据一致性密切相关;
如图,各副本中最小的 LEO 是 3,所以 HW 是 3,所以,消费者此刻最多能读Msg2;
不清洁选举[了解]
不清洁选举,是指允许“非 ISR 副本”可以被选举为 leader;非 ISR 副本被选举为 leader,将极大增加数据丢失及数据不一致的可能性!由参数 unclean.leader.election.enable=false(默认) 控制;
初始状态: follower2 严重落后于 leader,并且不属于 ISR 副本
此刻,所有 ISR 副本宕机
Follower2 成为新的 leader,并接收数据
之前宕机副本重启,按照最新 leader 的最新 leo 进行截断,产生数据丢失及不一致