← 回文章列表

2025年5月13日

Kafka 完整入門:Broker、Topic、Partition、Replica、Consumer Group 一次搞懂

Kafka事件串流分散式系統系統設計

Kafka 是一份分散式日誌

理解 Kafka 最好的方式,是把它想成一份只能往後附加(Append-only)的日誌檔:

  • Producer 寫入的訊息,一律附加在尾端,寫入後不能修改
  • 訊息被讀取後不會刪除,而是依保留期限(例如 7 天)或容量上限才清除
  • 每個讀取者自己記住「我讀到第幾筆」,想從頭重讀、從昨天重讀都可以

這和「訊息被取走就消失」的傳統佇列有本質上的不同,後面所有的設計——Partition、Offset、Consumer Group——都是從這個模型延伸出來的。

先認識所有元件

元件是什麼
Broker一台 Kafka 伺服器,負責儲存 Partition 的資料、處理讀寫請求
Cluster多台 Broker 組成的叢集,資料與負載分散在各台 Broker 上
Controller叢集的管理者,負責記錄 Topic 的設定、決定每個 Partition 由誰當 Leader、處理 Broker 上下線
Topic一類訊息的名稱,例如 orders、app-logs,是邏輯上的分類
PartitionTopic 被切成的多份日誌,是平行處理與順序保證的單位
Replica每個 Partition 在不同 Broker 上的副本,分成一個 Leader 與多個 Follower
Producer寫入訊息的程式
Consumer讀取訊息的程式
Consumer Group一群共同分工消費的 Consumer,用 group.id 識別

早期的 Kafka 需要另外架一組 ZooKeeper 來存放叢集的中繼資料、選出 Controller。從 2.8 開始可以改用 KRaft 模式,由 Kafka 自己內建的 Raft 共識機制管理;今年三月發布的 Kafka 4.0 已經完全移除 ZooKeeper,新的叢集直接用 KRaft 就好,少維護一整組服務。

Topic 與 Partition

Kafka Topic 與 Partition 示意圖:Producer 以 hash(key) % 3 決定 Partition,key 為帳號 A001 的訊息寫入 Partition 1 的尾端;Topic orders 有三個 Partition,每個 Partition 的 offset 各自從 0 開始編號,新訊息只會附加在尾端;同一個 Partition 內依 offset 嚴格有序,不同 Partition 之間沒有順序保證,同一個 Key 永遠進同一個 Partition,因此同帳號的事件有序;訊息被讀取後不會刪除,依保留期限或大小清除,不同的 Consumer Group 可以各自從任何 offset 重讀

  • Topic 實際上被切成多個 Partition,每個 Partition 才是一份獨立的日誌。Partition 分散在叢集中不同的 Broker 上,讀寫的負載也就分散開來
  • Offset 是訊息在 Partition 中的位置編號,每個 Partition 各自從 0 開始遞增

Partition 是 Kafka 能夠水平擴展的根本:一個 Topic 有 12 個 Partition,就可以分散在多台 Broker 上同時寫入、同時讀取。

順序只在 Partition 內成立

這是使用 Kafka 最重要的一件事:

  • 同一個 Partition 內,訊息依 Offset 嚴格有序
  • 不同 Partition 之間沒有順序保證

如果 Topic 有 3 個 Partition,先寫入的訊息 A 在 P0、後寫入的訊息 B 在 P1,消費端完全有可能先處理 B 再處理 A。

Key 決定訊息進哪個 Partition

Producer 寫入時可以帶一個 Key,預設的分區策略是 hash(key) % Partition 數量:

  • 相同 Key 的訊息,一定進同一個 Partition,因此彼此之間是有序的
  • 沒有 Key 的訊息會被分散到各個 Partition(新版本的預設是 Sticky Partitioner,先把一批訊息送到同一個 Partition 再換下一個),吞吐量最好,但沒有任何順序保證

所以 Key 的選擇,就是在決定「哪些訊息之間需要有序」:

Key保證適合
帳號 ID同一個帳號的事件有序委託、成交、帳務異動
訂單 ID同一筆訂單的狀態變化有序訂單建立 → 付款 → 出貨
不設 Key不保證順序,負載最平均日誌、監控指標、點擊紀錄

