關於 Apache Pulsar
Apache Pulsar 是 Apache 軟體基金會頂級專案,是下一代雲原生分散式訊息流平臺,集訊息、儲存、輕量化函式式計算為一體,採用計算與儲存分離架構設計,支援多租戶、持久化儲存、多機房跨區域資料複製,具有強一致性、高吞吐、低延時及高可擴充套件性等流資料儲存特性。
GitHub 地址:http://github.com/apache/pulsar/
作者介紹
本文作者:鮑明宇,騰訊 TEG 資料平臺部高階工程師,Apache Pulsar Contributor,熱衷於開源技術,在訊息佇列領域有豐富經驗,目前致力於 Pulsar 的落地和推廣。
騰訊資料平臺部 MQ 團隊對 Pulsar 做了深入調研以及大量的效能和穩定性方面優化,目前已經在 TDbank 落地上線。本文是 Pulsar 技術系列中的一篇,主要簡單梳理了 Pulsar 訊息儲存與 BookKeeper 儲存檔案的清理機制。其中,BookKeeper 可以理解為一個 NoSQL 的儲存系統,預設使用 RocksDB 儲存索引資料。
Pulsar 訊息儲存
Pulsar 的訊息儲存在 BookKeeper 中,BookKeeper 是一個胖客戶的系統,客戶端部分稱為 BookKeeper,伺服器端叢集中的每個儲存節點稱為 bookie。Pulsar 系統的 broker 作為 BookKeeper 儲存系統的客戶端,通過 BookKeeper 提供的客戶端 SDK 將 Pulsar 的訊息儲存到 bookies 叢集中。
Pulsar 中的每個 topic 的每個分割槽(非分割槽 topic,可以按照分割槽 0 理解,分割槽 topic 的編號是從 0 開始的),會對應一系列的 ledger,而每個 ledger 只會儲存對應分割槽下的訊息。對於每個分割槽同時只會有一個 ledger 處於 open 即可寫狀態。
Pulsar 在生產訊息,儲存訊息時,會先找到當前分割槽使用的 ledger,然後生成當前訊息對應的 entry ID,entry ID 在同一個 ledger 內是遞增的。非批量生產的情況(producer 端可以配置這個引數,預設是批量的),一個 entry 中包含一條訊息。批量方式下,一個 entry 可能包含多條訊息。而 bookie 中只會按照 entry 維度進行寫入、查詢、獲取。
因此,每個 Pulsar 下的訊息的 msgID 需要有四部分組成(老版本由三部分組成),分別為(ledgerID,entryID,partition-index,batch-index),其中,partition-index 在非分割槽 topic 的時候為 -1,batch-index 在非批量訊息的時候為 -1。
每個 ledger,當存在的時長或儲存的 entry 個數超過閾值後會進行切換,同一個 partition 下的,新的訊息會儲存到下一個 ledger 中。Ledger 只是一個邏輯概念,是資料的一種邏輯組裝維度,並沒有對應的實體。
BookKeeper 叢集中的每個 bookie 節點收到訊息後,資料會分三部分進行儲存處理,分別為:journal 檔案、entryLog 檔案、索引檔案。
其中 journal 檔案,entry 資料是按照 wal 方式寫入的到 journal 檔案中,每個 journal 檔案有大小限制,當超過單個檔案大小限制的時候會切換到下一個檔案繼續寫,因為 journal 檔案是實時刷盤的,所以為了提高效能,避免相互之間的讀寫 IO 相互影響,建議儲存目錄與儲存 entrylog 的目錄區分開,並且給每個 journal 檔案的儲存目錄單獨掛載一塊硬碟(建議使用 ssd 硬碟)。journal 檔案只會儲存儲存幾個,超過配置個數的檔案將會被刪除。entry 儲存到 journal 檔案完全是隨機的,先到先寫入,journal 檔案是為了保證訊息不丟失而設計的。
如下圖所示,每個 bookie 收到增加 entry 的請求後,會根據 ledger id 對映到儲存到那個 journal 目錄和 entry log 目錄,entry 資料會儲存在對應的目錄下。目前 bookie 不支援在執行過程中變更儲存目錄(使用過程中,增加或減少目錄會導致部分的資料查詢不到)。
如下圖所示,bookie 收到 entry 寫入請求後,寫入 journal 檔案的同時,也會儲存到 write cache 中,write cache 分為兩部分,一部分是正在寫入的 write cache, 一部分是正在正在刷盤的部分,兩部分交替使用。
write cache 中有索引資料結構,可以通過索引查詢到對應的 entry,write cache 中的索引是記憶體級別的,基於 bookie 自己定義的 ConcurrentLongLongPairHashMap 結構實現。
另外,每個 entorylog 的儲存目錄,會對應一個 SingleDirectoryDbLedgerStorage 類例項物件,而每個 SingleDirectoryDbLedgerStorage 物件裡面會有一個基於 RocksDB 實現的索引結構,通過這個索引可以快速的查到每個 entry 儲存在哪個 entrylog 檔案中。每個 write cache 在增加 entry 的時候會進行排序處理,在同一個 write cache,同一個 ledger 下的資料是相鄰有序的,這樣在 write cache 中的資料 flush 到 entrylog 檔案時,使得寫入到 entrylog 檔案中的資料是區域性有序的,這樣的設計能夠極大的提高後續的讀取效率。
SingleDirectoryDbLedgerStorage 中的索引資料也會隨著 entry 的刷盤而刷盤到索引檔案中。在 bookie 當機重啟時,可以通過 journal 檔案和 entry log 檔案還原資料,保證資料不丟失。
Pulsar consumer 在消費資料的時候,做了多層的快取加速處理,如下圖所示:
獲取資料的順序如下:
- 在 broker 端的 entry cache 中獲取,如果沒有在繼續;
- 在 bookie 的 write cache 正在寫的這部分中獲取,如果沒有則繼續;
- 在 bookie 的 write cache 正在刷盤的這部分中獲取,如果沒有則繼續;
- 從 bookie 的 read cache 中獲取,如果沒有則繼續;
- 通過索引讀取磁碟上的 entry log 檔案。
上面每一步,如果能獲取到資料,都會直接返回,跳過後面的步驟。如果是從磁碟檔案中獲取的資料,會在返回的時候將資料儲存到 read cache 中,另外如果是讀取磁碟的操作,會多讀取一部分磁碟上的時候,因為儲存的時候有區域性有序的處理,獲取相鄰資料的概率非常大,這種處理的話會極大的提高後續獲取資料的效率。
我們在使用的過程中,應儘量避免或減少出現消費過老資料即觸發讀取磁碟檔案中的訊息的場景,以免對整體系統的效能造成影響。
BookKeeper 的 GC 機制
BookKeeper 中的每個 bookie 都會週期的進行資料清理操作,預設 15 分鐘檢查處理一次,清理的主要流程如下:
- 清理 bookie 儲存的 ledger id(bookie 記憶體儲的 ledger id 與 zk 上面儲存的 ledger id 做比較,如果 zk 上面沒有則刪除 bookie 中儲存的 ledger id);
- 統計每個 entry log 中存活的 entry 佔比,當前 entry log 存活的 ledger 個數為 0 時刪除這個 entry log;
- 根據 entry log 的後設資料資訊,清理 entry log 檔案(當 entry log 包含的所有 ledger id 全部失效時刪除);
- 壓縮 entry log 檔案 ,分別在當前 entry log 檔案下存活的 entry 比例在 0.5-預設週期 1 天(major gc) 或比例 0.2-預設週期 1 個小時(minor gc) 的時候,Compaction entry log 檔案,將老的檔案中存活的 entry 轉移新的檔案中,然後將老的 entry log 檔案刪除,單次的 GC 如果處理的 entry log 檔案比較大的時候可能耗時比較長。
通過上面的流程,我們可以瞭解 bookie 在清理 entrylog 檔案時的大體流程。
需要特別說明的是,ledger 是否是可以刪除的,完全是客戶端的觸發的,在 Pulsar 中是 broker 觸發的。
broker 端有周期的處理執行緒(預設 2 分鐘),清理已經消費過的訊息所在的 ledger 機制,獲取 topic 中包含的 cursor 最後確認的訊息,將這個 topic 包含的 ledger 列表中,在這個 id 之前的(注意不包含當前的 ledger id)全部刪除(包括 zk 中的後設資料,同時通知 bookie 刪除對應的 ledger)。
運營中遇到的問題分析
在運用的過程中我們多次遇到了 bookie 磁碟空間不足的場景,bookie 中儲存了大量的 entry log 檔案。比較典型的原因主要有如下兩個。
原因一:
生產訊息過於分散,例如,舉個極端的場景,1w 個 topic,每個 topic 生產一條,1w 個 topic 順序生產。這樣每個 topic 對應的 ledger 短時間內不會因為時長或者儲存大小進行切換,active 狀態的 ledger id 分散在大量的 entry log 檔案中。這些 entry log 檔案是不能刪除或者及時壓縮的。
如果遇到這種場景,可以通過重啟,強制 ledger 進行切換進行處理。當然如果這個時候消費進行沒有跟上,消費的 last ack 位置所在的 ledger 也是處於 active 狀態的,不能進行刪除。
原因二:
GC 時間過程,如果現存的 enrylog 檔案比較多,且大量符合 minor 或 major gc 閾值,這樣,單次的 minor gc 或者 major gc 時間過長,在這段時間內是不能清理過期的 entry log 檔案。
這是由於單次清理流程的順序執行導致的,只有上次一輪執行完,才會執行下一次。目前,這塊也在提優化流程,避免子流程執行實現過長,對整體產生影響。
小結
本文首先,介紹了 Pulsar 訊息的儲存組織形式,儲存流程和訊息的獲取過程。其次,對單個 bookie 的 GC 流程做了詳盡的說明。在 Pulsar 的使用過程中,應該儘量避免消費過舊的歷史資料即需要讀取磁碟獲取資料的場景。
在運維 bookie 的過程中,是不能在執行過程中調整儲存目錄的個數的,在部署時需要對容量進行充分的評估。如果需要在運營的過程中進行調整時,需要對單個的 bookie 節點進行擴縮容處理。
相關閱讀
點選 連結 ,獲取 Apache Pulsar 硬核乾貨資料!