分散式訊息通訊Kafka(二) - 原理分析

at_1發表於2021-09-09

本文目標

  1. Topic&Partition
  2. 訊息分發策略
  3. 訊息消費原理
  4. 訊息的儲存策略
  5. Partition 副本機制

1 關於 Topic 和 Partition

1.1 Topic

在 kafka 中,topic 是一個儲存訊息的邏輯概念,可以認為是一個訊息集合
每條傳送到 kafka 叢集的訊息都有一個類別。物理上來說,不同 topic 的訊息是分開儲存的,每個 topic 可以有多個生產者向它傳送訊息,也可以有多 個消費者去消費其中的訊息。

圖片描述

1.2 Partition

每個 topic 可以劃分多個分割槽(每個 Topic 至少有一個分割槽),同一 topic 下的不同分割槽包含的訊息是不同的。
每個訊息在被新增到分割槽時,都會被分配一個 offset(偏移量),它是訊息在此分割槽中的唯一編號,kafka 透過 offset 保證訊息在分割槽內的順序,offset 的順序不跨分割槽,即 kafka 只保證在同一個分割槽內的訊息有序

下圖中,對於名字為 test 的 topic,做了 3 個分割槽,分別是 p0、p1、p2.

➢ 每一條訊息傳送到 broker 時,會根據 partition 的規則選擇儲存到哪一個 partition。如果 partition 規則設定合理,那麼所有的訊息會均勻的分佈在不同的 partition 中, 這樣就有點類似資料庫的分庫分表的概念,把資料做了分片處理。
圖片描述

1.3 Topic&Partition 的儲存

Partition 是以檔案的形式儲存在檔案系統中,比如建立一個名為 firstTopic 的 topic,其中有 3 個 partition,那麼在 kafka 的資料目錄(/tmp/kafka-log)中就有 3 個目錄, firstTopic-0~3
命名規則是 :

<topic_name>-<partition_id> ./kafka-topics.sh --create --zookeeper 192.168.11.156:2181 --replication-factor 1 --partitions 3 -- topic firstTopic

2 關於訊息分發

2.1 kafka 訊息分發策略

訊息是 kafka 中最基本的資料單元.
在 kafka 中,一條訊息由 key、value 兩部分構成,在傳送一條訊息時,我們可以指定這個 key,那麼 producer 會根據 key 和 partition 機制來判斷當前這條訊息應該傳送並儲存到哪個 partition 中.
我們可以根據需要進行擴充套件 producer 的 partition 機制。

2.2 訊息預設的分發機制

預設情況下,kafka 採用的是 hash 取模的分割槽演算法。
如果 Key 為 null,則會隨機分配一個分割槽。這個隨機是在這個參 數metadata.max.age.ms的時間範圍內隨機選擇一個。
對於這個時間段內,如果 key 為 null,則只會傳送到唯一的分割槽。該值預設情況下10 分鐘更新一次。
關於 Metadata,簡單理解就是 Topic/Partition 和 broker 的對映關係,每一個 topic 的每一個 partition,需要知道對應的 broker 列表是什麼,leader 是誰、follower 是誰。這些資訊都是儲存在 Metadata 這個類裡面。

2.3 消費端如何消費指定的分割槽

透過下面的程式碼,就可以消費指定該 topic 下的 0 號分割槽。 其他分割槽的資料就無法接收

//消費指定分割槽的時候,不需要再訂閱 
//kafkaConsumer.subscribe(Collections.singleto nList(topic));

//消費指定的分割槽
TopicPartition topicPartition=new TopicPartition(topic,0); 
kafkaConsumer.assign(Arrays.asList(topicPartit ion));

3 訊息的消費原理

3.1 kafka 訊息消費原理演示

在實際生產過程中,每個 topic 都會有多個 partitions,多 partitions 的好處在於

  • 一方面能夠對 broker 上的資料進行分片,有效減少了訊息的容量從而提升 I/O 效能
  • 另外,為了提高消費端的消費能力,一般會透過多個 consumer 去消費同一個 topic ,也就是消費端的負載均衡機制,也就是我們接下來要了解的,在多個 partition 以 及多個 consumer 的情況下,消費者是如何消費訊息的