Key 也要注意分布是否平均:如果用「商品類別」當 Key,而某個類別佔了八成的訊息,就會有一個 Partition 特別忙(Hot Partition),其他 Partition 閒置,整個 Topic 的吞吐量被那一個 Partition 卡住。

Replica:Broker 掛掉資料也不會掉

Kafka 複本示意圖:三台 Broker,orders 的 P0 Leader 在 Broker 1、Follower 在 Broker 2 與 3;P1 Leader 在 Broker 2;P2 Leader 在 Broker 3,Leader 平均分散;Producer 與 Consumer 只和 Leader 讀寫,Follower 持續向 Leader 複製;與 Leader 保持同步的副本集合稱為 ISR;設定 replication.factor=3、min.insync.replicas=2、acks=all 時,訊息要寫進至少兩個同步副本才算成功,可以容忍一台 Broker 故障;Leader 所在的 Broker 掛掉時,Controller 從 ISR 中選出新的 Leader

每個 Partition 可以有多個副本,數量由 replication.factor 決定,正式環境通常設 3:

  • Leader:每個 Partition 只有一個 Leader,所有讀寫都經過 Leader
  • Follower:持續從 Leader 複製資料,平常不對外服務,Leader 掛掉時接手
  • ISR(In-Sync Replicas):跟 Leader 保持同步、沒有落後太多的副本集合。Follower 太久沒跟上(replica.lag.time.max.ms),就會被踢出 ISR

Leader 所在的 Broker 掛掉時,Controller 會從 ISR 中選一個 Follower 成為新的 Leader,Producer 和 Consumer 自動改連新的 Leader。每個 Partition 的 Leader 會平均分散在各台 Broker 上,所以每台 Broker 都在分擔讀寫。

unclean.leader.election.enable 預設是 false,意思是寧可這個 Partition 暫時不能用,也不讓一個落後的副本當 Leader,因為落後的副本當上 Leader,就代表沒同步到的那些訊息永遠遺失了。

Producer:寫入怎樣才算成功

acks

Producer 的 acks 決定「要多少個副本收到,才算寫入成功」:

acks意思風險
0送出去就算成功,不等回應Broker 沒收到也不知道
1Leader 寫入就算成功Leader 寫完、還沒同步給 Follower 就掛了,訊息遺失
all(-1)ISR 中所有副本都寫入才算成功最安全,延遲稍高

acks=all 要搭配 Topic 的 min.insync.replicas 才有意義。如果 ISR 只剩 Leader 自己,「所有 ISR 都寫入」其實等於 acks=1。常見的組合是:

text
replication.factor  = 3
min.insync.replicas = 2
acks                = all

意思是每則訊息至少要寫進兩個副本才算成功,可以容忍一台 Broker 故障;如果同時掛了兩台,ISR 只剩一個,Producer 會收到錯誤而不是默默把資料寫進一個隨時可能消失的副本。

冪等 Producer

網路抖動時,Producer 可能送出了訊息、但沒收到 Broker 的確認,於是重送,造成同一則訊息寫入兩次。開啟 enable.idempotence=true 後,Broker 會用 Producer ID 加序號辨識重送的訊息並丟掉重複的,同時保證重試不會打亂同一個 Partition 內的順序。Java Client 從 Kafka 3.0 開始預設開啟;但 .NET 的 Confluent.Kafka 底層是 librdkafka,預設仍是關閉,要自己設定 EnableIdempotence = true。

批次與壓縮

Producer 不會每則訊息都送一次請求,而是累積成批次再送:

  • linger.ms:最多等多久湊一批,調大一點(例如 5~20ms)可以明顯提升吞吐量
  • batch.size:一批最大多少 Byte
  • compression.type:lz4、zstd 等壓縮,對日誌這類重複性高的文字效果很好

保留與壓縮:訊息什麼時候被清掉

