概述
HW即High Watermark
,高水位,经典定义如下:「在时刻 T,任意创建时间(Event Time)
为 T’,且
一个更容易理解的版本:「水位是一个单调增加且表征最早未完成工作
(oldest work not yet completed)
的时间戳」。(高水位和水位平常都是混着用的==。。。)
一般而言高水位都用时间戳来表示,但是Kafka中并不是这样做的,在Kafka中高水位是用位置信息即消息位移来表示的。
HW的作用
- 定义消息可见性,即用来标识分区下的哪些消息是可以被消费者消费的。
- 帮助 Kafka 完成副本同步。
高水位以下的消息被认为是已提交消息,反之就是未提交消息。消费者只能消费已提交消息 → 等于HW的也是不能被消费的。
需要注意,上面的说法是不考虑 Kafka 事务的,因为事务机制会影响消费者所能看到的消息的范围,它不只是简单依赖高水位来判断,它依靠一个名为
LSO(Log Stable Offset)
的位移值来判断事务型消费者的可见性。
Log End Offset(LEO)
表示副本写入下一条消息的位移值。介于高水位和 LEO 之间的消息就属于未提交消息。 → 同一个副本对象,其高水位值不会大于 LEO 值。
高水位和 LEO 是分区对象的两个重要属性。Kafka 所有分区都有对应的高水位和 LEO 值,而不仅仅是 Leader 副本。只不过 Leader 副本比较特殊,Kafka 使用 Leader 副本的高水位来定义所在分区的高水位。换句话说,分区的高水位就是其 Leader 副本的高水位。
HW更新机制
在 Leader 副本所在的 Broker 上,还保存了其他 Follower 副本的 LEO 值:
Kafka把保存的这些 Follower 副本又称为远程副本(Remote Replica)
。Kafka 运行过程中,会更新 Follower 副本的高水位和 LEO 值(非Leader分区所在的Broker),同时也会更新 Leader 副本的高水位、 LEO 以及所有远程副本的 LEO,但它不会更新远程副本的高水位值。这种机制的作用为:帮助 Leader 副本确定其高水位,也就是分区高水位。
Follower与Leader副本保持同步的判断条件:
- 远程 Follower 副本在 ISR 中。
- 远程 Follower 副本 LEO 值落后于 Leader 副本 LEO 值的时间不超过 Broker 端参数
replica.lag.time.max.ms
(默认10s) 的值。
看起来上面两个条件像一回事,实际上不是的,因为可能存在这样一种情况: Follower 副本已经追上了 Leader 的进度,却不在 ISR 中,比如某个刚刚重启回来的副本。如果 Kafka 只判断第 1 个条件的话,就可能出现某些副本具备了进入 ISR 的资格,但却尚未进入到 ISR 中的情况。此时,分区高水位值就可能超过 ISR 中副本 LEO,而高水位 > LEO 的情形是不被允许的。
Leader副本更新具体逻辑:
- 处理生产者请求的逻辑:
- 写入消息到本地磁盘。→ 自身LEO值更新
- 更新分区高水位值。
- 获取 Leader 副本所在 Broker 端保存的所有远程副本 LEO 值
(LEO-1,LEO-2,……,LEO-n
)。 - 获取 Leader 副本高水位值:
currentHW
。 - 更新
currentHW = max{currentHW, min(LEO-1, LEO-2, ……,LEO-n}
- 处理 Follower 副本拉取消息的逻辑:
- 读取磁盘(或页缓存)中的消息数据。
- 使用 Follower 副本发送请求中的位移值更新远程副本 LEO 值。
- 更新分区高水位值(具体步骤与处理生产者请求的步骤相同)。
Follower副本更新具体逻辑:
- 从 Leader 拉取消息的处理逻辑(唯一职责就是从Leader拉取消息,不对外提供服务):
- 写入消息到本地磁盘。
- 更新 LEO 值。
- 更新高水位值。
- 获取 Leader 发送的高水位值:
currentHW
。 - 获取步骤 2 中更新过的 LEO 值:
currentLEO
。 - 更新高水位为
nowHW = min(currentHW, currentLEO)
。
可以看到在上面的流程中,LEO和HW的更新是有时间错位的,LEO的更新比HW要早,且Leader的比Follower的更新要早,而这很有可能会导致数据丢失或数据不一致的问题。
举一个只依赖HW导致数据丢失的例子:
- 副本 A 和副本 B 都处于正常状态,A 是 Leader 副本。某个使用了默认 acks 设置的生产者向 A 发送了两条消息,A 全部写入成功,此时 Kafka 会通知生产者说两条消息全部发送成功。
- Leader 和 Follower 都写入了这两条消息,Leader 副本的高水位已经更新了,但 Follower 副本高水位还未更新。 → Follower 端高水位的更新与 Leader 端有时间错配。若此时副本 B 所在的 Broker 宕机,当它重启回来后,副本 B 会执行日志截断操作,将 LEO 值调整为之前的高水位值 → 1。
- 当执行完截断操作后,副本 B 开始从 A 拉取消息,执行正常的消息同步。而此时副本 A 所在的 Broker 宕机了,副本 B 成为新的 Leader,而这时 A 回来了,他不在是Leader,需要执行日志截断操作,将高水位调整为与 B 相同的值 → 1。这样位移值为 1 的那条消息就从这两个副本中被永远地抹掉了,即发生了数据丢失。
严格来说,这个场景发生的前提是 Broker 端参数
min.insync.replicas
设置为 1。此时一旦消息被写入到 Leader 副本的磁盘,就会被认为是“已提交状态”,但现有的时间错配问题导致 Follower 端的高水位更新是有滞后的。如果在这个短暂的滞后时间窗口内,接连发生 Broker 宕机,那么这类数据的丢失就是不可避免的。参考自胡夕老师《Kafka核心技术与实战》极客时间专栏,老师的课很棒,强烈推荐!