摘要
在這篇文章中,我將從訊息在Kafka中的物理儲存方式講起,介紹分割槽-日誌段-日誌的各個層次。
然後我將接著上一篇文章的內容,把消費者的內容展開講一講,區分消費者與消費者組,以及這麼設計有什麼用。
根據消費者的消費可能引發的問題,我將介紹Kafka中的位移主題,以及消費者要怎麼提交位移到這個位移主題中。
最後,我將聊一聊消費者Rebalance的原因,以及不足之處。
1. log
在上一篇文章中,我們提到了“partition”的概念。
我們那個時候所表達的意思是,訊息的生產跟消費是處於topic中的partition這個維度的,而不是位於主題的維度。
也就是說,我們那個時候對Kafka的理解,是處在topic下的每個parititon,都有一個稱為“佇列”的資料結構,所有送往這個主題的訊息,會被分配到其中的一個parititon中。
這樣的設計可以避免訊息佇列的效能在IO上具有瓶頸。
在這一節中,我們將進一步的解釋Kafka的訊息儲存方式。
我們所理解的“訊息”,在Kafka中被稱為日誌。
在每一個broker中,儲存了多個名字為{Topic}-{Parititon}
的資料夾,例如Test-1
、Test-2
。
這裡的意思是,這個broker中能夠處理topic為Test,分割槽為1和2的訊息。
但是注意,對於“parititon”這個名詞來說,他也是一個邏輯上的概念,對應在broker中只是一個資料夾,那麼什麼才是物理意義上的概念呢,我們接著往下看。
在{Topic}-{Parititon}
的資料夾內部,包含了很多很多的檔案,裡面的檔名都是64位的長整數。
例如:
在這張圖中,一個分割槽,包含了多個Log Segment
。注意,這裡的Log Segment
也是邏輯上的概念,只有具體到具體的日誌檔案,才是物理上的概念。
我們看圖片最右邊的部分,檔名都是20位的整數,這個數字稱為訊息的“基準偏移量”。例如我們第二個Log Segment是從121開始的,那麼代表了這個日誌段的第一條訊息的偏移量是從121開始的,也代表了在這之前有121條日誌記錄。
注意,因為我們的偏移量是從0開始的,所以在121這個偏移量之前有121條資料,而不是120條。
然後我們再聊聊檔案的格式,我們看到這裡有三種型別的檔案,*.log
、*.index
、*.timeindex
。
log格式的檔案記錄了訊息,index是偏移量索引,timeindex是時間戳索引。但是這個我們不展開聊,這篇文章的定位還是偏向於瞭解各個元件。
如此一來,broker在接收到生產者發過來的訊息的時候,需要將訊息寫在最後的Log Segment中。這樣還帶來了一個好處,訊息的寫入是順序的IO。也因為如此,最後的一個Log Segment,被稱為“active Log Segment”。
2. 消費者與消費者組
在上一篇文章中,我們只提到了“消費者”這個概念。
同樣在本文中,我們將更深入更準確的瞭解位於Kafka中的“消費者”。
其實在Kafka中,消費者是以消費者組的形式對外消費的。
我們作一個假設,假設沒有消費者組這種概念,我們現在有10個消費者訂閱了同一個主題,那麼當這個主題有新的訊息之後,我們這10個消費者是不是應該去“搶訊息”進行消費呢?
這是一種浪費資源的表現。
所以消費者組,也可以認為是一種更加合理分配資源,進行負載均衡的設計。
假設有5個消費者屬於同一個消費者組,這個消費者組訂閱了一個具有10個分割槽的主題,那麼組內的每一個消費者,都會負責處理2個分割槽的訊息。
這樣,能夠保證當一條訊息傳送到主題中,只會被一個消費者所消費,不會造成重複消費的情況。
此外,消費者組的設計還能夠令我們很方便的橫向擴充套件系統的消費能力。設想一下在我們發覺系統中訊息堆積越來越多,消費速度跟不上生產速度的時候,只需要新增消費者,並且將這個消費者劃入原來的消費者組中,Kafka會自動調整組內消費者對分割槽的分配,這個過程稱為重平衡,我們在後面會提到。
但是需要注意的是,組內消費者的數量不能超過主題的分割槽數目。否則,多出的消費者將會空閒。例如一個主題具有10個分割槽,而組內有11個消費者,那麼這多出來的一個消費者將空閒。
Kafka這樣的設計是為了同一個分割槽只能夠被一個消費者所消費,這個跟位移管理有關,我們在後文會提到。
另外,Kafka還支援多個消費者組訂閱同一個主題,這樣,相同的訊息將被髮送到所有訂閱了這個主題的消費者組中。
注意:我們說到了同一分割槽只能被同一個消費者消費,但是這個說法的前提是這些消費者位於同一個消費者組。也就是說,不同消費者組內的消費者,是可以消費同一個主題分割槽的。
所以,我們也可以認為Kafka的消費者組,是為了實現點對點以及廣播這兩種方式的訊息傳遞。
3. 位移主題
我們在上一個小節提到了消費者組內的消費者對分割槽內的資訊進行消費,並且存在了消費者的加入與退出這種情況。
所以在這節我們來聊聊Kafka是怎麼做到在消費者有變動的情況下,訊息不會丟失或者重複消費。
我們可以很容易的想到,只要記錄下消費過的位移,就能夠實現上述的目標了。
我們直接聊聊位移主題這種方式,不管以前的將位移儲存在zk中的實現方式。
在Kafka中有一種特殊的主題,稱為位移主題,在Kafka中的主題名稱是__consumer_offsets
。
因為位移主題也是一個主題,所以也符合Kafka中主題的各種特性,我們可以隨意的傳送訊息,拉取訊息,刪除主題。但是因為這個主題的資料是kafka設計好的,所以不能隨意的傳送訊息過去,否則在broker端不能解析的話,就會造成崩潰。
然後我們討論一下發往位移主題的訊息格式。因為我們是希望儲存位移,所以很容易會想到這是一個KV結構。那麼Key中應該儲存哪些訊息呢?
Key中包含了主題名,分割槽名,消費者組名。
其實在這裡是不需要儲存消費者id之類的資訊的,也就是說只需要具體到是哪個消費者組在哪個主題的哪個分割槽消費了多少資料,就足夠了。為什麼呢?因為我們上文也提到了,消費者是可能發生變動的,我們的目的是讓消費者發生變動後,能知道從哪裡繼續消費。因此,位移資訊的精確度到消費者組級別,就足夠了。
並且,在Value中,只需要儲存消費位移,就足夠了。
說完了位移資訊是怎麼儲存的,我們再來聊聊位移主題本身。因為位移主題也是一個主題,所以必然也會有分割槽,也會有副本。那麼消費者在消費了資訊之後,該把位移傳送到哪呢?
Kafka中的位移主題會在第一個消費者被建立的時候建立,預設會有50個分割槽。消費者在提交位移的時候,會根據自己組id的hash值模位移主題的分割槽數,所得到的結果就是位移資訊該提交的分割槽id,然後找到這個分割槽id的leader節點,將位移資訊提交到這個leader節點所在的broker中。
4. 位移的提交
聊了位移主題,我想你大概明白Kafka關於位移狀態的儲存了,那麼在這一節中,我們來聊聊位移是怎麼被提交的。
在說到位移的提交之前需要明確的是:雖然有了位移主題這樣的設計,但是並不代表了訊息一定不會被重複消費,也不代表訊息一定不會丟失。
另外,Kafka會嚴格的執行位移主題中所提交的資訊。例如已經消費了0-20的訊息,如果你提交的位移是100,那麼下一次拉取的資訊一定是從100開始的,20-99的訊息將會丟失。又比如你提交的位移是10,那麼10-20的訊息將會被重複消費。
在Kafka中,位移的提交有兩種方式,一種是自動提交,一種是手動提交。
4.1 自動提交
位移的自動提交是在POLL操作的時候進行的。
在消費者POLL拉取最新的訊息之前,會先判斷目前是否已經到了提交位移的Deadline時間點,如果已經到了這個時間,則先進行位移的提交,然後再拉取資訊。
注意,這裡可能會發生如下的情況:
在某一時刻提交了位移100,隨後你拉取了100-150的訊息,但是還沒有到下一次提交位移的時候,消費者當機了。可能這個時候只消費了100-120的訊息,那麼在消費者重啟後,因為120的位移沒有提交,所以這部分的訊息會被重複消費一次。
再設想一種情況,你拉取了100-150的訊息,這個時候到了自動提交的時間,提交了150的這個位移,而這個時候消費者當機了,重啟之後會從150開始拉取資訊處理,那麼在這之前的資訊將會丟失。
4.2 手動提交
對於因為自動提交而造成的資訊丟失和重複消費,你可以採取手動提交的方式來避免。
手動提交又分為同步提交和非同步提交兩種提交方式。
同步提交會直到訊息被寫入了位移主題,才會返回,這樣是安全的,但是可能造成的問題是TPS降低。
非同步提交是觸發了提交這個操作,就會返回。這樣速度是很快的,但是可能會造成提交失敗的情況。
5. Rebalance
我們在上面的內容中提到過這麼一種情況:
消費者組內的成員增減,導致組內的成員需要重新調整他需要負責的消費的分割槽。
這種情況我們稱為“Rebalance”,或者稱為“重平衡”。
用專業一點的話來下定義就是:某個消費組內的消費者就如何消費某個主題的所有分割槽達成一個共識的過程。
但是這個過程對Kafka的吞吐率影響是巨大的,因為這個過程有點像GC中的STW(世界停止),在Rebalance的時候,所有的消費者只能去做重平衡這一件事情,不能消費任何的訊息。
下面我們來說說哪些情況可能會導致Rebalance:
- 組內成員數量發生了變化
- 訂閱主題的數量發生了變化
- 訂閱主題的分割槽數量發生了變化
而且在Rebalance的時候,假設有消費者退出了,導致多出了一些分割槽,Kafka並不是把這幾個多出來的分割槽分配給原來的那些消費者,而是所有的消費者一起參與重新分配所有的分割槽。
當有新的消費者加入的時候,也不是原本的每個消費者分出一些分割槽給新的消費者,而是所有的消費者一起參與重新分配所有的分割槽。
這樣的分配策略聽起來就很奇怪且影響效率,但是沒有辦法。
不過社群新推出了StickyAssignor(粘性分配)策略,就可以做到我們上面假設的情況,但是目前還存在一些bug。
寫在最後
首先,謝謝你能看到這裡!
關於Kafka的前兩篇文章,我認為都是科普性質的,希望可以用比較簡單的方式給你梳理一遍Kafka具有的功能,以及各個功能的運作方式。
在後面的文章中,我也希望能夠比較清晰易懂的給你介紹Kafka的一些原理之類的東西。
因為作者也剛開始研究Kafka,很多地方的理解可能還是不到位的,所以在這期間如果你發現有什麼問題,或者有哪些地方是我解釋的不好的,請留言告訴我,謝謝你!
再次感謝你能看到這裡!
PS:如果有其他的問題,也可以在公眾號找到我,歡迎來找我玩~