Topic 的 cleanup.policy 有兩種:

  • delete(預設):依 retention.ms(例如 7 天)或 retention.bytes 清除舊的資料,不管有沒有被讀過。所以 Consumer 停機超過保留期限,沒讀到的資料就真的沒了
  • compact:同一個 Key 只保留最新的一筆。適合存放「狀態」,例如每個帳號最新的設定,新的服務上線時從頭讀一遍,就能拿到每個 Key 的最新值。發一則 Value 為 null 的訊息(Tombstone)可以刪除這個 Key

Consumer Group

Kafka Consumer Group 示意圖:Topic app-logs 有三個 Partition;Consumer Group A(log-indexer,寫入搜尋引擎)有四個 Consumer,Consumer 1、2、3 各分到 P0、P1、P2,第四個 Consumer 因為 Partition 不夠而閒置,組內分工時一個 Partition 只給組內一個 Consumer;Consumer Group B(log-archiver,封存到物件儲存)只有一個 Consumer,同時讀取 P0、P1、P2,組與組之間互不影響,每個 Group 都讀到完整的資料並各自記錄 offset

Consumer Group 是一群共同消費同一個 Topic 的 Consumer,用 group.id 識別。它同時解決了兩件事:

1. 組內分工:一個 Partition 只會分給組內的一個 Consumer

Group A 有 3 個 Consumer、Topic 有 3 個 Partition,剛好一人一個,三個 Consumer 平行處理。

這也代表:Partition 的數量,就是一個 Consumer Group 的平行度上限。Group A 開到第 4 個 Consumer 時,已經沒有 Partition 可以分給它,它只會閒置,什麼都不做,只有在其他 Consumer 離開、觸發重新分配時才可能分到工作。想要再提高消費速度,必須先增加 Partition。

反過來,Consumer 比 Partition 少時,一個 Consumer 會同時負責多個 Partition,例如 Group B 只有 1 個 Consumer,就自己讀全部 3 個 Partition。

2. 組與組之間互不影響:每個 Group 都讀到完整的資料

Group A 把日誌寫進搜尋引擎、Group B 把日誌封存到物件儲存,兩者都讀同一個 Topic,各自讀到全部的訊息,各自記錄自己讀到的 Offset。Group B 處理得比較慢,完全不會影響 Group A。

一句話記:同一個 Group 內是分工,不同 Group 之間是各自完整讀一份。

Offset 由 Consumer Group 自己記錄

每個 Consumer Group 會把自己在每個 Partition 讀到的位置,提交(Commit)回 Kafka 內部的 __consumer_offsets Topic。Consumer 重啟或換人接手時,就從上次提交的位置繼續讀。

一個全新的 Group(還沒有任何提交紀錄)要從哪裡開始讀,由 auto.offset.reset 決定:

  • latest(預設):從現在開始,之前的訊息都不讀
  • earliest:從保留範圍內最舊的訊息開始讀

新上線的服務如果需要補齊歷史資料,要記得設成 earliest,不然會以為「怎麼少了之前的資料」。

提交的時機決定了投遞語意:

  • 先提交、再處理:處理到一半當掉,重啟後會從下一筆開始,這一筆遺失(At-most-once)
  • 先處理、再提交:處理完、提交前當掉,重啟後會重複處理這一筆(At-least-once)

enable.auto.commit=true(預設)會在背景每 5 秒自動提交「已經拉回來的位置」,不管你處理完了沒有,所以兩種情況都可能發生。需要可靠處理時,建議關掉自動提交,處理完再手動提交。大多數系統選擇 At-least-once,並讓處理邏輯冪等,重複處理也不會出錯。

因為 Offset 可以自由調整,Kafka 還能做到傳統佇列做不到的事:發現某段時間的資料處理錯了,修好程式後把 Offset 倒回去重新消費:

Shell
# 把 log-indexer 這個 Group 倒回到昨天早上 9 點(Group 要先停止所有 Consumer)
kafka-consumer-groups.sh --bootstrap-server kafka:9092 \
  --group log-indexer --topic app-logs \
  --reset-offsets --to-datetime 2025-05-12T09:00:00.000 --execute

Rebalance

Consumer Group 的成員變動時——新增 Consumer、某個 Consumer 當掉或太久沒有回應、Topic 的 Partition 數量改變——Kafka 會重新分配 Partition,這個過程叫 Rebalance。