在上文,我們講了,kafka 存在 consumer group的概念,也就是group.id 一樣的 consumer,這些 consumer 屬於一個 consumer group.
組內的所有消費者協調在一起來消費訂閱主題的所有分割槽。當然每一個分割槽只能由同一個消費組內的 consumer 來消費,那麼同一個 consumer group 裡面的 consumer 是如何分配該消費哪個分割槽裡的資料的呢?

  • 如下圖所示,3 個分割槽,3 個消費者,那麼哪個消費者該消費哪個分割槽呢?
    圖片描述

對於上面這個圖來說,這 3 個消費者會分別消費 test 這個 topic 的 3 個分割槽,也就是每個 consumer 消費一個 partition。

3.2 什麼是分割槽分配策略

透過前面的案例演示,我們應該能猜到,同一個 group 中的消費者對於一個 topic 中的多個 partition,存在一定的分割槽分配策略.

在 kafka 中,存在兩種分割槽分配策略

  • Range(預設)
  • RoundRobin(輪詢)

透過 partition.assignment.strategy 引數設定.

3.2.1 Range strategy(範圍分割槽)

Range 策略是對每個主題而言的,首先對同一個主題裡面的分割槽按照序號進行排序,並對消費者按照字母順序進行 排序。

假設我們有 10 個分割槽,3 個消費者

排完序的分割槽將會是 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
消費者執行緒排完序將會是 C1-0, C2-0, C3-0。
然後將 partitions 的個數除以消費者執行緒的總數來決定每個消費者執行緒消費幾個分割槽。
如果除不盡,那麼前幾個消費者執行緒將會多消費一個分割槽。

在我們的例子裡面,我們有 10 個分割槽,3 個消費者執行緒, 10 / 3=3,且除不盡,那麼消費者執行緒 C1-0 將會多消費一 個分割槽,所以最後分割槽分配的結果看起來是這樣的:

  • C1-0 將消費 0, 1, 2, 3 分割槽
  • C2-0 將消費 4, 5, 6 分割槽
  • C3-0 將消費 7, 8, 9 分割槽

假如我們有 11 個分割槽,那麼最後分割槽分配的結果看起來是這樣

  • C1-0 將消費 0, 1, 2, 3 分割槽
  • C2-0 將消費 4, 5, 6, 7 分割槽
  • C3-0 將消費 8, 9, 10 分割槽

假如我們有 2 個主題(T1 和 T2),分別有 10 個分割槽,那麼最後分割槽分配的結果看起來是這樣的:

  • C1-0 將消費 T1主題的 0, 1, 2, 3 分割槽以及 T2主題的 0, 1, 2, 3 分割槽
  • C2-0 將消費 T1主題的 4,5,6 分割槽以及 T2主題的 4,5,6 分割槽
  • C3-0 將消費 T1主題的 7,8,9 分割槽以及 T2主題的 7,8, 9 分割槽

可以看出,C1-0 消費者執行緒比其他消費者執行緒多消費了 2 個 分割槽,這就是 Range strategy 的一個很明顯的弊端

3.2.2 RoundRobin strategy(輪詢分割槽)

輪詢分割槽策略是把所有 partition 和所有 consumer 執行緒都列出來,然後按照 hashcode 進行排序。最後透過輪詢演算法分配 partition 給消費執行緒。如果所有 consumer 例項的訂閱是相同的,那麼 partition 會均勻分佈。

在我們的例子裡面,假如按照 hashCode 排序完的 topic- partitions 組依次為
T1-5, T1-3, T1-0, T1-8, T1-2, T1-1, T1-4,T1-7, T1-6, T1-9

我們的消費者執行緒排序為 C1-0, C1-1, C2- 0, C2-1,最後分割槽分配的結果為:

  • C1-0 將消費 T1-5, T1-2, T1-6 分割槽;
  • C1-1 將消費 T1-3, T1-1, T1-9 分割槽;
  • C2-0 將消費 T1-0, T1-4 分割槽;
  • C2-1 將消費 T1-8, T1-7 分割槽;

使用輪詢分割槽策略必須滿足兩個條件

  1. 每個主題的消費者例項具有相同數量的流
  2. 每個消費者訂閱的主題必須是相同的

3.3 何時觸發分割槽策略

當出現以下幾種情況時,kafka 會進行一次分割槽分配操作, 也就是 kafka consumer 的 rebalance

  1. 同一個 consumer group 內新增了消費者
  2. 消費者離開當前所屬的 consumer group,比如主動停機或者當機
  3. topic 新增了分割槽(也就是分割槽數量發生了變化)

