分区副本的分配
副本分配的三个目标:
副本分配的三个目标:
1.
均衡地将副本分散于各个
broker
上
2.
对于某个
broker
上分配的分区,它的其他副本在其他
broker
上
3.
如果所有的
broker
都有机架信息,尽量将分区的各个副本分配到不同机架上的
broker
。
在不考虑机架信息的情况下:
1.
第一个副本分区通过轮询的方式挑选一个
broker
,进行分配。该轮询从
broker
列表的随机位置进行轮询。
2.
其余副本通过增加偏移进行分配
分配案例:
broker-0 broker-1 broker-2 broker-3 broker-4
p0 p1 p2 p3 p4 (1st replica)
p5 p6 p7 p8 p9 (1st replica)
p4 p0 p1 p2 p3 (2nd replica)
p8 p9 p5 p6 p7 (2nd replica)
p3 p4 p0 p1 p2 (3nd replica)
p7 p8 p9 p5 p6 (3nd replica)
p0 p1 p2 p3 p4 (1st replica)
p5 p6 p7 p8 p9 (1st replica)
p4 p0 p1 p2 p3 (2nd replica)
p8 p9 p5 p6 p7 (2nd replica)
p3 p4 p0 p1 p2 (3nd replica)
p7 p8 p9 p5 p6 (3nd replica)
![](https://i-blog.csdnimg.cn/blog_migrate/05aa45dd5d8f7550f9369d00b145125c.png)
副本机制
Kafka
在一定数量的服务器上对主题分区进行复制。
当集群中的一个
broker
宕机后系统可以自动故障转移到其他可用的副本上,不会造成数据丢失。
--replication-factor 3 1leader+2follower
1.
将复制因子为
1
的未复制主题称为复制主题。
2.
主题的分区是复制的最小单元。
3.
在非故障情况下,
Kafka
中的每个分区都有一个
Leader
副本和零个或多个
Follower
副本。
4.
包括
Leader
副本在内的副本总数构成复制因子。
5.
所有读取和写入都由
Leader
副本负责。
6.
通常,分区比
broker
多,并且
Leader
分区在
broker
之间平均分配。
Follower
分区像
普通的
Kafka
消费者
一样,消费来自
Leader
分区的消息,并将其持久化到自己的
日志中。
允许
Follower
对日志条目拉取进行
批处理
。
同步节点定义:
1.
节点必须能够维持与
ZooKeeper
的会话(通过
ZooKeeper
的心跳机制)
2.
对于
Follower
副本分区,它复制在
Leader
分区上的写入,并且不要延迟太多
Kafka
提供的保证是,只要有至少一个同步副本处于活动状态,提交的消息就不会丢失。
失效副本
失效副本的判定
失效副本的判定
replica.lag.time.max.ms
默认大小为
10000。
当
ISR
中的一个
Follower
副本滞后
Leader
副本的时间超过参数
replica.lag.time.max.ms
指定的值时即判定为副本失效,需要将此Follower
副本剔出除
ISR
。
具体实现原理:当
Follower
副本将
Leader
副本的
LEO
之前的日志全部同步时,则认为该
Follower
副本已经追赶上Leader
副本,此时更新该副本的
lastCaughtUpTimeMs标识。
Kafka
的副本管理器(
ReplicaManager
)启动时会启动一个副本过期检测的定时任务,而这个定时任务会定时检查当前时间与副本的lastCaughtUpTimeMs
差值是否大于参数replica.lag.time.max.ms 指定的值。
Kafka
源码注释中说明了一般有两种情况会导致副本失效:
1. Follower
副本进程卡住,在一段时间内没有向
Leader
副本发起同步请求,比如频繁的
FullGC。
2. Follower
副本进程同步过慢,在一段时间内都无法追赶上
Leader
副本,比如
IO开销过大
如果通过工具增加了副本因子,那么新增加的副本在赶上
Leader
副本之前也都是处于失效状态的。
如果一个
Follower
副本由于某些原因(比如宕机)而下线,之后又上线,在追赶上
Leader
副本之前也是出于失效状态
一致性保证
1.
水位标记
水位或水印(
watermark
)一词,表示位置信息,即位移(
offset
)。
Kafka
源码中使用的名字是高水位,HW
(
high watermark
)。
2. LEO
和
HW
每个分区副本对象都有两个重要的属性:
LEO
和
HW。
LEO
:即日志末端位移
(log end offset)
,记录了该副本日志中下一条消息的位移值。如果LEO=10,那么表示该副本保存了
10
条消息,位移值范围是
[0, 9]
。另外,
Leader LEO
和 Follower LEO的更新是有区别的。
HW
:即上面提到的水位值。对于同一个副本对象而言,其
HW
值不会大于
LEO
值。小于等于HW值的所有消息都被认为是
“
已备份
”
的(
replicated
)。
Leader
副本和
Follower
副本的
HW
更新不同。
![](https://i-blog.csdnimg.cn/blog_migrate/8ca3ad353cfc11a064e0c2737a38e458.png)
Follower
副本何时更新LEO
Follower
副本不停地向
Leader
副本所在的
broker
发送
FETCH
请求,一旦获取消息后写入自己的日志中进行备份。那么Follower
副本的
LEO
是何时更新的呢?首先我必须言明,
Kafka
有两套
Follower
副本
LEO
:
1.
一套
LEO
保存在
Follower
副本所在
Broker
的
副本管理机
中;
2.
另一套
LEO
保存在
Leader
副本所在
Broker
的副本管理机中。
Leader
副本机器上保存了所有的follower副本的
LEO
。
1. Follower
副本的本地
LEO
何时更新?
Follower
副本的
LEO
值就是日志的
LEO
值,每当新写入一条消息,LEO
值就会被更新。当
Follower
发送
FETCH
请求后,
Leader
将数据返回给Follower,此时
Follower
开始
Log
写数据,从而自动更新
LEO
值。
2. Leader
端
Follower
的
LEO
何时更新?
Leader
端的
Follower
的
LEO
更新发生在
Leader
在处理Follower FETCH请求时。一旦
Leader
接收到
Follower
发送的
FETCH
请求,它先从
Log
中读取相应的数据,给Follower
返回数据前,先更新
Follower
的
LEO
。
Follower
副本何时更新
HW
Follower
更新
HW
发生在其更新
LEO
之后,一旦
Follower
向
Log
写完数据,尝试更新自己的
HW
值。 比较当前LEO
值与
FETCH
响应中
Leader
的
HW
值,取两者的小者作为新的
HW
值。
即:
如果Follower的LEO大于Leader的HW,Follower HW值不会大于Leader的HW值
。
![](https://i-blog.csdnimg.cn/blog_migrate/253054821bc15b62ed0c4975f22a8ca4.png)
Leader
副本何时更新
LEO
和
Follower
更新
LEO
相同,
Leader
写
Log
时自动更新自己的
LEO
值。
Leader
副本何时更新
HW
值
Leader
的
HW
值就是分区
HW
值,直接影响分区数据对消费者的可见性 。
Leader
会
尝试
去更新分区
HW
的四种情况:
1. Follower
副本成为
Leader
副本时:
Kafka
会尝试去更新分区
HW
。
2. Broker
崩溃导致副本被踢出
ISR
时:检查下分区
HW
值是否需要更新是有必要的。
3.
生产者向
Leader
副本写消息时:因为写入消息会更新
Leader
的
LEO
,有必要检查
HW
值是否需要更新
4. Leader
处理
Follower FETCH
请求时:首先从
Log
读取数据,之后尝试更新分区
HW
值
结论:
当
Kafka broker
都正常工作时,分区
HW
值的更新时机有两个:
1. Leader
处理
PRODUCE
请求时
2. Leader
处理
FETCH
请求时
Leader
如何更新自己的
HW
值?
Leader broker
上保存了一套
Follower
副本的
LEO
以及自己的
LEO
。
当尝试确定分区
HW
时,它会选出所有
满足条件的副本
,比较它们的
LEO(
包括
Leader
的
LEO)
,并
选择最
小的
LEO
值作为
HW
值
。
需要满足的条件,(二选一):
1.
处于
ISR
中
2.
副本
LEO
落后于
Leader LEO
的时长不大于
replica.lag.time.max.ms
参数值
(
默认是
10s)
如果
Kafka
只判断第一个条件的话,确定分区
HW
值时就不会考虑这些未在
ISR
中的副本,但这些副本已经具备了“
立刻进入
ISR”
的资格,因此就可能出现分区
HW
值越过
ISR
中副本
LEO
的情况
——
不允许。
因为分区HW定义就是ISR中所有副本LEO的最小值。
HW
和
LEO
正常更新案例
我们假设有一个
topic
,单分区,副本因子是
2
,即一个
Leader
副本和一个
Follower
副本。我们看下当producer
发送一条消息时,
broker
端的副本到底会发生什么事情以及分区
HW
是如何被更新的。
1.
初始状态
初始时
Leader
和
Follower
的
HW
和
LEO
都是
0(
严格来说源代码会初始化
LEO
为
-1
,不过这不影响之后的讨论)
。
Leader
中的
Remote LEO
指的就是
Leader
端保存的
Follower LEO
,也被初始化成
0
。此时,生产者没有发送任何消息给Leader
,而
Follower
已经开始不断地给
Leader
发送
FETCH
请求了,但因为没有数据因此什么都不会发生。值得一提的是,Follower
发送过来的
FETCH
请求因为无数据而暂时会被寄存到Leader
端的
purgatory
(炼狱)中,待
500ms (
replica.fetch.wait.max.ms
参数
)
超时后会强制完成。倘若在寄存期间生产者发来数据,则Kafka
会自动唤醒该
FETCH
请求,让
Leader
继续处理。
![](https://i-blog.csdnimg.cn/blog_migrate/8c2726d0e79d20885ecc90b5bee3f9c7.png)
2.
Follower
发送
FETCH
请求在
Leader
处理完
PRODUCE
请求之后
![](https://i-blog.csdnimg.cn/blog_migrate/abb35f9101a36ce8a7c1128185af1ad1.png)
如上图所示,
Leader
接收到
PRODUCE
请求主要做两件事情:
1.
把消息写入
Log
,同时自动更新
Leader
自己的
LEO
2.
尝试更新
Leader HW
值。假设此时
Follower
尚未发送
FETCH
请求,
Leader
端保存的
Remote LEO依然是
0
,因此
Leader
会比较它自己的
LEO
值和
Remote LEO
值,发现最小值是
0
,与当前HW值相同,故不会更新分区
HW
值(仍为
0
)
PRODUCE
请求处理完成后各值如下,
Leader
端的
HW
值依然是
0
,而
LEO
是
1
,
Remote LEO
也是 0
![](https://i-blog.csdnimg.cn/blog_migrate/d268d5f21acba84f59bfacff8fef9beb.png)
假设此时
follower
发送了
FETCH
请求
,则状态变更如下:
![](https://i-blog.csdnimg.cn/blog_migrate/7b9442a0524877aad5e54e7437167564.png)
本例中当
follower
发送
FETCH
请求时,
Leader
端的处理依次是:
1.
读取
Log
数据
2.
更新
remote LEO = 0
(为什么是
0
? 因为此时
Follower
还没有写入这条消息。
Leader
如何确认Follower
还未写入呢?这是通过
Follower
发来的
FETCH
请求中的
Fetch offset
来确定的)
3.
尝试更新分区
HW
:此时
Leader LEO = 1
,
Remote LEO = 0
,故分区
HW
值
= min(Leader LEO, Follower Remote LEO) = 0
4.
把数据和当前分区
HW
值(依然是
0
)发送给
Follower
副本
而
Follower
副本接收到
FETCH Response
后依次执行下列操作
1.
写入本地
Log
,同时更新
Follower
自己管理的
LEO
为
1
2.
更新
Follower HW
:比较本地
LEO
和
FETCH Response
中的当前
Leader HW
值,取较小者, Follower HW = 0
此时,第一轮
FETCH RPC
结束,我们会发现虽然
Leader
和
Follower
都已经在
Log
中保存了这条消息,但分区HW
值尚未被更新,仍为
0
。
![](https://i-blog.csdnimg.cn/blog_migrate/57689f7f06f04153c51028e0a2e54cdf.png)
Follower
第二轮
FETCH
分区
HW
是在第二轮
FETCH RPC
中被更新的,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/2639cbf37b2ca12745f416b5f644fdd9.png)
Follower
发来了第二轮
FETCH
请求,
Leader
端接收到后仍然会依次执行下列操作:
1.
读取
Log
数据
2.
更新
Remote LEO = 1
(这次为什么是
1
了? 因为这轮
FETCH RPC
携带的
fetch offset
是
1
,那么为什么这轮携带的就是1
了呢,因为上一轮结束后
Follower LEO
被更新为
1
了)
3.
尝试更新分区
HW
:此时
leader LEO = 1
,
Remote LEO = 1
,故分区
HW
值
= min(Leader LEO, Follower Remote LEO) = 1
。
4.
把数据(实际上没有数据)和当前分区
HW
值(已更新为
1
)发送给
Follower
副本作为 Respons
同样地,
Follower
副本接收到
FETCH response
后依次执行下列操作:
1.
写入本地
Log
,当然没东西可写,
Follower LEO
也不会变化,依然是
1
。
2.
更新
Follower HW
:比较本地
LEO
和当前
LeaderHW
取小者。由于都是
1
,故更新
follower HW = 1
![](https://i-blog.csdnimg.cn/blog_migrate/3b8fffcaed8a921bda3b8d84abbe0d9a.png)
此时消息已经成功地被复制到
Leader
和
Follower
的
Log
中且分区
HW
是
1
,表明消费者能够消费 offset = 0的消息。
3.
FETCH
请求保存在
purgatory
中,
PRODUCE
请求到来。
当
Leader
无法立即满足
FECTH
返回要求的时候
(
比如没有数据
)
,那么该
FETCH
请求被暂存到
Leader 端的purgatory
中(炼狱),待时机成熟尝试再次处理。
Kafka
不会无限期缓存,默认有个超时时间 (500ms
),一旦超时时间已过,则这个请求会被强制完成。当寄存期间还没超时,生产者发送 PRODUCE请求从而使之满足了条件以致被唤醒。此时,
Leader
端处理流程如下:
1. Leader
写
Log
(自动更新
Leader LEO
)
2.
尝试唤醒在
purgatory
中寄存的
FETCH
请求
3.
尝试更新分区
HW
HW
和
LEO
异常案例
Kafka
使用
HW
值来决定副本备份的进度,而
HW
值的更新通常需要额外一轮
FETCH RPC
才能完成。 但这种设计是有问题的,可能引起的问题包括:
1.
备份数据丢失
2. 备份数据不一致
1. 数据丢失
1. 数据丢失
使用
HW
值来确定备份进度时其值的更新是在
下一轮
RPC
中完成的。如果
Follower
副本在标记上方的第一步与第二步之间发生崩溃,那么就有可能造成数据的丢失。
![](https://i-blog.csdnimg.cn/blog_migrate/1dd36c4d3a25ddaf5c6ca35ba890c852.png)
上图中有两个副本:
A
和
B
。开始状态是
A
是
Leader
。
假设生产者
min.insync.replicas
为
1
,那么当生产者发送两条消息给
A
后,
A
写入
Log
,此时Kafka会通知生产者这两条消息写入成功。
![](https://i-blog.csdnimg.cn/blog_migrate/514a3eb48aab10d31a2ede9c8f197e4f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/17733effbc3c07c43693af308f65e43c.png)
但是在
broker
端,
Leader
和
Follower
的
Log
虽都写入了
2
条消息且分区
HW
已经被更新到
2
,但Follower HW尚未被更新还是
1
,也就是上面标记的第二步尚未执行,表中最后一条未执行。
倘若此时副本
B
所在的
broker
宕机,
那么重启后B会自动把LEO调整到之前的HW值1
,故副本
B
会做日志截断(log truncation)
,将
offset = 1
的那条消息从
log
中删除,并调整
LEO = 1
。此时
follower
副本底层log
中就只有一条消息,即
offset = 0
的消息!
B
重启之后需要给
A
发
FETCH
请求,但若
A
所在
broker
机器在此时宕机,那么
Kafka
会令
B
成为新的Leader,而
当A重启回来后也会执行日志截断,将HW调整回1
。这样,
offset=1
的消息就从两个副本的log中被删除,也就是说这条已经被生产者认为发送成功的数据丢失。
丢失数据的前提是 min.insync.replicas=1 时,一旦消息被写入 Leader 端 Log 即被认为是committed 。 延迟一轮 FETCH RPC 更新 HW 值的设计使 follower HW 值是异步延迟更新 ,若在这个过程中Leader 发生变更,那么成为新 Leader 的 Follower 的 HW 值就有可能是过期的,导致生产者本是成功提交的消息被删除。
丢失数据的前提是 min.insync.replicas=1 时,一旦消息被写入 Leader 端 Log 即被认为是committed 。 延迟一轮 FETCH RPC 更新 HW 值的设计使 follower HW 值是异步延迟更新 ,若在这个过程中Leader 发生变更,那么成为新 Leader 的 Follower 的 HW 值就有可能是过期的,导致生产者本是成功提交的消息被删除。
2.
Leader
和
Follower数据离散
除了可能造成的数据丢失以外,该设计还会造成
Leader
的
Log
和
Follower
的
Log
数据不一致。 如Leader
端记录序列:
m1,m2,m3,m4,m5,…
;
Follower
端序列可能是m1,m3,m4,m5,…。 看图:
![](https://i-blog.csdnimg.cn/blog_migrate/dd22458e11767f409d4d83eb7291920f.png)
假设:
A
是
Leader
,
A
的
Log
写入了
2
条消息,但
B
的
Log
只写了
1
条消息。分区
HW
更新到
2
,但
B
的HW还是
1
,同时生产者
min.insync.replicas
仍然为
1
。
假设
A
和
B
所在
Broker
同时宕机,
B
先重启回来,因此
B
成为
Leader
,分区
HW = 1
。假设此时生产者发送了第3
条消息
(
红色表示
)
给
B
,于是
B
的
log
中
offset = 1
的消息变成了红框表示的消息,同时分区
HW更新到2
(
A
还没有回来,就
B
一个副本,故可以直接更新
HW
而不用理会
A
)之后
A重启回来,需要执行日志截断,但发现此时分区HW=2而A之前的HW值也是2,故不做任何调整
。此后
A
和
B
将以这种状态继续正常工作。
显然,这种场景下,
A
和
B
的
Log
中保存在
offset = 1
的消息是不同的记录,从而引发不一致的情形出现。
Leader Epoch
使用
Kafka
解决方案
造成上述两个问题的根本原因在于
1. HW
值被用于衡量副本备份的成功与否。
2.
在出现失败重启时作为日志截断的依据。
但
HW
值的更新是异步延迟的,特别是需要额外的
FETCH
请求处理流程才能更新,故这中间发生的任何崩溃都可能导致HW
值的过期。
Kafka
从
0.11
引入了
leader epoch
来取代
HW
值。
Leader
端使用
内存
保存
Leader
的
epoch
信息,即使出现上面的两个场景也能规避这些问题。
所谓Leader epoch实际上是一对值:<epoch, offset>:
1. epoch
表示
Leader
的版本号,从
0
开始,
Leader
变更过
1
次,
epoch+1
2. offset
对应于该
epoch
版本的
Leader
写入第一条消息的
offset
。因此假设有两对值:
<0, 0>
<1, 120>
表示第一个
Leader
从位移
0
开始写入消息;共写了
120
条
[0, 119]
;而第二个
Leader
版本号是
1
, 从位移120
处开始写入消息。
1. Leader broker
中会保存这样的一个缓存,并定期地写入到一个
checkpoint
文件中。
2.
当
Leader
写
Log
时它会尝试更新整个缓存:如果这个
Leader
首次写消息,则会在缓存中增加一个条目;否则就不做更新。
3.
每次副本变为
Leader
时会查询这部分缓存,获取出对应
Leader
版本的位移,则不会发生数据不一致和丢失的情况。
1.
规避数据丢失
![](https://i-blog.csdnimg.cn/blog_migrate/27761b837ea6e9cd288db76405d0cc20.png)
只需要知道每个副本都引入了新的状态来保存自己当
leader
时开始写入的第一条消息的
offset
以及leader版本。这样在恢复的时候完全使用这些信息而非
HW
来判断是否需要截断日志。
2.
规避数据不一致
![](https://i-blog.csdnimg.cn/blog_migrate/a81d621acfe6659341e5e61b87789e38.png)
依靠
Leader epoch
的信息可以有效地规避数据不一致的问题。
对于使用
unclean.leader.election.enable = true
设置的群集,该方案不能保证消息的一致性
宕机如何恢复
(
1
)少部分副本宕机
当
leader
宕机了,会从
follower
选择一个作为
leader
。当宕机的重新恢复时,会把之前
commit
的数据清空,重新从leader
里
pull
数据。
(
2
)全部副本宕机
当全部副本宕机了有两种恢复方式
1
、等待
ISR
中的一个恢复后,并选它作为
leader
。(等待时间较长,降低可用性)
2 、选择第一个恢复的副本作为新的 leader ,无论是否在 ISR 中。(并未包含之前 leader commit 的数据,因此造成数据丢失)
2 、选择第一个恢复的副本作为新的 leader ,无论是否在 ISR 中。(并未包含之前 leader commit 的数据,因此造成数据丢失)
Leader选举
如果某个分区所在的服务器出了问题,不可用,
kafka
会从该分区的其他的副本中选择一个作为新的Leader
。之后所有的读写就会转移到这个新的
Leader
上。现在的问题是应当选择哪个作为新的Leader。
只有那些跟
Leader
保持同步的
Follower
才应该被选作新的
Leader
。
Kafka
会在
Zookeeper
上针对每个
Topic
维护一个称为
ISR(
in-sync replica
,已同步的副本)的集合,该集合中是一些分区的副本。
只有当这些副本都跟
Leader
中的副本同步了之后,
kafka
才会认为消息已提交,并反馈给消息的生产者。
如果这个集合有增减,
kafka
会更新
zookeeper
上的记录。
如果某个分区的
Leader
不可用,
Kafka就会从ISR集合中选择一个副本作为新的Leader。(从AR列表中找到第一个存活的副本而且此副本在ISR列表中)
如果所有的
ISR副本都失败了怎么办?
此时有两种方法可选,
1.
等待
ISR
集合中的副本复活,
2.
选择任何一个立即可用的副本,而这个副本不一定是在
ISR
集合中。 需要设置 unclean.leader.election.enable=true
这两种方法各有利弊,实际生产中按需选择。 如果要等待
ISR
副本复活,虽然可以保证一致性,但可能需要很长时间。而如果选择立即可用的副本,则很可能该副本并不一致。
总结:
Kafka
中
Leader
分区选举,通过维护一个动态变化的
ISR
集合来实现,一旦
Leader
分区丢掉,则从ISR中随机挑选一个副本做新的
Leader
分区。
如果
ISR
中的副本都丢失了,则:
1.
可以等待
ISR
中的副本任何一个恢复,接着对外提供服务,需要时间等待。
2.
从
OSR
中选出一个副本做
Leader
副本,此时会造成数据丢失