Kafka是一種 消息中間件 。舉個例子,一個網站在用戶進行註冊的時候,後台的操作包括發送郵箱、寫入用戶表、寫入日誌等等,但是一般都不會等這些完全處理完才告訴你註冊成功。這一系列的操作通常是異步的。
但是異步跟消息中間件有什麼聯繫呢?我們試想一下,假如沒有消息中間件,後台操作的順序假設是
- (1)寫入用戶表
- (2)寫入日誌
- (3)發送郵箱
如果系統在寫入用戶表之後,異常重啟,或者需要更新(這個是很正常的,一般來說我們根本無法預估用戶喜歡在什麼時候在我們網站上註冊用戶,但是我們系統需要更新疊代)。那麼後面兩個步驟就因為系統重啟內存被清空而無法完成了。
如果有了消息中間件,生產者(當前服務)將這三個操作進行消息通知,暫時存放在Kafka或者其他消息中間件中,等待消費者(下游服務)進行消費,然後異步操作寫入用戶表、日誌以及發送郵箱。這麼做的好處就是,不管當前服務、下游服務什麼時候更新、或者出現了異常,都可以保證消息不會丟失,暫時保存在消息中間件中。
那麼消息中間件就一定是萬無一失的嗎?不一定,但是通常來說,消息中間件具有穩定性高、處理邏輯簡單、不需要更新疊代等特點,數據存在消息中間件中遠比存在服務內存中要穩靠得多。
那麼有了消息中間件,上下游之間的通信由原來的接口或者RPC調用改成了消息生產、消費,響應的性能會不會減弱呢?在一定程度上雖然會降低一些,但是目前Kafka等一些消息中間件,儘量保證消息的高吞吐、低延遲,每一秒可以處理幾十萬條消息。除去網絡性能IO帶來的開銷無法避免之外,消息中間件幾乎可達到毫秒級的延遲。
2、設計
消息中間件具有兩種模式,一種是點對點(P2P),一種是發布/訂閱(publish/subscribe),而Kafka屬於第二種,如下圖
發布者publisher發布一條消息message,訂閱了這個發布者的所有訂閱者subscriber都可以同時消費這條消息。
在消息隊列中,發布者通常叫做生產者producer,訂閱名稱叫做主題topic,訂閱者叫做消費者consumer。
Kafka在發布/訂閱模式之上,加入了組group的概念,每一個消費者consumer都屬於一個消費組consumer group,每個組group可以有多個消費者consumer,每個消費者也可以加入多個消費組consumer group。發送到主題topic下的消息,會被訂閱了這個主題topic的每個消費組consumer group消費。但是注意,一條消息只能被一個消費者組consumer group內的一個消費者consumer消費。 也就是說,假設所有的消費組consumer group都訂閱了主題topic,如果所有的消費者consumer都在同一個消費組consumer group中, 那麼是P2P模式,消息會在組內所有的消費者consumer之間負載均衡;相反,如果所有的消費者consumer都在自己單獨的消費組consumer group中,那麼每個消費者consumer都可以同時消費這個主題topic下的消息
如上圖所示,我們應該可以理解到,C4和C5能夠單獨消費消息message,但是同在消費組consumer group 1中的C1、C2、C3隻能通過默認狀態下(負載均衡)的形式進行消費。
其實這個分組是概念上的分組,我們只是用一些變量將這些消費者consumer進行約束。真正意義上的Kafka還引入一個物理上的分組概念
- 分區 partition
分區partition是主題topic的下一級劃分,一個主題topic至少有一個分區partition,一般都有多個分區(為了並發寫入,提高吞吐量)。如下圖所示
我們看到,該主題topic下有三個分區(分別是分區0、分區1、分區2),假如目前有一條消息message從生產者producer產生,需要進行寫入操作。那麼我們可以有兩種方式:
- 指定分區進行寫入
- 默認寫入
在代碼中,生產者producer發送消息message的時候,除了必須要指定topic之外,還有一個可選的值key,這個key如果非空,那麼是會通過Hash算法對分區數進行取模得到指定分區的。這個key通常應用於分布式中(以後會專門講);如果key為空,那麼就會負載均衡到每個分區,進行寫入。
圖中每個分區內的那些小框數字代表位移,它叫offset,在很多書上面的中文描述不盡相同,因此我這裡只叫位移。它是不可更改的,一旦寫入,無法修改,也無法單個刪除。這種append追加的方式,有以下好處:
- 因為不能修改和刪除,所以即使在並發讀寫時也無需加鎖;
- 採用了末尾追加寫入的方式,避免了磁碟隨機讀/寫操作;
另外,每個分區partiton其實就是一個append log文件,每次寫入都是進行 內存寫入 ,在伺服器中,會對消息進行緩存buffer起來,只有在消息的個數或者大小達到一定的閾值的時候,才會進行flush到磁碟中。
這種設計大大提高了性能,即使在大量的並發讀寫操作下,也不會被壓垮。
在讀取每一個message的時候,我們用以下的三元組進行定位
我們可以隨機消費每一個消息message,只要我們知道主題topic、分區partiton以及位移offset。
但是其實如果Kafka如果需要重啟或者伺服器突然宕機,那麼寫入在append log文件的信息如果沒有達到一定的閾值,就還是保存在內存中,此時突發情況下的消息就會丟失。
為了保險起見,我們需要對Kafka進行數據備份冗餘,也就是集群部署。
3、集群
集群部署最常見的有兩種,一種是mysql的 主備 (master-slave),另一種是zookeeper的 領導者追隨者 (leader-follower)。傳統的主備系統可以同時對外提供服務,只不過master提供讀寫而slave只提供讀。但是像zookeeper這種的卻不一樣, 只有leader對外提供服務 ,通俗一點說,follower只是用來等leader掛了,然後重新選舉,有機會就自己當leader,沒有機會就一直等著。等leader掛了, 重新選舉,leader掛了,重新選舉,如此循環(有的時候,不是你的,註定不是你的).....
這麼來說,其實主備系統可能命還好一點,雖然沒有改動權,但是還能對外提供服務(至少能露露臉)。
在Kafka中,這些備份下來的日誌被稱為 副本replica
為什麼目前會更新leader和follower模式,follower只用作備份而不對外提供服務呢。因為對於秒級幾十萬的寫入讀取並發量,主從一致是很難做到的。對於出現已經將消息寫入在了leader的分區partition中,但是沒有來得及備份到follower的情況,如果A和B分別訪問leader和follower的該分區partition,得到的是A消費了該數據,而B沒有消費該數據的不同的結果,這樣做不到消息獲取的 冪等性 。
因此,新的leader-follower模式中,只有leader對外提供讀寫服務。leader會對設好的副本因子數對每一份分區partition都進行冗餘備份,假設副本因子數為3,那麼就會分別在三個不同的伺服器broker中進行備份。 Kafka會保證同一個分區partition的多個replica一定不會分配在同一台伺服器broker上 。如下圖
在這裡,我們打破一下我們前面的觀點
- 注意,並不是所有的follower都有權力參與選主
意思就是說,對於主題topic有四個分區,副本因子是3的情況,如果因為網絡IO性能或者其他的原因,在同步的過程中與leader replica 5(假如是上圖中的副本5)保持一致的只有另外一個副本follower replica 2(假如是副本2),而剩下的一個副本follower replica 3卻因為服務時好時壞,始終無法與leader replica 5保持一致,那麼假如leader replica 5突然掛掉了,follower replica 3是無法參與重新選主的(可能follower replica 2就自然而然成了「繼承人」)。
說到這裡大概可能就明白了,對於leader replica 5(老大)來說,它會在所有的其他follower replica中選取一些作為它的"馬仔",這些follower replica(馬仔)需要聽這個leader replica 5老大的話,比如要及時處理leader replica 5(老大)發過來的信息同步的請求,並且做好備份,給出響應。這個「馬仔圈」在Kafka中的術語叫做 ISR ,全稱為in-sync replica。翻譯過來,就是與 leader replica保持同步的集合 。
- 也就是說,在ISR集合內的replica必須與leader replica保持同步狀態(主要是消息日誌),而在ISR集合之外的replica同樣會接收到leader replica的信息同步的請求,但是不要求時限。如果ISR集合內的replica沒有按時與leader replica保持消息日至的同步,會被「踢出」ISR集合的。相反的,如果這些replica重新「追上」了leader replica的進度,那麼會被「加回」到ISR集合中。
注意,哪些follower replica在ISR集合中,什麼時候會被「踢出」,什麼時候會被「加回」,都是Kafka自動維護的,不需要用戶進行人工干預。
因此,我們引出一個概念
- 消息的提交
什麼是消息提交?在Kafka中,如果該主題topic下的leader replica以及ISR集合中的所有replica都已經備份了這條消息,那麼Kafka就把這條消息置於「 已提交 」的狀態,即認為這條消息 發送成功 (在producer那一邊沒有問題)。
需要注意,所有的 ISR集合都是包括leader replica在內的 。也就是說,上面所說的例子中,ISR集合其實是包括replica 2(follower replica)和replica 5(leader replica)在內的。
Kafka能夠保證,只要ISR集合中至少存在一個replica,那些「已提交」狀態的消息就不會丟失(極端情況下就是老大沒有馬仔了,然後老大也不幸掛掉了,那麼這個時候保存在老大機器里的append log可能就真的沒了233333)
其實 只要ISR集合中只要有一個replica沒有備份這條消息,Kafka都不會將這條消息置為「已提交」 。
end:如果你覺得本文對你有幫助的話,記得關注點贊轉發,你的支持就是我更新動力。