博文推薦|深入解析 BookKeeper 多副本協議(一)

ApachePulsar發表於2022-04-08
本文翻譯自《A Guide to the BookKeeper Replication Protocol (TLA+ Series Part 2)》,作者 Jack Vanlightly。原文連結:https://medium.com/splunk-maa...

譯者簡介

王嘉凌@中國移動雲能力中心,移動雲Pulsar產品負責人,Apache Pulsar Contributor,活躍於 Apache Pulsar 等開源專案和社群

我們知道關係型資料庫中的資料是按表結構來儲存,客戶端可以將資料儲存到表中以及從表中讀取資料。Apache BookKeeper 中的資料是按日誌結構來儲存,客戶端以日誌的形式讀寫資料。日誌結構是一種只支援資料追加操作的簡單資料結構,支援多個客戶端同時讀取,以及非破壞性讀取。

作為資料結構,日誌和佇列的功能非常相似,區別在於日誌支援多個客戶端同時獨立地從不同位置讀取完整的資料。因此,日誌必須支援非破壞性讀取。而佇列則是破壞性讀取, 佇列的頭部元素被讀取後會被刪除。這意味著佇列中的每個元素只會被一個客戶端讀取到。

作為 Apache Pulsar 資料儲存層的 Apache BookKeeper,本身也是一個複雜的分散式系統。BookKeeper 利用多副本機制來實現資料的安全和高可用。多副本指的是每一份 entry 資料都會被複制到多個節點儲存,以便在發生部分節點故障時仍然可以提供讀寫服務,並且保證已儲存的資料不會丟失。BookKeeper 使用一套獨有的多副本協議,這個協議規定了多個服務節點之間如何協同來實現服務的高可用以及保證資料的安全。

基於分片的日誌資料結構

諸如 Apache Kafka 和 RabbitMQ 這樣使用基於佇列和日誌的訊息佇列,都是將每個佇列或分割槽的資料視為一個整體來儲存,這樣一來整個資料必須全部儲存在同一個儲存節點。BookKeeper 使用了一套基於分片的日誌資料結構,每個日誌資料由一系列的分片資料(Segment)串聯組成。Pulsar 的一個 Topic 分割槽 資料實際上是分為多個資料分片來儲存。

我們知道每個 Pulsar Topic 都有一個唯一的 Pulsar broker 作為 owner,這個 broker 負責給所屬的 Topic 建立資料分片,並將這些資料分片進行串聯以便在邏輯上組成一個完整的日誌資料。



圖1:Pulsar Topic 的資料由一組資料分片串聯組成

BookKeeper 將這些資料分片稱為 Ledger,並將它們儲存在 BookKeeper server 節點(稱為 bookie 節點)。



圖2:Pulsar broker 將 topic 資料儲存到多個 Bookie 節點

BookKeeper 多副本協議和每個 ledger 的生命週期息息相關。多副本協議本身的實現封裝在 BookKeeper 客戶端類庫中, 每個 Pulsar broker 通過呼叫BookKeeper 客戶端類庫中的介面來和 BookKeeper 進行互動,如建立 ledger,關閉 ledger,以及讀寫 entry。這些介面背後包含了非常複雜的協議邏輯,在本篇部落格中我們會逐層分析並展示協議的實現細節。

首先,建立 ledger 的客戶端即為這個 ledger 的唯一owner,只有 owner 可以往 ledger 裡寫資料。對於 Pulsar 來說,這個客戶端就是作為分割槽 Topic owner 的 broker,Broker 負責建立 ledger 來組成這個 Topic 的資料段。當這個客戶端由於某些原因發生故障時,另一個客戶端(對於 Pulsar 來說就是另一個 broker)會介入並接管這個 Topic,這個時候需要修復之前的 ledger 中處於正在複製(under-replicated )狀態的 entry 資料(即 recovery 操作)並將 ledger 關閉。



圖3: Ledger 的生命週期

每個 Pulsar topic 僅包含一個 open 狀態的 ledger 和多個 close 狀態的 ledger。所有的寫操作都會寫入到 open 狀態的 ledger,而讀操作則可以從任何 ledger 中讀取資料。



圖4: 寫操作只會寫入到 open 狀態的 ledger

