快到起飛的Kafka,是如何設(shè)計的?
圖片來自包圖網(wǎng)
消息隊列主要解決了應(yīng)用耦合、異步處理、流量削鋒等問題,當前使用較多的消息隊列有 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ 等。
而部分數(shù)據(jù)庫如 Redis,MySQL 以及 phxsql 也可實現(xiàn)消息隊列的功能。
消息隊列在實際應(yīng)用場景:
- 應(yīng)用解耦:多應(yīng)用間通過消息隊列對同一消息進行處理,避免調(diào)用接口失敗導致整個過程失敗。
- 異步處理:多應(yīng)用對消息隊列中同一消息進行處理,應(yīng)用間并發(fā)處理消息,相比串行處理,減少處理時間。
- 限流削峰:廣泛應(yīng)用于秒殺或搶購活動中,避免流量過大導致應(yīng)用系統(tǒng)掛掉的情況。
今天分享一篇經(jīng)典的 Kafka 設(shè)計剖析文章給大家,Kafka 作為頂級消息中間件,據(jù) Confluent 稱,超過三分之一的財富 500 強公司使用 Apache Kafka。
Kafka 的性能快,吞吐量大,并且高于其他消息隊列一個水平,即使在消息量巨大的情況下還能保持高性能,在互聯(lián)網(wǎng)公司中非常流行,希望大家領(lǐng)悟到 Kafka 設(shè)計的核心原理。
Kafka 架構(gòu)
Kafka 是一個被精心設(shè)計的東西,我只能這樣說。我這里所謂的精心不是說它很完備的實現(xiàn)了某種規(guī)范。
像個學生那般完成了某個作業(yè),比如 JMS,恰恰相反,Kafka 突破了類似 JMS 這種規(guī)范性的束縛,它是卓越的,乃 yet another JMS。
當我用 yet…如此稱呼一個技術(shù)的時候,意味著這玩意兒已經(jīng)進入了我的視野。好了,現(xiàn)在是 Kafka 和 Storm 時間,本文先談 Kafka。
Kafka 是什么?
參見官方文檔,它是 Apache 的一個項目。它是一個消息隊列。
消息隊列若何:消息隊列是生產(chǎn)者和消費者之間的信使,避免了二者之間直接的接觸。
在效果上,它可能和緩存所起的作用一樣,平滑了生產(chǎn)者和消費者之間的代謝速率差,但是在其根本目的上,它是為了解除生產(chǎn)者和消費者之間的耦合。如果你覺得有點費解,那么簡單點說。
fire and forget,這句話的意思再簡單點說,就是真男人從不看爆炸,煙頭往油箱里一丟,把風衣的領(lǐng)子一豎,手插褲兜里,徑直走開,決不不回頭。
消息隊列,以下簡稱 MQ,就是造就這種真男人的。它能讓生產(chǎn)者把消息扔進 MQ 就不管了,然后消費者從 MQ 里取消息即可,不用和生產(chǎn)者交互。
下面的篇幅,我將逐步用我的方式演化出 Kafka 的原型,為了掌握整體脈絡(luò),難免會隱掉很多細節(jié)。
當然這些細節(jié)可以隨便在其官方文檔以及別人的博客里搜到,我的目的只是希望能整理出一個脈絡(luò),在設(shè)計類似的系統(tǒng)的時候,見招拆招以備參考。
MQ 朝著“正確”方向的演化
Kafka 就一定正確嗎?客觀講,肯定不,但是它是本文的主角,所以它就一定正確。
我們先來看看作為通用的 MQ,其最簡單的形式,一般而言,這是大家在首次接觸到 MQ 后的一個課后作業(yè)。
現(xiàn)在有個問題,如果有兩個或者多個消費者需要消費消息,怎么辦?很簡單,廣播唄:
消費者是上帝,很難搞的,你推給它們的東西,并不是它們?nèi)慷枷胍?,只要一部分怎么辦?
好吧,消費者一定在怪 MQ 服務(wù)不周,然而 MQ 有什么錯,它又不理解消息的語義,面對百般刁難的消費者,它最多只能要求生產(chǎn)者把消息細分一下,因此就出現(xiàn)了多個 Topic:
這是很顯然的想法,就是是在消息入隊處區(qū)分消息的 Topic,然消費者從取自己感興趣的消息隊列取消息即可。
但還是會潛在的多個消費對同一 Topic 消息感興趣的情況:
如果采用廣播,那么就仍然會出現(xiàn)冗余傳播問題,如果單播,那么一個消費者取出消息后,這條消息該不該刪除呢?如果刪除了,另一個消費者怎么辦?廣播會浪費帶寬,不廣播也不行…
這貌似進入了一個死循環(huán),必須一勞永逸地從根源解決問題才行。顯然的想法是下面的方案(至少我自己設(shè)計的話就會這么做):
問題是解決了,然而我的天啊,仔細想一下先前的架構(gòu),把簡圖畫出來后,會發(fā)現(xiàn)事情會一發(fā)而不可收拾,MQ 本身的邏輯太復雜了:
回到 UNIX 哲學,遇到新問題的時候,要新編一個程序,而不是為已有的程序添加一個功能。
本著這個思路,為什么不把這件因為消費者而導致復雜化的事情完全交給消費者呢?
有點往 Kafka 上靠了啊。如果把 MQ 里面的數(shù)據(jù)全部持久化存儲,消費者不就可以各取所需了嗎?
這是一個根本的轉(zhuǎn)變,如果以前的方式是限量商務(wù)套餐-套餐強行推給你,不想要的自己扔掉,那么現(xiàn)在的方式就是無限量自助餐-想要什么自己去拿即可。
消息自取,消息永遠都在 MQ,消費者隨便取,取哪個消息都行,什么時候取都行。
消費者只需要告訴 MQ 它想要哪個消息就好,因此需要傳遞一個消息的 offset 參數(shù):
然而自助餐也有打烊的時候,部分也會限制就餐時長,這是 Kafka 策略化存儲的問題,詳見文檔。
簡化一下,現(xiàn)在看下圖:
一切 OK 了。嗯,是的,這就是 Kafka 的原始模型。然而 Kafka 遠不僅此而已。且看下文繼續(xù)演化。
集群化,容錯
先看一下現(xiàn)在的情況:
這是在邏輯上一個 Kafka 類似的 MQ 應(yīng)有的結(jié)構(gòu)。但是在物理實現(xiàn)上,它又如何呢?
常聽人說,Kafka 一開始就是為分布式而生的,這話怎么理解呢?我們只需要先理解它如何擴容,然后再理解它如何將擴容作用于不同的機器即可。先看擴容。
類似高速公路,一般當你聽到廣深高速的時候,我們知道這是從廣州到深圳的一條高速公路,這是邏輯上的說法,類似到目前為止我們討論的 MQ 的 Topic。
然而這條高速公路到底長什么樣子,沿途怎么路由,這就是物理實現(xiàn)了。此外,所有的道路都會分多個車道用于并行。
嚴格來講,每一個車道都會被細分,比如小型車道,客車道,大貨車道,超車道等等,所有這些車道上的車都是到達同一個目的地(屬于同一個 Topic),然而它們確實是細分的不同種類。
把一個叫做 partition 的概念類比為車道,如下圖:
注意這個 key hash 模塊,這里就是區(qū)分車子要進入哪個車道的邏輯。在 Kafka 的術(shù)語中,車道就是 partition,即分區(qū)。
在同一個 Topic 中分發(fā)消息的時候,你要自己設(shè)計 hash 函數(shù),該 hash 函數(shù)就是一個分發(fā)策略,決定把消息按序放到哪一個分區(qū)中去。
溫州皮鞋廠老板說類比和舉例不好,但這是技術(shù)散文,不是技術(shù)文檔,多半是給自己看,所以還要類比。
Topic Routing 做的事是決定從哪條高速公路到哪里,而 key hash 則是決定你是坐轎車,客車還是卡車過去。
值得注意的是,Kafka 只保證同一 Topic 內(nèi)同一 partition 內(nèi)消息的有序性,無法做到全局有序性。
這并不是一個缺陷,這是兩全不能齊美的。完全的順序就需要串行化,然而串行化就無法并行,這簡直就是廢話!
現(xiàn)在,在 Topic 之下,我們又有了一個新的單位,叫做 partition,這個叫做 partition 的就是 Kafka 中最基本的部署單位,這一點務(wù)必要記住,它關(guān)乎到如何組織你的集群。
好了,看一下這些 Topic 以及其旗下的 partition 是如何部署在 M1 和 M2 兩臺機器上的吧:
以上是花開兩朵,各表一枝,現(xiàn)在該說說消費者了。
消費者面對 MQ 本身進化到如此細粒度,該如何應(yīng)對呢?其實消費者也有橫向擴展的需求,如果說消費者對應(yīng) partition,那么對應(yīng) Topic 的就是消費者的上級了。
因此多加了一個層次,引出消費組的概念,解決問題:
從 CPU cache 到 Kafka,設(shè)計思路殊途同歸,這就是一個典型的全方位組相聯(lián)結(jié)構(gòu):
到此為止,全部圖景已經(jīng)完全繪制完畢,是時候展示集群的部署了。我們知道所謂的 Kafka 集群,就是將各個 Topic 的 partition 部署在不同的機器上,達到兩個目的。
一個是負載均衡,即提供訪問的并行性,另一個就是提供高可用性,即做熱備份,這兩個功能我希望能用一個圖展示:
總體的一個結(jié)構(gòu)如下:
持久化存儲/查詢機制
上面的兩個小節(jié),我已經(jīng)展示了 Kafka 是如何一步一步地肚子里面的勾當內(nèi)外有別的,雖然我不知道作者怎么去設(shè)計,但如果是我自己,我肯定就是上面這個思路了…
前面的敘述終究是概覽,不甚過癮。本節(jié)將給出半點細節(jié),瑾闡釋一下 Kafka 存儲的半景。
我們知道,Kafka 為了卸載 MQ 本身的復雜性,為了其真正無狀態(tài)的設(shè)計,它將狀態(tài)維護機制這口鍋完全甩給了消費者。
因此取消息的問題就轉(zhuǎn)化成了消費者拿著一個 offset 索引來 Kafka 存儲器里取消息的問題,這就涉及到了性能。But 如何能查的更快?How?
還是先給出一個最簡單的場景。假設(shè) Kafka 的每一個 partition 都一個完整獨立的文件,那么如果這個文件非常大,事實上也確實非常大(有可能到達 T 級別甚至 P 級別…)。
那么在大文件中檢索一個特定的消息本身就是一個頭疼的問題,并且該文件還在磁盤中,這更是雪上加霜,我們都知道磁盤的隨機讀寫是硬傷,順序讀寫也好不到哪去,這怎么辦?
遍歷?如果每一個 partition 只是一個獨立文件,那么只能遍歷:
面對這個遍歷問題,一般的解決方案就是建立索引,并且把索引數(shù)據(jù)常駐內(nèi)存,很多數(shù)據(jù)庫就是這么干的,Kafka 當然也可以這么干。
Kafka 比較帥的一點就是它并不借助任何特殊的文件系統(tǒng),它的數(shù)據(jù)就存在一般的文件中。
然而它把一個 partition 分成了等大小的一系列小文件,因此在物理上,并不存在一個完整的 partition 文件,partiotion 只是表現(xiàn)為一個目錄。
我們知道,文件系統(tǒng)管理幾個等大的文件是非常方便的:
以上的例子中,一個 partiton 被分成了 100M 大小的文件,這種小文件叫做分段。
在 Kafka 存儲的時候,每一個段文件存滿為止再開辟下一個,由于消息的長度并不一定統(tǒng)一,因此每一個小段文件里面包含的消息數(shù)量并不一定一樣多。
但是不管怎樣,抽取每一個段文件的首尾消息偏移作為元數(shù)據(jù)保存起來是一件一勞永逸的事情,這便于建立一個常駐內(nèi)存的索引:
通過這個區(qū)間查找樹,很快就能定位到特定的段文件,但是事情并沒有結(jié)束。
在 Kafka 中,每一個 partition 的段文件,均配帶一個 index 索引文件,這個文件是做什么的呢?它是段文件內(nèi)部消息的稀疏索引,見下圖:
最終,經(jīng)過兩次區(qū)間樹查找之后,最多再經(jīng)歷一次簡單的遍歷即可完成 offset 定位工作。
誠然,最終的遍歷可能是少不了的,但是 Kafka 盡可能地避免了大長段耗時的遍歷計算,而是將遍歷壓縮到一個很小的量級,這是一個權(quán)衡!跟誰權(quán)衡呢?為什么不把段文件所有消息的索引均建立起來呢?
很簡單,建立全部的索引會造成索引非常大,這樣如果你還想其常駐內(nèi)存的話,內(nèi)存占用會很大,這確實又是一個時間和空間之間的權(quán)衡了。
稀疏索引閑談
稀疏索引很有用,除了本文列舉的 Kafka 的 segment index 稀疏索引之外,還有兩個更為常見的例子(我不是應(yīng)用編程的,我是搞內(nèi)核網(wǎng)絡(luò)協(xié)議棧的,所以在我看來 Kafka 更不常見)
索引整個內(nèi)存地址空間,稀疏化的做法就是分頁,即采用規(guī)則的方式將內(nèi)存劃分為等大小的塊,叫做內(nèi)存頁,然后索引這些內(nèi)存頁即可,頁表而不是地址表稀疏化索引,減小索引的大小。
另外,IP 地址具有地域聚集性,因此對于路由器物理設(shè)備而言,對于每一個接口引出的方向,其 IP 地址集在很大程度上是可以聚集的。
路由表一開始采用地址分類的方法,后來采用了前綴匹配的方法稀疏化索引,地址分類有點像內(nèi)存地址分頁,只是頁面有多種大小而不僅僅是一種。
而這里的地址前綴則比較像 Kafka 使用的兩種索引,第一種是段索引,這是規(guī)則的,第二種是消息索引,這是不規(guī)則的。因為消息并不定長。
兩種說法總結(jié)如下:
OS 內(nèi)存頁表:在從虛擬地址定位物理地址的時候,需要一一對應(yīng)定位到每一個地址嗎?
假如真是這樣子,那么光頁表項這種管理內(nèi)存就要耗多少你算過嗎?虛擬地址和物理地址將會是全相聯(lián)結(jié)構(gòu)。
采用稀疏索引后,只需要定位一個 4K 大小的頁面即可,這將大大減小內(nèi)存頁表的內(nèi)存占用。從而更加高效。
路由表:將每一個 IP 地址均對應(yīng)到路由器設(shè)備的接口嗎?這不現(xiàn)實。解決方案一開始是基于分配機構(gòu)的分類地址稀疏索引,后來采用了基于使用結(jié)構(gòu)的無類子網(wǎng)的前綴系數(shù)索引,無論哪種情況,均大大減少了路由表項的數(shù)量。
UNIX 哲學的出路
沒出路了!Why?因為只有復雜才能體現(xiàn)自己的工作量。
人們都希望制造門檻,把程序做的非常復雜,方才體現(xiàn)自己的能力,畢竟簡單的東西大家都會,想體現(xiàn)區(qū)別,只能讓自己的東西更復雜。
如果你用幾行 Bash 腳本完成了一項艱巨的工作,經(jīng)理大概率會覺得你這是奇技淫巧,完全無法和 C++ 的方案相比。Python 好一點,Java 則更好。
4,5 年以前的曾經(jīng),我們有個編程道場的活動,有一次的一個題目是拼接字符串,即 join 操作,當時的經(jīng)理兼主持者強調(diào)盡量用現(xiàn)成的接口,然而…
多少個優(yōu)秀的極簡方案沒有被表揚,最后被表揚的方案你們知道其特征是什么嗎?其特征就是復雜。
我記得當時這個方案的作者上臺介紹他的方案,上來就說”我這個設(shè)計非常簡單…”結(jié)果呢,唉,用技術(shù)術(shù)語講,過度設(shè)計了,用白話講,裝逼了。主持者顯然也是完全半瓶子晃蕩的吧,哈哈。
事情必須做的盡量復雜,這樣才是能力的體現(xiàn),2 行能搞定的東西,必須湊夠 30 行才算牛逼。UNIX 哲學,在我們這,顯然不合適吧。
作者:極客重生
編輯:陶家龍
出處:轉(zhuǎn)載自公眾號極客重生(ID:geek__coding)