深入解析 Apache BookKeeper 系列:第二篇 — 寫操作原理

語言: CN / TW / HK

上一篇文章中,我們從組件、線程、讀寫流程三個方面講解了 bookie 服務端原理。在這篇文章中,我們將詳細介紹寫操作是如何通過各組件和線程模型的配合高效寫入和快速落盤的。我們儘量還是在架構層面剖析。

本系列文章基於 Apache Pulsar 中配置的 BookKeeper 4.14 版本。

寫操作中有很多線程調用 Journal 和 LedgerStorage 的 API。在上一篇文章中,我們已經知道寫操作中 Journal 為同步操作,DbLedgerStorage 為異步操作。

圖一:各線程是如何處理寫操作的

我們知道可以配置多個 Journal 實例和 DbLedgerStorage 實例,每個實例都有自己的線程、隊列和緩存。因此當講到某些線程、緩存和隊列的時候,它們可能是並行存在的。

Netty 線程

Netty 線程處理所有的 TCP 連接和這些連接中的所有請求。並將這些寫請求轉發到寫線程池,其中包括要寫入的 entry 請求、處理請求結束時的回調、發送響應到客户端。

寫線程池

寫線程池要做的事情不多,因此不需要很多的線程(默認值是 1)。每個寫請求添加 Entry 到 DbLedgerStorage 的 Write Cache,如果成功,則將寫請求添加到 Journal 的內存隊列(BlockingQueue)中。此時寫線程的工作就完成了,剩下的工作就交給其他線程處理。

每個 DbLedgerStorage 實例有兩個寫緩存,一個是活躍的,一個是空閒的,空閒的這個緩存可以在後台將數據刷到磁盤。當 DbLedgerStorage 需要將數據刷到磁盤時(活躍寫緩存寫滿後),兩個寫緩存就會發生交換。當空閒狀態的寫緩存將數據刷到磁盤的同時,可以使用一個空的寫緩存繼續提供寫服務。只要在活躍寫緩存被寫滿之前,將空閒寫緩存中的數據刷到磁盤,就不會出現什麼問題。

DbLedgerStorage 的刷盤操作可以通過同步線程(Sync Thread)定時執行檢查點(checkpoint)機制或通過 DbStorage 線程(DbStorage Thread,每個 DbLedgerStorage 實例對應一個 DbStorage 線程)觸發。

如果寫線程嘗試向寫緩存中添加 Entry 時,寫緩存已經滿了,則寫線程將刷盤操作提交到 DbStorage 線程;如果換出的寫緩存已經完成了刷盤操作,那麼兩個寫緩存將立即執行交換操作(swap),然後寫線程將這個 Entry 添加到新交換出來的寫緩存中,這部分的寫操作也就完成了。

然而,如果活躍狀態的寫緩存被寫滿了,同時交換出的寫緩存仍然在刷盤,那麼寫線程將等待一段時間,最終拒絕寫請求。等待寫緩存的時間由配置文件中的參數 dbStorage_maxThrottleTimeMs 控制,默認值為 10000(10 秒)。

默認情況下,寫線程池中只有一個線程,如果刷盤操作過長的話這將導致寫線程阻塞 10 秒鐘,這將導致寫線程池的任務隊列被寫請求迅速填滿,從而拒絕額外的寫請求。這就是 DbLedgerStorage 的背壓機制。一旦刷新的寫緩存再次能寫入之後,寫線程池的阻塞狀態才會被解除。

寫緩存的大小默認為可用直接內存(direct memory)的 25%,可以通過配置文件中的 dbStorage_writeCacheMaxSizeMb 來進行設置。總的可用內存是分配給每個 DbLedgerStorage 實例中的兩個寫緩存,每個 ledger 目錄對應一個 DbLedgerStorage 實例。如果有 2 個 ledger 目錄和 1GB 的可用寫緩存內存的話,每個 DbLedgerStorage 實例將分配 500MB,其中每個寫緩存將分配到 250MB。

DbStorage 線程

每個 DbLedgerStorage 實例都有自己的 DbStorage 線程。當寫緩存寫滿後,該線程負責將數據刷到磁盤。

Sync 線程

這個線程是在 Journal 模塊和 DbLedgerStorage 模塊之外的。它的工作主要是定期執行檢查點,檢查點有如下幾個:

  • • ledger 的刷盤操作(長期存儲)

  • • 標記 Journal 中已經安全的將數據刷到 ledger 盤的位置,通過寫入磁盤的 log mark 文件實現。

  • • 清理不再需要的、舊的 Journal 文件

這種同步操作可以防止兩個不同的線程同時刷盤。

當 DbLedgerStorage 刷盤時,交換出的寫緩存會被寫入到當前 entry 日誌文件中(這裏也會有日誌切分操作),首先這些 entry 會通過 ledgerId 和 entryId 進行排序,然後將 entry 寫入到 entry 日誌文件,並將它們的位置寫入到 Entry Locations Index。這種寫 entry 時的排序是為了優化讀操作的性能,我們將在本系列下一篇文章中介紹。