每個 ledger 都會儲存到多個 bookie 節點上,每個 ledger 和存有這個 ledger 的 bookie 池(稱為 ensemble)的對應關係儲存在 ZooKeeper。當 open 狀態的 ledger 大小達到了閾值,或者這個 ledger 的 owner 發生了故障,就會關閉這個 ledger 並重新建立一個新的 ledger。根據配置的多副本引數,新建立的 ledger 可能會被儲存到另一組 bookie 池上。



圖5:Ledger 資料的多個副本儲存在多個 bookie 節點,每個 Ledger 的後設資料以及一個 Topic 包含的 ledger 資訊儲存在 ZooKeeper

資料寫入 ledger 的過程

BookKeeper 包含以下 ledger 多副本配置相關的引數:

  • Write quorum (每份 entry 資料需要寫入多少個 bookie 節點), 簡稱 WQ。
  • Ack Quorum (需要從多少個 bookie 節點收到寫入成功的響應後可以確認這份 entry 寫入成功 ), 簡稱 AQ。
  • Ensemble size (用於儲存 ledger 資料的 bookie 池的節點數量), 簡稱 E。當 E > WQ 時,entry 資料會交錯地寫入到不同的 bookie 節點。

一條 entry 資料實際寫入的 bookie 節點的集合成為寫入集合。當 E > WQ 時,相鄰的 entry 的寫入集合可能會不一樣。

Pulsar 為每個 Topic 暴露了設定 AQ、WQ、E 引數的 API 來自定義副本設定。



圖6:WQ=3,AQ=2 時的訊息寫入和確認

最後新增確認 (Last Add Confirmed, LAC)

BookKeeper 客戶端會持續更新已確認寫入的 entry 中連續且最高的 entry ID,我們稱之為 Last Add Confirmed (LAC)。這是一條水位線,高於這個 entry ID 的 entry 都還沒有被確認寫入,而低於和等於這個 entry ID 的 entry 都已經被確認寫入。每一條發往 bookie 的 entry 資料中包含了當前最新的 LAC,這樣每個 bookie 都可以知道當前 LAC 的值,儘管有一些延遲存在。我們還會在下文看到 LAC 除了作為已提交 entry 的水位線,還發揮著其他作用。

Ledger 資料段

Ledger 本身也可以分成一個或多個資料段(fragment)。當 Ledger 建立時,包含了一個資料段,分配了一個 bookie 池用於儲存這個 Ledger 的資料。當發生某個 bookie 寫入失敗時,客戶端會用一個新的 bookie 來替代。這個時候會建立一個新的資料段,並重新傳送未確認的 entry 資料和之後的 entry 資料到新的 bookie 上。當 bookie 再次寫入失敗時,又會再次建立一個新的資料段,以此類推。Bookie 寫入失敗並不意味著這個 bookie 節點不可用,網路波動等其他情況也會造成單次的寫入失敗。不同資料段的資料儲存在不同的 bookie 池上。資料段也通常被認為是寫入集合(Ensemble)。



圖7:第二個資料段的建立過程

Ledger 資料段可以看作是告訴 BookKeeper 客戶端去哪裡找到某個 ledger 中的 entry 資料的後設資料。Bookie 節點自身是不知道這些後設資料資訊的,它們只負責儲存接收到的 entry 資料並建立基於 ledger ID 和 entry ID 的索引。



圖8:往 B3 bookie 節點寫入 entry 1000 失敗並導致 ledger 建立第二個資料段

從 ledger 讀取資料的過程

從 ledger 中讀取資料的操作分為以下幾種情況:

  • 正常讀取 entry 資料
  • 長輪詢讀取 LAC 資料
  • Quorum LAC 機制下的讀取資料
  • 恢復性讀取資料

和寫資料不一樣的是,我們只需要讀取一個存有資料的 bookie 節點就可以得到想要的資料。如果這次讀取失敗了,也只需要從存有這個資料其他副本的 bookie 節點上重新讀取資料即可。

客戶端通常只希望讀取到已確認的資料,所以只會讀取到 LAC 值標識的位置。在讀取歷史資料時,bookie 節點會依據當前的 LAC 值來通知客戶端何時停止讀取。當客戶端讀取到 LAC 值並停止讀取時,可以發起長輪詢讀取 LAC 資料。這個請求會先被 bookie 掛起,直到有新的 entry 資料被確認時才響應並返回新的 entry 資料。