kafka consuemr 的 rebalance 機制規定了一個 consumer group 下的所有 consumer 如何達成一致來分配訂閱 topic 的每個分割槽。
而具體如何執行分割槽策略,就是前面提到過的兩種內建的分割槽策略。而 kafka 對於分配策略這塊,提供了可插拔的實現方式, 也就是說,除了這兩種之外,我們還可以建立自己的分配機制。

3.4 誰來執行 Rebalance 以及管理 consumer 的 group

答 : coordinator

當 consumer group 的 第一個 consumer 啟動的時候,它會去和 kafka server 確定誰是它們組的 coordinator
之後該 group 內的所有成 員都會和該 coordinator 進行協調通訊

3.5 如何確定 coordinator

消 費者向 kafka 叢集中的任意一個 broker 傳送一個 GroupCoordinatorRequest請求
服務端會返回一個負載 最小的 broker 節點的 id,並將該 broker 設定為 coordinator

3.6 JoinGroup 的過程

在 rebalance 之前,需要保證 coordinator 是已經確定好了的,整個 rebalance 的過程分為兩個步驟,Join 和 Sync
join: 表示加入到 consumer group 中,在這一步中,所有 的成員都會向 coordinator 傳送 joinGroup 的請求。一旦所有成員都傳送了 joinGroup 請求,那麼 coordinator 會 選擇一個 consumer 擔任 leader 角色,並把組成員資訊和 訂閱資訊傳送消費者
圖片描述

  • protocol_metadata
    序列化後的消費者的訂閱資訊
  • leader_id
    消費組中的消費者,coordinator 會選擇一個 座位 leader,對應的就是 member_id
  • member_metadata
    對應消費者的訂閱資訊 members:consumer group 中全部的消費者的訂閱資訊
  • generation_id
    年代資訊,對於每一輪 rebalance, generation_id 都會遞增。主要用來保護 consumer group。 隔離無效的 offset 提交。也就是上一輪的 consumer 成員 無法提交 offset 到新的 consumer group 中。

3.7 Synchronizing Group State 階段

完成分割槽分配之後,就進入該階段
主要邏輯是

  • 向 GroupCoordinator 傳送 SyncGroupRequest 請求
  • 並且處理 SyncGroupResponse 響應

簡單來說,就是leader 將消費者對應的 partition 分配方案同步給 consumer group 中的所有 consumer
圖片描述

每個消費者都會向 coordinator 傳送 syncgroup 請求,不 過只有 leader 節點會傳送分配方案,其他消費者只是打打醬油而已。

當 leader 把方案發給 coordinator 以後, coordinator 會把結果設定到 SyncGroupResponse 中。這樣所有成員都知道自己應該消費哪個分割槽。

➢ consumer group 的分割槽分配方案是在客戶端執行的!
Kafka 將這個權利下放給客戶端主要是因為這樣做可以有更好的靈活性

3.8 如何儲存消費端的消費位置

3.8.1 什麼是 offset

前面在講解 partition 的時候,提到過 offset, 每個 topic可以劃分多個分割槽(每個 Topic 至少有一個分割槽),同一 topic 下的不同分割槽包含的訊息是不同的。

每個訊息在被新增到分割槽時,都會被分配一個 offset(稱之為偏移量),它是訊息在此分割槽中的唯一編號,kafka 透過 offset 保證訊息 在分割槽內的順序,offset 的順序不跨分割槽,即 kafka 只保證在同一個分割槽內的訊息是有序的

對於應用層的消費來說, 每次消費一個訊息並且提交以後,會儲存當前消費到的最近的一個 offset。那麼 offset 儲存在哪裡?
圖片描述

3.8.2 offset 在哪裡維護?

在 kafka 中,提供了一個__consumer_offsets_的一個 topic,把 offset 資訊寫入到該 topic 中。
__consumer_offsets儲存了每個 consumer group 某一時刻提交的 offset 資訊。__consumer_offsets 預設有 50 個分割槽。

根據前面我們演示的案例,我們設定了一個
KafkaConsumerDemo 的 groupid。
首先我們需要找到這 個 consumer_group 儲存在哪個分割槽中

properties.put(ConsumerConfig.GROUP_ID_CONFIG, "KafkaConsumerDemo");

