
大家好,我是武哥。這是《吃透 MQ 系列》之 Kafka 的第 3?篇,錯(cuò)過前兩篇文章的,建議再溫習(xí)下:
扒開 Kafka 的神秘面紗
Kafka 架構(gòu)設(shè)計(jì)的任督二脈
從這篇文章開始,我將從微觀角度切入,深入分析 Kafka 的設(shè)計(jì)原理。本文要講的是 Kafka 最具代表性的:存儲(chǔ)設(shè)計(jì)。談到 Kafka 的存儲(chǔ)設(shè)計(jì),了解不多的同學(xué),可能會(huì)有這樣的疑惑:為什么 Kafka 會(huì)采用 Logging(日志文件)這種很原始的方式來存儲(chǔ)消息,而沒考慮用數(shù)據(jù)庫或者 KV 來做存儲(chǔ)?而對(duì) Kafka 有所了解的同學(xué),應(yīng)該能快速說出一些 知識(shí)點(diǎn):比如 Append Only、Linear Scans、磁盤順序?qū)?、頁緩存、零拷貝、稀疏索引、二分查找等等?/span>我計(jì)劃寫兩篇文章,除了解釋清楚上面的疑惑,同時(shí)還會(huì)給出一個(gè)脈絡(luò),幫助大家迅速切中 Kafka 存儲(chǔ)設(shè)計(jì)的要點(diǎn),然后將上面這些零散的知識(shí)點(diǎn)串聯(lián)起來。此外,也希望大家在了解了 Kafka 的存儲(chǔ)設(shè)計(jì)后,能對(duì) Append Only Data Structures 這一經(jīng)典的底層存儲(chǔ)原理認(rèn)識(shí)更加深刻,因?yàn)樗?qū)動(dòng)了業(yè)界太多極具影響力的存儲(chǔ)系統(tǒng)走向成功,比如 HBase、Cassandra、RocksDB 等等。?1. Kafka 的存儲(chǔ)難點(diǎn)是什么???
為什么說存儲(chǔ)設(shè)計(jì)是 Kafka 的精華所在?之前這篇文章做過分析,Kafka 通過簡化消息模型,將自己退化成了一個(gè)海量消息的存儲(chǔ)系統(tǒng)。既然 Kafka 在其他功能特性上做了減法,必然會(huì)在存儲(chǔ)上下功夫,做到其他 MQ 無法企及的性能表現(xiàn)。
但是在講解 Kafka 的存儲(chǔ)方案之前,我們有必要去嘗試分析下:為什么 Kafka 會(huì)采用 Logging(日志文件)的存儲(chǔ)方式?它的選型依據(jù)到底是什么?
這也是本系列希望做到的,思考力勝過記憶力,多問 why,而不是死記 what。
Kafka 的存儲(chǔ)選型邏輯,我認(rèn)為跟我們開發(fā)業(yè)務(wù)需求的思路類似,到底用 MySQL、Redis 還是其他存儲(chǔ)方案?一定取決于具體的業(yè)務(wù)場(chǎng)景。
1、功能性需求:存的是什么數(shù)據(jù)?量級(jí)如何?需要存多久?CRUD 的場(chǎng)景都有哪些?
2、非功能性需求:性能和穩(wěn)定性的要求是什么樣的?是否要考慮擴(kuò)展性?
再回到 Kafka 來看,它的功能性需求至少包括以下幾點(diǎn):
1、存的數(shù)據(jù)主要是消息流:消息可以是最簡單的文本字符串,也可以是自定義的復(fù)雜格式。
但是對(duì)于 Broker 來說,它只需處理好消息的投遞即可,無需關(guān)注消息內(nèi)容本身。
2、數(shù)據(jù)量級(jí)非常大:因?yàn)?Kafka 作為 Linkedin 的孵化項(xiàng)目誕生,用作實(shí)時(shí)日志流處理(運(yùn)營活動(dòng)中的埋點(diǎn)、運(yùn)維監(jiān)控指標(biāo)等),按 Linkedin 當(dāng)初的業(yè)務(wù)規(guī)模來看,每天要處理的消息量預(yù)計(jì)在千億級(jí)規(guī)模。
3、CRUD 場(chǎng)景足夠簡單:因?yàn)橄㈥?duì)列最核心的功能就是數(shù)據(jù)管道,它僅提供轉(zhuǎn)儲(chǔ)能力,因此 CRUD 操作確實(shí)很簡單。
首先,消息等同于通知事件,都是追加寫入的,根本無需考慮 update。其次,對(duì)于 Consumer 端來說,Broker 提供按 offset(消費(fèi)位移)或者 timestamp(時(shí)間戳)查詢消息的能力就行。再次,長時(shí)間未消費(fèi)的消息(比如 7 天前的),Broker 做好定期刪除即可。
接著,我們?cè)賮砜纯捶枪δ苄孕枨螅?/span>
1、性能要求:之前的文章交代過,Linkedin 最初嘗試過用 ActiveMQ 來解決數(shù)據(jù)傳輸問題,但是性能無法滿足要求,然后才決定自研 Kafka。ActiveMQ 的單機(jī)吞吐量大約是萬級(jí) TPS,Kafka 顯然要比 ActiveMQ 的性能高一個(gè)量級(jí)才行。
2、穩(wěn)定性要求:消息的持久化(確保機(jī)器重啟后歷史數(shù)據(jù)不丟失)、單臺(tái) Broker 宕機(jī)后如何快速故障轉(zhuǎn)移繼續(xù)對(duì)外提供服務(wù),這兩個(gè)能力也是 Kafka 必須要考慮的。
3、擴(kuò)展性要求:Kafka 面對(duì)的是海量數(shù)據(jù)的存儲(chǔ)問題,必然要考慮存儲(chǔ)的擴(kuò)展性。
再簡單總結(jié)下,Kafka 的存儲(chǔ)需求如下:1、功能性需求:其實(shí)足夠簡單,追加寫、無需update、能根據(jù)消費(fèi)位移和時(shí)間戳查詢消息、能定期刪除過期的消息。
2、非功能性需求:是難點(diǎn)所在,因?yàn)?Kafka 本身就是一個(gè)高并發(fā)系統(tǒng),必然會(huì)遇到典型的高性能、高可用和高擴(kuò)展這三方面的挑戰(zhàn)。
為什么 Kafka 最終會(huì)選用 logging(日志文件)來存儲(chǔ)消息呢?而不是用我們最常見的關(guān)系型數(shù)據(jù)庫或者 key-value 數(shù)據(jù)庫呢?2.1 存儲(chǔ)領(lǐng)域的基礎(chǔ)知識(shí)先普及幾點(diǎn)存儲(chǔ)領(lǐng)域的基礎(chǔ)知識(shí),這是我們進(jìn)一步分析的理論依據(jù)。1、內(nèi)存的存取速度快,但是容量小、價(jià)格昂貴,不適用于要長期保存的數(shù)據(jù)。
2、磁盤的存取速度相對(duì)較慢,但是廉價(jià)、而且可以持久化存儲(chǔ)。
3、一次磁盤 IO 的耗時(shí)主要取決于:尋道時(shí)間和盤片旋轉(zhuǎn)時(shí)間,提高磁盤 IO 性能最有效的方法就是:減少隨機(jī) IO,增加順序 IO。
4、磁盤的 IO 速度其實(shí)不一定比內(nèi)存慢,取決于我們?nèi)绾问褂盟?/span>
關(guān)于磁盤和內(nèi)存的 IO 速度,有很多這方面的對(duì)比測(cè)試,結(jié)果表明:磁盤順序?qū)懭胨俣瓤梢赃_(dá)到幾百兆/s,而隨機(jī)寫入速度只有幾百KB/s,相差上千倍。此外,磁盤順序 IO 訪問甚至可以超過內(nèi)存隨機(jī) IO 的性能。
圖2:磁盤和內(nèi)存的 IO 速度對(duì)比再看數(shù)據(jù)存儲(chǔ)領(lǐng)域,有兩個(gè) “極端” 發(fā)展方向:1、加快讀:通過索引( B+ 樹、二份查找樹等方式),提高查詢速度,但是寫入數(shù)據(jù)時(shí)要維護(hù)索引,因此會(huì)降低寫入效率。
2、加快寫:純?nèi)罩拘?,?shù)據(jù)以 append 追加的方式順序?qū)懭?,不加索引,使得寫入速度非常高(理論上可接近磁盤的寫入速度),但是缺乏索引支持,因此查詢性能低。
基于這兩個(gè)極端,又衍生出來了 3 類最具代表性的底層索引結(jié)構(gòu):1、哈希索引:通過哈希函數(shù)將 key 映射成數(shù)據(jù)的存儲(chǔ)地址,適用于等值查詢等簡單場(chǎng)景,對(duì)于比較查詢、范圍查詢等復(fù)雜場(chǎng)景無能為力。
2、B/B+ Tree 索引:最常見的索引類型,重點(diǎn)考慮的是讀性能,它是很多傳統(tǒng)關(guān)系型數(shù)據(jù)庫,比如 MySQL、Oracle 的底層結(jié)構(gòu)。
3、 LSM Tree 索引:數(shù)據(jù)以 Append 方式追加寫入日志文件,優(yōu)化了寫但是又沒顯著降低讀性能,眾多 NoSQL 存儲(chǔ)系統(tǒng)比如 BigTable,HBase,Cassandra,RocksDB 的底層結(jié)構(gòu)。
2.2?Kafka 的存儲(chǔ)選型考慮
有了上面這些理論基礎(chǔ),我們繼續(xù)回到 Kafka 的存儲(chǔ)需求上進(jìn)行思考。
Kafka 所處業(yè)務(wù)場(chǎng)景的特點(diǎn)是:1、寫入操作:并發(fā)非常高,百萬級(jí) TPS,但都是順序?qū)懭耄瑹o需考慮更新
2、查詢操作:需求簡單,能按照 offset 或者 timestamp 查詢消息即可
如果單純滿足 Kafka 百萬級(jí) TPS 的寫入操作需求,采用 Append 追加寫日志文件的方式顯然是最理想的,前面講過磁盤順序?qū)懙男阅芡耆强梢詽M足要求的。剩下的就是如何解決高效查詢的問題。如果采用 B Tree 類的索引結(jié)構(gòu)來實(shí)現(xiàn),每次數(shù)據(jù)寫入時(shí)都需要維護(hù)索引(屬于隨機(jī) IO 操作),而且還會(huì)引來“頁分裂”等比較耗時(shí)的操作。而這些代價(jià)對(duì)于僅需要實(shí)現(xiàn)簡單查詢要求的 Kafka 來說,顯得非常重。所以,B Tree 類的索引并不適用于 Kafka。相反,哈希索引看起來卻非常合適。為了加快讀操作,如果只需要在內(nèi)存中維護(hù)一個(gè)「從?offset 到日志文件偏移量」的映射關(guān)系即可,每次根據(jù) offset 查找消息時(shí),從哈希表中得到偏移量,再去讀文件即可。(根據(jù) timestamp 查消息也可以采用同樣的思路)
但是哈希索引常駐內(nèi)存,顯然沒法處理數(shù)據(jù)量很大的情況,Kafka 每秒可能會(huì)有高達(dá)幾百萬的消息寫入,一定會(huì)將內(nèi)存撐爆。可我們發(fā)現(xiàn)消息的 offset?完全可以設(shè)計(jì)成有序的(實(shí)際上是一個(gè)單調(diào)遞增?long 類型的字段),這樣消息在日志文件中本身就是有序存放的了,我們便沒必要為每個(gè)消息建 hash 索引了,完全可以將消息劃分成若干個(gè) block,只索引每個(gè) block 第一條消息的 offset 即可,先根據(jù)大小關(guān)系找到 block,然后在 block 中順序搜索,這便是 Kafka “稀疏索引” 的來源。
圖3:Kafka 的稀疏索引示意圖
最終我們發(fā)現(xiàn):Append 追加寫日志 +?稀疏的哈希索引,形成了 Kafka 最終的存儲(chǔ)方案。而這不就是 LSM Tree 的設(shè)計(jì)思想嗎?
也許會(huì)有人會(huì)反駁 Kafka 的方案跟 LSM Tree 不一樣,并沒有用到樹型索引以及 Memtable 這一層。但我個(gè)人認(rèn)為,從「設(shè)計(jì)思想」從這個(gè)角度來看,完全可以將 Kafka 視為 LSM Tree 的極端應(yīng)用。
此外,關(guān)于 Append Only Data Structures 和 LSM Tree,推薦 Ben Stopford (Kafka 母公司的一位技術(shù)專家)?于 2017 年 QCon 上做的一個(gè)視頻分享,演講非常精彩,值得一看。
https://www.infoq.com/presentations/lsm-append-data-structures/
?3. Kafka 的存儲(chǔ)設(shè)計(jì)??
了解了 Kafka 存儲(chǔ)選型的來龍去脈后,最后我們?cè)倏聪滤唧w的存儲(chǔ)結(jié)構(gòu)。
圖4:Kafka 的存儲(chǔ)結(jié)構(gòu)
可以看到,Kafka 是一個(gè)「分區(qū) + 分段 + 索引」的三層結(jié)構(gòu):1、每個(gè) Topic 被分成多個(gè) Partition,Partition 從物理上可以理解成一個(gè)文件夾。之前的文章解釋過:Partition 主要是為了解決 Kafka 存儲(chǔ)上的水平擴(kuò)展問題,如果一個(gè) Topic 的所有消息都只存在一個(gè) Broker,這個(gè) Broker 必然會(huì)成為瓶頸。因此,將 Topic 內(nèi)的數(shù)據(jù)分成多個(gè) Partition,然后分布到整個(gè)集群是很自然的設(shè)計(jì)方式。
2、每個(gè) Partition 又被分成了多個(gè) Segment,Segment 從物理上可以理解成一個(gè)「數(shù)據(jù)文件 + 索引文件」,這兩者是一一對(duì)應(yīng)的。一定有讀者會(huì)有疑問:有了 Partition 之后,為什么還需要 Segment?如果不引入 Segment,一個(gè) Partition 只對(duì)應(yīng)一個(gè)文件,那這個(gè)文件會(huì)一直增大,勢(shì)必造成單個(gè) Partition 文件過大,查找和維護(hù)不方便。此外,在做歷史消息刪除時(shí),必然需要將文件前面的內(nèi)容刪除,不符合 Kafka 順序?qū)懙乃悸贰6谝?Segment 后,則只需將舊的 Segment 文件刪除即可,保證了每個(gè) Segment 的順序?qū)憽?/span>本文從需求分析、到選型對(duì)比、再到具體的存儲(chǔ)方案,一步步撥開了 Kafka 選用 logging(日志文件)這一存儲(chǔ)方案的奧秘。也是希望大家能去主動(dòng)思考 Kafka 在存儲(chǔ)選型時(shí)的難點(diǎn),把它當(dāng)做一個(gè)系統(tǒng)設(shè)計(jì)題去思考,而不僅僅記住它用了日志存儲(chǔ)。另外一個(gè)觀點(diǎn):越底層越通用,你每次多往下研究深一點(diǎn),會(huì)發(fā)現(xiàn)這些知識(shí)在很多優(yōu)秀的開源系統(tǒng)里都是相通的。推薦閱讀:
如出一轍。。。
Java 中的監(jiān)控與管理原理概述
《吃透 MQ 系列》之 Kafka 架構(gòu)設(shè)計(jì)的任督二脈
《吃透 MQ 系列》之扒開 Kafka 的神秘面紗
網(wǎng)關(guān)技術(shù)選型,為什么選擇 Openresty ?
歡迎關(guān)注微信公眾號(hào):互聯(lián)網(wǎng)全棧架構(gòu),收取更多有價(jià)值的信息。