Rebalance 期間,相關的 Partition 會暫停消費。傳統的做法(Eager)是所有 Consumer 先放掉全部 Partition,再重新分配,整個 Group 停下來一次;改用 Cooperative Sticky 的漸進式 Rebalance(librdkafka/.NET 設定 partition.assignment.strategy=cooperative-sticky,Java 是 CooperativeStickyAssignor),只會搬動需要移動的 Partition,其他的照常消費。

判斷 Consumer 是否還活著有兩個逾時:

  • session.timeout.ms(預設 45 秒):多久沒收到心跳就判定 Consumer 掛了
  • max.poll.interval.ms(預設 5 分鐘):兩次 poll() 之間最多隔多久

常見的問題是處理單批訊息的時間太長,超過 max.poll.interval.ms,Kafka 以為這個 Consumer 卡住而把它踢出 Group、觸發 Rebalance,接著另一個 Consumer 接手、重複處理,一直循環。處理時間長的工作,要調小每次拉取的筆數(max.poll.records)或調大這個逾時設定。

Partition 要開幾個?

Partition 數量是設計 Topic 時最重要的決定之一:

  • 由消費端反推:預估尖峰時每秒要處理多少訊息、單一 Consumer 每秒能處理多少,相除就是需要的平行度,Partition 數量至少要這麼多,再預留成長空間
  • 可以增加、不能減少:Partition 可以事後增加,但增加後 hash(key) % N 的結果會改變,同一個 Key 之後會進到不同的 Partition,增加的那一刻前後的順序就無法保證。對順序敏感的 Topic,一開始就要開足
  • 不是越多越好:每個 Partition 都有檔案控制代碼、複本同步、Rebalance 的成本,數量過多會拉長故障恢復與 Rebalance 的時間

使用情境一:集中式日誌收集

跨機房的應用程式日誌全部送進 Kafka,再由不同的系統各自消費:

  • Topic app-logs 不設 Key,讓訊息平均分散在所有 Partition,追求最大吞吐量——日誌之間不需要嚴格順序
  • Producer 開啟 lz4 壓縮與較大的 linger.ms,日誌的文字重複性高,壓縮後網路與磁碟用量都能省下一大截
  • Group log-indexer:寫入搜尋引擎供查詢,Consumer 數量隨 Partition 數量水平擴展
  • Group log-archiver:封存到物件儲存做長期保存
  • 搜尋引擎維護或故障時,log-indexer 停下來,日誌仍然安全地留在 Kafka 中;恢復後從上次的 Offset 繼續追上,不會遺失——前提是停機時間不能超過 retention.ms

日誌量成長時,增加 Partition 與 Consumer 就能擴充處理量;Kafka 本身也成為應用程式與搜尋引擎之間的緩衝,尖峰流量不會直接打垮後端。

使用情境二:需要順序的交易事件

委託、成交、帳務異動這類事件,同一個帳號的事件順序不能錯:先成交再取消,和先取消再成交,結果完全不同。

  • 以帳號 ID 作為 Key,同一個帳號的所有事件都進同一個 Partition,依序被同一個 Consumer 處理
  • 不同帳號之間不需要順序,分散在不同 Partition 平行處理
  • Producer 使用 acks=all 加冪等,Topic 設定 replication.factor=3、min.insync.replicas=2,一台 Broker 故障也不掉資料
  • Consumer 關閉自動提交,採用「先處理、再提交 Offset」,並以事件 ID 做冪等檢查,重複投遞也不會重複入帳
  • 一開始就依預估量開足 Partition,避免日後擴充造成同一帳號的事件跨 Partition

一句話總結

Kafka 是一份可重複讀取的分散式日誌:Topic 切成多個 Partition 分散在各台 Broker,每個 Partition 再複製成 Leader 與 Follower,搭配 acks=all 與 min.insync.replicas 撐過 Broker 故障;順序只在 Partition 內成立,所以用 Key 決定「哪些訊息要有序」;Consumer Group 內一個 Partition 只給一個 Consumer,因此 Partition 數量就是平行度上限,不同 Group 各自讀到完整的資料並各自記錄 Offset,讓同一份資料能被多個系統獨立、可重播地消費。