計算公式
➢ Math.abs(“groupid”.hashCode())%groupMetadataTopicPartitionCount
由於預設情況下 groupMetadataTopicPartitionCount有 50 個分割槽,計 算得到的結果為:35
意味著當前的consumer_group的位移資訊儲存在__consumer_offsets 的第 35 個分割槽

➢ 執行如下命令,可以檢視當前 consumer_goup 中的 offset 位移資訊

sh kafka-simple-consumer-shell.sh --topic __consumer_offsets --partition 35 --broker-list 
192.168.11.153:9092,192.168.11.154:9092,192.168.11.157:90 92 --formatter
"kafka.coordinator.group.GroupMetadataManager$ OffsetsMessageFormatter" 

從輸出結果中,我們就可以看到 test 這個 topic 的 offset 的位移日誌

4 訊息的儲存

4.1 訊息的儲存路徑

訊息端傳送訊息到 broker 上以後,訊息是如何持久化的呢?

首先我們需要了解的是,kafka 是使用日誌檔案的方式來儲存生產者和傳送者的訊息,每條訊息都有一個 offset 值來表示它在分割槽中的偏移量。
Kafka 中儲存的一般都是海量的訊息資料,為了避免日誌檔案過大,Log 並不是直接對應在一個磁碟上的日誌檔案,而是對應磁碟上的一個目錄, 這個目錄的命名規則是

<topic_name>_<partition_id> 

比如建立一個名為 firstTopic 的 topic,其中有 3 個 partition, 那麼在 kafka 的資料目錄(/tmp/kafka-log)中就有 3 個目錄,firstTopic-0~3

4.2 多個分割槽在叢集中的分配

如果我們對於一個 topic,在叢集中建立多個 partition,那 麼 partition 是如何分佈的呢?
1.將所有 N Broker 和待分配的 i 個 Partition 排序
2.將第 i 個 Partition 分配到第(i mod n)個 Broker 上

圖片描述
瞭解到這裡的時候,大家再結合前面講的訊息分發策略, 就應該能明白訊息傳送到 broker 上,訊息會儲存到哪個分割槽中,並且消費端應該消費哪些分割槽的資料了。

4.3 訊息寫入的效能

我們現在大部分企業仍然用的是機械結構的磁碟,如果把訊息以隨機的方式寫入到磁碟,那麼磁碟首先要做的就是定址,也就是定位到資料所在的實體地址,在磁碟上就要找到對應的柱面、磁頭以及對應的扇區;這個過程相對內 存來說會消耗大量時間,為了規避隨機讀寫帶來的時間消耗,kafka 採用順序寫的方式儲存資料。

即使是這樣,但是頻繁的 I/O 操作仍然會造成磁碟的效能瓶頸,所以 kafka 還有一個效能策略

4.4 零複製

訊息從傳送到落地儲存,broker 維護的訊息日誌本身就是檔案目錄,每個檔案都是二進位制儲存,生產者和消費者使用相同的格式來處理。在消費者獲取訊息時,伺服器先從硬碟讀取資料到記憶體,然後把記憶體中的資料原封不動的通 過 socket 傳送給消費者。

雖然這個操作描述起來很簡單, 但實際上經歷了很多步驟
圖片描述

▪ 作業系統將資料從磁碟讀入到核心空間的頁快取
▪ 應用程式將資料從核心空間讀入到使用者空間快取中
▪ 應用程式將資料寫回到核心空間到 socket 快取中
▪ 作業系統將資料從 socket 緩衝區複製到網路卡緩衝區,以便將資料經網路發出

這個過程涉及到 4 次上下文切換以及 4 次資料複製,並且有兩次複製操作是由 CPU 完成。但是這個過程中,資料完全沒有進行變化,僅僅是從磁碟複製到網路卡緩衝區。

透過“零複製”技術,可以去掉這些沒必要的資料複製操作, 同時也會減少上下文切換次數。現代的 unix 作業系統提供 一個最佳化的程式碼路徑,用於將資料從頁快取傳輸到 socket; 在 Linux 中,是透過 sendfile 系統呼叫來完成的。
Java 提 供了訪問這個系統呼叫的方法:FileChannel.transferTo API
圖片描述

使用 sendfile,只需要一次複製就行,允許作業系統將資料直接從頁快取傳送到網路上。所以在這個最佳化的路徑中, 只有最後一步將資料複製到網路卡快取中是需要的

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1817/viewspace-2822718/,如需轉載,請註明出處,否則將追究法律責任。

相關文章