close

本文翻譯自《Apache BookKeeper Internals — Part 1 — High Level》,作者 Jack Vanlightly。

譯者簡介

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

本系列關於 BookKeeper 的博客希望幫助大家理解和掌握 BookKeeper 原理和內部邏輯。理解系統內部運行邏輯是快速定位並解決生產問題以及開發和修改新功能的基石。在本系列後續文章中,我會將BookKeeper各項指標與運行機制相結合,為大家展現高效進行性能問題定位的方法。

BookKeeper 中包含很多不同的插件,我們主要關注 BookKeeper 作為 Apache Pulsar 的存儲層使用的場景,不會涉及其他一些不相關的插件。同時,我們主要關注單個 bookie 節點內部的邏輯,對於 BookKeeper 在集群模式下多個節點之間的副本備份和通信協議相關的內容可以參考我們之前的博客。

我們主要先關注數據的讀寫流程,後續我們也會涉及數據回收、數據壓縮和CLI工具等內容。

如果你看過 BookKeeper 的代碼你會發現有些模塊具有多個抽象實現,我們不去關注具體每個抽象實現的區別(這些區別只和一些 BookKeeper Contributor 有關),而是重點關注處理請求的線程、數據的流向以及線程、數據結構和持久化存儲之間的關聯。

架構原理視圖

我們可以簡單的把一個 BookKeeper 服務端節點(即 bookie)分為三層:頂層是網絡通信層(使用Netty),底層是磁盤IO層,中間層包含大量的緩存。我們可以把 Bookie 理解為一個純粹的存儲節點,負責儘可能快地寫入和讀取 ledger entry 數據,以及保證這些數據的安全。

圖1. bookie 的簡單分層視圖

這個視圖內部包含了多個模塊和線程模型,在本篇博客中我們會逐層分析和解釋它們之間的關係。

組成模塊

每個 ledger entry 都會被寫入 journal 和 ledger 兩個存儲模塊,Ledger 存儲模塊是持久化的存儲,它有多種實現,在 Pulsar 集群中我們使用的是 DbLedgerStorage。

Journal 模塊保證落盤的數據不會丟並提供低延遲的寫性能。Entry 數據成功寫入 journal 後會立即觸發同步的寫請求的響應通知客戶端 entry 已經被成功寫入磁盤。DbLedgerStorage 模塊則以異步的方式將數據批量刷盤,並在刷盤時對批量數據進行優化,將相同 ledger 的數據按 entry 進行排序,以便後續能夠順序讀取。稍後我們會進行更詳細的描述。

讀請求只會由 DbLedgerStorage 模塊來處理,一般情況下我們會從讀緩存中讀到數據。如果在讀緩存中沒有讀取到數據,我們會從磁盤上讀取相應的 entry 數據,同時我們會預讀一些後續的數據並放到讀緩存中,這樣在進行順序讀的時候,後續的 entry 數據就可以直接在讀緩存里讀到。稍後我們也會進行詳細的描述。

圖2. 讀寫請求的組件架構圖

接下來,看看 Journal 和 DbLedgerStorage 兩個模塊在處理寫請求時的內部實現。當我們從 Netty server 接收到一個寫請求,寫請求的內容會被封裝為一個對象並提交給處理寫請求的線程池。這個 entry 數據首先會被傳給 DbLedgerStorage 模塊並被添加到寫緩存(內存緩存),然後傳給 journal 模塊並被添加到一個內存隊列緩存中。journal 和 ledger 模塊中的線程會分別從對應的緩存里獲取 entry 內容並寫入磁盤。當 entry 寫入 journal 磁盤後會觸發同步的寫請求響應。

圖3. Journal 和 DbLedgerStorage 模塊內部架構圖

在了解了有哪些模塊之後,我們還需要了解一下 bookie 的線程模型。每個 bookie 包含多個線程池和多個單線程來調用Journal 和 Ledger Storage 模塊的 API 接口。

線程模型

圖4. bookie的線程和線程池

上圖簡單的展示了 bookie 中包含哪些線程和線程池,以及它們之間的通信關係。Netty 線程池負責處理所有的網絡請求和響應,然後根據不同的請求類型會提交給4個線程池來處理後續邏輯。

Read 線程不受其他線程影響,它們可以獨立完成整個讀處理。Long Poll 線程則需要等待 Write 線程的寫事件通知。Write 線程則會跨多個線程以同步的方式完成寫處理。其他像 Sync 和 DbStorage 線程則會進行異步寫處理。

High Priority 線程池用來處理帶有 high priority 標識的讀寫請求。通常包含 fencing 操作(對 journal 進行寫操作)以及 recovery 相關的讀寫操作。在集群穩定的狀態下,這個線程池基本上會處於空閒狀態。

