2025年5月13日
Kafka 完整入門:Broker、Topic、Partition、Replica、Consumer Group 一次搞懂
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,是邏輯上的分類 |
| Partition | Topic 被切成的多份日誌,是平行處理與順序保證的單位 |
| 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
- 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 掛掉資料也不會掉
每個 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 沒收到也不知道 |
1 | Leader 寫入就算成功 | Leader 寫完、還沒同步給 Follower 就掛了,訊息遺失 |
all(-1) | ISR 中所有副本都寫入才算成功 | 最安全,延遲稍高 |
acks=all 要搭配 Topic 的 min.insync.replicas 才有意義。如果 ISR 只剩 Leader 自己,「所有 ISR 都寫入」其實等於 acks=1。常見的組合是:
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:一批最大多少 Bytecompression.type:lz4、zstd等壓縮,對日誌這類重複性高的文字效果很好
保留與壓縮:訊息什麼時候被清掉
Topic 的 cleanup.policy 有兩種:
delete(預設):依retention.ms(例如 7 天)或retention.bytes清除舊的資料,不管有沒有被讀過。所以 Consumer 停機超過保留期限,沒讀到的資料就真的沒了compact:同一個 Key 只保留最新的一筆。適合存放「狀態」,例如每個帳號最新的設定,新的服務上線時從頭讀一遍,就能拿到每個 Key 的最新值。發一則 Value 為null的訊息(Tombstone)可以刪除這個 Key
Consumer Group
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 倒回去重新消費:
# 把 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,讓同一份資料能被多個系統獨立、可重播地消費。