BookKeeper 是一個可擴充套件、可容錯和低延遲的儲存服務;本文主要介紹其基本概念及特性。
1、基本概念
在 BookKeeper 中:
- 日誌的單元是 entry (又名 record)
- 日誌 entries 流稱為 ledgers
- 儲存 ledgers 的獨立伺服器稱為 bookies
BookKeeper 被設計為可靠且能夠抵禦各種故障。bookies 可能會崩潰、損壞資料或丟棄資料,但只要在整個集合中有足夠數量的 bookies 正常工作,服務將能夠正常執行。
1.1、Entries
Entries 包含寫入 ledgers 的資料資料,以及一些重要的後設資料。Entries 是寫入 ledgers 的位元組序列,每個 entry 有如下欄位:
欄位 | Java 型別 | 說明 |
Ledger number | long | Ledger ID |
Entry number | long | Entry ID |
Last confirmed (LC) | long | 最後提交的 Entry ID |
Data | byte[] | entry 的資料 |
Authentication code | byte[] | 訊息認證碼,包含 entry 中的所有其他欄位 |
1.2、Leaders
Ledgers 是 BookKeeper 中的基本儲存單元。Ledgers 是一系列 entries 的序列,每個 entry 是一系列位元組。entries 按以下方式寫入 ledges:
- 順序寫入
- 最多寫入一次
這意味著 ledger 具有僅追加的語義。一旦條目被寫入ledger,就不能修改。確定正確的寫入順序是客戶端應用程式的責任。
1.3、Bookies
Bookies 是處理 ledger(更具體地說是 ledger 的片段)的單個 BookKeeper 伺服器。Bookies 作為集合的一部分執行。一個 bookie 是一個獨立的 BookKeeper 儲存伺服器。獨立的 bookies 儲存 ledger 的片段,而不是整個ledger(出於效能考慮)。對於給定的 ledger L,一個集合是儲存 L 中 entries 的 bookies 組。每當 entries 被寫入 ledger 時,這些 entries 會被分散式地儲存在叢集中的 bookies 的子組中,而不是儲存在所有的 bookies 上。
1.4、後設資料儲存
BookKeeper 後設資料維護著 BookKeeper 叢集的所有後設資料,包括 ledger 後設資料、可用的 bookie 等等。目前,BookKeeper 使用 ZooKeeper 進行後設資料儲存。
1.5、bookies 中的資料管理
Bookies 以一種日誌結構的方式管理資料,透過三種型別的檔案來實現:journals、entry logs、index files。
1.5.1、Journals
一個 journal 檔案包含了 BookKeeper 的事務日誌。在對 ledger 進行任何更新之前,bookie 確保將描述該更新的事務寫入到非易失性儲存中。一旦 bookie 啟動或者舊的 journal 檔案達到檔案大小閾值,就會建立一個新的 journal 檔案。
1.5.2、Entry logs
一個 entry 日誌檔案管理來自 BookKeeper 客戶端的已寫入條目。來自不同 ledgers 的 entries 被聚合並按順序寫入,而它們的偏移量被儲存在 ledger 快取中作為指標,以便進行快速查詢。
一旦 bookie 啟動或者舊的 entry 日誌檔案達到檔案大小閾值,就會建立一個新的 entry 日誌檔案。一旦舊的 entry 日誌檔案與任何活躍的 ledger 都沒有關聯,垃圾收集執行緒就會刪除它們。
1.5.3、Index files
為每個 ledger 建立一個索引檔案,它包括一個頭部和若干固定長度的索引頁,記錄了儲存在 entry 日誌檔案中的資料的偏移量。
由於更新索引檔案會引入隨機磁碟 I/O,索引檔案透過後臺執行的同步執行緒進行延遲更新。這確保了更新的快速效能。在索引頁被持久化到磁碟之前,它們會被放到 ledger 快取以便查詢。
1.5.4、Ledger cache
Ledger 索引頁被快取在記憶體池中,這使得磁碟頭排程的管理更加高效。
1.5.5、Adding entries
當客戶端需要將一個 entry 寫入 ledger 時,entry 需經過如下幾個步驟才能持久化到磁碟上:
- entry 被追加到 entry 日誌中
- entry 的索引在 ledger 快取中被更新
- 與此 entry 更新對應的事務被追加到 journal 中
- 響應被髮送給 BookKeeper 客戶端
出於效能考慮,entry 日誌在記憶體中緩衝 entry 並批次提交它們,而 ledger 快取則在記憶體中儲存索引頁並延遲重新整理它們。
1.5.6、Data flush
Ledger 索引頁在以下兩種情況下會重新整理到索引檔案中:
- 當達到 ledger 快取的記憶體限制時。沒有更多空間可用來儲存新的索引頁。髒索引頁將被從 ledger 快取中驅逐,並持久化到索引檔案中。
- 後臺執行緒負責定期從 ledger 快取中將索引頁重新整理到索引檔案中。
除了重新整理索引頁之外,同步執行緒還負責在 journal 檔案使用過多磁碟空間的情況下滾動 journal 檔案。同步執行緒中的資料重新整理流程如下:
- 1、在記憶體中記錄 LastLogMark。LastLogMark 表示其之前的所有 entries 都已被持久化(包括索引和 entry 日誌檔案),包含兩個部分:
txnLogId(journal 檔案ID)
txnLogPos(journal 檔案中的偏移量)
- 2、從 ledger 快取中重新整理髒索引頁到索引檔案,並重新整理 entry 日誌檔案以確保所有快取的 entries 都已持久化到磁碟。
理想情況下,一個 bookie 只需要重新整理包含在 LastLogMark 之前的索引頁和 entry 日誌檔案。但是,在與 journal 檔案對應的 ledger 和 entry 日誌中沒有這樣的資訊。因此,執行緒在這裡完全重新整理了 ledger 快取和 entry 日誌,可能會重新整理 LastLogMark 之後的 entry。儘管重新整理更多 entry 不是問題,但是多餘的。
- 3、LastLogMark 被持久化到磁碟,這意味著在 LastLogMark 之前新增的 entry 資料和索引頁也已被持久化到磁碟。現在是安全地刪除早於 txnLogId 的 journal 檔案的時候了。
如果在將 LastLogMark 持久化到磁碟之前,bookie 崩潰了,journal 檔案仍然包含可能尚未持久的 entries。因此,當 bookie 重新啟動時,它會檢查 journal 檔案以恢復這些 entries,資料不會丟失。
使用上述資料重新整理機制,當 bookie 關閉時,同步執行緒跳過資料重新整理是安全的。然而,在 entry 記錄器中,它使用一個快取通道來批次寫入 entries,而在關閉時快取通道中可能有資料。bookie 需要確保在關閉時重新整理快取通道中的 entry 資料。否則,entry 日誌檔案將因缺少部分 entries 而損壞。
1.5.7、Data compaction
在 bookies 中,不同 ledgers 的 entries 被交錯儲存在 entry 日誌檔案中。每個 bookie 執行一個垃圾收集器執行緒來刪除未關聯的 entry 日誌檔案,以回收磁碟空間。如果某個 entry 日誌檔案包含尚未刪除的 ledger 的 entries,則該 entry 日誌檔案將永遠不會被移除,佔用的磁碟空間也永遠無法回收。為了避免這種情況,bookie 伺服器在垃圾收集器執行緒中壓縮 entry 日誌檔案,以回收磁碟空間。
有兩種不同頻率執行的壓縮:次要壓縮和主要壓縮。次要壓縮和主要壓縮之間的區別在於它們的閾值和壓縮間隔。
- 垃圾收集閾值是未刪除的 ledgers 佔據的 entry 日誌檔案大小的百分比。預設的次要壓縮閾值為 0.2,而主要壓縮閾值為 0.8。
- 垃圾收集間隔是執行壓縮的頻率。預設的次要壓縮間隔為 1 小時,而主要壓縮間隔為 1 天。
如果閾值或間隔設定為小於或等於零,壓縮將被禁用。
垃圾收集器執行緒中的資料壓縮流程如下:
- 該執行緒掃描 entry 日誌檔案以獲取 entry 後設資料,其中記錄了包含 entry 資料的 ledgers 列表。
- 在正常的垃圾收集流程中,一旦 bookie 確定某個 ledger 被刪除,該 ledger 將從 entry 後設資料中刪除,同時 entry 日誌的大小將減少。
- 如果 entry 日誌檔案的剩餘大小達到指定的閾值,entry 日誌檔案中活躍的 ledgers 的 entries 將被複制到一個新的 entry 日誌檔案中。
- 一旦所有有效 entries 都已複製,舊的 entry 日誌檔案將被刪除。
1.5.8、ZooKeeper metadata
BookKeeper 需要安裝 ZooKeeper 來儲存 ledger 後設資料。構建 BookKeeper 客戶端物件時,需要將 ZooKeeper 伺服器列表作為引數傳遞給建構函式,如下所示:
String zkConnectionString = "127.0.0.1:2181"; BookKeeper bkClient = new BookKeeper(zkConnectionString);
1.5.9、Ledger manager
ledger 管理器處理 ledger 的後設資料(儲存在 ZooKeeper 中)。BookKeeper 提供了兩種型別的 ledger 管理器:flat ledger manager 和 hierarchical ledger manager。這兩種 ledger 管理器都擴充套件了 AbstractZkLedgerManager 抽象類。
A、Hierarchical ledger manager
預設的 ledger 管理器。Hierarchical ledger manager 能夠管理大量的 BookKeeper 賬本(> 50,000)。
在 HierarchicalLedgerManager 類中實現了 hierarchical ledger manager,首先使用 EPHEMERAL_SEQUENTIAL znode 從 ZooKeeper 獲取全域性唯一識別符號。由於 ZooKeeper 的序列計數器採用 %10d(10位數字,用 0 填充,例如 <path>0000000001)的格式,hierarchical ledger manager 將生成的 ID 分為 3 部分:
{level1 (2 digits)}{level2 (4 digits)}{level3 (4 digits)}
這三個部分被用來構成實際的 ledger 節點路徑,以儲存 ledger 後設資料:
{ledgers_root_path}/{level1}/{level2}/L{level3}
例如,ledger 0000000001 被分為三個部分,分別是 00、0000 和 00001,然後儲存在 znode "/{ledgers_root_path}/00/0000/L0001" 中。每個 znode 可以包含多達 10,000 個 ledger,這避免了子列表大於最大 ZooKeeper 資料包大小的問題(正是這個限制促使建立 Hierarchical ledger manager)。
B、Flat ledger manager
自 4.7.0 版本起已被棄用,現在不建議使用。
在 FlatLedgerManager 類中實現了 Flat ledger manager,它將所有 ledger 的後設資料儲存在單個 ZooKeeper 路徑的子節點中。Flat ledger manager 建立順序節點以確保 ledger ID 的唯一性,並將所有節點字首設定為 L。Bookie 伺服器在雜湊對映中管理其自己的活動 ledgers,以便輕鬆查詢已從 ZooKeeper 中刪除的 ledgers,然後對其進行垃圾回收。
2、Bookkeeper 特性
2.1、節點對等架構
建立 ledger 時有 3 個控制資料寫入方式的引數:ensSize 選擇幾個節點儲存這個 ledger,writeQuorumSize 控制資料寫幾個副本(併發寫,不同於 Kafka 或 HDFS,BookKeeper 資料節點之間沒有主從關係,資料同步從服務端移到了客戶端),ackQuorumSize 表示幾個副本 ack 就表示寫入成功。
以下圖為例,createLedger(5,3,2),在儲存 ledger 時,選擇了 5 個節點,但是隻寫 3 個副本,2 個副本 ack 就表示寫入成功。第一個引數一般可用於調整併發度,因為寫 3 個副本是透過輪轉的方式寫入,例如第 1 個 entry 是寫 1-3 節點,第 2 個 entry 寫 2-4 節點,第 3 個 entry 寫 3-5 節點,第 4 個 entry 寫 4-5 和 1 節點這樣輪轉。這種方式即便 3 個副本,也可以把 5 個節點都用起來。
這幾個引數可讓使用者透過機架感知、機房感知等各種方式進行靈活設定。當選好節點後節點之間的排序就已完成,每個 entry 會帶個 index,index 和節點已有繫結關係,例如 index 為1 的,都放在 123上,為 2 的都放在 234 上。透過這種方式可以讓我們知道每個節點存了哪些訊息,當某個節點當機,根據這個節點的位置資訊,把對應 entry 還在哪些節點上有副本的資訊找出來進行多對多的恢復。這麼做的另一個好處是不用再維護後設資料資訊,只需要有每個節點 entry index 資訊,在 createLedger 時記錄好每個節點的順序即可。
createLedger(5,3,2)資料儲存結構是下圖中右邊的結構,如果選擇 ensSize=3,writeQuorumSize=3,資料儲存結構就是下圖中左邊的結構:
綜上,可以透過 ensSize 來調整讀寫頻寬,透過 writeQuorumSize 調整強一致性的控制,透過 ackQuorumSize 權衡在有較多副本時也可以有較低的時延(但一致性就可能有一定的損失)。
2.2、可用性
A、讀可用性
讀的訪問是對等的,任意一個節點返回就算讀成功。這個特性可以把延遲固定在一個閾值內,當遇到網路抖動或壞節點,透過延遲引數避障。例如讀的延遲時間為 2ms,讀節點 3 時超過 2ms,就會併發地讀節點 4,任意一個節點返回就算讀成功,如上面圖 1 中 Reader 部分。
B、寫可用性
在 createLedger 時會記錄每個節點的順序,假如寫到 5 節點當機,會做一次後設資料的變更,從這個時間開始,先進行資料恢復,同時新的 index 中會把 5 節點變為 6 節點,如上面圖 1 中 x 節點替換 5 節點。
2.3、一致性
BookKeeper 底層節點對等設計讓寫入資料的 Writer 成為了協調者,Writer 來儲存資料是否儲存成功的狀態,例如節點是否出現問題、副本夠不夠、在寫入過程中出現當機時透過 fencing 的方式防止腦裂等。所以,Writer 維護了 2 個 index:LastAddPushed 和 LastAddConfirmed。
LastAddPushed 記錄最後寫入的 entry;LastAddConfirmed 記錄最後連續成功寫入的 entry,需要確保 LastAddConfirmed 之前的 entry 都已寫入成功,例如有 id 為 1、2 、3 的 entry,1 和 3 的 entry 先返回成功了,2 的 entry 還未返回,按連續的規則 LastAddConfirmed 是 1 而不是 3。
2.4、讀寫分離
下圖是每個資料節點的資料流轉過程,資料寫入時,Writer 透過 append only 方式寫入到 Journal,Journal 在把資料寫到記憶體的同時會按一定頻率(預設 1ms 或 500 byte)把資料持久化到 Journal Device 裡,寫完後會告訴 Writer 這個節點寫入成功了(持久化到磁碟是預設配置)。
讀的時候,如果讀最新的資料,可以直接從記憶體中返回,如果讀歷史資料,也只去讀資料盤,不會對 Journal Device 寫入有影響。這樣針對有讀瓶頸或寫瓶頸的使用者,可以把 Journal Disk 或 Ledger Disk換成 SSD 盤,提升效能,並且防止讀寫的互相干擾。
參考:
https://bookkeeper.apache.org/docs/overview/
https://cloud.tencent.com/developer/article/2054521