摘要
在這一篇文章中,我將向你介紹消費者的一些引數。
這些引數影響了每次poll()
請求的資料量,以及等待時間。
在這之後,我將向你介紹Kafka用來保證消費者擴充套件性以及可用性的設計——消費者組。
在消費者組的介紹中,我將重點放在了Rebalance
的過程上,因為這是一個很重要又經常發生,還會導致消費者組不可用的操作。
1 消費者引數配置
對於一個消費者來說,他要做的事情只有一件,那就是使用poll()
來拉取訊息。
至於他是從哪個分割槽拉取,則是靠消費者組來動態的調整這個消費者所消費的分割槽,又或者是由開發者來自定義。
但無論如何,這個消費者都需要通過poll()
來拉取訊息。
這也是這一節的內容:通過引數配置能夠影響poll操作的哪些內容。
首先需要確定一點,當消費者使用poll()
拉取訊息的時候,他只能拉到HW水位線及以下的訊息。
1.1 分割槽配置
我們可以讓消費者針對於某一個分割槽進行消費。
為了實現這個目標,我們可以用assign()
方法。
但是注意,當這個消費者不是單獨的一個消費者,而是屬於某個消費者組的時候,將不允許使用自定義的分割槽分配。
1.2 POLL操作拉取的位元組數目
對應的配置分別是:
fetch.min.bytes
對於每次拉取的最小位元組數,預設是1。當拉取的訊息大小小於設定的這個限度時,將會等待,直到這次被拉取的訊息大小大於這個值。
於是我們可以得知,當我們即將要消費的訊息比較小時,可以適當的調大這個引數的值,以提高吞吐量。
但是注意,這也可能造成訊息的額外延遲。
fetch.max.bytes
這個引數跟上面的一樣,只不過他代表的意義是最大的位元組數。
但是這存在一個問題,如果我們的訊息大小全都大於這個引數的值,會發生什麼情況呢?
答案是會返回即將拉取分割槽的第一條訊息。
也就是說在這個引數中,不存在“不符合條件就不返回資料”的情況。
還有一個引數,叫做max.partition.fetch.bytes
這個引數跟上面提到的每次拉取的最大位元組數工作原理是一樣的,也是會保證當訊息大於設定的值的時候,一定會返回資料。
而不同的地方在於,這個引數代表的是分割槽。也就是說,一個引數代表的是一次拉取請求,而另外一個引數代表的是針對於每一個分割槽的拉取請求。
1.3 拉取訊息的超時時間
fetch.max.wait.ms
這個引數的意義在於:如果拉取訊息的時間達到了這個引數設定的值,那麼無論符不符合其他條件,都會返回資料。
那麼你很容易可以猜到,這個引數跟fetch.min.bytes
是有關係的,這是為了防止當fetch.min.bytes
引數設定的過大,導致無法返回訊息的情況。
當然了,這個引數還有一個意義,如果你的業務需要更小的延遲,那麼應該調小這個引數。
1.4 最大拉取訊息數
如果我們的最大拉取位元組數設定成了非常大,那麼是不是代表我們每一次的poll()
,都能直接拉到HW水位呢?
答案是否定的。
還存在一個引數:
max.poll.records
這個引數的意義在於,每次拉取訊息的最大數量。
同樣的,如果訊息的大小都比較小,那麼可以調大這個引數,以提高消費速度。
1.5 消費者組相關的引數
另外,還存在一些消費者組相關的引數,我在這裡先提一下,具體更詳細的解釋,將在後文給出。
heartbeat.interval.ms
這個引數是設定消費者與消費者組對應的Coordinator傳送心跳響應的間隔時間。
session.timeout.ms
這個引數是用於Coordinator判斷多長時間沒收到消費者的心跳響應而認為這個消費者已經下線的時間。
max.poll.interval.ms
這個引數用於Coordinator判斷多長時間內消費者都沒有拉取訊息,而認為這個消費者已經下線的時間。
auto.offset.reset
這個引數其實跟消費者組的聯絡不是很大,但是我認為可以寫在這裡。
因為有這麼一個場景,當消費者Rebalance之後,如果位移主題之前儲存的位移已經被刪除了,那麼這個引數就決定了消費者該從哪裡開始消費。
當然了,關於消費者還有許多的引數,不僅僅是上文提到的這些。
而上文提到的這些引數,是我認為可以讓初學者更好的理解消費者的工作原理。
2 Rebalance原理
在解釋Rebalance的原理之前,我想先跟你說一下我的思路,免得你看的一頭霧水。
當然了,這個思路是我認為更適合我自己去理解的。你也可以先看第三大節,再有了一個大概的認識後,再來看這一節的內容。
我希望先告訴你Rebalance的過程是怎麼樣的,這裡說的過程指的是Rebalance已經發生了,那麼在Rebalance的過程中,會發生哪些事情。
在這之後,我再跟你說說Rebalance的五種狀態。
那麼,我們開始。
2.1 尋找Coordinator
首先,應該有一個認識。Rebalance的所有操作都是通過Coordinator的協調下完成的,組內的消費者之間並不會進行相關的通訊與交流。
Coordinator你可以理解為是一個服務,位於某個broker節點上。
假設當前的消費者已經儲存了這個這個節點的資訊,那麼將會直接進入第二步。
如果當前的消費者沒有儲存這個資訊(比如這是一個新加入這個消費者組的消費者),那麼他需要先找到這個Coordinator所在的broker節點。
這裡的broker節點,是這個消費者對應的消費者組對應的位移主題的分割槽的leader節點。
聽起來有點繞,讓我來再解釋一下。
消費者 -> 消費者組 -> __consumer_offsets -> partition -> leader
關於位移主題,我已經在第二篇文章中提到過了,在這裡不再贅述。
但是在這裡,讓我們來再來回憶一遍消費者組對應的partition是怎麼找到的。
- 先獲取
Group ID
的hash值 - 將這個hash值,對
__consumer_offsets
的分割槽數取模 - 獲得的數字,就是這個消費者組提交位移的分割槽
- 找到這個分割槽對應的leader副本,即為Coordinator對應的broker節點
2.2 Join Group
在找到了對應的broker節點後,第二步是傳送加入Group的請求。
在這一步中,無論是之前已經在Group內的成員,還是準備加入Group的成員,都需要傳送Join Group的申請。
在發起的JoinGroupRequest中,需要包含如下的資料:
-
Group id
-
Session_timeout
-
Rebalance_timeout
-
Menber_id
-
Partition assignor
需要事先說明的是,這裡的名稱並不嚴格,是為了更好的理解而這樣寫的。如果你想要知道更加嚴謹的請求內容,可以去看廝大的《深入理解Kafka》。
下面我們挨個解釋:
Group ID
,消費者組ID,代表了即將加入的消費者組。
Session_timeout
,上文中提到過這個引數,用於Coordinator判斷多長時間內沒收到客戶端的心跳包而認為這個客戶端已經下線。
Rebalance_timeout
,值等同於max.poll.interval.ms
,意義在於告知Coordinator用多長的時間來等待其他消費者加入這個消費者組。
我們在上文中提到,無論之前是不是這個消費者組的成員,只要開啟了Rebalance,就需要重新加入這個消費者組。因此,Coordinator需要一段時間來接受JoinGroupRequest的請求。
至於為什麼需要一段時間來接受請求,以及這段時間發生了什麼,我將在後面給你解釋。
menber_id
,作為組內消費者的識別編號,如果是新加入組的消費者,這個欄位留空。
Partition assignor
,指的是分割槽分配方式。因為Rebalance這個過程,就是分割槽分配的一個過程。每個消費者將其接受的分配方式放在這個欄位中,隨後由Coordinator選出每個消費者都認可的分割槽分配方式。
然後我們來聊聊在這個階段,Coordinator需要做什麼。
Coordinator需要一段時間來接收來自客戶端的JoinGroupRequest請求,是因為Coordinator需要收集每一個成員的資訊,選出leader和分割槽分配方式,因此,Coordinator需要足夠的時間來“收集資訊”。這就回答了上文說到的為什麼“Coordinator需要一段時間來接受JoinGroupRequest的請求”。
選舉leader的演算法很簡單,第一個傳送請求的consumer,就是leader。
選出分割槽分配策略的演算法也很簡單,首先Coordinator會收集所有消費者都支援的分割槽分配方式,然後每個消費者為它支援的分配方式投上一票。注意,這裡的投票行為沒有經過多一次的互動,而是Coordinator選取每個消費者的JoinGroupRequest中的第一個分割槽分配方式,作為這個消費者所投的票。
當Coordinator選取好Leader和分割槽分配方式後,將返回JoinGroupResponse給各個消費者。
在返回給各個消費者的JoinGroupResponse中,包含了menber_id,分割槽分配方式等。而對於leader消費者來說,還將獲得組內其他消費者的後設資料,包含了各個消費者的menber_id,分割槽分配方式。
至此,JoinGroup階段完成。
注意,每個消費者從傳送JoinGroupRequest到接收到JoinGroupResponse請求這段時間,是阻塞的。
2.3 分配分割槽
在第二步結束之後,每個消費者已經知道了自己的menber_id
,以及Coordinator所選擇的分割槽分配方式。
但是此時每個消費者還不知道自己應該消費哪個分割槽。
這個分割槽分配的過程,是交給Leader消費者來完成的。
但是注意,雖然說這個過程是Leader消費者完成的,但是Leader消費者並不會跟其他消費者直接通訊,而是將分配方式告知Coordinator,由Coordinator來告知各個消費者。
這個過程,稱為Sync_Group
。
在這個過程中,每一個消費者都會傳送SyncGroupRequest給Coordinator。要注意的是,Leader消費者在這個Request中還附帶了其他消費者的分割槽分配資訊。
在Coordinator收到了這些請求後,會將這個分割槽分配方案等後設資料儲存在__consumer_offsets
主題中。
隨後,Coordinator將傳送響應給各個消費者。
在這個響應中,包含了各個消費者應該負責消費的分割槽編號。
至此,每個消費者都瞭解了自己應該消費的分割槽是哪些了。
2.4 消費併傳送心跳包
在上一個階段中,組內各個消費者已經知道了自己負責的是哪些分割槽。
但是還存在一個問題,消費者應該從分割槽的哪個位置開始消費呢?
這就用到了__consumer_offsets
主題了,這個主題儲存了某個消費者組的各個分割槽的消費位移。
此外,每個消費者還需要不斷地傳送心跳包給Coordinator,以告知Coordinator自己沒有下線。
這個傳送心跳包的時間,就是我們設定的heartbeat.interval.ms
引數。
在每個心跳包的響應中,Coordinator就會告知這個消費者,需不需要Rebalance。
那麼也就說明了,這個引數設定的越小,消費者就越早能夠得知是否需要Rebalance。
而對應的session.timeout.ms
,指的就是Coordinator在這麼長的時間內沒收到消費者的心跳包,而認為這個消費者過期的引數。
3 消費者組的狀態轉移
在上面說完了Rebalance的核心原理後,我們再來聊聊消費者組的各個狀態。
先來介紹一下消費者組有哪幾種狀態:
- Empty:組內沒有任何的成員,但是保留著這些成員的後設資料,比如在發生Rebalance的時候,Coordinator在心跳包的響應中告知消費者應該要進行Rebalance了,這個時候所有的消費者都離開了消費者組,那麼這個消費者組就會處於Empty狀態。注意,一個新建立的消費者組,也處於這個狀態。
- Dead:組內沒有任何的成員,並且在
__consumer_offsets
中也沒有儲存這個消費者組的後設資料。通常發生在這個消費者組被刪除了,或者__consumer_offsets
分割槽leader發生了改變。(至於這個狀態我瞭解的也不是很多,如果可以的話,麻煩你評論區告訴我。) - PreparingRebalance:這個狀態為Coordinator正在等待Consumer加入。這個狀態對應於JoinGroup階段,會持續
Rebalance_timeout
這麼長的時間。 - CompletingRebalance:也被稱為AwaitingSync,為Coordinator正在等待Leader消費者的分割槽分配方案。對應於SyncGroup階段。
- Stable:到了這個階段,消費者組已經在正常工作了。
消費者組的狀態介紹大概就是這樣的。
簡單的來講,當一個消費者組需要Rebalance的時候,他就會進入PreparingRebalance階段,然後一直流轉到Stable階段。
在這個期間,如果有任何的成員變動,就會回到PreparingRebalance階段。
在這個期間,如果Coordinator改變,或者消費者組被刪除等,就會進入Dead階段。
寫到最後
首先,謝謝你能看到這裡!
在這一篇文章中,我沒有像介紹生產者那樣介紹一遍原始碼。
因為對於生產者來說,他只需要將訊息傳送到broker中,而對於消費者來說,這個過程複雜得多,我希望能夠用比較淺顯易懂的方式,讓你能夠了解消費者組的工作方式。
在有了這樣的一個認識之後,無論使用什麼客戶端,我認為都不會有太大的問題。
此外,在這一篇中我花了較大的筆墨去介紹Rebalance的過程,是因為Rebalance是一個很常見的現象,而且在這期間會導致Kafka消費者的不可用,所以我希望瞭解了Rebalance的工作原理,能夠讓你更容易的避免不必要的Rebalance。
當然了,因為作者才疏學淺能力有限,可能在這個過程中忽略了一些很重要的細節,又或者有一些錯誤的理解。如果你發現了,還請不吝指教,謝謝你!
再次謝謝你能看到這裡,感恩~
PS:如果有任何的問題,可以在公眾號找到我,歡迎來找我玩!