僅需3分鐘,你就能明白Kafka的工作原理
周末無聊刷著手機,某寶網(wǎng) App 突然蹦出來一條消息“為了回饋老客戶,女朋友買一送一,活動僅限今天!”。
買一送一還有這種好事,那我可不能錯過!忍不住立馬點了去。于是選了兩個***款,下單、支付一氣呵成!滿足的躺在床上,想著馬上有女朋友了,竟然幸福的失眠了……
第二天正常上著班,突然接到快遞小哥的電話:
小哥:“你是 xx 嗎?你的女朋友到了,我現(xiàn)在在你樓下,你來拿一下吧!”。
我:“這……我在上班呢,可以晚上送過來嗎?“。
小哥:“晚上可不行哦,晚上我也下班了呢!”。
于是兩個人僵持了很久……
***小哥說,要不我?guī)湍惴诺綐窍滦》急憷臧?,你晚上下班了過來拿,尷尬的局面這才得以緩解!
為什么需要消息隊列
回到正題,如果沒有小芳便利店,那快遞小哥和我的交互圖就應:
會出現(xiàn)什么情況呢?
- 為了這個女朋友,我請假回去拿(老板不批)。
- 小哥一直在你樓下等(小哥還有其他的快遞要送)。
- 周末再送(顯然等不及)。
- 這個女朋友我不要了(絕對不可能)!
小芳便利店出現(xiàn)后,交互圖如下:
在上面例子中,“快遞小哥”和“買女朋友的我”就是需要交互的兩個系統(tǒng),小芳便利店就是我們本文要講的消息中間件。
總結下來小芳便利店(消息中間件)出現(xiàn)后有如下好處:
解耦
快遞小哥手上有很多快遞需要送,他每次都需要先電話一一確認收貨人是否有空、哪個時間段有空,然后再確定好送貨的方案。這樣完全依賴收貨人了!如果快遞一多,快遞小哥估計得忙瘋了……
如果有了便利店,快遞小哥只需要將同一個小區(qū)的快遞放在同一個便利店,然后通知收貨人來取貨就可以了,這時候快遞小哥和收貨人就實現(xiàn)了解耦!
異步
快遞小哥打電話給我后需要一直在你樓下等著,直到我拿走你的快遞他才能去送其他人的。
快遞小哥將快遞放在小芳便利店后,又可以干其他的活兒去了,不需要等待你到來而一直處于等待狀態(tài),提高了工作的效率。
削峰
假設雙十一我買了不同店里的各種商品,而恰巧這些店發(fā)貨的快遞都不一樣,有中通、圓通、申通、各種通等……更巧的是他們都同時到貨了!
中通的小哥打來電話叫我去北門取快遞、圓通小哥叫我去南門、申通小哥叫我去東門。我一時手忙腳亂……
我們能看到在系統(tǒng)需要交互的場景中,使用消息隊列中間件真的是好處多多,基于這種思路,就有了豐巢、菜鳥驛站等比小芳便利店更專業(yè)的“中間件”了。
***,上面的故事純屬虛構……
消息隊列通信的模式
通過上面的例子我們引出了消息中間件,并且介紹了消息隊列出現(xiàn)后的好處,這里就需要介紹消息隊列通信的兩種模式了:
點對點模式
如上圖所示,點對點模式通常是基于拉取或者輪詢的消息傳送模型,這個模型的特點是發(fā)送到隊列的消息被一個且只有一個消費者進行處理。
生產者將消息放入消息隊列后,由消費者主動的去拉取消息進行消費。點對點模型的優(yōu)點是消費者拉取消息的頻率可以由自己控制。
但是消息隊列是否有消息需要消費,在消費者端無法感知,所以在消費者端需要額外的線程去監(jiān)控。
發(fā)布訂閱模式
如上圖所示,發(fā)布訂閱模式是一個基于消息送的消息傳送模型,該模型可以有多種不同的訂閱者。
生產者將消息放入消息隊列后,隊列會將消息推送給訂閱過該類消息的消費者(類似微信公眾號)。
由于是消費者被動接收推送,所以無需感知消息隊列是否有待消費的消息!但是 Consumer1、Consumer2、Consumer3 由于機器性能不一樣,所以處理消息的能力也會不一樣,但消息隊列卻無法感知消費者消費的速度!
所以推送的速度成了發(fā)布訂閱模式的一個問題!假設三個消費者處理速度分別是 8M/s、5M/s、2M/s,如果隊列推送的速度為 5M/s,則 Consumer3 無法承受!
如果隊列推送的速度為 2M/s,則 Consumer1、Consumer2 會出現(xiàn)資源的極大浪費!
Kafka
上面簡單的介紹了為什么需要消息隊列以及消息隊列通信的兩種模式,接下來就到了我們本文的主角 Kafka 閃亮登場的時候了!
Kafka 是一種高吞吐量的分布式發(fā)布訂閱消息系統(tǒng),它可以處理消費者規(guī)模的網(wǎng)站中的所有動作流數(shù)據(jù),具有高性能、持久化、多副本備份、橫向擴展能力………
基礎架構及術語
話不多說,先看圖,通過這張圖我們來捋一捋相關的概念及之間的關系:
如果看到這張圖你很懵逼,木有關系!我們先來分析相關概念:
- Producer:Producer 即生產者,消息的產生者,是消息的入口。
- Kafka Cluster:
Broker:Broker 是 Kafka 實例,每個服務器上有一個或多個 Kafka 的實例,我們姑且認為每個 Broker 對應一臺服務器。
每個 Kafka 集群內的 Broker 都有一個不重復的編號,如圖中的 Broker-0、Broker-1 等……
Topic:消息的主題,可以理解為消息的分類,Kafka 的數(shù)據(jù)就保存在 Topic。在每個 Broker 上都可以創(chuàng)建多個 Topic。
Partition:Topic 的分區(qū),每個 Topic 可以有多個分區(qū),分區(qū)的作用是做負載,提高 Kafka 的吞吐量。
同一個 Topic 在不同的分區(qū)的數(shù)據(jù)是不重復的,Partition 的表現(xiàn)形式就是一個一個的文件夾!
Replication:每一個分區(qū)都有多個副本,副本的作用是做備胎。當主分區(qū)(Leader)故障的時候會選擇一個備胎(Follower)上位,成為 Leader。
在 Kafka 中默認副本的***數(shù)量是 10 個,且副本的數(shù)量不能大于 Broker 的數(shù)量,F(xiàn)ollower 和 Leader 絕對是在不同的機器,同一機器對同一個分區(qū)也只可能存放一個副本(包括自己)。
- Message:每一條發(fā)送的消息主體。
- Consumer:消費者,即消息的消費方,是消息的出口。
- Consumer Group:我們可以將多個消費組組成一個消費者組,在 Kafka 的設計中同一個分區(qū)的數(shù)據(jù)只能被消費者組中的某一個消費者消費。
同一個消費者組的消費者可以消費同一個 Topic 的不同分區(qū)的數(shù)據(jù),這也是為了提高 Kafka 的吞吐量!
- Zookeeper:Kafka 集群依賴 Zookeeper 來保存集群的的元信息,來保證系統(tǒng)的可用性。
工作流程分析
上面介紹了 Kafka 的基礎架構及基本概念,不知道大家看完有沒有對 Kafka 有個大致印象,如果還比較懵也沒關系!
我們接下來再結合上面的結構圖分析 Kafka 的工作流程,***再回來整個梳理一遍我相信你會更有收獲!
發(fā)送數(shù)據(jù)
我們看上面的架構圖中,Producer 就是生產者,是數(shù)據(jù)的入口。注意看圖中的紅色箭頭,Producer 在寫入數(shù)據(jù)的時候永遠在找 Leader,不會直接將數(shù)據(jù)寫入 Follower!
那 Leader 怎么找呢?寫入的流程又是什么樣的呢?我們看下圖:
發(fā)送的流程就在圖中已經說明了,就不單獨在文字列出來了!需要注意的一點是,消息寫入 Leader 后,F(xiàn)ollower 是主動的去 Leader 進行同步的!
Producer 采用 Push 模式將數(shù)據(jù)發(fā)布到 Broker,每條消息追加到分區(qū)中,順序寫入磁盤,所以保證同一分區(qū)內的數(shù)據(jù)是有序的!
寫入示意圖如下:
上面說到數(shù)據(jù)會寫入到不同的分區(qū),那 Kafka 為什么要做分區(qū)呢?相信大家應該也能猜到,分區(qū)的主要目的是:
- 方便擴展。因為一個 Topic 可以有多個 Partition,所以我們可以通過擴展機器去輕松的應對日益增長的數(shù)據(jù)量。
- 提高并發(fā)。以 Partition 為讀寫單位,可以多個消費者同時消費數(shù)據(jù),提高了消息的處理效率。
熟悉負載均衡的朋友應該知道,當我們向某個服務器發(fā)送請求的時候,服務端可能會對請求做一個負載,將流量分發(fā)到不同的服務器。
那在 Kafka 中,如果某個 Topic 有多個 Partition,Producer 又怎么知道該將數(shù)據(jù)發(fā)往哪個 Partition 呢?
Kafka 中有幾個原則:
- Partition 在寫入的時候可以指定需要寫入的 Partition,如果有指定,則寫入對應的 Partition。
- 如果沒有指定 Partition,但是設置了數(shù)據(jù)的 Key,則會根據(jù) Key 的值 Hash 出一個 Partition。
- 如果既沒指定 Partition,又沒有設置 Key,則會輪詢選出一個 Partition。
保證消息不丟失是一個消息隊列中間件的基本保證,那 Producer 在向 Kafka 寫入消息的時候,怎么保證消息不丟失呢?
其實上面的寫入流程圖中有描述出來,那就是通過 ACK 應答機制!在生產者向隊列寫入數(shù)據(jù)的時候可以設置參數(shù)來確定是否確認 Kafka 接收到數(shù)據(jù),這個參數(shù)可設置的值為 0、1、all:
- 0 代表 Producer 往集群發(fā)送數(shù)據(jù)不需要等到集群的返回,不確保消息發(fā)送成功。安全性***但是效率***。
- 1 代表 Producer 往集群發(fā)送數(shù)據(jù)只要 Leader 應答就可以發(fā)送下一條,只確保 Leader 發(fā)送成功。
- all 代表 Producer 往集群發(fā)送數(shù)據(jù)需要所有的 Follower 都完成從 Leader 的同步才會發(fā)送下一條,確保 Leader 發(fā)送成功和所有的副本都完成備份。安全性***,但是效率***。
***要注意的是,如果往不存在的 Topic 寫數(shù)據(jù),能不能寫入成功呢?Kafka 會自動創(chuàng)建 Topic,分區(qū)和副本的數(shù)量根據(jù)默認配置都是 1。
保存數(shù)據(jù)
Producer 將數(shù)據(jù)寫入 Kafka 后,集群就需要對數(shù)據(jù)進行保存了!Kafka 將數(shù)據(jù)保存在磁盤,可能在我們的一般的認知里,寫入磁盤是比較耗時的操作,不適合這種高并發(fā)的組件。
Kafka 初始會單獨開辟一塊磁盤空間,順序寫入數(shù)據(jù)(效率比隨機寫入高)。
①Partition 結構
前面說過了每個 Topic 都可以分為一個或多個 Partition,如果你覺得 Topic 比較抽象,那 Partition 就是比較具體的東西了!
Partition 在服務器上的表現(xiàn)形式就是一個一個的文件夾,每個 Partition 的文件夾下面會有多組 Segment 文件。
每組 Segment 文件又包含 .index 文件、.log 文件、.timeindex 文件(早期版本中沒有)三個文件。
Log 文件就是實際存儲 Message 的地方,而 Index 和 Timeindex 文件為索引文件,用于檢索消息。
如上圖,這個 Partition 有三組 Segment 文件,每個 Log 文件的大小是一樣的,但是存儲的 Message 數(shù)量是不一定相等的(每條的 Message 大小不一致)。
文件的命名是以該 Segment 最小 Offset 來命名的,如 000.index 存儲 Offset 為 0~368795 的消息,Kafka 就是利用分段+索引的方式來解決查找效率的問題。
②Message 結構
上面說到 Log 文件就實際是存儲 Message 的地方,我們在 Producer 往 Kafka 寫入的也是一條一條的 Message。
那存儲在 Log 中的 Message 是什么樣子的呢?消息主要包含消息體、消息大小、Offset、壓縮類型……等等!
我們重點需要知道的是下面三個:
- Offset:Offset 是一個占 8byte 的有序 id 號,它可以唯一確定每條消息在 Parition 內的位置!
- 消息大?。合⒋笮≌加?4byte,用于描述消息的大小。
- 消息體:消息體存放的是實際的消息數(shù)據(jù)(被壓縮過),占用的空間根據(jù)具體的消息而不一樣。
③存儲策略
無論消息是否被消費,Kafka 都會保存所有的消息。那對于舊數(shù)據(jù)有什么刪除策略呢?
- 基于時間,默認配置是 168 小時(7 天)。
- 基于大小,默認配置是 1073741824。
需要注意的是,Kafka 讀取特定消息的時間復雜度是 O(1),所以這里刪除過期的文件并不會提高 Kafka 的性能!
消費數(shù)據(jù)
消息存儲在 Log 文件后,消費者就可以進行消費了。在講消息隊列通信的兩種模式的時候講到過點對點模式和發(fā)布訂閱模式。
Kafka 采用的是點對點的模式,消費者主動的去 Kafka 集群拉取消息,與 Producer 相同的是,消費者在拉取消息的時候也是找 Leader 去拉取。
多個消費者可以組成一個消費者組(Consumer Group),每個消費者組都有一個組 id!
同一個消費組者的消費者可以消費同一 Topic 下不同分區(qū)的數(shù)據(jù),但是不會組內多個消費者消費同一分區(qū)的數(shù)據(jù)!
是不是有點繞?我們看下圖:
圖示是消費者組內的消費者小于 Partition 數(shù)量的情況,所以會出現(xiàn)某個消費者消費多個 Partition 數(shù)據(jù)的情況,消費的速度也就不及只處理一個 Partition 的消費者的處理速度!
如果是消費者組的消費者多于 Partition 的數(shù)量,那會不會出現(xiàn)多個消費者消費同一個 Partition 的數(shù)據(jù)呢?
上面已經提到過不會出現(xiàn)這種情況!多出來的消費者不消費任何 Partition 的數(shù)據(jù)。
所以在實際的應用中,建議消費者組的 Consumer 的數(shù)量與 Partition 的數(shù)量一致!
在保存數(shù)據(jù)的小節(jié)里面,我們聊到了 Partition 劃分為多組 Segment,每個 Segment 又包含 .log、.index、.timeindex 文件,存放的每條 Message 包含 Offset、消息大小、消息體……
我們多次提到 Segment 和 Offset,查找消息的時候是怎么利用 Segment+Offset 配合查找的呢?
假如現(xiàn)在需要查找一個 Offset 為 368801 的 Message 是什么樣的過程呢?我們先看看下面的圖:
①先找到 Offset 的 368801message 所在的 Segment 文件(利用二分法查找),這里找到的就是在第二個 Segment 文件。
②打開找到的 Segment 中的 .index 文件(也就是 368796.index 文件,該文件起始偏移量為 368796+1。
我們要查找的 Offset 為 368801 的 Message 在該 Index 內的偏移量為 368796+5=368801,所以這里要查找的相對 Offset 為 5)。
由于該文件采用的是稀疏索引的方式存儲著相對 Offset 及對應 Message 物理偏移量的關系,所以直接找相對 Offset 為 5 的索引找不到。
這里同樣利用二分法查找相對 Offset 小于或者等于指定的相對 Offset 的索引條目中***的那個相對 Offset,所以找到的是相對 Offset 為 4 的這個索引。
③根據(jù)找到的相對 Offset 為 4 的索引確定 Message 存儲的物理偏移位置為 256。
打開數(shù)據(jù)文件,從位置為 256 的那個地方開始順序掃描直到找到 Offset 為 368801 的那條 Message。
這套機制是建立在 Offset 為有序的基礎上,利用 Segment+有序 Offset+稀疏索引+二分查找+順序查找等多種手段來高效的查找數(shù)據(jù)!
至此,消費者就能拿到需要處理的數(shù)據(jù)進行處理了。那每個消費者又是怎么記錄自己消費的位置呢?
在早期的版本中,消費者將消費到的 Offset 維護在 Zookeeper 中,Consumer 每間隔一段時間上報一次,這里容易導致重復消費,且性能不好!
在新的版本中消費者消費到的 Offset 已經直接維護在 Kafka 集群的 __consumer_offsets 這個 Topic 中!
作者:蘇靜
簡介:有過多年大型互聯(lián)網(wǎng)項目的開發(fā)經驗,對高并發(fā)、分布式、以及微服務技術有深入的研究及相關實踐經驗。經歷過自學,熱衷于技術研究與分享!格言:始終保持虛心學習的態(tài)度!