Kafka 存儲選型的奧祕

從這篇文章開始,我將從微觀角度切入,深入分析 Kafka 的設計原理。本文要講的是 Kafka 最具代表性的:存儲設計。

談到 Kafka 的存儲設計,瞭解不多的同學,可能會有這樣的疑惑:爲什麼 Kafka 會採用 Logging(日誌文件)這種很原始的方式來存儲消息,而沒考慮用數據庫或者 KV 來做存儲?
而對 Kafka 有所瞭解的同學,應該能快速說出一些 知識點:比如 Append Only、Linear Scans、磁盤順序寫、頁緩存、零拷貝、稀疏索引、二分查找等等。

我計劃寫兩篇文章,除了解釋清楚上面的疑惑,同時還會給出一個脈絡,幫助大家迅速切中 Kafka 存儲設計的要點,然後將上面這些零散的知識點串聯起來。
此外,也希望大家在瞭解了 Kafka 的存儲設計後,能對 Append Only Data Structures 這一經典的底層存儲原理認識更加深刻,因爲它驅動了業界太多極具影響力的存儲系統走向成功,比如 HBase、Cassandra、RocksDB 等等

Kafka 的存儲難點是什麼?

爲什麼說存儲設計是 Kafka 的精華所在?之前這篇文章做過分析,Kafka 通過簡化消息模型,將自己退化成了一個海量消息的存儲系統。
既然 Kafka 在其他功能特性上做了減法,必然會在存儲上下功夫,做到其他 MQ 無法企及的性能表現。


圖1:Kafka 的消息模型
但是在講解 Kafka 的存儲方案之前,我們有必要去嘗試分析下:爲什麼 Kafka 會採用 Logging(日誌文件)的存儲方式?它的選型依據到底是什麼?

這也是本系列希望做到的,思考力勝過記憶力,多問 why,而不是死記 what。

Kafka 的存儲選型邏輯,我認爲跟我們開發業務需求的思路類似,到底用 MySQL、Redis 還是其他存儲方案?一定取決於具體的業務場景。

我們試着從以下兩個維度來分析下:

  • 1、功能性需求:存的是什麼數據?量級如何?需要存多久?CRUD 的場景都有哪些?
  • 2、非功能性需求:性能和穩定性的要求是什麼樣的?是否要考慮擴展性?

再回到 Kafka 來看,它的功能性需求至少包括以下幾點:

存的數據主要是消息流:消息可以是最簡單的文本字符串,也可以是自定義的複雜格式。

但是對於 Broker 來說,它只需處理好消息的投遞即可,無需關注消息內容本身。

數據量級非常大:因爲 Kafka 作爲 Linkedin 的孵化項目誕生,用作實時日誌流處理(運營活動中的埋點、運維監控指標等),按 Linkedin 當初的業務規模來看,每天要處理的消息量預計在千億級規模。

CRUD 場景足夠簡單:因爲消息隊列最核心的功能就是數據管道,它僅提供轉儲能力,因此 CRUD 操作確實很簡單。

首先,消息等同於通知事件,都是追加寫入的,根本無需考慮 update。其次,對於 Consumer 端來說,Broker 提供按 offset(消費位移)或者 timestamp(時間戳)查詢消息的能力就行。再次,長時間未消費的消息(比如 7 天前的),Broker 做好定期刪除即可。

接着,我們再來看看非功能性需求:

性能要求:之前的文章交代過,Linkedin 最初嘗試過用 ActiveMQ 來解決數據傳輸問題,但是性能無法滿足要求,然後才決定自研 Kafka。ActiveMQ 的單機吞吐量大約是萬級 TPS,Kafka 顯然要比 ActiveMQ 的性能高一個量級纔行。

穩定性要求:消息的持久化(確保機器重啓後歷史數據不丟失)、單臺 Broker 宕機後如何快速故障轉移繼續對外提供服務,這兩個能力也是 Kafka 必須要考慮的。

擴展性要求:Kafka 面對的是海量數據的存儲問題,必然要考慮存儲的擴展性。

再簡單總結下,Kafka 的存儲需求如下:

  • 1、功能性需求:其實足夠簡單,追加寫、無需update、能根據消費位移和時間戳查詢消息、能定期刪除過期的消息。
  • 2、非功能性需求:是難點所在,因爲 Kafka 本身就是一個高併發系統,必然會遇到典型的高性能、高可用和高擴展這三方面的挑戰。

Kafka 的存儲選型分析

有了上面的需求梳理,我們繼續往下分析。
爲什麼 Kafka 最終會選用 logging(日誌文件)來存儲消息呢?而不是用我們最常見的關係型數據庫或者 key-value 數據庫呢?

2.1 存儲領域的基礎知識
先普及幾點存儲領域的基礎知識,這是我們進一步分析的理論依據。

  • 1、內存的存取速度快,但是容量小、價格昂貴,不適用於要長期保存的數據。
  • 2、磁盤的存取速度相對較慢,但是廉價、而且可以持久化存儲。
  • 3、一次磁盤 IO 的耗時主要取決於:尋道時間和盤片旋轉時間,提高磁盤 IO 性能最有效的方法就是:減少隨機 IO,增加順序 IO。
  • 4、磁盤的 IO 速度其實不一定比內存慢,取決於我們如何使用它。

關於磁盤和內存的 IO 速度,有很多這方面的對比測試,結果表明:磁盤順序寫入速度可以達到幾百兆/s,而隨機寫入速度只有幾百KB/s,相差上千倍。此外,磁盤順序 IO 訪問甚至可以超過內存隨機 IO 的性能。


