副本(Replica)攻略
在分布式系统中,副本是最常见的概念之一,一般指的是对数据或服务的冗余方式。数据副本是在不同的节点上持久化存储同一份数据,防止数据丢失;服务副本指的是多个节点提供同样的服务,单节点挂掉不影响正常服务。
Kafka 中的副本相关:
- 副本是特定分区 Partition 的副本
- Kafka 中每个 Partition 都有一个或多个副本,至少一个为 leader 副本,其他为 follower 副本,各个副本都应该在不同的broker中 即副本数不能大于 broker 数。
- AR:分区的所有副本; ISR:跟上 leader 的副本,包含 leader 副本。
- LEO 即 Log EndOffset,每个分区中最好一条消息的下一个 offset。每个分区都有自己的 LEO,而 ISR 中最小的 LEO 即为 HW,俗称高水位,consumer 只能消费 HW 之前的消息。
副本失效
副本失效,即在AR中的副本,未跟上 leader 同步的副本,被移出了 ISR 的副本,其对应的分区就叫同步失效分区。通过命令查看:
1 | [root@tnode1 bin]# ./kafka-topics.sh --zookeeper tnode3:2181 --topic LxmTest --describe |
如果没有失效分区,第二个命令返回应该是空。
Kafka 通过参数 replica.lag.time.max.ms
来判断分区副本是否失效。当 ISR 中 follower 副本滞后 leader 副本的时间超过此参数的值时则判断为 同步失败,需要将此副本移出 ISR 列表(replica.lag.time.max.ms
默认值为 10000 即 100秒 )。具体判断滞后的原理也很简单:
- follower 副本将 leader 副本 LEO 之前的日志全部同步时,表示已经追上 leader 副本
- 更新 lastCaughtUpTime 标识
- 定时任务,定时检测 lastCaughtUpTime 与当前时间的差值是否大于
replica.lag.time.max.ms
,如果大于则判断失效。
这种滞后的情况会有两种情况:
- follower 副本一段时间内未发起同步请求
- follower 副本同步速度过慢,一段时间内无法追上 leader 副本
!## !在早期版本,还有一个根据 follower 副本和 leader 副本同步消息条数的差值来判断同步是否失效,已被放弃
ISR 列表变动
ISR 列表变动与两个定时任务相关 isr-expiraion 和 isr-change-propagation 。isr-expiration 任务会周期性的检查每个分区的 ISR 列表是否需要缩减,周期为上面 replica.lag.time.max.ms
参数的一半,默认是 5000, 即 5 秒。当检测到 ISR 中有失效副本时,就会缩减 ISR 列表,并将变更后的数据记录到 Zookeeper 对应节点中,同时将记录缓存到 isrChangeSet 中。而 isr-change-propagation 任务会周期性 (固定2500ms) 检查 is人ChangeSet,如果有变更记录,那么将在 Zookeeper 对应目录下创建持久的顺序节点,并添加 Watcher。如果 isr-change-propagation 对应目录下有节点发生变化,将触发 Watcher 通知 Kafka 控制器,更新元数据。
但是如果频繁的触发 Watcher,将影响 ZK 和 Kafka 性能,因此,在检测到分区 ISR 列表发生变化时,还需要:
- 上一次 ISR 列表 发生变化时间距离现在超过 5 秒
- 上一次写入 ZK 的时间距离现在超过 60 秒
以上满足其一,才会将 ISR 列表的变化写入目标节点。
ISR 扩充的情况和 ISR 缩减的原理类似,只不过是,定时检测 是否有 follower 副本有资格进入 ISR 列表。这个资格为:follower 副本的 LEO 不小于 leader 副本的 HW(注意是 HW,而不是 LEO)。
!## !无论 ISR 增加,还是任一副本的LEO发生变化时,都会影响整个分区的 HW !## !
LEO 和 HW
在 Kafka 中,同一个分区的信息会存在多个 broker 节点上,并被其上的副本管理器所管理。先看一下消息写入的过程:
生产者向 leader 副本发送消息
leader 副本存储消息,并更新 LEO,HW
(假设 msg=10,LEO=10,HW=10 )
follower 副本们 向 leader 副本请求同步数据
(同时会附带自己的 LEO信息 LEO.1 = LEO.2 =0)
leader 副本读取本地日志,并更新 leader 副本上 关于 follower 副本的信息
(LEO=10, HW = min(10, 0, 0) = 0 )
leader 副本将读取的结果返回给 follower 副本们,并将 HW 传给 follower 们
follower 副本们接收 leader的返回结果,追加消息到日志,更新 LEO,HW
(LEO.1 = 4,HW.1 = min(LEO.1, HW) = 0,LEO.2 = 5,HW.2 = min(LEO.2, HW) = 0)
leader同一时间也会接收新消息
(假设 msg=5,LEO.0=15,HW = 0)
follower 们再次请求拉取同步数据
(同时带上自己的 LEO信息,LEO.1 =4 ,LEO.2=5)
leader 拉取本地数据,并获取 follower 的请求信息 ( LEO ) 等,将请求结果返回 follower们,带上 HW
(LEO.0=15,HW = min(15, 4, 5) = 4)
follower 们获取返回结果,写日志,更新 LEO 和 HW
(LEO.1 = 8,HW.1 = min(8, 4) =4,LEO.2 = 10,HW.2 = min(10, 4) = 4)
接着 下一批,follower 再次请求数据
(LEO.1 = 8,LEO.2 = 10)
leader 获取本地数据,返回结果给 follower,带上 HW
(LEO.0=20, HW = min(20, 8, 10) = 8)
follower 们接收消息,写日志,更新 LEO 和 HW
(LEO.1 = 18,HW.1 = min(18, 8) = 8,LEO.2 = 10,HW.2 = min(10, 8) = 8)
….
这样子算下来,可能会发现,leader 的 HW 并不等于 三个最新 LEO 的最小值。因为 leader 上的 HW 是要用来提供服务的,因此只能计算返回结果到 follower 之前的 LEO 的最小值。如果计算了返回结果之后的 LEO,那么当前 的 HW不能得到可靠保证,因为返回结果到 follower 可能失败,失败的话,如果 leader 重置,而新 leader 的LEO 达不到 旧 leader 的 HW,那么这中间的数据就丢了。