高可用是很多分布式系統(tǒng)中必備的特征之一,Kafka 日志的高可用是通過基于 leader-follower 的多副本同步實現(xiàn)的糜工,每個分區(qū)下有多個副本宇智,其中只有一個是 leader 副本,提供發(fā)送和消費消息捞蚂,其余都是 follower 副本鞍盗,不斷地發(fā)送 fetch 請求給 leader 副本以同步消息需了,如果 leader 在整個集群運行過程中不發(fā)生故障,follower 副本不會起到任何作用般甲,問題就在于任何系統(tǒng)都不能保證其穩(wěn)定運行肋乍,當 leader 副本所在的 broker 崩潰之后,其中一個 follower 副本就會成為該分區(qū)下新的 leader 副本敷存,那么問題來了墓造,在選為新的 leader 副本時,會導(dǎo)致消息丟失或者離散嗎锚烦?Kafka 是如何解決 leader 副本變更時消息不會出錯觅闽?以及 leader 與 follower 副本之間的數(shù)據(jù)同步是如何進行的?帶著這幾個問題涮俄,我們接著往下看蛉拙,一起揭開 Kafka 水印備份的神秘面紗。
水印相關(guān)概念
在講解水印備份之前彻亲,我們必須要先搞清楚幾個關(guān)鍵的術(shù)語以及它們的含義孕锄,下面我用一張圖來示意 Kafka 分區(qū)副本的位移信息:
如上圖所示吮廉,綠色部分表示已完全備份的消息,對消費者可見畸肆,紫色部分表示未完全備份的消息宦芦,對消費者不可見。
LEO(last end offset):日志末端位移轴脐,記錄了該副本對象底層日志文件中下一條消息的位移值调卑,副本寫入消息的時候,會自動更新 LEO 值大咱。
HW(high watermark):從名字可以知道令野,該值叫高水印值,HW 一定不會大于 LEO 值徽级,小于 HW 值的消息被認為是“已提交”或“已備份”的消息,并對消費者可見聊浅。
leader 會保存兩個類型的 LEO 值餐抢,一個是自己的 LEO,另一個是 remote LEO 值低匙,remote LEO 值就是 follower 副本的 LEO 值旷痕,意味著 follower 副本的 LEO 值會保存兩份,一份保存到 leader 副本中顽冶,一份保存到自己這里欺抗。
remote LEO 值有什么用呢?
它是決定 HW 值大小的關(guān)鍵强重,當 HW 要更新時绞呈,就會對比 LEO 值(也包括 leader LEO),取最小的那個做最新的 HW 值间景。
以下介紹 LEO 和 HW 值的更新機制:
LEO 更新:
- leader 副本自身的 LEO 值更新:在 Producer 消息發(fā)送過來時佃声,即 leader 副本當前最新存儲的消息位移位置 +1;
- follower 副本自身的 LEO 值更新:從 leader 副本中 fetch 到消息并寫到本地日志文件時倘要,即 follower 副本當前同步 leader 副本最新的消息位移位置 +1圾亏;
- leader 副本中的 remote LEO 值更新:每次 follower 副本發(fā)送 fetch 請求都會包含 follower 當前 LEO 值,leader 拿到該值就會嘗試更新 remote LEO 值封拧。
leader HW 更新****:
故障時更新:
副本被選為 leader 副本時:當某個 follower 副本被選為分區(qū)的 leader 副本時志鹃,kafka 就會嘗試更新 HW 值;
副本被踢出 ISR 時:如果某個副本追不上 leader 副本進度泽西,或者所在 broker 崩潰了曹铃,導(dǎo)致被踢出 ISR,leader 也會檢查 HW 值是否需要更新尝苇,畢竟 HW 值更新只跟處于 ISR 的副本 LEO 有關(guān)系铛只。
正常時更新:
producer 向 leader 副本寫入消息時:在消息寫入時會更新 leader LEO 值埠胖,因此需要再檢查是否需要更新 HW 值;
leader 處理 follower FETCH 請求時:follower 的 fetch 請求會攜帶 LEO 值淳玩,leader 會根據(jù)這個值更新對應(yīng)的 remote LEO 值直撤,同時也需要檢查是否需要更新 HW 值。
follower HW 更新:
- follower 更新 HW 發(fā)生在其更新 LEO 之后蜕着,每次 follower Fetch 響應(yīng)體都會包含 leader 的 HW 值谋竖,然后比較當前 LEO 值,取最小的作為新的 HW 值承匣。
圖解水印備份過程
在了解了 Kafka 水印備份機制的相關(guān)概念之后蓖乘,下面我用圖來幫大家更好地理解 Kafka 的水印備份過程,假設(shè)某個分區(qū)有兩個副本韧骗,min.insync.replica=1:
Step 1:leader 和 follower 副本處于初始化值嘉抒,follower 副本發(fā)送 fetch 請求,由于 leader 副本沒有數(shù)據(jù)袍暴,因此不會進行同步操作些侍;
Step 2:生產(chǎn)者發(fā)送了消息 m1 到分區(qū) leader 副本,寫入該條消息后 leader 更新 LEO = 1政模;
Step 3:follower 發(fā)送 fetch 請求岗宣,攜帶當前最新的 offset = 0,leader 處理 fetch 請求時淋样,更新 remote LEO = 0耗式,對比 LEO 值最小為 0,所以 HW = 0趁猴,leader 副本響應(yīng)消息數(shù)據(jù)及 leader HW = 0 給 follower刊咳,follower 寫入消息后,更新 LEO 值躲叼,同時對比 leader HW 值芦缰,取最小的作為新的 HW 值,此時 follower HW = 0枫慷,這也意味著让蕾,follower HW 是不會超過 leader HW 值的。
Step 4:follower 發(fā)送第二輪 fetch 請求或听,攜帶當前最新的 offset = 1探孝,leader 處理 fetch 請求時,更新 remote LEO = 1誉裆,對比 LEO 值最小為 1顿颅,所以 HW = 1,此時 leader 沒有新的消息數(shù)據(jù)足丢,所以直接返回 leader HW = 1 給 follower粱腻,follower 對比當前最新的 LEO 值 與 leader HW 值庇配,取最小的作為新的 HW 值,此時 follower HW = 1绍些。
基于水印備份機制的一些缺陷
從以上步驟可看出捞慌,leader 中保存的 remote LEO 值的更新總是需要額外一輪 fetch RPC 請求才能完成,這意味著在 leader 切換過程中柬批,會存在數(shù)據(jù)丟失以及數(shù)據(jù)不一致的問題啸澡,下面我用圖來說明存在的問題:
- 數(shù)據(jù)丟失
前面也說過,leader 中的 HW 值是在 follower 下一輪 fetch RPC 請求中完成更新的氮帐,如上圖所示嗅虏,有副本 A 和 B,其中 B 為 leader 副本上沐,A 為 follower 副本皮服,在 A 進行第二段 fetch 請求,并接收到響應(yīng)之后参咙,此時 B 已經(jīng)將 HW 更新為 2冰更,如果這是 A 還沒處理完響應(yīng)就崩潰了,即 follower 沒有及時更新 HW 值昂勒,A 重啟時,會自動將 LEO 值調(diào)整到之前的 HW 值舟铜,即會進行日志截斷戈盈,接著會向 B 發(fā)送 fetch 請求,但很不幸的是此時 B 也發(fā)生宕機了谆刨,Kafka 會將 A 選舉為新的分區(qū) Leader塘娶。當 B 重啟后,會從 向 A 發(fā)送 fetch 請求痊夭,收到 fetch 響應(yīng)后刁岸,拿到 HW 值,并更新本地 HW 值她我,此時 HW 被調(diào)整為 1(之前是 2)虹曙,這時 B 會做日志截斷,因此番舆,offsets = 1 的消息被永久地刪除了酝碳。
可能你會問,follower 副本為什么要進行日志截斷恨狈?
這是由于消息會先記錄到 leader疏哗,follower 再從 leader 中拉取消息進行同步,這就導(dǎo)致 leader LEO 會比 follower 的要大(f****ollower 之間的 offset 也不盡相同禾怠,雖然最終會一致返奉,但過程中會有差異)贝搁,假設(shè)此時出現(xiàn) leader 切換,有可能選舉了一個 LEO 較小的 follower 成為新的 leader芽偏,這時該副本的 LEO 就會成為新的標準雷逆,這就會導(dǎo)致 follower LEO 值有可能會比 leader LEO 值要大的情況,因此 follower 在進行同步之前哮针,需要從 leader 獲取 LastOffset 的值(該值后面會有解釋)关面,如果 LastOffset 小于 當前 LEO,則需要進行日志截斷十厢,然后再從 leader 拉取數(shù)據(jù)實現(xiàn)同步等太。
可能你還會問,日志截斷會不會造成數(shù)據(jù)丟失蛮放?
前面也說過缩抡,HW 值以上的消息是沒有“已提交”或“已備份”的,因此消息也是對消費者不可見包颁,即這些消息不對用戶作承諾瞻想,也即是說從 HW 值截斷日志,并不會導(dǎo)致數(shù)據(jù)丟失(承諾用戶范圍內(nèi))娩嚼。
- 數(shù)據(jù)不一致/離散
以上情況蘑险,需要滿足以下其中一個條件才會發(fā)生:
- 宕機之前,B 已不在 ISR 列表中岳悟,unclean.leader.election.enable=true佃迄,即允許非 ISR 中副本成為 leader;
- B 消息寫入到 pagecache贵少,但尚未 flush 到磁盤呵俏。
分區(qū)有兩個副本,其中 A 為 Leader 副本滔灶,B 為 follower 副本普碎,A 已經(jīng)寫入兩條消息,且 HW 更新到 2录平,B 只寫了 1 條消息麻车,HW 為 1,此時 A 和 B 同時宕機斗这,B 先重啟绪氛,B 成為了 leader 副本,這時生產(chǎn)者發(fā)送了一條消息涝影,保存到 B 中枣察,由于此時分區(qū)只有 B,B 在寫入消息時把 HW 更新到 2,就在這時候 A 重新啟動序目,發(fā)現(xiàn) leader HW 為 2臂痕,跟自己的 HW 一樣,因此沒有執(zhí)行日志截斷猿涨,這就造成了 A 的 offset=1 的日志與 B 的 offset=1 的日志不一樣的現(xiàn)象握童。
leader epoch
為了解決 HW 更新時機是異步延遲的,而 HW 又是決定日志是否備份成功的標志叛赚,從而造成數(shù)據(jù)丟失和數(shù)據(jù)不一致的現(xiàn)象澡绩,Kafka 引入了 leader epoch 機制,在每個副本日志目錄下都創(chuàng)建一個 leader-epoch-checkpoint 文件俺附,用于保存 leader 的 epoch 信息肥卡,如下,leader epoch 長這樣:
它的格式為 (epoch offset)事镣,epoch 指的是 leader 版本步鉴,它是一個單調(diào)遞增的一個正整數(shù)值,每次 leader 變更璃哟,epoch 版本都會 +1氛琢,offset 是每一代 leader 寫入的第一條消息的位移值,比如:
(0, 0)
以上第二個版本是從位移 300 開始寫入消息随闪,意味著第一個版本寫入了 0-299 的消息阳似。
leader epoch 具體的工作機制如下:
- 當副本成為 leader 時:
這時,如果此時生產(chǎn)者有新消息發(fā)送過來铐伴,會首先新的 leader epoch 以及 LEO 添加到 leader-epoch-checkpoint 文件中障般。
- 當副本變成 follower 時:
- 發(fā)送 LeaderEpochRequest 請求給 leader 副本,該請求包括了 follower 中最新的 epoch 版本盛杰;
- leader 返回給 follower 的相應(yīng)中包含了一個 LastOffset,如果 follower last epoch = leader last epoch藐石,則 LastOffset = leader LEO即供,否則取大于 follower last epoch 中最小的 leader epoch 的 start offset 值,舉個例子:假設(shè) follower last epoch = 1于微,此時 leader 有 (1, 20) (2, 80) (3, 120)逗嫡,則 LastOffset = 80;
- follower 拿到 LastOffset 之后株依,會對比當前 LEO 值是否大于 LastOffset驱证,如果當前 LEO 大于 LastOffset,則從 LastOffset 截斷日志恋腕;
- follower 開始發(fā)送 fetch 請求給 leader 保持消息同步抹锄。
基于 leader epoch 的工作機制,我們接下來看看它是如何解決水印備份缺陷的:
(1)解決數(shù)據(jù)丟失:
如上圖所示,A 重啟之后伙单,發(fā)送 LeaderEpochRequest 請求給 B获高,由于 B 還沒追加消息,此時 epoch = request epoch = 0吻育,因此返回 LastOffset = leader LEO = 2 給 A念秧,A 拿到 LastOffset 之后,發(fā)現(xiàn)等于當前 LEO 值布疼,故不用進行日志截斷摊趾。就在這時 B 宕機了,A 成為 leader游两,在 B 啟動回來后砾层,會重復(fù) A 的動作,同樣不需要進行日志截斷器罐,數(shù)據(jù)沒有丟失梢为。
(2)解決數(shù)據(jù)不一致/離散:
如上圖所示,A 和 B 同時宕機后轰坊,B 先重啟回來成為分區(qū) leader铸董,這時候生產(chǎn)者發(fā)送了一條消息過來,leader epoch 更新到 1肴沫,此時 A 啟動回來后粟害,發(fā)送 LeaderEpochRequest(follower epoch = 0) 給 B,B 判斷 follower epoch 不等于 最新的 epoch颤芬,于是找到大于 follower epoch 最小的 epoch = 1悲幅,即 LastOffset = epoch start offset = 1,A 拿到 LastOffset 后站蝠,判斷小于當前 LEO 值汰具,于是從 LastOffset 位置進行日志截斷,接著開始發(fā)送 fetch 請求給 B 開始同步消息菱魔,避免了消息不一致/離散的問題留荔。