圖2:磁盤和內存的 IO 速度對比

再看數據存儲領域,有兩個 “極端” 發展方向:
1、加快讀:通過索引( B+ 樹、二份查找樹等方式),提高查詢速度,但是寫入數據時要維護索引,因此會降低寫入效率。

2、加快寫:純日誌型,數據以 append 追加的方式順序寫入,不加索引,使得寫入速度非常高(理論上可接近磁盤的寫入速度),但是缺乏索引支持,因此查詢性能低。

基於這兩個極端,又衍生出來了 3 類最具代表性的底層索引結構:
1、哈希索引:通過哈希函數將 key 映射成數據的存儲地址,適用於等值查詢等簡單場景,對於比較查詢、範圍查詢等複雜場景無能爲力。

2、B/B+ Tree 索引:最常見的索引類型,重點考慮的是讀性能,它是很多傳統關係型數據庫,比如 MySQL、Oracle 的底層結構。

3、 LSM Tree 索引:數據以 Append 方式追加寫入日誌文件,優化了寫但是又沒顯著降低讀性能,衆多 NoSQL 存儲系統比如 BigTable,HBase,Cassandra,RocksDB 的底層結構。

2.2 Kafka 的存儲選型考慮
有了上面這些理論基礎,我們繼續回到 Kafka 的存儲需求上進行思考。

Kafka 所處業務場景的特點是:

  • 1、寫入操作:併發非常高,百萬級 TPS,但都是順序寫入,無需考慮更新
  • 2、查詢操作:需求簡單,能按照 offset 或者 timestamp 查詢消息即可

如果單純滿足 Kafka 百萬級 TPS 的寫入操作需求,採用 Append 追加寫日誌文件的方式顯然是最理想的,前面講過磁盤順序寫的性能完全是可以滿足要求的。

剩下的就是如何解決高效查詢的問題。如果採用 B Tree 類的索引結構來實現,每次數據寫入時都需要維護索引(屬於隨機 IO 操作),而且還會引來“頁分裂”等比較耗時的操作。而這些代價對於僅需要實現簡單查詢要求的 Kafka 來說,顯得非常重。所以,B Tree 類的索引並不適用於 Kafka。

相反,哈希索引看起來卻非常合適。爲了加快讀操作,如果只需要在內存中維護一個「從 offset 到日誌文件偏移量」的映射關係即可,每次根據 offset 查找消息時,從哈希表中得到偏移量,再去讀文件即可。(根據 timestamp 查消息也可以採用同樣的思路)

但是哈希索引常駐內存,顯然沒法處理數據量很大的情況,Kafka 每秒可能會有高達幾百萬的消息寫入,一定會將內存撐爆。

可我們發現消息的 offset 完全可以設計成有序的(實際上是一個單調遞增 long 類型的字段),這樣消息在日誌文件中本身就是有序存放的了,我們便沒必要爲每個消息建 hash 索引了,完全可以將消息劃分成若干個 block,只索引每個 block 第一條消息的 offset 即可,先根據大小關係找到 block,然後在 block 中順序搜索,這便是 Kafka “稀疏索引” 的來源。


圖3:Kafka 的稀疏索引示意圖

最終我們發現:Append 追加寫日誌 + 稀疏的哈希索引,形成了 Kafka 最終的存儲方案。而這不就是 LSM Tree 的設計思想嗎?

也許會有人會反駁 Kafka 的方案跟 LSM Tree 不一樣,並沒有用到樹型索引以及 Memtable 這一層。但我個人認爲,從「設計思想」從這個角度來看,完全可以將 Kafka 視爲 LSM Tree 的極端應用。
此外,關於 Append Only Data Structures 和 LSM Tree,推薦 Ben Stopford (Kafka 母公司的一位技術專家) 於 2017 年 QCon 上做的一個視頻分享,演講非常精彩,值得一看。

Kafka 的存儲設計

瞭解了 Kafka 存儲選型的來龍去脈後,最後我們再看下它具體的存儲結構。


圖4:Kafka 的存儲結構

可以看到,Kafka 是一個「分區 + 分段 + 索引」的三層結構:
1、每個 Topic 被分成多個 Partition,Partition 從物理上可以理解成一個文件夾。
之前的文章解釋過:Partition 主要是爲了解決 Kafka 存儲上的水平擴展問題,如果一個 Topic 的所有消息都只存在一個 Broker,這個 Broker 必然會成爲瓶頸。因此,將 Topic 內的數據分成多個 Partition,然後分佈到整個集羣是很自然的設計方式。
2、每個 Partition 又被分成了多個 Segment,Segment 從物理上可以理解成一個「數據文件 + 索引文件」,這兩者是一一對應的。
一定有讀者會有疑問:有了 Partition 之後,爲什麼還需要 Segment?
如果不引入 Segment,一個 Partition 只對應一個文件,那這個文件會一直增大,勢必造成單個 Partition 文件過大,查找和維護不方便。
此外,在做歷史消息刪除時,必然需要將文件前面的內容刪除,不符合 Kafka 順序寫的思路。而在引入 Segment 後,則只需將舊的 Segment 文件刪除即可,保證了每個 Segment 的順序寫。

寫在最後

本文從需求分析、到選型對比、再到具體的存儲方案,一步步撥開了 Kafka 選用 logging(日誌文件)這一存儲方案的奧祕。
也是希望大家能去主動思考 Kafka 在存儲選型時的難點,把它當做一個系統設計題去思考,而不僅僅記住它用了日誌存儲。
另外一個觀點:越底層越通用,你每次多往下研究深一點,會發現這些知識在很多優秀的開源系統裏都是相通的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章