最近我們的 Pulsar 存儲有很長一段時間數據一直得不到回收,但消息確實已經是 ACK 了,理論上應該是會被回收的,隨着時間流逝不但沒回收還一直再漲,最後在沒找到原因的情況下就只有一直不停的擴容。
最後磁盤是得到了回收,過程先不表,之後再討論。
爲了防止類似的問題再次發生,我們希望可以監控到磁盤維度,能夠列出各個日誌文件的大小以及創建時間。
這時就需要對 Pulsar
的存儲模型有一定的瞭解,也就有了這篇文章。
講到 Pulsar 的存儲模型,本質上就是 Bookkeeper 的存儲模型。
Pulsar 所有的消息讀寫都是通過 Bookkeeper 實現的。
Bookkeeper
是一個可擴展、可容錯、低延遲的日誌存儲數據庫,基於 Append Only 模型。(數據只能追加不能修改)
這裏我利用 Pulsar 和 Bookkeeper 的 Admin API 列出了 Broker 和 BK 中 Ledger 分別佔用的磁盤空間。
關於這個如何獲取和計算的,後續也準備提交給社區。
背景
但和我們實際 kubernetes
中的磁盤佔用量依然對不上,所以就想看看在 BK 中實際的存儲日誌和 Ledger
到底差在哪裏。
知道 Ledger 就可以通過 Ledger 的元數據中找到對應的 topic,從而判斷哪些 topic 的數據導致統計不能匹配。
Bookkeeper 有提提供一個Admin API 可以返回當前 BK 所使用了哪些日誌文件的接口:
https://bookkeeper.apache.org/docs/admin/http#endpoint-apiv1bookielist_disk_filefile_typetype
從返回的結果可以看出,落到具體的磁盤上只有一個文件名稱,是無法知道具體和哪些 Ledger 進行關聯的,也就無法知道具體的 topic 了。
此時只能大膽假設,應該每個文件和具體的消息 ID 有一個映射關係,也就是索引。
所以需要搞清楚這個索引是如何運行的。
存儲模型
我查閱了一些網上的文章和源碼大概梳理了一個存儲流程:
- BK 收到寫入請求,數據會異步寫入到
Journal
/Entrylog
- Journal 直接順序寫入,並且會快速清除已經寫入的數據,所以需要的磁盤空間不多(所以從監控中其實可以看到 Journal 的磁盤佔有率是很低的)。
- 考慮到會隨機讀消息,EntryLog 在寫入前進行排序,保證落盤的數據中同一個 Ledger 的數據儘量挨在一起,充分利用 PageCache.
- 最終數據的索引通過
LedgerId+EntryId
生成索引信息存放到RockDB
中(Pulsar
的場景使用的是DbLedgerStorage
實現)。 - 讀取數據時先從獲取索引,然後再從磁盤讀取數據。
- 利用
Journal
和EntryLog
實現消息的讀寫分離。
簡單來說 BK 在存儲數據的時候會進行雙寫,Journal
目錄用於存放寫的數據,對消息順序沒有要求,寫完後就可以清除了。
而 Entry
目錄主要用於後續消費消息進行讀取使用,大部分場景都是順序讀,畢竟我們消費消息的時候很少會回溯,所以需要充分利用磁盤的 PageCache,將順序的消息儘量的存儲在一起。
同一個日誌文件中可能會存放多個 Ledger 的消息,這些數據如果不排序直接寫入就會導致亂序,而消費時大概率是順序的,但具體到磁盤的表現就是隨機讀了,這樣讀取效率較低。
所以我們使用 Helm
部署 Bookkeeper
的時候需要分別指定 journal
和 ledgers
的目錄
volumes:
# use a persistent volume or emptyDir
persistence: true
journal:
name: journal
size: 20Gi
local_storage: false
multiVolumes:
- name: journal0
size: 10Gi
# storageClassName: existent-storage-class
mountPath: /pulsar/data/bookkeeper/journal0
- name: journal1
size: 10Gi
# storageClassName: existent-storage-class
mountPath: /pulsar/data/bookkeeper/journal1
ledgers:
name: ledgers
size: 50Gi
local_storage: false
storageClassName: sc
# storageClass:
# ... useMultiVolumes: false
multiVolumes:
- name: ledgers0
size: 1000Gi
# storageClassName: existent-storage-class
mountPath: /pulsar/data/bookkeeper/ledgers0
- name: ledgers1
size: 1000Gi
# storageClassName: existent-storage-class
mountPath: /pulsar/data/bookkeeper/ledgers1
每次在寫入和讀取數據的時候都需要通過消息 ID 也就是 ledgerId 和 entryId 來獲取索引信息。
也印證了之前索引的猜測。
所以藉助於 BK 讀寫分離的特性,我們還可以單獨優化存儲。
比如寫入 Journal
的磁盤因爲是順序寫入,所以即便是普通的 HDD
硬盤速度也很快。
大部分場景下都是讀大於寫,所以我們可以單獨爲 Ledger
分配高性能 SSD 磁盤,按需使用。
因爲在最底層的日誌文件中無法直接通過 ledgerId 得知佔用磁盤的大小,所以我們實際的磁盤佔用率對不上的問題依然沒有得到解決,這個問題我還會持續跟進,有新的進展再繼續同步。