線程之間通過以下方式進行通信:

•處理請求提交給另一個線程或者線程池(Java executors),每個 executor 有自己的 task 隊列,處理請求會放入 task 隊列中等待被執行。•利用 blocking queues 之類的內存隊列,一個線程將請求封裝為 task 對象後添加到這個隊列,另一個線程從隊列中獲取 task 對象並執行。•利用緩存,一個線程將數據添加到寫緩存(write cache),另一個線程從寫緩存(write cache)中讀取數據並寫到磁盤。

在我們深入了解這些線程和模塊的具體處理和交互邏輯之前,我們先看一下計算(線程)和 IO(Journal / Ledger Storage)是如何實現並發處理的。

並行處理和順序保證

BookKeeper 支持計算和磁盤 IO 的並行處理。計算的並行處理通過線程池來實現,磁盤IO的並行處理則是通過將磁盤IO分散到不同的磁盤目錄來實現(每個磁盤目錄可以掛載到不同的磁盤卷)。

Write, read, long poll 和 high priority 這四個線程池都是 OrderedExecutor 類的實例,這個類會根據需要讀寫的 entry 所屬的 ledgerId 來分配負責處理的線程。

圖5. OrderedExecutor 根據 ledgerId 來分配處理線程

根據 ledgerId 來分配執行線程的方式使得我們能夠在進行並行處理的同時,還能保證針對同一個 ledger 的處理是按順序執行的。每個線程都有獨自的 task 隊列,從而保證提交到這個線程的處理能夠按順序被執行。

配置線程數的參數如下:

•serverNumIOThreads (Netty 線程, 默認為 2xCPU 核心數)•numAddWorkerThreads (默認為 1)•numReadWorkerThreads (默認為 8)•numLongPollWorkerThreads (默認為 0,表示長輪詢讀處理提交到讀線程池)•numHighPriorityWorkerThreads (默認為 8)•numJournalCallbackThreads (默認為 1)

對於磁盤 IO,我們可以通過將 Journal 和 Ledger 目錄設為多個磁盤目錄來實現磁盤IO操作的並行處理。

每個單獨的 journal 目錄都會創建一個獨立的 Journal 實例,每個 Journal 實例包含獨立的線程模型來進行寫磁盤和回調寫處理響應的操作。

圖6. 多個 journal 實例可以提高寫入速率

我們可以在 journalDirectories 配置多個 journal 磁盤目錄。

對應每個 ledger 磁盤目錄,DbLedgerStorage 會創建一個 SingleDirectoryDbLedgerStorage 實例,每個實例包含一個寫緩存、一個讀緩存、DbStorage 線程、一組 ledger entry logs 文件和 RocksDB 索引文件。各實例之間互相獨立,不會共享緩存和文件。

圖7. 多個 SingleDirectoryDbLedgerDirectory 實例可以提高寫入速率 (勘誤:上圖 RockDB 應為 RocksDB)

我們可以通過 ledgerDirectories 來配置多個 ledger 目錄。

為了方便閱讀,在本文後面將 SingleDirectoryDbLedgerStorage 簡稱為 DbLedgerStorage。

一次請求由哪個線程和組件來處理,取決於線程池的大小以及 journal 和 ledger 目錄的數量。

圖8. 一次讀請求的處理路徑包含8個讀線程和2個 ledger 目錄中的一個

默認情況下,寫線程池只有1個線程。我們在後續博客里會介紹,這個線程池沒有太多的處理需要完成。

圖9. 一次寫請求的處理路徑包含一個寫線程和 4個 journal 目錄或2個 ledger 目錄中的一個

這樣的並發處理架構使得 bookie 在具有多核 CPU 和多塊磁盤的大型服務器上運行時,可以同時提高計算和磁盤IO的並發處理能力來提高性能。

當然,我們知道給 BookKeeper 擴容最簡單的方式還是增加 bookie 節點的數量,因為BookKeeper 本身具有彈性擴容的特性。

線程命名規則

如果你拉取一下 bookie 進程的堆棧信息,你會看到帶有以下前綴的線程和線程池:

•bookie-io (Netty 線程)•BookieReadThreadPool-OrderedExecutor•BookieWriteThreadPool-OrderedExecutor•BookieJournal-3181 (使用默認端口的情況)•ForceWriteThread•bookie-journal-callback•SyncThread•db-storage

總結

在本篇博客中我們從線程和組件的角度介紹了 bookie 的架構,了解了 bookie 的請求是如何調度並交由這些線程和組件處理的。在本系列下一篇博客中,我們會詳細介紹寫請求具體是如何在這些線程和組件中處理的。

▼關注「Apache Pulsar」,獲取乾貨與動態▼


👇🏻 加入 Apache Pulsar 中文交流群 👇🏻

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()