一旦將所有寫請求的數據刷到磁盤,則交換出的寫緩存就會被清空,以便再次與活躍的寫緩存進行交換。

Journal 線程

Journal 線程是一個循環,它從內存隊列(BlockingQueue)中獲取 entry,並將 entry 寫到磁盤,並且週期性的向強制寫隊列(Force Write queue)添加強制寫請求,這會觸發 fsync 操作。

Journal 不會為隊列中獲取的每個 entry 執行 write 系統調用,它會對 entry 進行累計,然後批量的寫入磁盤(這就是 BookKeeper 的刷盤方式),這也稱為組提交(group commit)。以下幾個條件會觸發刷盤操作:

  • • 達到最大等待時間(通過 journalMaxGroupWaitMSec 配置,默認值為 2ms)

  • • 達到最大累計字節數(通過 journalBufferedWritesThreshold 配置,默認值為 512Kb)

  • • entry 累計的數量達到最大值(通過 journalBufferedEntriesThreshold 配置,默認值為 0,0 表示不使用該配置)

  • • 當隊列中最後一個 entry 被取出時,也就是隊列由非空變為空(通過 journalFlushWhenQueueEmpty 配置,默認值為 false

每次刷盤都會創建一個強制寫請求(Force Write Request),其中包含要刷盤的 entry。

強制寫線程

強制寫線程是一個循環,循環從強制寫隊列中獲取強制寫請求,並對 journal 文件執行 fsync 操作。強制寫請求包括要寫入的 entry 和這些 entry 請求的回調,以便在持久化到磁盤後,這些 entry 寫入請求的回調能被提交到回調線程執行。

Journal 回調線程

這個線程執行寫請求的回調,並將響應發送到客户端。

常見問題梳理

  • • 寫操作的瓶頸通常在 Journal 或 DbLedgerStorage 中的磁盤 IO 上。如果寫 Journal 或同步操作(Fsync)太慢的話,那麼 Journal 線程和強制寫線程(就不能快速地從各自的隊列中獲取 entry。同樣,DbLedgerStorage 刷磁盤太慢,那麼 Write Cache 就無法清空,也無法快速的進行互換。

  • • 如果 Journal 遇到瓶頸,將導致寫線程池的任務隊列的任務數量達到容量上限,entry 將阻塞在 Journal 隊列中,寫線程也將被阻塞。一旦線程池任務隊列滿了,寫操作就會在 Netty 層被拒絕,因為 Netty 線程將無法向寫線程池提交更多的寫請求。如果你使用了火焰圖,你會發現寫線程池中的寫線程都很繁忙。如果瓶頸在於 DbLedgerStorage,那麼 DbLedgerStorage 自身就可以拒絕寫操作,在 10 秒(默認情況下)之後,寫線程池的資源很快就會被佔滿,然後導致 Netty 線程拒絕寫請求。

  • • 如果磁盤 IO 不是瓶頸,而是 CPU 利用率非常高的話,很有可能是因為使用了高性能磁盤,但是 CPU 性能比較低,導致 Netty 線程和其他各種線程處理效率降低。這種情況通過系統的監控指標就能很容易地定位。

總結

本文在 Journal 和 DBLedgerStorage 層面講解了寫操作流程,以及涉及到寫操作的線程是如何工作的。在下一篇文章中,我們將介紹讀操作。

相關閲讀

博文推薦|深入解析 Apache BookKeeper 系列:第一篇 — 架構原理

本文翻譯自《Apache BookKeeper Internals — Part 2 — Writes》[1] ,作者 Jack Vanlightly。

譯者簡介

邱峯 @360 技術中台基礎架構部中間件產品線成員,主要負責 Pulsar、Kafka 及周邊配套服務的開發與維護工作。

引用鏈接

[1] 《Apache BookKeeper Internals — Part 2 — Writes》: https://medium.com/splunk-maas/apache-bookkeeper-internals-part-2-writes-359ffc17c497




轉發本文章到朋友圈集贊 30 個,掃碼添加 Pulsar Bot 👇🏻👇🏻👇🏻微信憑藉朋友圈截圖領取👆🏻👆🏻👆🏻技術書籍《深入解析 Apache Pulsar》一本。

限量 5 本!

先到先得,送完即止!

雲原生時代消息隊列和流融合系統,提供統一的消費模型,支持消息隊列和流兩種場景,既能為隊列場景提供企業級讀寫服務質量和強一致性保障,又能為流場景提供高吞吐、低延遲;採用存儲計算分離架構,支持大集羣、多租户、百萬級 Topic、跨地域數據複製、持久化存儲、分層存儲、高可擴展性等企業級和金融級功能。 


GitHub 地址:http://github.com/apache/pulsar/


場景關鍵詞

異步解耦 削峯填谷 跨城同步 消息總線 

流存儲  批流融合  實時數倉  金融風控

本文分享自微信公眾號 - ApachePulsar(ApachePulsar)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閲讀的你也加入,一起分享。

「其他文章」