另外兩種讀取資料的情況主要發生在資料修復時,我們稍後再介紹。

完成不同的操作需要不同的響應數量

完成不同的操作需要從 bookie 節點接收到的成功響應的數量不一樣。比如,對於正常讀資料的操作,只需要從一個 bookie 節點成功收到響應即可完成。而有些操作則需要從多個 bookie 節點(quorum)收到成功的響應才可完成。

這些操作根據需要收到響應數量的不同,可以分為以下幾種型別:

  • Ack quorum (AQ)
  • Write quorum (WQ)
  • Quorum Coverage (QC) QC = (WQ - AQ) + 1
  • Ensemble Coverage (EC) EC = (E - AQ) + 1

Quorum Coverage (QC) 和 Ensemble Coverage (EC) 都滿足於以下定義(以下兩種定義本質上相同,只是說法不同),QC 和 EC 的區別僅在於“集合”的範圍 :

  • 對於指定請求,從足夠多的 bookie 節點收到成功響應,使得在給定集合中 ack quorum(AQ)數量的 bookie 節點組成的任意組合中,都至少包含一個收到成功響應的 bookie 節點。
  • 對於指定請求,從足夠多的 bookie 節點收到成功響應,使得在給定集合中不存在 ack quorum(AQ)數量的 bookie 節點沒有收到成功響應。

對於 Quorum Coverage (QC) 來說,這個集合是指某個 entry 的寫入集合。QC 主要用於保證單個 entry 資料一致性的場景,如校驗單個 entry 寫入操作是否已被客戶端確認。對於Ensemble Coverage (EC) 來說,這個集合是指儲存當前 ledger 資料段對應的 bookie 池,EC 主要用於保證 ledger 資料段一致性的場景,如設定 ledger 的 fence 狀態。

WQ 和 AQ 主要用於寫資料,而 QC 和 EC 主要用於 ledger 修復過程。

Ledger 修復的過程

前面我們講到每個 ledger 只有一個客戶端作為 owner,當這個客戶端不可用時,另一個客戶端就會介入並觸發 ledger 修復過程然後關閉這個 ledger。對於 Pulsar 來說就相當於作為一個 Topic owner 的 broker 變得不可用,然後這個 Topic 的所有權轉移到另一個 broker 上。

Ledger 修復過程包括找到最高的已被 bookie 確認的 entry ID,保證在這之前的每個 entry 都已複製了足夠多的副本數量。之後將這個 ledger 關閉,此時會將這個 ledger 的狀態設定為 CLOSED,並將最新的 entry ID 設定為最後被確認的 entry ID。

如何防止腦裂

BookKeeper 是一個分散式系統,這意味著網路波動可能會導致叢集被分隔成兩個或者更多的區塊。我們設想如果一個客戶端和 ZooKeeper 斷開連線,那麼這個客戶端就被認為已不可用,另一個客戶端會接管這個客戶端負責的 ledger 並開始 ledger 修復流程。但這個客戶端可能仍在正常執行,它可以正常的連線到 BookKeeper 叢集,於是就會出現兩個客戶端試圖同時操作同一個 ledger,這種情況就屬於腦裂。腦裂是指一個分散式系統由於網路波動分裂為多個獨立的系統,在一定時間後網路恢復導致的資料不一致的情況。

BookKeeper 引入了 fence 這個概念來防止腦裂的發生。當第二個客戶端 (例如另一個 Pulsar broker)試圖開始 ledger 修復流程時,會先將 ledger 設定為 fence 狀態,在這個狀態下 ledger 會拒接所有新的寫入請求。當足夠多的 bookie 節點將這個 ledger 狀態設定為 fence 時,就算第一個客戶端仍然處於正常執行狀態,它也不能再進行任何新的寫入操作。然後第二個客戶端就可以在沒有其他客戶端會繼續寫入資料或者試圖修復同一個 ledger 的安全狀態下開始 ledger 修復流程。



圖9:一個新的 Topic owner 開始將 ledger 設定為 fence,原先的 owner 寫入新資料時無法寫入 Ack Quorum 設定的副本數,則無法完成寫入

修復流程第一步 — 設定 fence 狀態

將 ledger 設為 fence 狀態,並確認 LAC 的值。

Fence 請求實際上是一次 Ensemble Coverage 型別的讀請求,獲取 LAC 的值並帶有 fencing 標識。每個 bookie 節點收到這個請求時會將這個 ledger 的狀態設為 fence,並返回這個節點上對應 ledger 的 LAC 值。當客戶端從足夠多的 bookie 節點收到響應時,就表示請求成功可以進行下一步操作。那麼從多少個 bookie 節點收到響應才算足夠呢?

我們將 ledger 設定為 fence 狀態是為了防止之前的客戶端繼續往 ledger 裡寫入資料。所以我們只要保證還沒有將這個 ledger 設定為 fence 狀態的 bookie 節點的數量小於設定的 Ack Quorum 值,那麼之前的客戶端因為無法收到足夠多的寫入確認而無法寫入新資料。新的客戶端發起的 fence 操作不需要等到所有的 bookie 節點都將這個 ledger 設定為 fence,只需要滿足還沒有設定為 fence 狀態的 bookie 節點數小於設定的 Ack Quorum 就可確認 fence 操作完成。滿足這個條件所需要收到的響應數量就是 Ensemble Coverage。

修復流程第二步 — 修復 entry 資料

接下來,客戶端從 LAC + 1 的 entry ID 開始傳送恢復性讀取資料的請求,並將這些 entry 資料重新寫到新的 bookie 池中。寫操作屬於冪等操作,也就是說如果這個 entry 已經寫入到了某個 bookie 節點,再次向這個節點寫入同樣的 entry 不會造成資料重複寫入。客戶端會持續進行讀和寫的操作直到讀完所有資料。確保在關閉 ledger 之前,這個 entry 的寫入集合中的所有 bookie 節點都寫入了該 entry 的副本。

正常的讀操作只需要從一個 bookie 節點接收到響應。與之不同的是,Recovery讀操作需要根據從這個 entry 的所有寫入集合的 bookie 節點上收到的響應內容來明確這個 entry 是否已確認。具體來說有以下兩種情況:

  • 已確認 :收到 Ack Quorum 數量的成功響應
  • 未確認 :收到 Quorum Coverage 數量的資料不存在響應 (已寫入這個 entry 資料的 bookie 節點數量未達到 Ack Quorum)

如果所有響應都已收到,但兩個閾值都未達到,那就無法判斷這個 entry 是否已確認,修復流程就會終止(可能存在收到其他錯誤型別響應的情況,如網路波動,這種情況無法判斷 entry 是否已成功寫入對應 bookie 節點)。修復流程可以重複執行直到可以明確每個 entry 最終的確認狀態。



圖10:新的客戶端在讀取 entry 3 時收到了足夠多的資料不存在請求,可以判斷 entry 3 的狀態為未確認。然後保證到 entry 2 為止的資料都複製到足夠多的副本數

修復流程第三步 — 關閉 Ledger

一旦明確了所有已確認的 entry ,且這些 entry 複製了足夠多的副本數,客戶端就會關閉 ledger。關閉 ledger 的操作主要是對 ZooKeeper 上 ledger 後設資料的更新,將狀態設定為 CLOSED,並將 Last Entry Id 設定為最新的已確認的 entry ID。這些操作和 bookie 本身不相關,bookie 也不會感知 ledger 是否被關閉,bookie 自身沒有 open 或 closed 的概念。

ZooKeeper 上後設資料的更新是一個基於版本控制的 CAS 操作。如果有另一個客戶端同時在修復這個 ledger 並且已經將 ledger 關閉,那麼這次 CAS 操作就會失敗。通過這種方式可以防止多個客戶端同時對同一個 ledger 進行修復操作。

總結

本篇部落格介紹了 BookKeeper 多副本協議的大部分實現內容。需要記住的重點是,bookie 節點只是單純用來儲存和讀取 entry 資料的儲存節點,在 BookKeeper 客戶端中包含了建立 ledger、選擇儲存 ledger 的 bookie 池、建立 ledger 資料段的操作,通過 Write Quorum 和 Ack Quorum 來保證多副本的機制,以及在發生故障時對 ledger 進行修復和關閉等一系列邏輯。

相關閱讀


? Pulsar Storage 特別興趣小組(SIG)已成立!掃描下方?️ Pulsar Bot 二維碼,回覆 BookKeeper 加入 Pulsar Storage 討論群。



掃碼加入

關注公眾號「Apache Pulsar」,獲取更多技術乾貨

相關文章