Chapter 9. Kafka: From Distributed Log to Event Streaming Platform
《給學弟妹的矽谷 AI / Software 指南》是一個矽谷工程師自發的眾籌寫作計劃,由源來適你社群推動。這將是一個在台灣軟體學生圈內很有影響力的文章系列。已有 >10 篇準備發布,目標 100 篇並出書,歡迎 tag 強者大神來寫!
OLTP 和 OLAP 兩種系統分工明確,兩個系統中間需要一座橋樑,將 OLTP 資料庫中的資料轉送到 OLAP 系統中。這個過程往往不只是搬運,而是一次轉化:欄位需對齊,格式要清洗,不同系統之間的語意差異也必須整合。這套資料流通與轉換的流程,被稱為 ETL:Extract、Transform、Load。
在 1980–2000 這個年代,夜間批次任務(batch job)搭起這座橋樑。每天深夜,資料會從來源資料庫中抽取(Extract),進入中繼區(staging area),接著進行格式轉換、缺值補齊與商業邏輯處理(Transform),最後再將清洗完的資料載入(Load)至 Data Warehouse。這整套流程通常交由 ETL 工具(如 Informatica、DataStage)執行,由工程師設計並排程自動化運行。
然而,進入 Data Lake 時代後,另一種選擇逐漸興起:透過 streaming 的方式即時傳輸資料。不再等待深夜批次處理,而是讓資料持續流動,即時同步到目標系統。若採用這種方式,Kafka 就成為不可或缺的工具 — — 它不僅是資料傳輸的管道,更是整個即時資料架構的核心。這一章,就讓我們來深入探究 Kafka。
本章的架構如下:
首先,我們會在 Some House-Keeping Definitions 中釐清一些基本概念,確保我們在同一個語境下討論。接著,The Issue of Big Data Transmission 會探討大數據傳輸面臨的核心挑戰 — — 當資料量從 GB 成長到 TB、PB 時,傳統的點對點傳輸模式為何開始失效。
在 Scribe: A Multi-Tier Aggregation Tree 中,我們會回顧 Facebook 在 2008 年提出的 Scribe 系統,看看業界最早是如何用分層聚合的方式解決大規模日誌收集問題。這為理解 Kafka 的設計提供了重要的歷史背景。
從 2026 年回頭看,Kafka 早已從 2011 年論文中的初始版本,演進成一個功能完備的分散式串流平台。為了區分不同時期的 Kafka,接下來的小節標題會明確標示為「Early Kafka」或「Modern Kafka」 — — 「Early」指 2011 年論文中描述的版本,「Modern」則是後來逐步演進、添加了各種新能力的版本。
Early Kafka’s Mission: Bridging to Near Real-Time 會帶我們回到 LinkedIn 在 2010 年開發 Kafka 的初衷 — — 他們需要一個系統能夠以接近即時的方式,將活動日誌從前端網站傳送到後端的資料倉儲和分析系統,而當時的消息隊列都無法滿足這個需求。
在 Early Kafka: The Architecture 中,我們會深入 2011 年論文中描述的 Kafka 核心設計:基於日誌的儲存模型、partition 的概念、以及 consumer group 的機制。這些設計至今仍是 Kafka 的基石。
隨後,Modern Kafka: The Improvements 會介紹 Kafka 從最初版本到現代的重要演進,包括 replication 機制的加入、exactly-once 語意的實現,以及 WarpStream 這類雲原生架構的出現。
最後,Kafka wire protocol: The De Facto Standard 會展示 Kafka 如何從一個專用的日誌傳輸系統,演變成為整個業界的事實標準 — — 幾乎所有的串流處理框架、資料整合工具都選擇與 Kafka API 相容。
讓我們開始吧!
Some House-Keeping Definitions
在深入探討 Kafka 的技術細節之前,讓我們先釐清一些容易混淆的定義,確保我們對關鍵概念有共同的理解。
In-Memory vs Persistent
In-Memory(記憶體中)意味著資料存儲在 RAM 中,特點是存取速度極快(納秒級),但系統重啟或斷電後資料會丟失。Persistent(持久化)則意味著資料存儲在持久化存儲介質上(硬碟、SSD),即使系統重啟,資料依然存在。在分散式系統和資料庫領域,In-Memory 和 Persistent 的區別至關重要,因為它直接影響到資料的可靠性、性能和系統設計。
當我們在大學的資料結構課程中學習 Array、Linked List、Hash Table、Binary Tree 時,教科書通常不會特別強調這些是「In-Memory Data Structure」。但事實上,它們確實都是駐留在記憶體中的。同樣地,當我們在 LeetCode 或其他平台刷題時,使用的資料結構也都是 In-Memory 的,程式執行結束後,資料就消失了。
值得一提的是,Computer Science 中確實存在 Persistent Data Structure 的概念,但它的意思完全不同。它指的是修改操作會產生新版本而不改變舊版本的資料結構,比如 Clojure 中的 Persistent Vector 或函數式程式設計中的 Immutable Data Structure。這個概念最早由 Chris Okasaki 在 1996 年的博士論文中系統性地研究,並在 1998 年出版成《Purely Functional Data Structures》一書。
有趣的是,Persistent Data Structure 的「多版本」概念其實借鑒自資料庫的 MVCC(Multi-Version Concurrency Control)。MVCC 早在 1978 年就由 MIT 的 David P. Reed 在博士論文中提出,用於解決資料庫的並發控制問題。但兩者有個關鍵差異:MVCC 的多個版本是可以持久化到磁碟的,系統重啟後資料依然存在;而 Persistent Data Structure 仍然是駐留在記憶體中的,斷電後資料依然會丟失,只是在程式執行期間保留了歷史版本而已。
Examples Across Different Levels
讓我們從不同層級來看 In-Memory 和 Persistent 的例子:
檔案格式層級:
- Apache Arrow 是 In-Memory 的 column format,設計用於記憶體中的高效資料處理和零拷貝共享。
- Apache Parquet 是 Persistent 的 PAX format,設計用於長期存儲在硬碟上。
工具層級:
- Redis 本質上是 In-Memory Database,所有資料主要存儲在記憶體中以提供極低延遲。但 Redis 也提供 RDB(快照)和 AOF(Append-Only File)等機制來支援持久化模式,讓資料可以在重啟後恢復。
- Memcached 則是純粹的 In-Memory Cache,沒有任何持久化機制。
RDBMS 內部:
- Buffer Pool 是 In-Memory 的,用於緩存熱點數據頁,加速查詢。
- WAL(Write-Ahead Log)、B-tree 索引、Data File 都是 Persistent 的,確保資料不會因為斷電而丟失。
Cassandra 內部:
- Memtable 是 In-Memory 的寫入緩衝區。
- CommitLog(WAL)和 SSTable 是 Persistent 的,提供資料的持久化保證。
Storage Implies Persistence
值得注意的是,Storage 這個詞本身就隱含了 Persistent 的意義。當我們談論 Storage Systems 時,預設就是在討論持久化的存儲系統:
- Block Storage、File Storage、Object Storage 都是在強調它們的持久化性質
- 在資料庫領域,Row Store、Column Store、Wide-Column Store、Document Store 同樣都是在描述資料如何被持久化存儲
Kafka 明顯是一種 Store,但在界定它的分類時花了我一些力氣。Kafka 在單一 partition 內保證嚴格的 FIFO 順序,但跨 partition 則沒有順序保證。它的消息確認機制也與傳統 Message Queue 不同 — — Consumer 透過提交 offset 來批次確認消費進度,而非逐條確認。更重要的是,Kafka 也截然不同於上述的每一種資料庫,它不是為了複雜查詢或事務處理而設計的。
就在山窮水盡之際,”Kafka: a Distributed Messaging System for Log Processing” 這篇開山鼻祖般的論文給了我靈感。雖然不是業界的正式類名,從此我就把它歸為 Message Store。這個分類強調了 Kafka 的核心特性:它是一個專門用來存儲和傳遞消息的持久化系統。下一章會介紹的 Kafka 的前輩(如 RabbitMQ)和後輩(如 Apache Pulsar, Apache Fluss),都屬於 Message Store。
Storage vs Compute, Batch vs Stream
在資料系統的演進歷史中,Storage 和 Compute 的關係經歷了多次重大變革,這些變革深刻影響了系統的可擴展性、性能和成本結構。
在 Compute 和 Storage 的分工中,Storage 負責資料的持久化存儲,而 Compute 則負責資料的處理和轉換。在資料庫系統中,Compute Engine 的主要職責包括 Query Optimization(查詢優化)、Query Execution(查詢執行)、Join 操作、Aggregation 聚合等。它需要決定如何最有效地讀取資料、用什麼順序執行操作、是否使用索引,以及如何分配記憶體和 CPU 資源。
The Evolution of Storage and Compute Architecture
關於 Compute Engine 如何共享 Memory 和 Disk 等資源,是個亙古談不膩的話題。在 1986 年,Michael Stonebraker 發表了影響深遠的論文 “The Case for Shared Nothing”,系統性地比較了三種多處理器資料庫架構。這三種架構按照耦合的緊密程度,由緊到鬆可以分為:
Shared Memory (SM)
在這種架構中,多個 CPU 透過匯流排(bus)或交換機(switch)存取同一個中央記憶體。論文中提到的例子包括 Synapse 和 IBM/AP 配置。現代的例子則是單機多核心系統上的資料庫,比如在一台配備 128 核心的伺服器上運行的 PostgreSQL(值得注意的是,1986 年論文中的 Shared Memory 指的是「多個 CPU 晶片」共享記憶體,而非現代的多核 CPU — — 多核要到 2000 年代才普及)。所有 CPU 核心共享同一個記憶體空間,可以直接存取相同的資料結構。
這種架構的優點是程式設計簡單,不需要考慮資料分佈。但致命的缺陷是無法擴展到大量處理器 — — 當處理器數量增加時,記憶體匯流排會成為瓶頸,多個處理器競爭存取共享記憶體會導致嚴重的爭用(contention)問題。
Shared Disk (SD)
每個處理器擁有自己的記憶體,但所有處理器透過高速網路存取同一組磁碟陣列。論文中提到的例子是 VAX/cluster 和多埠磁碟系統。現代最典型的例子是 Oracle RAC (Real Application Clusters),多個 Oracle 實例運行在不同節點上,但共享同一個儲存系統(通常是 SAN)。
這種架構的問題在於需要協調多個 lock table 副本,同步寫入共享日誌,導致複雜的 transaction 管理。當多個節點試圖修改同一筆資料時,必須透過分散式鎖機制來保證一致性,這會帶來顯著的效能開銷。
Shared Nothing (SN)
每個處理器擁有完全獨立的記憶體和磁碟,節點之間只透過網路通訊。論文中提到的例子是 Tandem 和 Tolerant 系統。現代的例子包括 Teradata、Greenplum。每個節點負責一部分資料,處理針對其資料分片(data partition)的查詢。
這種架構的優勢在於線性擴展能力 — — 增加節點就能幾乎等比例增加處理能力,不會像 Shared Memory 那樣受限於記憶體匯流排的瓶頸。
Stonebraker’s Conclusion
Stonebraker 的結論很明確:對於「scalable、tunable、almost nice」的資料庫 — — 即大部分交易都是本地自足(locally sufficient)的資料庫 — — Shared Nothing 架構是最具成本效益的架構選擇。
但有個重要的背景:當時 Stonebraker 討論的完全是 on-premises 環境。1986 年雲端運算還不存在,所有系統都是企業自己購買硬體、建置資料中心。在那個年代,儲存是昂貴且難以擴展的資源,網路頻寬也很有限。
Cloud Era: Compute-Storage Separation
然而,進入雲端時代後,遊戲規則完全改變了。
首先是物件儲存的崛起。Amazon S3 在 2006 年推出,提供了幾乎無限可擴展、高度耐用、且成本極低的儲存服務。相比之下,本地磁碟(block storage)不僅貴得多,還需要手動管理容量。這個經濟學的轉變是根本性的:在 on-premises 時代,儲存是稀缺資源;在雲端時代,儲存變得幾乎是免費的。
其次是彈性需求的普遍性。雲端應用的工作負載往往有明顯的高峰和低谷 — — 白天查詢量大,晚上幾乎沒有;月底報表跑批次,平時只有零星查詢。在 Shared Nothing 架構中,計算和儲存是綁定的,你無法單獨擴展其中一個。但在 compute-storage separation 架構中,你可以在需要時快速增加計算節點,用完就釋放,而資料始終安全地存放在共享儲存層。
第三是資料共享的需求。現代資料平台需要支援多種工作負載:BI 分析、機器學習訓練、即時查詢、批次處理。在 Shared Nothing 架構中,每個系統都需要維護自己的資料副本。但在 compute-storage separation 架構中,所有計算引擎都可以直接存取同一份資料 — — Spark 可以讀取,Presto 也可以讀取,不需要複製。
Snowflake 在 2012 年創立時,徹底擁抱了這個新典範。他們將計算層(虛擬倉庫)完全與儲存層(S3)分離。Snowflake 官方文件將自己的架構描述為「shared-disk 和 shared-nothing 資料庫架構的混合體」 — — 提供了 shared-disk 架構的資料管理簡便性,同時具備 shared-nothing 架構的性能和擴展優勢,也稱之為 “multi-cluster, shared data architecture”。這不是回到傳統的 Shared Disk,而是一種全新的架構 — — 儲存是雲原生的物件儲存,而非傳統的共享磁碟陣列;計算節點是短暫的、無狀態的,可以隨時啟動和關閉。
Databricks 的 Delta Lake、AWS 的 Athena、Google 的 BigQuery、ClickHouse Cloud,幾乎所有現代雲端資料倉儲都採用了類似的 Compute-Storage Separation 架構。這不是因為 Stonebraker 錯了,而是因為雲端改變了底層假設:
- 儲存從稀缺變成豐富
- 網路頻寬大幅提升(從 Mbps 到 Gbps 甚至 100 Gbps)
- 工作負載從穩定變成突發性的
- 資料共享從例外變成常態
在這樣的背景下,這股「Compute-Storage Separation」的浪潮,也開始向訊息與串流的領域擴散,而不同系統選擇了不同的切入方式。
WarpStream
從零開始重新設計 Kafka 的雲端架構,直接以 S3 為主要儲存層,徹底移除對本地磁碟的依賴。這是一條最激進、也最徹底的 compute-storage separation 路線,目標是讓 Kafka broker 成為可隨時啟停的無狀態計算資源。
AutoMQ
採取類似的思路,主打 Kafka-on-object-storage,原生支援 S3-compatible 儲存(如 S3、OSS、OBS),並在架構層面弱化本地磁碟的重要性,試圖以雲原生方式重新詮釋 Kafka。
Aiven
作為託管服務提供者,Aiven 透過 tiered storage 與雲端儲存後端的整合,將 Kafka 的資料逐步延伸到 object storage,在維持既有 Kafka 架構的前提下,降低儲存成本並提升彈性。
Confluent
一方面在 Confluent Platform 與 Confluent Cloud 中提供 Tiered Storage,將冷資料下沉至 object storage;另一方面透過收購 WarpStream,補齊了以 BYOC 模式全面擁抱 S3-first Kafka 的產品線,形成從傳統 Kafka 到雲原生 Kafka 的完整光譜。
Huawei Cloud
在其 Kafka 服務中深度整合 OBS(S3-compatible 物件儲存),以雲服務的方式推動 Kafka 去本地磁碟化,滿足大型企業與政府場景對成本、擴展性與運維簡化的需求。
Apache Pulsar
值得一提的是,Pulsar 並非在 Kafka 架構上「事後補上」物件儲存,而是從一開始就將 broker 與儲存層解耦,將資料持久化交由 BookKeeper 負責,broker 本身則專注於流量調度與協議處理。從設計哲學來看,Pulsar 其實更早體現了 compute-storage separation 的精神,只是選擇了一條與 Kafka 生態平行、而非演進自 Kafka 的路線。
這些實作在技術細節與成熟度上各不相同:有些選擇以 tiered storage 漸進演進,有些則直接重構儲存模型,甚至從一開始就假設計算與儲存是分離的。但它們共同指向一個明確趨勢:即便是誕生於 shared-nothing 時代,compute-storage separation 也正在成為雲端時代下串流與訊息系統的新常態。
Cloud Era: Compute-Compute Separation
更進一步,現代雲端資料倉庫如 Snowflake 和 BigQuery 還實現了 Compute-Compute Separation。在這種架構下,不僅 Storage 是共享的,多個獨立的 Compute Cluster 也可以同時存取同一份資料。這意味著不同的團隊或不同的工作負載(如 ETL、即時分析、機器學習訓練)可以使用各自獨立的 Compute 資源,互不干擾。當某個團隊的查詢負載突然增加時,只需要擴展該團隊的 Compute Cluster,而不會影響其他團隊。這種架構將資源隔離和彈性擴展推向了極致。
Stream come into play
在 Lakehouse 架構下,每個階層的職掌分明。在 Storage 層,資料必須是可被計算的,而 Storage 本身也分為不同的類型以適應不同的處理模式。
傳統的資料庫本質上是 Batch Store,它們將資料以靜態的方式存儲起來,等待 Batch 處理作業來讀取和計算。這些 Batch Store 只能被 ETL Batch Job 或分析引擎(如 Spark、Presto、Trino)以批次模式處理。資料通常是在某個時間點的快照,處理的是「已經發生的事情」。
而 Kafka 則是 Stream Store,它的定位截然不同。Stream Store 不僅可以被 Batch Job 處理(例如每小時從 Kafka 讀取一批資料進行分析),更重要的是它也能被 Stream Compute Engine(如 Apache Flink、Kafka Streams)處理。這意味著資料可以在產生的瞬間就被處理,處理的是「正在發生的事情」。
這個區別至關重要。Batch Store 假設資料是完整的、不可變的,適合處理歷史分析、報表生成這類場景。Stream Store 則假設資料是持續流入的、永無止境的,適合處理即時監控、即時推薦、即時風控這類場景。
在現代 Lakehouse 架構中,兩者並非對立,而是互補的。資料通常先流經 Stream Store(如 Kafka),被 Stream Compute Engine 處理後產生即時結果,同時也會被定期批次寫入 Batch Store(如 Object Storage 上的 Parquet 文件),供後續的深度分析使用。這正是 Lambda 架構和 Kappa 架構試圖解決的問題:如何在同一個系統中同時支援 Stream 和 Batch 兩種處理模式。
下表整理了 Storage 和 Compute 在 Batch 和 Stream 兩種處理模式下的代表性工具:
這個分類幫助我們理解不同工具在資料處理管道中的定位。值得注意的是,現代系統的界線越來越模糊:Spark 既可以做 Batch 也可以做 Stream,Kafka 既可以被 Batch Engine 讀取也可以被 Stream Engine 處理。
(傳統上,Spark 的 stream processing 是以 micro-batch 為實作基礎的「Near-Real Time」處理模型,而非 record-by-record 的連續流處理。Spark 4.1 開始支持 Real-Time Mode。)
Delivery Guarantees in RPC
在分散式消息系統中,當 Client 傳訊息給 Server,兩者之間能提供什麼樣的 Delivery Guarantees?
在理想情況下,我們希望遠端程序呼叫能維持與本地程序呼叫相同的語義 — — 也就是「exactly once」,當 Client 的呼叫返回時,程序確實執行了恰好一次。但在分散式環境中,訊息可能遺失、Server 可能當機,要維持這種語義非常困難。因此,根據 RPC 系統願意付出多少代價, Bruce Nelson 提出了三種不同的語義:At-Least-Once、At-Most-Once、Exactly-Once。
這三種語意規範的是 Server 實際執行了幾次,而不是 Server 收到請求幾次。理解這一點對接下來的討論至關重要!
At-Least-Once
只要伺服器機器沒有故障,呼叫至少會執行一次。這種語義的實作非常簡單,開銷很小。客戶端機器會持續向伺服器發送呼叫請求,直到收到確認訊息。如果一個或多個確認訊息遺失,伺服器可能會執行多次呼叫。
這種方法只適用於「冪等操作」(idempotent operation),也就是多次呼叫會返回相同結果的操作。實作 at-least-once 的 Server 必須是無狀態的(stateless),不會因為 Client 的請求而改變全域狀態。
At-Most-Once: Three Implementation Approaches
At-Most-Once 保證操作最多執行一次,有三種主要的實作方式。
第一種是利用 TCP 層面的保證。如果 RPC 建立在 TCP 之上,可以直接利用 TCP 本身的特性來實現 At-Most-Once。TCP 透過 sequence number 保證封包不重複、不亂序,因此只要 Client 發送一次請求後不重試,Server 最多只會收到一次請求,自然也就最多執行一次操作。這種情況下不需要在應用層實作任何去重機制,因為 TCP 已經在網路層面處理了重複問題。
第二種是 Fire-and-Forget 加上不重試的策略。這是最簡單的實作方式:Client 發送請求後就放棄,既不等待回應也不重試。由於 Client 只發送一次,Server 自然不會收到重複請求。這種方式常見於使用 UDP 的場景,如日誌記錄或監控指標上報 — — UDP 本身不保證可靠傳輸,但換來的是極低的延遲和開銷。操作可能完全沒執行(封包在網路中丟失),也可能執行了一次,但絕不會執行多次。對於可以容忍少量數據遺失的應用來說,這種簡單的策略往往就足夠了。
第三種是 Request ID 加上去重表的機制,這是最通用也最常見的實作方式,特別適合不可靠的網路環境。在這種機制中,Client 為每個請求分配唯一的 Request ID,並在沒收到回應時重試(可能重試多次)。Server 端維護一個「已處理請求」的記錄表。當 Server 收到請求時,會先檢查 Request ID 是否已存在於去重表中。如果存在,就識別為重複請求,不再執行操作,直接返回成功(或之前的結果);如果不存在,則執行操作、記錄 Request ID,然後返回結果。這種機制確保了即使 Client 因為網路問題多次重試,Server 端的操作也只會執行一次。
然而,這種看似完美的「Client retry + Server 去重」組合,仍然無法達到 Exactly-Once 的保證。問題的關鍵在於一個微妙但致命的時序窗口。
想像這樣的場景:Client 發送一個轉帳請求(Request ID: 12345),Server 收到後記錄了這個 Request ID 到去重表,並執行了轉帳操作 — — 錢已經轉出去了。但就在 Server 準備回傳結果的時候,它突然崩潰了。Client 那邊因為超時沒收到回應,不知道操作到底成功了沒有,於是重試了相同的請求。新的 Server(或重啟後的 Server)接手後,檢查去重表發現 Request ID: 12345 確實存在,於是判斷這是重複請求,不再執行轉帳。
但問題來了:之前的執行結果在哪裡?
若 Server 只記錄了 Request ID,知道「這個請求我處理過」,但不知道當時的執行結果是什麼。它只能回傳一個模糊的「成功」或「我見過這個請求」。Client 收到回應,但仍然不知道具體結果 — — 轉帳真的成功了嗎?餘額變成多少了?這就是 At-Most-Once 的本質:保證不重複執行,但不保證 Client 一定能知道結果。
Exactly-Once: The Holy Grail
要實現真正的 Exactly-Once,需要更進一步。首先要持久化 Request ID,不能只存在記憶體的去重表中;其次要持久化執行結果,不只是記錄「處理過」,還要記錄「結果是什麼」;最後需要原子性保證,執行操作和記錄結果必須是一個原子操作,不能被中斷。只有這樣,當 Client 重試時,Server 才能識別出「這個請求我執行過」,並且返回當時的執行結果。
這就是為什麼 Exactly-Once 如此難以實現 — — 它需要複雜的狀態管理、持久化機制,以及類似兩階段提交的協議。正如 RPC 文獻中提到的,Bruce Nelson 在其博士論文中提出了 Exactly-Once 的實作方案,但「因為實作的複雜性,據我所知從未被實際部署過」。大多數系統最終都選擇了在 At-Least-Once 或 At-Most-Once 之間權衡,而將 Exactly-Once 的保證留給應用層去處理。
The Reality: Choose Your Trade-off
在實務中,選擇哪種語義取決於具體場景。
對於非關鍵數據(如用戶行為日誌、監控指標),At Most Once 或 At Least Once 通常就足夠了。對於關鍵業務數據(如訂單、交易),需要 Exactly Once,但也要評估實現成本。
很多時候,At Least Once 加上下游的冪等性處理,是性價比最高的方案。比如使用唯一的 message-id 進行去重,或者設計天然冪等的操作(如「將庫存設定為 10」而不是「庫存減 1」)。
理解這三種語義的權衡,是設計可靠的分散式系統的基礎。Kafka 提供了靈活的配置選項,讓用戶可以根據需求選擇合適的語義保證,這也是它能夠適應各種場景的原因之一。
Throughput, Latency, Freshness
在評估分散式系統和資料處理系統的性能時,有三個關鍵指標經常被提及:Throughput、Latency 和 Freshness。理解這三者的定義和權衡,對於設計和優化系統至關重要。
Throughput: How Much Can We Process?
Throughput(吞吐量)指的是系統在單位時間內能夠處理的數據量或請求數量。常見的度量單位包括:每秒處理的消息數(messages per second, msg/s)、每秒處理的請求數(requests per second, req/s)、每秒傳輸的數據量(MB/s 或 GB/s)。
在 Kafka 的場景中,吞吐量通常指每秒能夠寫入或讀取多少條消息。高吞吐量意味著系統能夠處理大規模的數據流,這對於處理海量日誌、用戶行為數據等場景至關重要。
Kafka 的設計重點就是追求高吞吐量,透過順序寫入、批次處理、零拷貝等技術,Kafka 能在普通硬件上達到每秒數百萬條消息的吞吐量。
Latency: How Fast Can We Respond?
Latency(延遲)指的是從請求發出到收到響應的時間間隔。在不同的系統中,延遲的定義可能略有不同。在消息系統中,延遲通常指:
- 端到端延遲(End-to-End Latency):從 Producer 發送消息,到 Consumer 接收並處理該消息的總時間。
- 寫入延遲(Write Latency):Producer 發送消息到收到 Broker 確認的時間。
- 讀取延遲(Read Latency):Consumer 請求數據到收到數據的時間。
延遲通常以毫秒(ms)或微秒(μs)為單位。低延遲系統能夠快速響應請求,這對於即時交互、即時告警等場景非常重要。
需要注意的是,Throughput 和 Latency 往往存在權衡。為了提高吞吐量,系統通常會採用批次處理(batching),將多個請求打包一起處理,這會增加單個請求的延遲。
Kafka 在這個權衡上的選擇很有意思。在 Kafka 4.0 之前,預設的策略是「盡快發送」 — — Producer 一有消息就立刻送出去,linger.ms(等待時間)預設為 0。這個設計優先考慮低延遲,適合需要即時響應的場景。但從 Kafka 4.0 開始,預設值改為 5ms,讓 Producer 稍微等一下,看看能不能湊齊更多消息一起打包發送。這個看似微小的改變,反映了 Kafka 設計理念的演進:從「預設追求最低延遲」轉向「預設追求更好的吞吐量」。
這個轉變背後的邏輯很實際:在大多數生產環境中,增加幾毫秒的延遲通常是可以接受的,但透過 batching 提升 30–50% 的吞吐量,卻能顯著降低網路開銷和 Broker 負載。當然,對於確實需要極低延遲的場景(如高頻交易、即時告警),用戶仍然可以手動將 linger.ms 設為 0。
這也體現了 Kafka 一貫的哲學:提供合理的預設值(針對大多數場景優化),同時保留完整的配置彈性,讓用戶根據自己的需求做出權衡。
Freshness: How Current Is Our Data?
Freshness(新鮮度)指的是數據的時效性,也就是「數據有多新」。這個概念在資料分析和決策系統中尤為重要,因為過時的數據可能導致錯誤的決策。
Freshness 通常與這三種處理模式對應:
- Offline/ Batch Analysis(離線分析)的數據新鮮度為小時級到天級。數據通常是昨天或更早的快照,適合歷史分析。典型場景包括每日業務報表、月度趨勢分析、離線機器學習訓練。
- Near Real-Time(準即時)的數據新鮮度為分鐘級到秒級。數據有一定延遲但仍然相對新鮮,適合運營監控。典型場景包括每小時的銷售統計、運營儀表板、A/B 測試結果。
- Real-Time/ Streaming(即時)的數據新鮮度為毫秒級到秒級。數據幾乎是即時產生即時處理,適合需要立即響應的場景。典型場景包括金融交易反欺詐、即時推薦、系統異常告警。
The Trade-offs
這三個指標之間存在複雜的權衡關係。
Throughput vs Latency:提高吞吐量通常需要批次處理,這會增加延遲。Kafka 允許配置 linger.ms 和 batch.size 來調整這個權衡。
Throughput vs Freshness:批次處理可以提高吞吐量,但會降低數據新鮮度。如果要求 Real-Time 的 Freshness,就需要犧牲一些吞吐量。
Latency vs Reliability:如前面討論的 At-Least-Once vs Exactly-Once,更強的可靠性保證通常意味著更高的延遲。
Kafka 的設計哲學是:優先保證高吞吐量,然後透過配置讓用戶根據需求調整延遲和新鮮度。對於需要低延遲的場景,可以減小 batch size 和 linger time;對於需要高新鮮度的場景,可以讓 Consumer 更頻繁地拉取數據。
理解這三個指標及其權衡,是選擇和優化消息系統的基礎。不同的業務場景對這三個指標的要求不同,沒有一個「完美」的配置,關鍵是找到適合自己場景的平衡點。
SLA: Service Level Agreement
What is SLA?
SLA(Service Level Agreement,服務水準協議)是服務提供者與客戶之間就服務品質達成的正式協議。在雲端服務和資料系統領域,SLA 定義了服務提供者承諾提供的最低服務水準,以及未達標時的補償措施。
SLA 通常包含多個關鍵指標,每個指標都有明確的數值承諾。這些指標讓客戶能夠量化評估服務品質,也為服務提供者設定了明確的目標。
Common SLA Metrics in the Industry
業界常見的 SLA 指標包括:
Availability(可用性)是最常見的 SLA 指標,通常以「幾個九」來表示。99.9%(Three Nines)意味著服務每年最多只能停機 8.76 小時。99.99%(Four Nines)意味著每年最多停機 52.56 分鐘。99.999%(Five Nines)意味著每年最多停機 5.26 分鐘。
舉例來說,AWS S3 承諾 99.99% 的可用性,Google Cloud Storage 承諾 99.95% 的可用性。Confluent Cloud 根據不同的方案,提供從 99.9% 到 99.99% 的可用性保證。
Durability(持久性) 規定數據丟失的最大機率。AWS S3 承諾 99.999999999%(11 個九)的年度持久性,這意味著如果你存儲 10,000,000 個物件,平均每 10,000 年才會丟失一個。這是針對正常運作期間,由於硬體故障、位元翻轉等隨機因素造成的資料損壞或遺失。
Latency(延遲)規定了請求響應時間的上限。例如,某些資料庫服務可能承諾 99% 的查詢在 100ms 內完成,99.9% 的查詢在 1 秒內完成。
Throughput(吞吐量)保證系統能夠處理的最小流量。例如,Kafka 的雲端服務可能承諾每秒至少能處理 X MB 的數據。
而以下兩個承諾跟災難發生有關。
Recovery Point Objective (RPO) 定義了災難發生時可接受的最大數據遺失量。例如,RPO=0 意味著零數據遺失,RPO=1 小時意味著災難發生時最多可能遺失 1 小時的數據。RPO 關注的是「能承受丟失多少資料」。
Recovery Time Objective (RTO) 定義了系統故障後恢復服務的最長時間。例如,承諾在 4 小時內恢復服務。RTO 關注的是「能承受停機多久」。
值得注意的是,Durability 和 RPO 看似相似但其實不同:Durability 描述的是平時運作中資料自然損壞的機率;RPO 描述的是當災難(如資料中心被摧毀、人為誤刪)發生時,你最多能接受丟失多少資料。
SLA for Data Freshness
Data Freshness(數據新鮮度)的 SLA 在即時數據處理系統中越來越重要。這類 SLA 通常規定數據從產生到可被查詢的最大延遲時間。例如,某個即時分析平台可能承諾:所有事件在產生後 5 秒內可被查詢(P99),或者所有事件在產生後 10 秒內必定可被查詢(P100)。
在 Kafka 的場景中,雖然 Kafka 本身不直接提供 Freshness 的 SLA,但整個資料管道可能會有這樣的承諾。例如,從用戶點擊事件發生,到該事件出現在即時儀表板上,承諾不超過 30 秒。
Data Pipeline SLA 可能包括多個階段:
- 數據從 Source 到 Kafka 的延遲不超過 X 秒。
- 數據在 Kafka 中的保留時間至少為 Y 天。
- 從 Kafka 到下游系統(如資料倉庫)的同步延遲不超過 Z 分鐘。
SLA Violations and Compensation
當服務提供者未能達到 SLA 承諾時,通常會提供補償。常見的補償方式包括:
- 服務抵免(Service Credits):按照停機時間比例退還費用。例如,如果月度可用性低於 99.9%,退還 10% 的月費;低於 99.0%,退還 25% 的月費。
- 優先支援(Priority Support):在發生問題時提供更快的響應時間。
- 合約終止權(Right to Terminate):如果 SLA 持續未達標,客戶有權提前終止合約而不受罰。
The Reality of SLA
需要注意的是,SLA 通常會包含一些免責條款(Exclusions)。例如:
- 計劃性維護期間不計入停機時間。
- 因客戶自身配置錯誤導致的問題不算違反 SLA。
- 因不可抗力(如自然災害、戰爭)導致的服務中斷可能不受 SLA 約束。
此外,高階的 SLA(如 99.99% 或更高)通常需要額外付費。這反映了提供高可用性服務的成本:需要更多的冗餘、更頻繁的監控、更快的故障恢復能力。
對於設計資料系統的工程師來說,理解 SLA 很重要,因為:
- 你需要設計系統以滿足對下游的 SLA 承諾。
- 你需要評估上游服務的 SLA 是否能支撐你的需求。
- 你需要在成本和服務品質之間做出權衡。
Kafka 作為資料管道的核心組件,其配置和部署方式直接影響整個系統的 SLA。選擇合適的 replication factor、acks 設定、Consumer Group 配置等,都是為了在成本和 SLA 之間找到平衡點。
Lambda Architecture vs Kappa Architecture
現代業務有時必須一口氣滿足 Offline, Near Real-Time, Real-Time 這三種需求。舉個實際的例子:一筆銀行轉帳發生時,交易的同時需要進行反欺詐檢測(real-time),交易結束後需要立即反映在即時報表中(near real-time),而在較長時間內,這筆交易還會被用於報表統計、數據挖掘和機器學習(offline)。
因此,該如何權衡批處理(Batch Processing)和流處理(Stream Processing)的運用,這正是 Lambda 架構和 Kappa 架構試圖解決的核心問題。
Lambda Architecture: The Dual-Track Solution
2011 年,Nathan Marz(Apache Storm 的創建者)在他的部落格中提出了 Lambda Architecture ,嘗試透過「雙軌並行」的方式同時支持批處理和即時處理。這個架構的核心思想是讓數據同時流入批處理層和速度層,分別處理後再合併結果,從而兼顧準確性和即時性。
Kappa Architecture: Simplifying the Complexity
2014 年 7 月,LinkedIn 的 Jay Kreps(Apache Kafka 的共同創建者)在文章 “Questioning the Lambda Architecture” 中提出了 Kappa Architecture,提供了一個更簡潔的解決方案。Kappa 架構的核心理念是:如果流處理引擎足夠強大,為什麼不用它來處理所有數據?透過移除批處理層,只保留流處理層和服務層,Kappa 架構大幅降低了系統的複雜度。
按照我的規劃,之後會有一整章的篇幅,將這些看似零散、實則高度相關的名詞 — — ETL/ELT、Lambda Architecture、Kappa Architecture、Medallion Architecture 與 shift-left — — 用層次分明的方式串接起來。在此我先給出一個整體輪廓,作為後續討論的引子:
從更高的抽象層次來看,ETL/ELT、Lambda Architecture、Kappa Architecture、Medallion Architecture 與 shift-left,其實並非彼此競爭的名詞,而是資料工程在不同發展階段,針對不同核心問題所提出的回應。ETL 與 ELT 解決的是「資料怎麼進來、什麼時候轉換」:ETL 假設轉換應該發生在載入前,使資料一進入系統就符合既定 schema;而 ELT 則傾向先將 raw data 可靠地落地,再在下游用更彈性的計算引擎完成轉換,這也更貼近 data lake 與 lakehouse 的運作方式。Lambda 與 Kappa 進一步聚焦於「資料能不能同時即時且正確地算出來」:前者以雙軌架構在批次準確性與即時性之間折衷,後者則承認事件流才是主體,試圖用單一串流模型消除兩套邏輯所帶來的系統複雜度。隨著資料規模與使用場景不斷擴張,工程挑戰逐漸從「算不算得出來」轉向「算出來的結果能不能信」,這正是 Medallion Architecture 透過 Bronze/Silver/Gold 分層來表達資料成熟度的出發點;而 shift-left 則更進一步,將焦點從技術架構推向組織與責任分工,主張在資料產生的最上游就定義 schema、品質與契約(例如 data contract、schema registry 與前置驗證),避免把風險與不確定性一路傳遞到管線末端。整體而言,這些概念共同勾勒出一條清晰的演進路線:資料工程的重心,正不斷從資料搬運與計算能力,走向語義治理與責任前移。
如果你對這條演進路線背後的真理感到好奇,那麼之後的某一章會為此而寫!
Kafka: The Streaming Bridge
無論是 Lambda 還是 Kappa 架構,Kafka 都是不可或缺的核心組件。
在 Lambda 架構中,Kafka 扮演著統一數據入口的角色,負責將數據分發到批處理和流處理兩個系統。它就像一個交通樞紐,確保相同的數據能夠同時送達兩條不同的處理管道。
而在 Kappa 架構中,Kafka 的角色更加關鍵 — — 它不僅是數據的傳輸管道,更是唯一的數據來源(Single Source of Truth)。透過 Kafka 的數據持久化和重放(Replay)能力,流處理引擎既可以處理即時進來的數據,也可以重新處理歷史數據。這種設計讓 Kappa 架構能夠用單一的處理引擎同時滿足批處理和流處理的需求。
因此,在這兩個架構中,Data Source 的下一個站點都是 Kafka,而 Kafka 之所以能在 Data Source 的下游站穩腳跟,只因它當初解決了日誌大數據的傳輸問題。
The Issue of Big Data Transmission
我們先來了解一個根本問題:大數據系統的數據究竟從哪裡來?
在 Data Lake Era,大數據的源頭,其實就是應用系統或業務系統產生的日誌(Log)。最早用上這些大數據技術的,往往都是互聯網公司的廣告和搜索業務。你每瀏覽一次網頁、每點擊一次廣告、每進行一次搜索,這些操作都會在服務器上記錄下對應的訪問日誌。
在一開始,對於這些數據處理的需求還不是流式處理,而是透過 MapReduce 來進行批處理的。所以,我們就有了一個最簡單的需求:把每台應用服務器上的日誌,放到 Hadoop 集群的 HDFS 上去。
The Naive Approaches and Their Failures
Approach 1: Scheduled Upload with Cronjob
最直接的方法是把日志先寫到本地硬盤,按時間分割文件(例如每小時一個),再透過 Linux 的 cronjob 定時上傳到 HDFS。這個方式簡單粗暴,但問題顯而易見:在日誌上傳到 HDFS 之前,整個系統是沒有災難復原機制的。如果某台服務器出現硬件故障,那段時間的日誌就丟了。
Approach 2: Direct Write to HDFS
那能不能直接透過 HDFS 客戶端寫入日誌呢?這樣一旦寫入就有三份副本,不會丟數據。但這會帶來兩個致命問題。
首先是並發壓力。如果有 100 台應用服務器,就會有 100 個客戶端 7×24 小時不斷往 HDFS 寫入,HDFS 並不是為這種高並發、持續寫入的場景設計的。
其次是文件組織的兩難。如果各自寫各自的文件,每小時會產生 100 個小文件,造成大量存儲浪費和 MapReduce 處理開銷。如果都往同一個文件寫,所有客戶端會在 chunkserver(HDFS 中負責實際存儲數據的服務器)的主副本上排隊競爭,形成串行化的瓶頸。
因此,我們需要的是一個專門設計的中間層,能夠接受數百個客戶端的高並發寫入、提供數據的持久化和容錯,並高效地將數據傳遞給 HDFS 和其他處理系統。
Scribe: A Multi-Tier Aggregation Tree
為了更適合 HDFS(GFS)這樣的特性,我們需要一個中間層來幫助解決問題。這個中間層,通常被稱為日誌收集器(Log Collector)。其中,Facebook 於 2008 年推出了開源的 Scribe,Cloudera 於 2010 年推出了 Flume。
這些日誌收集系統的架構並不難理解。簡單來說,各個應用服務器上都有一個日誌收集器的客戶端,多個客戶端會把日誌發送到一個日誌匯集(Log Aggregator)服務器,而多個日誌匯集服務器還可以再次用類似的方式進行匯聚。透過這樣一個類似於多層樹狀的結構,最終只有幾個日誌匯聚服務器會向 HDFS 寫入數據。
這種方式的優勢很明顯:既不會有太多的並發寫入請求直接打到 HDFS 上,同時又盡可能發揮了 HDFS 順序寫入數據高吞吐量的優勢。這些系統本身也已經設計了各種容錯機制。比如,在網絡傳輸中斷的情況下,Scribe 會先把數據寫入到本地磁盤,等待網絡恢復的時候再做「斷點續傳」,這樣就不會因為暫時的網絡問題而丟失數據。
實際上,它並不是即時不斷地向 HDFS 寫入數據,而是定時地向 HDFS 上轉存(Dump)文件。只不過,這個「定時」很頻繁,比如每 5 分鐘,甚至每 1 分鐘就向 HDFS 上寫入一個日誌文件。這樣做的好處是,因為我們已經做了所有應用服務器的日誌匯聚,即使有 1000 台服務器,這一分鐘也只有一個日誌文件,並不會遇到大量碎片化的小文件的問題。
然而,這種架構仍有其局限性:它仍然是批處理模式,只是批次變小了(從小時級降到分鐘級),無法做到秒級或毫秒級的即時處理,而且每次都需要啟動完整的 MapReduce 任務,overhead 仍然存在。
Early Kafka’s Mission: Bridging to Near Real-Time
根據 Kafka 的原始論文 “Kafka: a Distributed Messaging System for Log Processing”(2011)和當時的技術背景,我們需要理解一個重要的事實:Kafka 最初的設計目標並不是為了支持 Real-Time 的即時查詢,而是為了解決更實際的日誌傳輸問題。
在 2008–2010 年的 Scribe/Flume 時代,系統的主要需求是將日誌收集到 HDFS,延遲通常介於 Near Real-Time 到 Offline 之間,分析方式是定期的批處理 MapReduce。這個時期的挑戰是「如何高效地把數據送進去」,而不是「如何即時處理數據」。隨著技術的進步,Scribe 和 Flume 已經能夠將延遲壓縮到 Near Real-Time 的範圍,但仍然無法滿足 Real-Time 的需求。
2011 年 Kafka 誕生時,LinkedIn 的工程師們看到了三個核心需求。首先是高效的日誌收集和傳輸 — — 需要將數百台服務器的日誌快速、可靠地收集起來,並傳遞到 HDFS 進行批處理分析。其次是 Near Real-Time 分析 — — 將數據分析的延遲從 Offline 提升到 Near Real-Time,支持更頻繁的 MapReduce 任務執行。第三,雖然當時主流還是批處理,但 LinkedIn 已經看到流處理的需求,為未來的 Real-Time 處理做準備。值得注意的是,Storm 也是在 2011 年開源,流處理的概念正在興起。
因此,2011 年的 Kafka 定位在 Near Real-Time,並非因為技術能力的限制,而是因為整個數據生態系統還以批處理為主。當時的分析工具(HDFS、MapReduce、Hive)都是批處理導向,業務場景也不需要秒級響應。Kafka 的核心價值是提供更高效、更可靠的日誌傳輸,並為未來的流處理需求做好準備。
真正的 Real-Time 需求爆發是在 2014–2015 年之後。隨著 Spark Streaming、Flink 等流處理引擎的成熟,以及即時推薦、風控、監控等業務場景的興起,Real-Time 處理才成為主流需求。Kafka 也在這個過程中水到渠成地演化,它的底層架構從一開始就是 Stream Store,當生態系統(如 Stream Compute Engine)和業務需求成熟時,Kafka 自然能夠支援 Real-Time Processing 場景。
好的系統往往從解決眼前的實際問題開始,但要為未來的需求留下足夠的設計空間。
Early Kafka: The Architecture
Core Entities
Kafka 的架構由幾個核心實體所構成。理解這些實體各自的角色,以及它們之間如何協作,是掌握 Kafka 系統行為與設計取捨的關鍵。本節將以一則訊息(message)的生命週期為主線,從它被寫入系統、儲存、再到被讀取與處理的過程,逐步說明 Kafka 的核心組件,以及它們在整體架構中所扮演的角色。
從系統視角來看,這條生命週期描述的是一條完整的 Data Plane 路徑:Producer 將資料寫入、Broker 負責儲存與複寫、Consumer 從中讀取並處理事件。這樣的敘述方式,與在 relational database 中討論一條 DML 指令(INSERT / UPDATE / SELECT)的執行流程十分相似 — — 關注的是資料如何流動、如何被存取,而不是系統如何做出全域決策。
正因如此,現在先將焦點放在 Data Plane 的角色與互動上,而暫時不深入討論 Control Plane 中關於 metadata、選舉與 consensus 的細節。那些問題,將在 ”Kafka 4.0 (2024): From ZooKeeper Era to KRaft Era” 中被單獨拆解與對照。
千里之行,始於足下。讓我們開始吧!
一則訊息的旅程始於 Producer(生產者)。Producer 是資料的來源,負責將訊息發送到 Kafka。每一筆訊息在送出時,都必須指定要寫入哪一個 Topic,而在實作層面,Producer 也會決定該訊息最終要被寫入哪一個 Partition。實務上,Producer 可能是一個應用伺服器、日誌收集器,或任何需要對外發送事件或資料的系統。
需要特別釐清的是,Kafka 所謂的「Producer」是一種邏輯角色(client),而不是程式檔案或程式碼數量的概念。一個應用程式通常會建立一個 Producer client 實例來對 Kafka 發送訊息,但在需要時,也可以在同一個應用中建立多個 Producer client。Kafka 並不關心這些 Producer 來自同一個程式檔案、同一個 process,或是不同的服務;在系統眼中,它們都是獨立的 Producer client。
同樣需要釐清的是:訊息要進入哪個 Partition,是 Producer 的行為選擇,而不是 Kafka 協定層的強制規則。雖然最常見、也最被建議的做法,是根據訊息的 key 進行分配,以確保相同 key 的訊息落在同一條序列中,但 Kafka 並不要求「相同 key 必須進入相同 Partition」。Producer 完全可以選擇將相同 key 的訊息分別送到不同的 Partition;只是這樣一來,原本依賴 Partition 所提供的區域順序語義就會被破壞。換言之,順序並不是 Kafka 自動替你維護的性質,而是使用者在設計 Producer 行為時所做出的選擇。
當訊息被送出後,它會被寫入 Broker。Broker 是 Kafka 叢集中的服務節點,負責實際儲存資料,並處理 Producer 與 Consumer 的讀寫請求。一個 Kafka 叢集通常由多個 Broker 組成,每個 Broker 可以同時承載多個 Topic 的多個 Partition。為了在多節點環境下協調叢集狀態與角色分工,Kafka 必須仰賴某種共識(Consensus)機制,而這套機制在 ZooKeeper 時代與 KRaft 時代,採取了截然不同的設計。
在 Broker 內部,訊息首先被歸類到對應的 Topic(主題)。Topic 是 Kafka 中用來對訊息進行邏輯分類的單位,可以理解為一個事件或資料的命名空間。例如「使用者行為日誌」、「訂單事件」、「系統監控指標」都可以是不同的 Topic。Producer 在發送訊息時必須指定 Topic,Consumer 則透過訂閱 Topic 來接收資料。若以資料庫做類比,Topic 類似於 PostgreSQL 中的 table — — 都是用來組織與描述同一類型資料的邏輯單位。
每一個 Topic 在實體上會被切分為多個 Partition(分區)。Partition 是 Topic 的物理切分單位,分散儲存在不同的 Broker 上。這個設計是 Kafka 能夠實現高吞吐量與平行處理的核心:多個 Producer 可以同時向不同的 Partition 寫入資料,多個 Consumer 也可以同時從不同的 Partition 讀取資料。Kafka 僅在 Partition 內提供區域順序保證;每一個 Partition 本身都是一條獨立的 append-only log。
這個概念與 PostgreSQL 的 partition 有相似之處:兩者都是將大型資料集切分為較小的單元,以提升效能與可管理性,而且都可以依照 key 來決定資料應該落在哪個 partition。不同之處在於,Kafka 的 Partition 更強調順序性與串流語義,每個 Partition 都擁有獨立的 offset 序列;而資料庫的 partition 則主要用於查詢最佳化,在邏輯上仍屬於同一張 table。
在每一條 Partition 之中,Kafka 透過 Offset(偏移量) 來標識訊息的位置。Offset 是一個在單一 Partition 內單調遞增的數字,用來表示每一筆訊息在該 Partition 中的順序位置。由於每個 Partition 都有自己獨立的 Offset 序列,Offset 的語義只在 Partition 內成立,而不存在跨 Partition 的全域 Offset。Consumer 正是透過維護 Offset,來追蹤自己已經處理到哪一個位置;這也是 Kafka 能夠支援資料重播、回溯與精確控制消費進度的基礎。
當訊息準備被讀取時,輪到 Consumer(消費者) 登場。Consumer 從 Kafka 拉取訊息並進行處理,它可以訂閱一個或多個 Topic,並從其所分配到的 Partition 中讀取資料。與 Producer 一樣,Consumer 也是一種邏輯上的 client,而不是程式檔案的概念。一個應用程式通常會建立一個 Consumer client 實例,但也可以在同一個應用中建立多個 Consumer client,視需求而定。
與許多傳統 message queue 不同,Kafka 並不由 Broker 代為追蹤「訊息是否已被消費完成」,而是將消費進度的管理權交給 Consumer。Consumer 可以透過設定 `enable.auto.commit = true`,啟用自動提交 Offset 的機制,由 Kafka client 依照 `auto.commit.interval.ms` 所指定的時間間隔,定期提交目前消費到的位置;也可以將 `enable.auto.commit` 設為 `false`,關閉自動提交,改由應用程式在確認業務邏輯完成後,主動提交 Offset。這使得「何時算處理完成」成為應用語義的一部分,而不再是系統隱含的行為,進而影響錯誤恢復、重試策略與整體處理語義。
當多個 Consumer 需要共同處理同一批資料時,Kafka 透過 Consumer Group(消費者組) 來進行協調。Consumer Group 並不是一個需要額外建立的實體,而是一種執行時的邏輯關係:只要多個 Consumer client 使用相同的 group.id,Kafka 就會將它們視為同一個 Consumer Group。Group 的責任在於協調 Partition 的分配與重新平衡,而不是定義 Offset 的語義。
在同一個 Consumer Group 中,每一個 Partition 在任一時間點,只會被分配給其中一個 Consumer,以確保 Partition 內的區域順序不被破壞;而當某個 Consumer 發生故障或離線時,Group 會觸發 rebalancing,將其負責的 Partition 重新分配給其他仍然存活的 Consumer。由於消費進度是以 Offset 形式被記錄下來,新接手的 Consumer 可以從正確的位置繼續處理,確保資料不會遺失,最多只會出現重複處理的情況。
從 client 的角色分工來看,Kafka 的設計其實相當直白:Producer 本質上是一個 Writer client,負責將資料寫入系統;Consumer 則是一個 Reader client,負責從系統中讀取資料並進行處理。這種讀寫角色在 client 層級上的明確分離,是 Kafka 架構的重要特徵之一。
相較之下,在 PostgreSQL 等傳統關聯式資料庫中,讀與寫並不是透過不同種類的 client 來區分,而是由同一種 client 透過 SQL 語義來表達 — — 同一個連線既可以執行 SELECT 進行讀取,也可以執行 INSERT、UPDATE 或 DELETE 進行寫入。換言之,資料庫系統將讀寫角色視為操作語義的一部分,而 Kafka 則選擇在 client 類型與系統互動模型上,直接將讀與寫拆分為不同的角色。
這種在 client 層級就明確區分讀寫角色的做法,也讓 Kafka 更自然地成為一個以資料流動為中心,而非以複雜查詢為中心的系統。
最後值得一提的是,Producer 與 Consumer 並不是 Kafka 唯一的 client 類型。Kafka 後續也提供 Admin client,用來建立與管理 Topic、查詢叢集狀態與設定;在後續的發展中,還出現了 Kafka Streams 與 Kafka Connect 等更高階的 client,將 Producer 與 Consumer 的能力進一步封裝,用於串流處理與資料整合。不過在 Early Kafka 的架構中,Producer、Consumer 與 Consumer Group,已經構成了訊息從寫入到被處理的完整生命週期。
Pull Model: Consumers Pull Data
Kafka 採用了一個與許多傳統消息隊列不同的設計:Consumer 主動拉取(Pull)數據,而不是 Broker 推送(Push)數據。
這個設計選擇看似簡單,但帶來了深遠的影響。在傳統的 Push 模型中,Broker 需要追蹤每個 Consumer 的消費速度,決定何時推送多少數據。如果 Consumer 處理速度慢,Broker 需要緩存數據或限流;如果 Consumer 處理速度快,Broker 又可能成為瓶頸。
而在 Pull 模型中,Consumer 完全掌控消費速度。處理能力強的 Consumer 可以快速拉取大批數據,處理能力弱的 Consumer 可以慢慢拉取。Broker 不需要關心 Consumer 的狀態,只需要簡單地響應數據請求即可。這大幅簡化了 Broker 的設計,也讓系統更容易擴展。
此外,Pull 模型還讓 Consumer 可以自由控制消費位置。想要重新處理昨天的數據?只需要將 Offset 重置到昨天的位置再拉取即可。想要跳過某些數據?直接調整 Offset。這種靈活性在 Push 模型中很難實現。
當然,Pull 模型也有代價。如果沒有新數據,Consumer 可能會不斷發起空請求,造成資源浪費。Kafka 通過「長輪詢(Long Polling)」機制解決這個問題 — — 如果沒有數據,Broker 會保持連接一段時間,等待新數據到達後再響應,而不是立即返回空結果。
How Early Kafka Implements Reading and Writing in a Single Partition
在實際的實現上,Kafka 是這麼做的。每一個 Topic 會有很多個 Partition,分布到不同的物理機器上。一個物理機上,可能會分配到多個 Partition。
實際存儲的時候,我們的一個 Partition 是一個邏輯上的日誌文件。在物理上,這個日誌文件會實現成一組大小基本相同的 Segment 文件,比如每個 Segment 是 1GB 大小。每當有新消息從 Producer 發過來的時候,Broker 就會把消息追加寫入到最後那個 Segment 文件裡。
而為了性能考慮,Kafka 支持我們自己設置,是每次寫入都把數據刷新到硬盤裡,還是在寫入了一定數量的日誌或者經過一個固定的時間的時候,才把文件刷新到硬盤裡。如果選擇後者,概念上確實類似於 Cassandra 從 MemTable flush 到 SSTable:資料先累積在記憶體中,達到一定條件後才批次寫入磁碟。這讓我們可以在數據可靠性和寫入性能之間做權衡。
Broker 會在內存裡維護一個簡單的索引,這個索引其實就是通過一個虛擬的偏移量,指向一個具體的 Segment 文件。那麼在 Consumer 要消費數據的時候,就是根據 Consumer 本地維護的已經處理完的偏移量,在索引裡找到實際的 Segment 文件,然後去讀取數據就好了。
這個設計的精妙之處在於:寫入是順序追加,讀取是順序掃描。這兩種操作在機械硬盤上都非常高效,即使在 SSD 上也能發揮最佳性能。Kafka 正是通過這種「化隨機為順序」的設計,在使用普通硬件的情況下,達到了極高的吞吐量。
Leveraging the Linux File System
Kafka 的一個關鍵設計決策是:完全依賴 Linux 文件系統,而不是自己實現任何緩存機制。
Kafka 直接使用本地文件系統承擔消息持久化的功能,沒有實現任何緩存機制,而是直接依賴 Linux 文件系統的 Page Cache。Kafka 寫入的數據本質上都在 Page Cache 裡。
這個設計特別適合串流數據處理的場景。因為讀寫的數據有很強的時間局部性(Temporal Locality) — — Broker 剛剛寫入的數據,幾乎立刻會被下游的 Consumer 讀取,所以大量的讀寫都會命中緩存。Kafka 透過 mmap(Memory Mapped Files)來寫入數據,使得讀寫性能優異。
這個設計避免了兩個重要問題。第一是 JVM 的 GC 開銷 — — 如果大量消息緩存在 JVM 記憶體裡,GC 可能會短時間大幅影響 Broker 性能,造成延遲峰值。第二是緩存的「冷啟動問題」 — — 如果 Broker 進程重啟,內存中的緩存會消失,讀取性能會大幅下降。透過利用文件系統的 Page Cache,即使進程重啟,只要 OS 沒有重啟,Page Cache 仍然存在,性能依然穩定。
Zero-Copy with sendfile
除了利用文件系統,Kafka 還利用了 Linux 的 sendfile API,透過 DMA(Direct Memory Access)直接將數據從文件系統傳輸到網絡通道,實現了零拷貝(Zero-Copy)。
在傳統的數據傳輸中,數據需要經過多次複製:從磁碟讀到內核緩衝區、從內核複製到用戶空間、從用戶空間複製回內核、最後從內核發送到網絡。每次複製都會消耗 CPU 和記憶體頻寬。
sendfile 讓數據直接在內核空間內從文件系統傳輸到網絡 socket,完全繞過用戶空間,避免了多次複製。搭配 DMA,數據傳輸甚至不需要 CPU 參與,CPU 可以專注於處理其他任務。這大幅降低了網絡數據傳輸的開銷。
這些設計選擇加在一起,造就了 Kafka 驚人的性能表現:在普通硬件上就能達到每秒數百萬條消息的吞吐量,而且延遲穩定可預測。Kafka 的速度,很大程度上來自於它不做多餘的事情 — — 讓 OS 做它最擅長的事(文件系統和網絡傳輸),自己只專注於消息路由和協調。
ZooKeeper: The Coordination Backbone in Kafka Cluster
在 Kafka 的原始設計中,ZooKeeper 扮演著至關重要的協調角色。它就像是整個 Kafka cluster 的「中央控制台」,負責管理所有的 metadata 和協調工作。
Broker Registration and Management
每個 Broker 啟動時,都會在 ZooKeeper 中註冊自己。具體來說,Broker 會在 /brokers/ids/[broker_id] 路徑下創建一個 ephemeral node(臨時節點),包含該 Broker 的 host 和 port 資訊。這個 ephemeral node 的特性很重要:只要 Broker 保持與 ZooKeeper 的連接,這個節點就存在;一旦 Broker 掛掉或失去連接,這個節點就會自動消失,通知整個 cluster 這個 Broker 已經不可用了。
此外,Broker 還會在 /brokers/topics/[topic] 下註冊它所管理的 topics 和 partitions 資訊。這樣整個 cluster 就能知道哪些 topics 分布在哪些 Brokers 上。
Consumer Registration and Coordination
Consumer 同樣需要在 ZooKeeper 中註冊。每個 Consumer 會在 /consumers/[group_id]/ids 路徑下註冊自己,同樣使用 ephemeral node。Consumer 還會在 ZooKeeper 中追蹤自己消費到哪個 offset 了,這些資訊存儲在 /consumers/[group_id]/offsets/[topic]/[partition] 路徑下。
更重要的是,Consumer 會在 ZooKeeper 中註冊 watches(監聽器),用來監聽:
- Consumer Group 的變化(有新 Consumer 加入或離開)
- Broker 的變化(有 Broker 加入或離開)
- Topic 的變化(有新 Topic 被創建)
當這些變化發生時,ZooKeeper 會通知相關的 Consumers,觸發 rebalancing 過程,重新分配 partitions。
Core Functions of ZooKeeper
總結來說,在早期 Kafka 架構中,ZooKeeper 負責:
- Broker 註冊與健康檢查
- Consumer 註冊與 offset 管理
- Topic 和 partition 的 metadata 儲存
- Controller election(選擇哪個 Broker 作為 Controller)
- Consumer Group 的協調與 rebalancing 觸發
值得注意的是,從 Kafka 0.9 開始,Consumer offset 的存儲逐漸從 ZooKeeper 遷移到 Kafka 內部的 __consumer_offsets topic。但在 2011 年的論文和早期版本中,ZooKeeper 確實是 Broker 和 Consumer 都必須連接的中心協調服務。
Delivery Order/Guarantees in Early Kafka
Kafka 保證來自同一個 partition 的消息會按順序傳遞給 consumer,但不同 partition 之間的消息順序就沒有保證了。這其實是 Kafka 和傳統 Message Queue 最大的不同之一 — — 傳統的 Message Queue(如 RabbitMQ、ActiveMQ)通常維護一個全域的消息順序,所有消息在單一隊列中按照抵達順序排列。
須特別注意的是,Kafka 論文中將傳統 Message Queue 描述為具備全域順序,指的是它常見的使用方式:
- 假設單一的 queue
- 假設 consumer 行為不破壞 FIFO
但在更嚴格的語義層級上,Message Queue 並未將全域順序建模為不可破壞的一級契約,multi-consumer、retry、requeue 都會破壞全域順序。
而 Kafka 透過 partition 實現水平擴展,直接犧牲了全域順序性,卻換來了更高的吞吐量和平行處理能力,這樣的選擇也導致 Kafka 只在 partition 內提供可預期的區域順序。如果用戶須保證相關消息的順序,就得先確保相關消息都發送到同一個 partition。
原則上,每個 consumer group 都能恰好收到一次消息。但有個例外:當 consumer process 異常崩潰(沒有乾淨地關閉)時,接手這些 partition 的新 consumer 可能會收到一些重複的消息 — — 那些在最後一次成功 commit offset 到 ZooKeeper 之後的消息。因此,論文中只提到 Broker-Comsumer 的訊息傳遞是 At-Least-Once。
事實上,如果要完整地討論 Delivery Guarantee,應該是要將視野擴大到 Producer-Broker-Consumer 這整條 End-to-End 路徑的訊息傳遞,而 Modern Kafka 也真的在 End-to-End 上做到了 Exactly-Once!
Modern Kafka: The Improvements
在 2011 年的原始論文中,Kafka 的作者們在結論部分描述了系統當時的狀態,並給出了 Future Works。究竟這些願景後來如何一步步實現?
The Original Vision: What They Planned
在論文的 Conclusion and Future Works 章節中,Kafka 的創建者們明確指出了兩個主要的發展方向。
第一個方向是增加內建的跨 Broker 複製機制(Built-in Replication)。在 2011 年的版本中,Kafka 還沒有內建的高可用機制。論文中提到:「We plan to add built-in replication of messages across multiple brokers to allow durability and data availability guarantees even in the case of unrecoverable machine failures.」
他們計劃同時支援非同步複製(Asynchronous Replication)和同步複製(Synchronous Replication)兩種模式,讓應用程式可以根據對持久性、可用性和吞吐量的需求,選擇適當的冗餘級別。這個設計理念體現了 Kafka 一貫的哲學:不是提供一個「萬能」的解決方案,而是提供靈活的配置選項,讓用戶根據場景自行權衡。
第二個方向是增加串流處理能力(Stream Processing Capability)。論文中觀察到:「After retrieving messages from Kafka, real time applications often perform similar operations such as window-based counting and joining each message with records in a secondary store or with messages in another stream.」
Get Ryan Fu’s stories in your inbox
Join Medium for free to get updates from this writer.
他們認為,在最底層,這可以透過在發布時對消息進行語義分區(Semantic Partitioning)來支援,確保具有相同 key 的所有消息都進入同一個 Partition,從而到達同一個 Consumer 進程。這為跨 Consumer 機器集群處理分散式串流提供了基礎。在此之上,他們希望提供一個實用的串流工具庫,包含不同的窗口函數(Windowing Functions)或 Join 技術,這將對這類應用非常有幫助。
What Changed From 2011 Till 2026
從 2011 年的原始論文到 2026 年,Kafka 經歷了 15 年的持續演進。讓我們按照時間軸來看看主要的里程碑版本和關鍵功能。
Kafka 0.8 (2013): The Foundation of High Availability
Kafka 0.8 是一個里程碑版本,它實現了論文中提出的第一個 Future Work:Built-in Replication。這個版本引入的 replication 機制,讓 Kafka 從「日誌收集工具」真正邁向「企業級消息系統」。
Replication: Leader and Followers
為了實現高可用性,Kafka 0.8 為每個 Partition 引入了多副本機制,分散存儲在不同的 Broker 上。這些副本採用 Leader-Follower 模型:其中一個副本擔任 Leader,其餘的是 Followers。Producer 寫入數據時,只需要寫入 Leader,Leader 會將數據寫入本地日誌文件。接著,每個 Follower 會主動從 Leader 拉取最新數據,拉取成功後向 Leader 發送 Ack 確認。
ISR (In-Sync Replica)
Kafka 引入了 ISR(In-Sync Replica)的概念來管理副本同步狀態。只有與 Leader 保持同步的 Followers 才會被納入 ISR 集合中。當 Leader 失效時,只有 ISR 中的 Followers 才有資格被選為新的 Leader。這個機制確保了數據的一致性,同時也提供了靈活的容錯能力。
Tuning Durability with acks
最關鍵的創新是 acks 配置,它讓用戶可以在性能和可靠性之間自由權衡:
- acks=0:Producer 發送消息後立即返回,不等待任何確認。性能最高,但可能丟失數據。
- acks=1:等待 Leader 寫入成功即返回。如果 Leader 在數據複製到 Followers 之前掛掉,可能會丟失少量數據。
- acks=all:等待所有 ISR 中的副本都寫入成功才返回。最安全,但延遲較高。
對於廣告點擊日誌這類可容忍少量丟失的場景,可以選擇 acks=1 獲得更好的性能;對於金融交易這類關鍵數據,則可以選擇 acks=all 確保不丟失任何消息。
這裡有個重要的細節:acks=all 等待的是 ISR 中的副本,而不是所有 Follower 副本。舉例來說,假設某個 Partition 配置了 3 個副本:
- 1 個 Leader: partition_1
- 2 個 Followers: partition_2 & partition_3
如果 partition_3 因為網路問題或處理速度過慢而掉隊,它會被移出 ISR。此時,即使設定 acks=all,Producer 也只需等待 partition_1(Leader)和 partition_2(仍在 ISR 中的 Follower)寫入成功即可,不會無限期等待已掉隊的 partition_3。這個設計在保證數據安全性的同時,也避免了單一慢節點拖累整個系統的問題。
那麼,掉隊的 partition_3 如何歸隊呢?答案是:自動且持續進行。
partition_3 不會被替換或刪除,而是會持續嘗試從 partition_1(Leader)拉取數據。一旦它解決了導致落後的問題(例如網路恢復或處理速度提升),就會開始追趕 partition_1 的進度。當 partition_3 完全追上 partition_1 的日誌末端(log end offset)時,partition_1 會自動將它重新加入 ISR 列表。
整個過程完全自動化,不需要人工干預。partition_1(Leader)會持續監控 partition_2 和 partition_3 的同步狀態:如果某個 Follower 的延遲時間超過設定的閾值(replica.lag.time.max.ms),就會被移出 ISR;一旦追上進度,又會自動歸隊。這個機制讓 Kafka 既能保持高效能,又具備自我修復的能力。
這個版本奠定了 Kafka 高可用性的基礎,使其能夠支撐生產級別的應用場景。
Kafka 0.9 (2015)
如果說早期的 Kafka 主要是一個高吞吐量的分散式 log 系統,那麼 Kafka 0.9 可以被視為它正式邁向「平台化」的起點。從這個版本開始,Kafka 不再只關心資料如何被寫入與讀取,而是開始正面回應「資料如何在系統之間流動」與「平台該承擔哪些通用責任」這兩個更高層次的問題。
Kafka Connect
Kafka Connect 提供了一個標準化的資料整合框架,用來連接 Kafka 與各種外部系統。它將過去必須自行撰寫 Producer/Consumer 程式碼、處理錯誤與重試邏輯的工程問題,提升為以設定檔驅動、可重複部署的系統能力,使資料整合成為 Kafka 平台的一部分,而非零散的應用實作。
在設計上,Kafka Connect 明確區分為兩種角色:Source Connector 與 Sink Connector,分別對應資料流動的兩個方向。
Source Connector 的任務,是將外部系統中的資料導入 Kafka,轉換為持續產生的事件流。這些資料來源涵蓋了多種類型:最典型的是關聯式資料庫,透過 JDBC 方式定期拉取資料表或查詢結果;更進一步的,則是以資料庫交易日誌為基礎的 Change Data Capture(CDC),例如 MySQL 的 binlog、PostgreSQL 的 logical decoding、Oracle 的 redo log、或 SQL Server 的 transaction log。CDC 類型的 Source Connector 將資料的每一次變化視為事件,這使得資料庫正式成為事件來源,而 Kafka 則成為承載這些變更歷史的事件骨幹。
除了資料庫之外,Source Connector 也常被用來整合檔案與日誌系統,例如持續監控檔案新增內容、收集應用程式日誌;在雲端環境中,也可以從物件儲存(如 S3、GCS、Blob Storage)將 batch 或半結構化資料導入 Kafka。某些情境下,Source Connector 甚至被用來橋接既有的訊息系統、SaaS 平台或業務 API,作為系統現代化或事件化過程中的過渡層。
相對地,Sink Connector 的角色,則是將 Kafka 中的事件流可靠地輸出到各種下游系統。最常見的目的地包括關聯式資料庫與資料倉儲,用來將事件寫回 OLTP 系統或分析平台;在資料湖與雲端分析場景中,事件也常被持續寫入 HDFS 或物件儲存,作為長期保存與批次分析的基礎。對於即時查詢與搜尋需求,Kafka 事件則可以透過 Sink Connector 寫入 Elasticsearch 或類似系統;而在狀態同步與低延遲查詢的場景中,NoSQL 或 Key-Value Store(如 Cassandra、HBase、Redis)也成為常見的下游。
透過 Kafka Connect,Kafka 不再只是資料流經的一個中繼點,而是成為系統之間資料流動的中樞。無論是傳統的 batch ingestion、即時事件串流,或後來逐漸成為主流的 CDC 模式,都可以被納入同一套抽象之中。
Other Updates
除了 Connect 之外,Kafka 0.9 也在企業級可用性上邁出了關鍵一步。這個版本正式引入安全機制,包括 SSL/TLS 加密傳輸、SASL 身份驗證(支援 Kerberos、PLAIN、SCRAM 等),以及以 ACL(Access Control List)為基礎的授權管理,使 Kafka 能夠更安全地運行在多租戶與受管控的環境中。
此外,Kafka 0.9 也將 Consumer offset 的管理,從 ZooKeeper 遷移至 Kafka 內部的 __consumer_offsets topic。這個改變不僅大幅減輕了 ZooKeeper 的負擔,也象徵著 Kafka 開始將更多關鍵的系統狀態內聚回自身,為後續去 ZooKeeper 化(KRaft)的演進奠定了基礎。
Kafka 0.10 (2016): Kafka Streams
Kafka 0.10 版本推出了 Kafka Streams,這是實現論文中第二個 Future Work 的重要一步。
Kafka Streams 是一個輕量級的串流處理 library,可以直接嵌入 Java 應用程式中,無需部署獨立的處理 cluster。它提供了 Java API 來進行串流處理操作,包括 map、filter、join、aggregate、windowing 等。採用 one-record-at-a-time 的處理模型,延遲可以達到毫秒級。
後來,Confluent 為了降低使用門檻,推出了 ksqlDB(最早稱為 KSQL),讓不熟悉程式設計的使用者也能用 SQL 語法來做串流處理。ksqlDB 底層其實是用 Kafka Streams 實作的。
不過,對於更複雜的串流處理需求,業界普遍認為 Apache Flink 是更成熟、功能更強大的選擇。Confluent 也意識到這點,在 2023 年 1 月收購了 Immerok,一家由 Apache Flink 核心貢獻者創立的公司,開始在 Confluent Cloud 中提供 Flink 作為串流處理選項。
值得注意的是,Confluent 採取的是多工具並存的策略。Kafka Streams、ksqlDB 和 Flink 各有其適用場景:Kafka Streams 適合嵌入式應用,ksqlDB 提供簡單的 SQL 介面,而 Flink 則處理更複雜的分析需求。這三個工具目前都持續在 Confluent 平台中獲得支援和更新。
Kafka 0.11 (2017): Exactly-Once Semantics
對我而言,這個版本是一個別具匠心的里程碑,因為它實現了分散式系統的聖杯:Exactly-Once Semantics(EOS)。EOS 的實現讓 Kafka 能夠支援金融交易、訂單處理等對數據完整性要求極高的應用。
回想一下我們在討論 RPC 時提到的傳遞語意。在 RPC 的世界裡,討論的是兩台機器之間單次呼叫的執行次數:At-least-once 意味著函數可能執行多次,At-most-once 意味著函數最多執行一次,而 Exactly-once 則是理想狀態 — — 函數恰好執行一次。這些概念源於 1980 年代的 Remote Procedure Call 研究,關注的是點對點的呼叫語意。
但 Kafka 作為一個消息中間件,面臨的挑戰更為複雜。資料流經三個階段:Producer → Broker → Consumer。每個階段都可能失敗,每個階段都需要重試機制,而重試就可能導致重複。這不是單純的「函數執行幾次」問題,而是「消息在整個生命週期中被傳遞和處理了幾次」的問題。
Producer to Broker: Idempotent Producer
第一個挑戰是 Producer 到 Broker 的階段。當 Producer 發送消息後,Broker 成功寫入並回傳確認,但確認在網路中遺失了。Producer 看不到確認,只能假設失敗並重試。這時 Broker 會再次收到相同的消息,導致重複寫入。
Kafka 0.11 引入了 Idempotent Producer 來解決這個問題。每個 Producer 在初始化時會獲得一個唯一的 Producer ID(PID),並為每個消息分配一個單調遞增的 sequence number。Broker 會追蹤每個 PID 和 partition 的最大 sequence number。當收到重複的消息(sequence number 較小或相同)時,Broker 會識別出這是重試,直接回傳成功但不再寫入,從而避免重複。
啟用方式很簡單:設定 enable.idempotence=true。從 Kafka 3.0 開始,這已經成為預設行為。Idempotent Producer 保證了單個 partition 內的 exactly-once 語意。
Broker to Consumer: Transactional Read
第二個挑戰是 Consumer 端的處理。傳統上,Consumer 需要自己實作去重邏輯 — — 記住已處理過的消息 ID,過濾掉重複的消息。這把複雜度推給了應用程式開發者。
Kafka 0.11 提供了另一個選擇:Transactional Read(isolation.level=read_committed)。但在理解這個機制之前,我們需要先區分兩種類型的 Producer:
- 非事務性 Producer:這是最常見的模式。Producer 發送消息後,消息立即對所有 Consumer 可見。無論是否啟用 idempotence,這些都是非事務性消息。
- 事務性 Producer:使用 Transactional API(需設定 transactional.id)的 Producer。它可以將多個消息的發送包裝成一個原子性事務。這些消息在事務提交之前會被標記為「未提交」,只有當 Producer 呼叫 commitTransaction() 後才會被標記為「已提交」;如果呼叫 abortTransaction(),則會被標記為「已中止」。
值得注意的是,事務性 Producer 會自動啟用 idempotence — — 你不能使用事務而不啟用冪等性。
當 Consumer 設定 isolation.level=read_committed 時,它的讀取行為是:
- 讀取所有非事務性消息(無論 Producer 是否啟用 idempotence)
- 讀取所有已提交的事務性消息
- 跳過所有未提交或已中止的事務性消息
舉個例子,假設一個 topic 按順序收到:
- 非事務性 Producer 發送消息 A
- 事務性 Producer 開始事務,發送消息 B、C
- 非事務性 Producer 發送消息 D
- 事務性 Producer 提交事務
- 另一個事務性 Producer 發送消息 E,但事務被中止
read_uncommitted Consumer 會看到:A, B, C, D, E(全部消息);read_committed Consumer 會看到:A, B, C, D(跳過被中止的消息 E)
這種設計的巧妙之處在於:read_committed 不是「只讀取事務性消息」,而是「只讀取已確定的消息」。這確保了向後相容性 — — 即使在啟用事務功能後,舊的非事務性 Producer 發送的消息仍然可以被正常讀取。
更巧妙的是,read_committed Consumer 實際上仍然會從 Broker 讀到那些已中止的記錄(如消息 E),只是在 client 端選擇過濾掉,不交給應用程式處理。這意味著該付出的 I/O 成本還是省不掉。不過理論上,事務中止不應該是常態,所以這個開銷在正常情況下是可以接受的。
End-to-End: Transactional API
最強大的是 Transactional API,它能夠將多個操作包裝成一個原子性事務。例如,一個串流處理應用可能需要:從 Topic A 讀取消息、處理、寫入 Topic B、提交 offset。如果中間任何步驟失敗,整個操作都應該回滾。
Transactional API 讓這一切成為可能。Producer 可以使用 beginTransaction()、commitTransaction() 和 abortTransaction() 來控制事務邊界。Kafka 會確保事務內的所有寫入操作要麼全部成功,要麼全部失敗,而且 Consumer 使用 read_committed 時只會看到已提交的資料。
這個設計的精妙之處在於:它不是靠昂貴的兩階段提交(Two-Phase Commit)來實現,而是利用 Kafka 自身的日誌機制 — — 將事務狀態本身也寫入特殊的 Topic(__transaction_state),用日誌的順序性和持久性來保證事務的正確性。(但 2PC 有在未來的規劃之中)
Three Layers of Guarantees
Kafka 0.11 提供了三種不同層次的 exactly-once 保證:
- Idempotent Producer:單個 partition 內的 exactly-once,防止 Producer 重試導致的重複
- Transactional Read:Consumer 端的 exactly-once,確保只讀取已確定的消息
- Transactional API:端到端的 exactly-once,將讀取、處理、寫入包裝成原子操作
值得一提的是,Kafka 的 isolation.level(還記得上一章 RDBMS 的 Transaction Consistency 嗎?那其實就是 Isolation level。)只提供兩個選項:read_uncommitted 和 read_committed。這比傳統資料庫的四個隔離級別簡單得多,但這正反映了 Kafka 的本質 — — 它是一個 append-only 的日誌系統,而非資料庫。Kafka 的消息不會被修改,Consumer 按順序讀取,因此不需要 Repeatable Read 或 Serializable 這類複雜的隔離級別。兩個選項已經足夠:要麼看到所有消息(包括未確定的),要麼只看到已確定的消息。
正如論文中所說,Kafka 的哲學是提供靈活的配置選項。對於可以容忍少量重複的應用(如日誌收集),可以使用 At-least-once。對於需要嚴格保證的應用(如金融交易),可以使用 Exactly-once。選擇權在用戶手中。
Kafka 2.8 (2021): KRaft Mode (Experimental)
Kafka 2.8.0 透過 KIP-500 引入了 KRaft 模式作為實驗性功能,這是 Kafka 架構的重大轉變。
KRaft(Kafka Raft)使用 Raft consensus 算法來管理 metadata,不再依賴 ZooKeeper。Controller quorum 由多個 Kafka broker 組成,使用 Raft 協議選舉 leader 和複製 metadata。所有 cluster metadata(topics、partitions、configurations、ACLs 等)都存儲在內部的 __cluster_metadata topic 中。
這個改變簡化了部署(不需要額外維護 ZooKeeper 集群)、提高了擴展性(metadata 更新更快)、加快了故障恢復速度。
不過,在 2.8 版本中,KRaft 還只是實驗性功能,不建議用於生產環境。
Kafka 3.0 (2021): Breaking Changes and Modernization
Kafka 3.0 包含了一些重大變更:棄用對 Java 8 和 Scala 2.12 的支援、Kafka Raft 支援 metadata topic 的 snapshots 和其他自管理 quorum 的改進、預設啟用更強的 Producer delivery guarantees。
這個版本移除了許多已經被棄用超過 12 個月的 API,簡化了平台。雖然版本號跳到了 3.0,但實際上沒有單一的革命性功能,而是許多漸進式的改進和清理工作。
Kafka 3.3 (2022): KRaft Production-Ready
KRaft 模式在 Kafka 3.3 版本中正式成為 production-ready。
從這個版本開始,用戶可以在生產環境中使用 KRaft 模式,徹底擺脫對 ZooKeeper 的依賴。不過,ZooKeeper 模式仍然被支援,讓用戶有時間遷移。
Kafka 3.6 (2023): Tiered Storage
Kafka 3.6.0 透過 KIP-405 實現了 tiered storage 支援,從根本上改變了組織可以如何架構其存儲策略。
Tiered Storage 允許將舊的日誌資料移動到更便宜的 Object Storage(如 S3、Azure Blob Storage、Google Cloud Storage),而不是全部存儲在昂貴的本地 SSD 上。這大幅降低了存儲成本,同時仍然保留了數據的可訪問性。Hot data 保留在本地 SSD 以提供低延遲訪問,Cold data 可以移到 Object Storage 以降低成本。
這個功能對於需要長時間保留數據的場景(如合規要求、歷史數據分析)非常有價值。
Kafka 4.0 (2024): From ZooKeeper Era to KRaft Era
Kafka 4.0 標誌著一個長達十餘年的架構時代正式落幕。從這個版本開始,Apache Kafka 成為第一個完全不依賴 ZooKeeper 即可運行的主要版本,Control Plane 的共識機制全面轉向 KRaft(Kafka Raft)。這個改變不只是移除一個外部元件,而是 Kafka 對自身架構邊界的一次重新收斂:從「資料在 Kafka、共識在 ZooKeeper」,走向「資料與共識都在 Kafka 之內」。
要真正理解這個轉換的意義,單靠靜態的架構圖其實並不夠清楚。更直觀的方式,是沿著一個具體 request 的生命週期,觀察它在系統中如何流動、在哪裡被決策,以及最終是如何被提交為全叢集一致的狀態。這種觀察方式,某種程度上就像是在理解 relational database 時,實際追蹤一條 DDL 指令在系統內部所經歷的完整路徑。
因此,在本節中,我們將以「create topic」這個最基本、卻同時最關鍵的操作作為切入點。這個操作本質上是一個 Control Plane request,語義上也非常接近 relational database 中的 DDL(例如 CREATE TABLE):它不直接處理資料內容,卻會永久改變系統的結構與後續所有 Data Plane 行為。
以下,我們將分別以 ZooKeeper Era 與 KRaft Era 為背景,沿著這個 request 的生命週期,逐步拆解 Kafka 在兩個時代中是如何取得 consensus、如何劃分 Control Plane 與 Data Plane,以及這些設計差異又是如何影響整個系統的複雜度。
千里之行,始於足下。讓我們開始吧!
ZooKeeper Era: A ZooKeeper-Centric Control Plane
以建立一個名為 orders 的 topic 為例,假設 Kafka 叢集的設定如下:
- Data Plane 由三個 Broker(B1、B2、B3)組成,B1 為 controller。
- Control Plane 是一個由三個節點(Z1、Z2、Z3)組成的 ZooKeeper ensemble,Z1 為 leader。
Step 1: An Admin Request Enters the Cluster
「建立一個名為 orders 的 topic」的請求會由一個 Admin Client 發起,可能是命令列工具,也可能是應用程式中的管理 API。這個請求會先被送到 Kafka 叢集中任意一個 Broker,例如 B2。
需要特別強調的是,這個請求一開始進入的 Broker,並不代表它就有權限或責任來決定叢集的結構性變更。對 Kafka 而言,這仍然只是一個入口節點;真正的決策不會在這裡發生。
Step 2: Distinguishing Control Plane from Data Plane
當 B2 收到「建立 topic」的請求時,它會立即判斷這是一個 Control Plane 操作,而不是屬於 Data Plane 的資料讀寫行為。Broker 在 Data Plane 中負責的是 partition 的實際讀寫與複寫,而 topic 的建立、partition 的配置,屬於叢集層級的 metadata 變更。
因此,B2 不會嘗試自行處理這個請求,而是將它轉交給當前的 Controller。這個轉送行為本身,就反映了 ZooKeeper 時代 Kafka 的一個核心設計原則:Broker 不擁有最終的控制權,Control Plane 必須集中處理。
Step 3: Controller as a Logical Role
在 Kafka 叢集中,Controller 並不是一個獨立部署的服務,而是一個由某一台 Broker 擔任的邏輯角色(logical role)。假設此時 B1 是 Controller,那麼 B2 便會將建立 topic 的請求轉送給 B1。現在,B1 就是 Control Plane 的協調者,負責序列化所有 metadata 的變更請求,確保叢集狀態不會出現競爭條件或分歧。
Step 4: Delegating Consensus to ZooKeeper
然而,即便 B1 是 Controller,也不擁有最終裁決權。在 ZooKeeper Era 中,Kafka 的設計選擇是將 Control Plane 的共識問題外包給 ZooKeeper。因此,B1 不會直接在 Kafka 內部宣告「topic 已建立」,而是必須將這次 metadata 變更寫入 ZooKeeper。
Controller B1 作為 ZooKeeper 的 client,可以連線到 ZooKeeper ensemble 中的任一節點,例如 Z2。即便 Z2 本身是 follower,只要這是一個寫入請求,它就會將請求轉發給當前的 ZooKeeper leader Z1。這一步,正式將 Kafka 的 Control Plane 操作,交由 ZooKeeper 的 consensus 來處理。
Step 5: ZooKeeper Leader Commits Metadata Changes
真正執行寫入的是 ZooKeeper leader Z1。它會將「建立 orders topic」這個操作,轉換為對 ZooKeeper data tree 的一系列修改,例如建立 /brokers/topics/orders 這個 znode。
在這個節點之下,ZooKeeper 會儲存該 topic 的 partition assignment、replica list,以及每個 partition 的初始 leader(例如 leader = B1)。這些資料全部都是 Kafka 的 Control Plane metadata,而非任何實際的 message 資料。
Step 6: Replication Inside the ZooKeeper Ensemble
這些 metadata 並不是只存在於 ZooKeeper leader 上。ZooKeeper 會透過自身的共識協議(Zab),將這些狀態變更以 log 的形式複寫到所有 follower 節點(例如 Z2、Z3)。只有當多數 ZooKeeper 節點確認這次變更後,這個 metadata 才會被視為正式提交。
此時,Control Plane 狀態,已經在 ZooKeeper ensemble 中達成全域一致。ZooKeeper 成為這個 topic metadata 的 single source of truth。
Step 7: Control Plane State Propagates Back to Brokers
一旦 ZooKeeper 中的 metadata 發生變化,Controller 會透過 watch 機制接收到通知。Controller 隨即將新的 topic 與 partition 配置資訊,分發給所有相關的 Broker。
各個 Broker 依據這些 Control Plane 指示,在自己的 Data Plane 中建立對應的 partition replica、初始化 log 結構,並準備開始接受 Producer 與 Consumer 的請求。
KRaft Era: An Internalized Control Plane
KRaft Era 的關鍵改變,是 Kafka 不再把 Control Plane 的共識外包給 ZooKeeper,而是把「共識、持久化與一致性」收回到 Kafka 自己體內。換言之,Kafka 不只是移除了一個外部依賴,而是重新定義了 Control Plane 的事實來源:在 KRaft 模式下,叢集 metadata 的 single source of truth 不再是一棵 ZooKeeper znode tree,而是一條由 Raft 複寫保護的 metadata log。
為了延續前一節的敘事,我們仍然以建立 orders topic 的操作為例,來走一遍 KRaft Era 中 create topic 的完整生命週期。現在,Kafka 叢集的設定如下:
- Data Plane 由三個 Broker(B1、B2、B3)組成。
- Control Plane 是一組 Controller quorum(C1、C2、C3),C1 為 leader controller。所有 metadata 變更都必須經過這個 quorum 的同意才能生效。
Step 1: An Admin Request Enters the Cluster
如同 ZooKeeper Era,create topic 的請求依然可能先進入任意一個 Broker,例如 B2。這個入口節點的存在,主要是為了提供叢集對外的管理介面,而不是意味著 B2 擁有 Control Plane 的決策權。
Step 2: Routing to the Active Controller
B2 會辨識這是一個 Control Plane 操作,並將請求路由到目前的 active controller。不同於 ZooKeeper Era 中「Controller 是某個 Broker 暫時擔任的角色」,在 KRaft 模式下,Controller 由一組專門的節點形成 quorum;因此,路由的目標通常是目前的 leader controller(C1)。若 B2 先送到某個 follower controller(例如 C2),C2 會像典型 Raft 系統一樣將請求導向 leader controller,確保 metadata 變更被序列化處理。
Step 3: Metadata as a Raft-Replicated Log Entry
當 leader controller(C1)接到 create topic 請求後,它不會「直接在記憶體裡改狀態」或「直接對所有 Broker 廣播結論」。相反地,它會把這次變更轉成一筆或多筆 append-only 的 metadata log entry。這些 log entry 會描述 orders topic 的結構與配置,例如 partitions 的數量、replication factor、partition assignment(各 partition 對應哪些 Broker 做 replicas)、以及每個 partition 的 initial leader 指派與相關狀態(例如 ISR 的初始構成)。
在 KRaft Era,這一步就是 Control Plane 的核心:不是「我決定」而是「我寫入一筆可被共識保護的 log」。只有 log 被 quorum commit 之後,這個狀態才被承認為叢集的一致事實。
Step 4: Raft Replication and Quorum Commit
接下來,C1 會將這些 metadata log entry 複寫到其他 Controller(C2、C3)。每個 follower controller 會將 entry 先寫入自己的 log,再回 ACK 給 C1。當 C1 收到多數節點(quorum)的確認後,便宣告這些 entry commit。commit 的意義與 ZooKeeper Era 非常類似:它代表這次 metadata 變更已經成為 Control Plane 的 single source of truth,只是此時「一致事實」不再由 ZooKeeper ensemble 的 data tree 表達,而是由 Controller quorum 的 metadata log 表達。
這也是 KRaft 與 ZooKeeper Era 最本質的差異:ZooKeeper 以 znode tree 作為狀態表達,Kafka 以 watch 取得變更;KRaft 則直接以 log 為狀態表達,並且在 Kafka 系統內完成複寫與提交。
Step 5: Brokers Learn Metadata via the Metadata Log
一旦 metadata log entry commit,Kafka 的各個 Broker(B1、B2、B3)就需要得知新的叢集狀態,才能在 Data Plane 中落實它。KRaft 模式下,Broker 不再透過 ZooKeeper watch 來感知變更,而是透過 Kafka 內建的 metadata propagation 機制,從 Controller quorum 取得最新的 metadata。你可以把它理解為:Broker 會持續跟上這條 metadata log 的進度,確保自己看到的 Control Plane 狀態與 quorum commit 的狀態一致。
因此,當 orders topic 被建立後,Broker 會看到一個已提交的 metadata 狀態:這個 topic 存在、它有哪些 partitions、每個 partition 的 replicas 分佈在哪些 brokers、以及每個 partition 的 leader replica 是誰。
Step 6: Data Plane Materialization on Each Broker
當 Broker 得到一致的 Control Plane metadata 後,才會在 Data Plane 真正「把它變成可用的資料結構」。這包含在本地建立對應 partition replica 的 log 目錄與檔案結構、初始化 replica state、準備與 leader/follower 角色相關的複寫鏈路,並使得 partition leader 能夠開始接受 Producer 的寫入與 Consumer 的讀取。
值得注意的是,這一步和 ZooKeeper Era 並沒有本質不同:Data Plane 仍然由 Broker、Partition、Replica 構成,資料仍然以 append-only log 的形式存放在 Broker 的儲存層。不同之處在於,Broker 所依賴的 Control Plane 狀態來源已經內建化,不再透過外部系統提供。
Step 7: Service Becomes Visible — Reads/Writes Follow the Leader Replica
當 orders topic 的 leader replica 完成初始化並對外宣告可服務後,Producer 的寫入流量會被導向該 leader 所在的 Broker(例如 B1),資料會被 append 到 leader replica 的 log 中;其他 Broker 上的 follower replicas(例如 B2、B3)則會從 leader 複寫資料,維持同步狀態。Consumer 的讀取同樣依賴 Partition 的 leader/follower 模型與 offset 序列,但這整條 Data Plane 路徑,從始至終都不再需要任何 ZooKeeper 參與。
Summary
從 ZooKeeper Era 到 KRaft Era,可以清楚看出 Kafka 在 Control Plane consensus 上的根本轉變。
在 ZooKeeper Era,Kafka 將所有關於叢集狀態的 consensus 外包給 ZooKeeper ensemble。ZooKeeper 本身就是一個共識系統,透過 Zab 協議在多個節點之間維持一致的 metadata 狀態,並以 quorum 為基礎來提交變更。Kafka 的 Controller 雖然負責發起與協調控制操作,但並不持有最終決策權;真正被視為「事實來源(source of truth)」的,是 ZooKeeper 中被一致提交的資料。換言之,Kafka 的 Control Plane 是「依附在 ZooKeeper 之上」的。
為了理解 ZooKeeper 在 Kafka Control Plane 中所扮演的角色,有必要先簡單說明 ZooKeeper 的 metadata model。ZooKeeper 內部並不是以表格或物件形式儲存資料,而是維護一棵階層式的 metadata tree,結構上非常類似檔案系統。每一個節點稱為一個 znode,以路徑(例如 /brokers/topics/orders)作為唯一識別。Kafka 會將叢集相關的控制資訊 — — 例如 Broker 註冊狀態、Topic 定義、Partition assignment、Replica 與 leader 資訊 — — 映射成這棵樹上的不同節點,並透過 ZooKeeper 的 consensus 機制來確保這些節點內容在 ensemble 中保持一致。
在眾多 znode 類型中,ephemeral znode 是 Kafka 特別依賴的一種。Ephemeral znode 的生命週期與建立它的 client session 綁定:只要該 client 與 ZooKeeper 的 session 中斷,對應的 ephemeral znode 就會自動被刪除。Kafka 正是利用這個特性來實作「活著的角色」判定,例如 Broker 是否仍在線、目前哪一個 Broker 擔任 Controller。當某個 Broker 異常死亡時,ZooKeeper 會因為 session 消失而自動移除對應的 ephemeral znode,進而觸發其他元件重新進行角色選舉。
除了 ephemeral znode 之外,ZooKeeper 還有其他類型的節點,例如 persistent znode,其內容在 client 離線後仍會保留,常用來儲存需要長期存在的 metadata(如 Topic 與 Partition 結構);以及 sequential znode,會在建立時自動附加單調遞增的序號,常被用來實作排序或選舉原語。Kafka 雖然不是 ZooKeeper 的全部功能使用者,但正是透過這些 znode 語義的組合,將 Control Plane 的狀態外包給 ZooKeeper 來進行一致性維護。
在分布式系統中,任何叢集節點都有掛掉的可能。在 ZooKeeper Era 中,Kafka 正是依賴 ZooKeeper,讓關鍵角色的失效成為可被吸收的事件,而非系統性災難。這一點,可以從三種最關鍵的故障情境中看得特別清楚。
第一種情況是 ZooKeeper leader 發生故障。這對 Kafka 的影響其實相對有限,因為 ZooKeeper ensemble 本身就是一個 quorum-based consensus 系統。Leader 在 Zab 協議中扮演的是協調者角色,而不是 metadata 的唯一持有者。當 leader 節點失效時,其餘 ZooKeeper 節點會自動重新選舉新的 leader,只要仍有多數節點存活,所有已經被 quorum 提交的 metadata(例如 topic 定義、partition assignment、replica list)都不會遺失。對 Kafka 而言,Control Plane 的不變性完全由 ZooKeeper ensemble 所保證,Kafka 只需要在新 leader 穩定後重新連線即可。
第二種情況是 Kafka Controller 發生故障。Controller 在 ZooKeeper Era 中是一個邏輯角色,由某一個 Broker 暫時擔任,負責執行 Control Plane 的操作,例如 leader 選舉與 metadata 更新。但 Controller 本身並不是 consensus 的一部分,也不是事實來源。當 Controller 所在的 Broker 掛掉時,ZooKeeper 會因為對應的 ephemeral znode 消失而察覺此事,其餘 Broker 便會重新競選新的 Controller。新的 Controller 並不是接手前任的記憶狀態,而是重新從 ZooKeeper 中讀取已被一致提交的 metadata,並以此為基礎繼續運作。正因如此,Controller 在 ZooKeeper Era 被刻意設計成「可重建角色」,其失效不會破壞 Control Plane 的不變性。
第三種情況則發生在 Data Plane:Partition leader 發生故障。Partition leader 負責實際的讀寫請求,是資料流的關鍵節點。當 leader replica 所在的 Broker 掛掉時,Kafka 並不立即做出不可逆的決策,而是由 Controller 透過 ZooKeeper 中記錄的 metadata 與 ISR(in-sync replicas)資訊,選出新的 leader replica。這個 leader 變更本身,仍然必須被寫入 ZooKeeper,並經過 quorum 提交後,才會被視為正式生效。換言之,即使是 Data Plane 的角色切換,最終仍需回到 Control Plane 的 consensus 層,才能成為系統可依賴的事實。
綜合這三種情境可以看出,在 ZooKeeper Era 中,Kafka 的所有關鍵不變性都錨定在 ZooKeeper 的 consensus 之上。ZooKeeper leader、Kafka Controller、甚至 Partition leader 都可能失效,但只要 ZooKeeper ensemble 能維持 quorum,Kafka 就能透過重新指派角色、重建狀態,持續前進而不破壞整體一致性。
進入 KRaft Era 之後,Kafka 將 consensus 能力完整收回系統內部。KRaft 使用 Raft 協議,在一組 Controller 節點之間形成 controller quorum,所有 metadata 變更都被序列化為 append-only 的 metadata log,並且必須經過 quorum 同意後才能生效。這使得 Kafka 的 Control Plane 不再仰賴外部系統,而是以與 Data Plane 相同的 log-based 複寫模型來維持一致性。兩個時代其實都使用 quorum-based consensus,但差別在於:ZooKeeper Era 是「Kafka 依賴 ZooKeeper 的 quorum」,而 KRaft Era 則是「Kafka 自己成為擁有 quorum 的系統」。
在 KRaft Era 中,當 Controller leader 發生故障時,系統的行為與 ZooKeeper Era 有本質上的不同。Controller 不再是一個「由某個 Broker 暫時擔任的角色」,而是存在於一組專責的 Controller 節點之中。這些節點共同維護一條 metadata log,並透過 Raft 協議選出 leader controller。當 leader controller 掛掉時,其餘 Controller 節點會立即透過 Raft 重新進行選舉;只要仍有過半節點存活,新的 leader controller 就能被選出,並從已經複寫完成的 metadata log 位置繼續服務。所有在故障發生前已經被 quorum 提交的 metadata 變更,都不會回滾或遺失;而尚未完成提交的操作,則會被自然地丟棄。對 Kafka 而言,Control Plane 的不變性不再依賴「某個外部系統是否仍然存在」,而是由 Kafka 自己的 controller quorum 直接保證。
另一方面,當 Data Plane 中的 Partition leader 發生故障時,處理流程在概念上仍然熟悉,但其背後的 Control Plane 路徑已經完全不同。當某個負責讀寫的 leader replica 所在的 Broker 掛掉,Broker 層會偵測到副本狀態的變化,並向 Controller 回報。Controller 並不立即做出本地決策,而是將「新的 leader 選舉結果」視為一個 metadata 變更事件,寫入 metadata log。這個 leader 變更只有在被 controller quorum 一致提交後,才會正式生效並被所有 Broker 觀察到。換言之,即便是 Data Plane 的角色切換,其「是否成立」的判準,仍然完全取決於 Control Plane 的 consensus。
這裡有一個非常關鍵、但常被忽略的差異:在 KRaft Era 中,Controller leader 的選舉、Topic 與 Partition 的結構變更、Replica 與 ISR 的調整、以及 Partition leader 的轉移,全部都被納入同一條 metadata log 中,使用同一套 quorum-based consensus 規則來處理。Kafka 不再需要在「ZooKeeper 的世界」與「Kafka 自己的世界」之間來回切換心智模型;Control Plane 與 Data Plane 雖然角色不同,但在一致性語義上已經完全對齊。
另一個容易混淆、但必須釐清的重點,是 Kafka 架構中其實同時存在多層 leader / follower 關係,而且這些層級彼此獨立、服務於不同目的。在 ZooKeeper Era 中,ZooKeeper ensemble 本身就有 leader 與 follower,用來維持 Control Plane metadata 的 consensus;Kafka 叢集內則有一個 Controller(由某個 Broker 擔任),負責協調控制決策,但它並不是共識 leader,而是 ZooKeeper 共識結果的執行者。到了 KRaft Era,Controller 本身形成一個 Raft quorum,其中同樣有 leader controller 與 follower controllers,直接承擔 Control Plane 的 consensus 職責。
在 Data Plane 中,Partition 也一直存在 leader 與 follower replica 的概念。Partition leader 負責實際的讀寫請求,follower replicas 則負責資料複寫與容錯,確保資料在 Broker 故障時仍可恢復。這一層的 leader / follower,關注的是資料一致性與可用性,與 Control Plane 的 metadata consensus 是不同維度的問題。
總結來看,不論是 ZooKeeper Era 還是 KRaft Era,Kafka 都不是「只有一個 leader / follower 架構」的系統,而是同時在 Control Plane 與 Data Plane 各自運作著不同層級、不同目的的 leader / follower 與 quorum 機制。KRaft 的關鍵突破,不在於引入 leader 或 quorum 本身,而在於讓 Kafka 從資料到控制,全面使用一致、內聚的 consensus 模型,完成了一次架構層級的自我收斂。
Kafka 4.0 (2024): Next Generation Consumer Rebalance Protocol
Kafka 4.0 也引入了 Next Generation Consumer Rebalance Protocol(KIP-848),大幅改善 rebalance 的效能與可預期性,並消除了過去「stop-the-world」式的重平衡行為。
在討論 Kafka 的 consumer rebalance 之前,必須先釐清一個最基本的問題:rebalance 到底是在平衡什麼?
在 Kafka 的語義中,rebalance 指的並不是資料本身的重新分佈,也不是系統資源的重新調度,而是Partition 與 Consumer 之間的對應關係發生改變。換言之,rebalance 發生的時刻,是當一個 consumer group 內的成員變動,或是 topic 的 partition 結構改變時,Kafka 需要重新決定「哪一個 consumer 負責處理哪一些 partition」。Rebalance 的核心,是維持一個不變的約束條件:在同一個 consumer group 中,每一個 partition 在任一時間點只能由一個 consumer 消費。
這也意味著,rebalance 是一個純粹發生在 Consumer 與 Partition 之間的控制流程。它不涉及資料重寫、不涉及 partition 的移動,更不會影響 Producer 的寫入路徑。Partition 的資料仍然固定存在於 Broker 上,rebalance 改變的,只是「誰來讀」。
也正因為如此,Kafka 並不存在「Producer rebalance」這個概念。Producer 與 Partition 的關係,在設計上是鬆耦合的:Producer 在送出訊息時,可以根據 key、partitioner、或自訂邏輯選擇目標 partition,但這個選擇是一次性的、訊息層級的行為,而不是一個需要在系統中被維持的一致性狀態。Producer 不會「擁有」某個 partition,也不需要在其他 Producer 加入或離開時重新協調責任範圍。即便 Producer 的數量改變,Kafka 也不需要啟動任何形式的 rebalance;唯一可能發生變化的,是訊息被寫入哪個 partition,但那完全屬於 Producer 自身的行為選擇。
因此,rebalance 是 Kafka 為了解決 Consumer 並行處理與容錯而引入的機制,而不是一個通用的系統調度概念。它的存在前提,是「一組 Consumer 需要共同消費一組 Partition,且必須確保 partition 不被重複消費」。在這個基礎之上,才能理解為什麼 consumer rebalance 會成為 Kafka 架構演進中一個如此關鍵的議題。
早期的 Kafka consumer 設計,誕生於雲端運算尚未普及的年代。當時的系統假設是:Consumer 多半是長時間存活的進程,數量相對穩定,rebalance 屬於低頻事件。在這樣的前提下,將 partition 分配的計算邏輯放在 client 端是一個合理的選擇。Kafka 讓 consumer group 中的某一個 consumer 擔任 leader,負責計算 partition 與 consumer 之間的對應關係,其餘 consumer 則被動接受結果。這種設計降低了 server 端的複雜度,也符合當時分散式系統偏好「client 智慧化」的設計風格。
然而,雲端環境的成熟徹底改變了這些假設。在容器化與自動擴展成為常態後,consumer 的生命週期變得短暫且高度動態。Consumer 可能因為 autoscaling、rolling update、或基礎設施調度而頻繁啟停;consumer 數量不再穩定,而是隨著負載持續波動;不同語言與不同版本的 client 同時存在於同一個 consumer group 中。在這樣的環境下,rebalance 不再是偶發事件,而是系統日常運作的一部分。
舊有的 consumer rebalance 模型,在這種高 churn 環境中逐漸顯露限制。每一次 consumer 的進出,都可能觸發一次 stop-the-world 式的 rebalance,迫使整個 consumer group 暫停處理資料;由 client 擔任的 leader consumer 成為隱性的協調瓶頸;複雜的分區分配邏輯分散在各種 client 實作中,使得行為難以一致,除錯與觀察成本居高不下。這些問題不只是效能上的摩擦,而是系統可預期性與可運維性的根本挑戰。
Kafka 4.0 所引入的 Next Generation Consumer Rebalance Protocol,正是對這種雲端現實的回應。新的設計承認 consumer 的 churn 是常態,而非異常,並據此重新分配系統責任。Partition 與 consumer 的對應關係,不再由 client 端計算,而是交由 server 端的 group coordinator 負責,讓協調邏輯集中化、可觀察。Rebalance 流程也從過去的全域同步事件,轉為增量式調整,避免因小規模變動而讓整個 consumer group 停擺。Consumer 仍然維持 pull model,自行向 Broker 拉取資料,但不再承擔跨成員協調的責任。
從 client 邏輯轉向 server 邏輯的這個轉變,並不是否定早期設計,而是反映了底層運算環境的改變。在雲端時代,consumer 被視為可隨時替換的運算資源,而不是系統中的穩定中樞;而協調與一致性,則必須由平台本身承擔。
Kafka 4.0 (2024): Other Updates
Kafka 4.0 同時也代表了一次全面的技術現代化。Consumer 正式升級至 Java 11,Broker 升級至 Java 17,並完成從 Log4j 到 Log4j2 的遷移;此外,這個版本也正式移除了對 Scala 2.12 的支援,進一步收斂語言與執行環境的複雜度,為後續的維護與演進鋪平道路。
另一項值得關注的變化,是 Queues for Kafka(KIP-932)首次以 early access 形式登場,透過引入 share group 的概念,讓 Kafka 開始能夠覆蓋更多傳統 message queue 的使用場景。
Kafka 4.2: Queues for Kafka GA
在討論 Kafka 4.2 所引入的 Share Groups(Queues for Kafka)之前,必須先釐清一個關鍵問題:全域順序究竟是不是 message queue 與 Kafka 之間的核心差異?
在多數工程師的直覺印象中,傳統 message queue(例如 RabbitMQ、ActiveMQ)似乎天然具備全域順序:訊息進入 queue 後排成一列,consumer 依序處理。這樣的描述在單一 queue、單一 consumer 的典型使用場景下確實成立,因此也經常被簡化為「message queue 提供全域順序」。然而,從更嚴格的語義角度來看,message queue 真正保證的是每個 message 的工作分派與完成,而不是全域順序本身。一旦引入多個 consumer、不同的處理時間、或 retry 與 requeue 機制,原本看似存在的全域順序便可能被打破。換言之,在 message queue 中,全域順序多半是常見使用方式下的結果,而非系統不可破壞的一級設計契約。
回到 Apache Kafka,Kafka 從設計之初就明確沒有承諾全域順序,而是只在 partition 內提供嚴格的區域順序。不過,這並不代表 Kafka 永遠無法呈現全域順序的行為。當一個 topic 只有單一 partition 時,Kafka 所提供的區域順序在實際效果上就等同於全域順序:所有訊息進入同一條 append-only log,由單一 offset 序列管理,consumer 所觀察到的就是一條嚴格排序的事件序列。
這樣的使用方式在實務中確實存在。例如在某些 Kafka Connect 的內部實作中,為了確保狀態更新、配置變更或控制類訊息具有明確且不可歧義的處理順序,便會刻意使用單一 partition 的 topic,藉此獲得全域順序等價的效果。這也再次凸顯 Kafka 的一個關鍵設計理念 — — Kafka 並非「不能有全域順序」,而是將是否需要全域順序的選擇權,明確地交回給使用者。
一旦系統需求成長,topic 被切分為多個 partition,Kafka 就明確選擇犧牲全域順序,以換取水平擴展與平行處理能力。跨 partition 的資料從設計上就不存在任何全域順序,這並不是妥協,而是 Kafka 架構的核心取捨。但與此同時,Kafka 對區域順序的保障依然是嚴格且可推理的:只要相關訊息被路由到同一個 partition,consumer 所看到的事件順序就會與寫入順序一致。這種區域順序是一級語義,構成了 Kafka offset 模型、重播能力與事件處理語義的基礎。
Kafka 4.2 所引入的 Share Groups,則是在這條演進路線上再往前一步。透過 Share Groups,Kafka 允許多個 consumer 協作式地消費 topic,而不再將 consumer 綁定到特定 partition。在這個消費模式下,Kafka 不再嘗試維持任何形式的區域順序;每一筆訊息都被視為獨立的工作項目(work item),只要求最終能被處理完成即可。這樣的語義,正好對應了傳統 message queue 中常見的工作負載,例如背景任務、非同步處理或高度彈性的 worker pool,而不再受限於 partition 的數量。
實作上來看,Share Groups 並不是在 Kafka 中引入一個全新的 queue 系統。Kafka 的底層依然是 topic、partition 與 append-only log,資料仍然是持久化、可重播,並且可以被不同的 consumer group 以不同方式觀察。Share Groups 所做的,是在既有 Kafka 架構之上,提供一種刻意放棄區域順序的消費語義,讓使用者可以在事件串流與工作分派之間自由選擇,而不必離開 Kafka 的整體模型與生態系。
從 single partition 到 multi-partition,再到 Share Groups,Kafka 的演進並不是在不同系統模型之間來回擺盪,而是逐步把「順序邊界」攤開來,讓使用者清楚決定:何時需要全域順序,何時只需要區域順序,以及何時可以完全不需要順序。這正是 Kafka 4.2「Queues for Kafka」的真諦。
Unparalleled Contributions from opensource4you
在 Kafka 4.2 邁向「Queues for Kafka」正式 GA 的過程中,除了核心設計方向的成熟,更值得一提的是開源社群在這段時間所投入的巨大工程能量。其中,opensource4you 在近兩年間,已成為 Apache Kafka 專案中最活躍、也最具影響力的貢獻團隊之一。
在短短兩年內,opensource4you 向 Apache Kafka 專案貢獻了數千筆 commits,實際推動了多項關鍵且結構性的演進,包含但不限於:
- 新一代 Consumer 架構的演進
針對 Kafka 在高併發、多租戶場景下的消費模型限制,持續推進更具彈性與可擴展性的 consumer 設計,為 Share Groups 等新能力奠定基礎。 - 日誌系統現代化(Log4j → Log4j2)與全面重構
不僅完成框架升級,更同步進行大規模重構,改善效能、可觀測性與維護性,降低長期技術負債。 - 大規模 Scala → Java 的系統性改寫
這並非單純語言轉換,而是為了提升可讀性、降低貢獻門檻,並讓 Kafka 核心程式碼更容易被更廣泛的工程師社群理解與維護。 - Quorum Controller APIs 的補齊與完善
持續強化 KRaft 架構下的控制平面能力,使 Kafka 能逐步擺脫對 ZooKeeper 的結構性依賴。
除了程式碼貢獻本身,opensource4you 在 code review 與社群治理 上的投入同樣關鍵。其創辦人長期扮演 Kafka 社群中最核心的 reviewer 角色之一,累積的審查數量在整個專案中名列前茅,甚至超過多家主要商業公司團隊的總和。這種高密度、長時間的 review 參與,對於維持 Kafka 在快速演進下仍能保持工程品質,具有不可替代的價值。
值得注意的是,在 Kafka 仍需同時維護 legacy ZooKeeper 架構與新一代 KRaft 架構的過渡階段,opensource4you 也是目前承擔 ZooKeeper 相關維護工作的主要團隊之一,確保既有大型叢集能在演進過程中保持穩定運作。
總結來看,opensource4you 是實質參與並推動 Kafka 架構轉型的重要力量之一。Kafka 今日能以相對穩健的姿態,從事件串流平台邁向更通用的資料基礎設施,背後正是這類長期、低調、但高度工程密集的社群投入所累積的成果。
Notable Features from Confluent Platform
值得注意的是,有些功能是 Confluent Platform(Kafka 的商業版本)獨有的,並未包含在開源 Apache Kafka 中。
Schema Registry
Schema Registry 是 Confluent 提供的重要組件(雖然是開源的,但不是 Apache Kafka 的一部分),用於管理 Avro、Protobuf、JSON Schema 等格式的 schema,提供 schema evolution 和相容性檢查,確保 Producer 和 Consumer 使用一致的 data schema。
Kafka and Flink
即便 Kafka 有了 Streams API,當遇到更複雜、更大規模的串流處理需求,業界普遍認為 Flink 是更成熟的選擇。這些串流處理需求包括:
- 需要複雜的 window 操作:像是要計算「過去 24 小時內每 5 分鐘的平均交易金額,但每分鐘更新一次」這種滑動視窗(sliding window)加上跳躍更新的場景
- 多層次的狀態管理:例如同時追蹤每個用戶的短期行為(最近 1 小時)、中期趨勢(最近 7 天)、以及長期偏好(最近 90 天),並且這些狀態之間還需要互相關聯
- 需要處理 TB 級的即時數據:像是大型電商在雙 11 期間,每秒需要處理數百萬筆交易事件,並即時計算全站商品排行、庫存預警、以及個人化推薦
Confluent Platform 的一大優勢是能在單一平台上整合 Kafka 和 Flink。透過 2023 年所收購 Immerok,Confluent 將 Flink 深度整合到其雲端服務中,讓使用者可以:用 Kafka 處理 event streaming 和消息傳遞、用 Flink 進行複雜的串流處理和分析、在同一個管理介面中操作兩者、無縫地在 Kafka topics 和 Flink jobs 之間傳遞數據。
值得一提的是,Apache Flink 從誕生之初(2015 年正式發布)就將 exactly-once 語意作為核心設計目標,透過分散式快照(distributed snapshot)機制實現了狀態的精確一次處理;而 Kafka 在 2017 年推出 exactly-once 支援。當我們將視野放得更大之後,一個有趣的問題浮現了:兩個都支援 exactly-once 的系統該如何搭配使用?
讓我們看一個典型的場景:
Kafka Topic A → Flink 處理 → Kafka Topic B
這種架構模式在實務中非常常見。舉例來說,原始的用戶行為日誌從 Topic A 進來,Flink 進行數據清洗(去除異常值、填補缺失欄位)和 enrichment(關聯資料庫補充用戶屬性),然後將處理好的數據寫回 Topic B。清洗後的數據可能被多個下游系統使用:推薦系統、報表系統、機器學習訓練系統都訂閱 Topic B。又或者在事件驅動架構中,Topic A 是原始業務事件(如「訂單創建」),Flink 進行複雜的事件關聯(如檢測「5 分鐘內同一用戶創建了 10 筆小額訂單」這種異常模式),識別出的異常事件寫入 Topic B,觸發風控服務、告警系統等下游動作。
為什麼要寫回 Kafka,而不是直接寫入最終目的地?因為 Flink 處理結果往往需要被多個系統消費,Kafka 作為中介提供了解耦、可重放、背壓處理等優勢。這種模式本質上是「流式 ETL」 — — 一切都在即時發生。
在這個資料流中,其實有兩層 exactly-once 保證在運作:
第一層是 Flink 內部的 exactly-once:透過 checkpoint 機制,Flink 保證即使 job 失敗重啟,內部狀態(如聚合計算、窗口結果)也能恢復到一致的狀態,不會重複計算。
第二層是 Flink 寫入 Kafka 的 exactly-once:Flink 使用 Kafka 的 Transactional Producer API,將處理結果的寫入和 Kafka offset 的提交包裝在同一個事務中。要麼全部成功(處理完成 + offset 提交 + 寫入 Topic B),要麼全部失敗回滾。
這兩層保證是互補而非重複的。Kafka 的 exactly-once 確保消息在 Kafka 生態內不重複寫入;Flink 的 exactly-once 確保處理邏輯和狀態管理不會因為失敗而重複執行。只有同時配置兩邊的 exactly-once,才能達到真正的端到端保證。
配置方式也很直觀。在 Flink 端,啟用 checkpoint 並設定 exactly-once 模式;在 Kafka Sink 端,設定 DeliveryGuarantee.EXACTLY_ONCE 並指定 transactional ID prefix。這樣的組合讓 Flink + Kafka 成為目前業界處理關鍵業務串流資料最可靠的架構之一。
當然,exactly-once 並非沒有代價 — — Kafka 事務會增加幾毫秒到幾十毫秒的寫入延遲,Flink checkpoint 也會增加狀態儲存成本。但對於金融交易、訂單處理等不容許資料重複的場景,這個代價是完全值得的。而對於日誌收集、監控指標等場景,你仍然可以選擇 at-least-once 來換取更高的吞吐量。正如 Kafka 一貫的哲學:選擇權在用戶手中。
Notable Features from WarpStream
2024 年 9 月,Confluent 收購了 WarpStream,一家成立僅 13 個月的新創公司。這次收購補足了 Confluent 產品線的最後一塊拼圖 — — BYOC(Bring Your Own Cloud)部署模式。
WarpStream 的核心創新在於徹底重新思考 Kafka 在雲端的架構設計。傳統 Kafka 是為本地磁碟設計的,即使搬到雲上,仍然需要管理大量的 EBS volume 和處理跨可用區的網路成本。WarpStream 的創辦人 Richard Artoul 和 Ryan Worl 意識到,既然資料庫層可以透過分離計算和儲存來降低運維負擔,資料串流層也應該可以做到同樣的事。
WarpStream 從零開始設計,直接將資料寫入物件儲存(如 S3),而不是依賴本地磁碟。這種架構帶來了幾個關鍵優勢:
首先是成本優化。物件儲存比 block storage 便宜得多,而且不需要為跨可用區的資料複製付出高額的網路傳輸費用。對於大規模的日誌記錄、可觀測性資料、或是填充 data lake 這類場景,成本節省相當可觀。
其次是部署靈活性。WarpStream 採用 BYOC 模式,資料和計算都在客戶自己的雲端帳號中,滿足了那些因為法規或合約限制而無法使用完全託管服務的組織。同時,透過 Confluent 的管理平面,又能享受到類似託管服務的便利性 — — 這是一種「共同責任框架」,客戶和供應商共同負責系統的運作和健康狀態。
第三是運維簡化。傳統 Kafka 需要精心調校磁碟、記憶體、網路等各種參數,還要處理 rebalancing、leader election 等複雜的運維任務。WarpStream 的雲原生設計大幅降低了這些負擔。
WarpStream 也保留了 Kafka 生態系統的相容性,支援標準的 Kafka protocol,並提供一些獨特功能,像是 offset-preserving replication(類似 Confluent Cluster Linking)、Cluster Quotas(用於多租戶環境的配額管理)、以及即將推出的 Kafka transactions 支援。
值得一提的是,WarpStream 在被收購後並沒有停止發展,反而加速了產品路線圖。他們推出了 Orbit 產品,能夠完美鏡像任何 Kafka 相容的來源,實現災難恢復、本地快取、分層儲存等功能。
Jay Kreps 在宣布收購時表示,WarpStream 展示了「BYOC 的正確做法」。透過這次收購,Confluent 現在擁有完整的產品組合:想要完全託管?用 Confluent Cloud。想要自己管理?用 Confluent Platform。想要在自己雲端帳號中運行但又不想處理所有運維細節?那就用 WarpStream。
這也反映了一個更大的趨勢:資料基礎設施正在全面擁抱物件儲存。從 Snowflake 到 Databricks,從 ClickHouse 到現在的 Kafka,幾乎所有現代資料平台都在朝這個方向演進。物件儲存不再只是冷資料的歸檔地,而是成為了高性能、即時資料處理的基礎。
Notable Acquisition: When Confluent Joined IBM
IBM 和 Confluent 在 2025 年 12 月 8 日宣布,IBM 將以每股 31 美元、總價值 110 億美元收購 Confluent,交易預計在 2026 年中完成。
事實上,Confluent 的總可達市場(TAM)在過去四年從 500 億美元翻倍成長到 2025 年的 1000 億美元,市場本身確實在擴張。真正的問題是 Confluent 能獲得的市場份額比預期少。市場很大,但 Confluent 作為獨立上市公司卻面臨成長瓶頸,難以將市場規模轉化為自己的營收。
這背後有幾個關鍵因素。首先是市場現實:Kafka 已經成熟為一種商品(commodity)。任何人都可以使用開源版本的 Kafka,AWS 提供 MSK、Azure 有 Event Hubs、Google Cloud 也有自己的託管 Kafka 服務。這些雲端廠商不僅提供了便利的託管服務,還能與各自的雲端生態系統深度整合,直接搶走了 Confluent 的潛在客戶。Confluent 雖然是 Kafka 的原創者,卻在商業化上面臨激烈競爭。
其次是 IBM 包裝這次收購的 AI 敘事。IBM 強調這次收購是為了打造「為 AI 而生的智能數據平台」,聲稱 AI agents 需要即時數據流。但這更像是一個行銷包裝 — — Kafka 本身的價值主張並沒有改變,只是現在被重新定位在 AI 浪潮之下。即時數據處理一直是 Kafka 的核心能力,無論有沒有 AI。
從更宏觀的角度看,這次收購是 IBM 多年策略佈局的一部分。透過收購 Red Hat(2019 年,340 億美元)、HashiCorp(2024 年,65 億美元)、以及現在的 Confluent(2025 年,110 億美元),IBM 創造了一個完整的企業基礎設施「三位一體」:作業系統層(Red Hat OpenShift)、基礎設施佈建層(HashiCorp)、以及數據傳輸層(Confluent)。這讓 IBM 能夠提供從底層到數據流的完整解決方案,在與 AWS、Azure、Google Cloud 等雲端巨頭的競爭中找到差異化定位。
Confluent 的 CEO 兼 Kafka 共同創造者 Jay Kreps 在官方部落格中表示,成為 IBM 的一部分不會改變 Confluent 的使命,反而會放大它。這是標準的收購後公關話術,但字裡行間也透露出一些無奈 — — 如果 Confluent 有足夠的成長空間和競爭優勢,或許不會選擇被收購。
這是一個有點悲傷但也很真實的科技業故事。即使你創造了改變世界的開源技術,建立了一個市值數十億美元的公司,最終還是可能因為市場動態、競爭壓力、或商業模式的侷限而被更大的企業收購。Kafka 改變了整個數據處理產業,但 Confluent 能從中獲得的商業回報,卻遠不如當初想像的那麼豐厚。開源的理想與商業的現實,在這個案例中形成了鮮明的對比。
Kafka wire protocol: The De Facto Standard
Kafka 定義了事件串流的標準。隨著 Kafka 在網路公司中的廣泛採用,Kafka wire protocol 逐漸成為事件串流領域的事實標準(de facto standard)。它包含兩個緊密關聯的層級:
1. 通信協議層:Kafka 定義了一套完整的二進位協議,規範了 client 與 broker 之間的通信方式。這套協議涵蓋了 produce request、fetch request、metadata request 等各種操作,採用請求-回應(request-response)模式,並經過精心設計以支援高效能的批次處理和零拷貝傳輸。
2. 訊息格式層:Kafka 定義了訊息的儲存與傳輸格式,包括如何組織 batch、如何編碼 offset、如何處理壓縮等。巧妙的是,Kafka 讓磁碟儲存格式與線路傳輸格式保持一致,這帶來了零拷貝傳輸的效能優勢。這套格式經過了 LinkedIn、Netflix、Uber 等公司在極端規模下的實戰驗證。
值得注意的是,Kafka 並未使用 gRPC、Protocol Buffers 或 Thrift 等現成的 RPC 框架,而是基於 TCP 自行設計了這套二進位協議。這個決策讓 Kafka 能夠精確控制通信格式與儲存格式之間的映射關係,實現最佳效能。
至於 Producer API、Consumer API 和 Streams API,這些其實只是 Kafka 官方提供的 Java/Scala 實作。它們展示了如何使用底層 wire protocol 來實現事件串流的程式設計模型,但並非標準本身。其他語言的開發者可以基於相同的 Kafka wire protocol,實作出符合各自語言慣例的 client library — — 事實上,社群已經為 Python、Go、C/C++、.NET 等語言提供了各種實作,它們遵循相同的底層協議,但 API 設計各有不同。
當 Redpanda 或 Pulsar(透過 KoP)宣稱「Kafka API 兼容」時,它們指的就是實作了這套完整的 Kafka wire protocol。這讓任何為 Kafka 開發的 client(無論用什麼語言)都能與它們通信,而無需修改程式碼。
這種分層的標準化設計,讓 Kafka 既保持了協議層的統一性,又給予了各語言生態系統足夠的靈活性。
Summary
像 Scribe 這樣的日誌收集系統,專注於極致的資料攝取與傳輸效率,卻刻意不介入下游如何消費與處理資料。這種設計在當時非常合理,也極其成功。即使到了今天,Meta 仍然持續演進其內部的 Scribe 系統,支撐超過 15 TB/s 的資料攝取量。然而,2008 年開源的 Scribe 專案早已停止維護;相對地,Apache Kafka 則在開源世界中接下了這條路線,並將它推向更遠的地方。
傳統 message queue 關注的是「每一筆訊息都必須被完整處理」,這些訊息往往直接對應業務交易,因此需要嚴格的確認與回覆機制。但這樣的設計,通常以犧牲吞吐量與可擴展性為代價。Kafka 從一開始就選擇了不同的取捨:讓 Consumer 主動拉取資料並自行維護進度,將上游資料傳輸與下游業務處理解耦;充分利用 Linux 檔案系統與磁碟順序 I/O 的特性;再透過 replication 與可調整的 acks 設定,讓使用者能在效能與可靠性之間自行決定平衡點。這些選擇,使 Kafka 在本質上成為一個可與 Scribe 並列的大規模 Log Collector。
隨著 Kafka Streams 與 ksqlDB 的加入,Kafka 開始具備基本的串流運算能力;而當 Apache Flink、Apache Storm、Spark Streaming 等成熟的 Stream Compute Engine 成為其下游核心,Kafka 也正式完成了從「Log Collector」到「Event Streaming Platform」的角色轉換。
耐人尋味的,不只是功能的堆疊。
在 KRaft 中,Kafka 將 Control Plane 的 consensus 從外部的 ZooKeeper 收回到系統內部,讓 metadata 與 data 都遵循同一套 log-based、quorum-driven 的模型;在 新的 consumer rebalance protocol 中,Kafka 將 group control 的決策權,從 client 端的 leader consumer 收斂回 server 端的 group coordinator;而在 Share Groups / Queues for Kafka 中,Kafka 進一步把「如何消費訊息」這件事,從過去隱含在 consumer 假設中的語義,提升為平台本身可以明確提供的能力。
這三條演進線索,其實彼此高度呼應:Kafka 不斷把關鍵決策,從分散、不穩定、難以觀測的邊緣角色,收斂到系統核心;不斷將原本依賴使用者默契與慣例的行為,轉化為平台可被明確建模與保證的能力。
回頭看,Kafka 並不是突然「加上 Queue 功能」,也不是偶然「移除 ZooKeeper」。它只是一步一步,補回自己早年為了極致吞吐量而刻意放下的東西。當 Share Groups 正式落地,Kafka 將同時支援事件串流與工作分派這兩種原本分屬不同系統的負載模型。
一個「Unified Queuing and Streaming Platform」,逐漸浮現。
(To be continued …)
Appendix
- Kafka: a Distributed Messaging System for Log Processing: https://notes.stephenholiday.com/Kafka.pdf
- Persistent data structures: https://www.geeksforgeeks.org/dsa/persistent-data-structures/
- Introducing Warehouses: Compute-compute separation in ClickHouse Cloud: https://clickhouse.com/blog/introducing-warehouses-compute-compute-separation-in-clickhouse-cloud
- Delta vs. Lambda: Why Simplicity Trumps Complexity for Data Pipelines:
https://www.databricks.com/blog/2020/11/20/delta-vs-lambda-why-simplicity-trumps-complexity-for-data-pipelines.html - Lambda Architecture: https://www.databricks.com/glossary/lambda-architecture
- Questioning the Lambda Architecture: https://www.oreilly.com/radar/questioning-the-lambda-architecture/
- The Case for Shared Nothing: https://dsf.berkeley.edu/papers/hpts85-nothing.pdf
- Semantics: https://www.cs.unc.edu/~dewan/242/f97/notes/ipc/node27.html
- Realtime Data Processing at Facebook: https://scontent-sjc3-1.xx.fbcdn.net/v/t39.8562-6/240826476_579340586413244_313948440943029532_n.pdf?_nc_cat=100&ccb=1-7&_nc_sid=e280be&_nc_ohc=spm591wIEm0Q7kNvwHZclZb&_nc_oc=AdmMo0iPlLPY6gIxP35MTmG7yEOieGh50iGTQueUiAwxPpQHLrdvMdP2WvUtoMe_oOo&_nc_zt=14&_nc_ht=scontent-sjc3-1.xx&_nc_gid=fIbwYC6pUAcV3yYENOjLzQ&oh=00_AfrXC8XfYLXNUixlvm6oxULXtK2FY6M_ZVSls-GzWZgelg&oe=6969AEBE
- Facebook Open Sources LogDevice — a Distributed Data Store for Log Storage: https://www.infoq.com/news/2018/09/logdevice-distributed-logstorage/
- Scribe: Transporting petabytes per hour via a distributed, buffered queueing system: https://engineering.fb.com/2019/10/07/core-infra/scribe/
- Scribe、chukwa、kafka、flume日志系统对比: https://cloud.tencent.com/developer/article/1084773
- Open Source Log system comparison: Scribe, Chukwa, Kafka, flume__ message log system Kafka/flume, etc: https://topic.alibabacloud.com/a/open-source-log-system-comparison-scribe-chukwa-kafka-flume__-message-log-system-kafkaflume-etc_8_8_20290153.html
- [極客時間] 大数据经典论文解读 27 | Kafka(一):消息队列的新标准
- [極客時間] 大数据经典论文解读 28 | Kafka(二):从Lambda到Kappa,流批一体计算的起源
- Kafka Version History: A Comprehensive Guide: https://www.codestudy.net/blog/kafka-version-history/
- Keeping Up With the Kafka Lifecycle: https://www.openlogic.com/blog/keeping-up-with-kafka-lifecycle
- Introducing Apache Kafka® 4.0: https://www.confluent.io/blog/latest-apache-kafka-release/
- [WarpStream Blog] Hacking the Kafka PRoTocOL: https://www.warpstream.com/blog/hacking-the-kafka-protocol
- Diskless Kafka on top of PostgreSQL, S3 or SQLite: https://tansu.io/
- [Ververica] A World Without Kafka: https://www.ververica.com/blog/a-world-without-kafka
- When Tables Became the Language of Time: https://ipolyzos.substack.com/p/when-tables-became-the-language-of
- Why “Streaming vs Batch” Is the Wrong Question in Modern Data Architectures: https://ipolyzos.substack.com/p/the-streaming-vs-batch-debate-is?r=anqog&utm_campaign=post&utm_medium=web&triedRedirect=true
- The Past, Present and Future of Stream Processing: https://www.kai-waehner.de/blog/2024/03/20/the-past-present-and-future-of-stream-processing/
Notes: Data Plane vs Control Plane, Consistency vs Consensus
關於 Data Plane, Control Plane, Consensus 的論述也極為有趣,但我目前想不到合適的擺放而不搶走 Kafka 的丰采,故在此補上。
Paxos:以提案為中心的最小共識單元
Paxos 是由 Leslie Lamport 在 1990 年代提出的經典共識演算法,目標是在不可靠的分散式環境中,讓多個節點對「某一個值」達成一致。Paxos 的設計非常純粹:它不關心資料內容本身,只關心是否能對某個提案(proposal)在多數節點間取得同意。
在架構層次上,Paxos 並沒有明確區分 Data Plane 與 Control Plane。它更像是一個原子化的一致性原語(consensus primitive),可被用來保護任何需要共識的狀態。換言之,Paxos 本身並不是一個系統架構,而是一個可被嵌入、反覆使用的理論基礎。
也正因如此,原始 Paxos 每一次決議都需要完整跑一輪協議,在實務上並不適合用於高頻率、連續的 metadata 更新。它更多扮演的是理論起點的角色,而非直接部署於大型系統中的解法。
Multi-Paxos:將共識延伸為持續運作的 Control Plane
Multi-Paxos 是 Paxos 在工程實務中的自然延伸,用來處理「連續多個值」的共識問題。它的關鍵假設是:在多次決策過程中,leader 通常是穩定的,因此可以在建立 leader 之後,省略後續提案中重複的協商步驟,將 Paxos 攤平成一條持續成長的 log。
這使 Multi-Paxos 非常適合作為 Control Plane 的共識核心,用來序列化 metadata 變更,例如配置更新、節點成員調整、partition 重新分配等。此時,Data Plane 仍然是各節點各自處理請求;真正需要 consensus 的,是對「系統狀態變更順序」達成一致。
在實際系統中,幾乎所有「宣稱使用 Paxos」的平台,實際上運行的都是 Multi-Paxos 或其變形。以 Amazon DynamoDB 為代表的多個 AWS 內部系統,正是利用 Paxos / Multi-Paxos 家族來保護關鍵 metadata(例如 partition ownership、membership 與 reconfiguration)。因此,在討論工程實作時,直接以 Multi-Paxos 作為代表,反而更貼近真實運作情況。
Zab(2008):以樹狀結構管理 Control Plane metadata
Zab(ZooKeeper Atomic Broadcast)是 ZooKeeper 所使用的共識協議,設計目標非常明確:維護一棵一致的 metadata tree。Zab 採用 leader / follower 架構,確保所有寫入操作都以相同順序被廣播並套用到每個節點。
在 Zab 的世界中,Control Plane 的 metadata 被建模為一個階層式結構(znode tree),並支援 ephemeral znode,用來表示「與 client session 綁定的暫態狀態」。ZooKeeper 本身不承載 Data Plane 的實際資料,而是作為純粹的 Control Plane 系統,為其他分散式系統提供一致性服務。
Apache Kafka(ZooKeeper Era)、HBase、Hadoop 等系統,都是將 Control Plane 的 consensus 外包給 Zab / ZooKeeper 的典型代表。
Spanner(2012):以時間為核心的 metadata 排序
Google Spanner 採用的是基於 Multi-Paxos 的複合設計,但它的真正創新,在於引入 TrueTime,將時間納入一致性模型之中。
在 Spanner 中,Data Plane 的資料(table rows)與 Control Plane 的 metadata(schema、replica placement、transaction state)都被統一放在同一套共識架構下,但透過時間戳與版本化機制來管理順序。這使得 metadata 不只是「被排序」,而是被放進一條可驗證的時間軸中。
Spanner 是第一批清楚展示「metadata 本身也需要版本與時間語義」的系統,對後來分散式資料庫與交易系統的設計影響深遠。
Raft(2014):將 consensus 變成可理解的系統元件
Raft 的誕生,很大程度上是為了降低 Multi-Paxos 類協議在理解與實作上的門檻。它明確定義了 leader、log replication 與 state machine 的角色分工,使共識機制不再只是理論模型,而是可以直接構成一個系統的 Control Plane。
在 Raft 架構中,Control Plane 的 metadata 被建模為 append-only log,所有狀態變更都必須先寫入 log,經過 quorum 複寫後,才能套用到狀態機;Data Plane 則依賴這些已提交狀態來運作。
etcd、Consul,以及 Kafka 在 KRaft Era 中的 Controller quorum,都是 Raft 作為 Control Plane 共識核心的代表案例。
Accord(2021):為交易而生的 leaderless consensus
Accord 是 Cassandra 為支援跨 partition ACID 交易所提出的新一代共識協議。與前述以 leader 與 log 為中心的協議不同,Accord 刻意採用 leaderless 的設計,直接以交易為單位進行協調。
Accord 的核心任務不是維護全域系統 metadata,而是對「交易之間的依賴關係與提交順序」達成一致。因此,它處理的是位於 Data Plane 之上的交易協調狀態,而非傳統意義上的 Control Plane。
在這樣的模型中,共識不再圍繞「下一個值是什麼」,而是圍繞「哪些 transaction 可以安全地一起提交」。
Summary of Consensus
回顧從 Paxos、Zab、Spanner、Raft 到 Accord,可以發現分散式系統並不是在追求「某一種正確的共識演算法」,而是在不斷重新界定哪些狀態需要被一致地決定、以及這些狀態屬於系統的哪一個層次。
有些系統從誕生之初,就清楚劃分了 Control Plane 與 Data Plane。以 Spanner 為例,系統一開始便假設:資料的讀寫、交易執行屬於 Data Plane,而 schema、replica placement、transaction coordination 等關鍵狀態,必須由一個具備強一致性的 Control Plane 來統一管理。TrueTime 與 Paxos 的結合,正是為了讓這條控制路徑能夠穩定、可推理地存在。
也有一些系統是在演進過程中,才逐步將 Control Plane「收回」到自身之內。Kafka 正是最具代表性的例子。在 ZooKeeper Era,Kafka 將 Control Plane 的 consensus 外包給 ZooKeeper;而進入 KRaft Era 後,Kafka 選擇自行承擔 metadata 的一致性責任,讓資料與控制都落在同一個 log-based 的架構之中。這個轉變,並不是引入了新的需求,而是讓原本分散在外部的共識邏輯,重新成為系統的內在能力。
對於這類明確區分 Control Plane 與 Data Plane 的系統而言,只要是在 Control Plane 上,為了維護強一致的 metadata 而採用的協議,不論其具體形式是 Multi-Paxos、Zab、Raft,或 Spanner 所採用的變體,都可以被合理地歸類為 consensus algorithm。它們的共同目的,不在於處理使用者資料本身,而在於確保「系統如何運作」這件事本身是可被一致理解的。
相對地,在 Data Plane 中,討論的焦點則完全不同。這裡關心的是資料讀寫之間的語義關係,使用的語言也隨之改變:transaction consistency 對應的是隔離層級與交易語義,而 data consistency 則通常落在 CAP 的框架下,描述資料如何被「觀察與體驗」。
然而,並非所有系統都一開始就擁有清楚的 Control Plane。Cassandra 代表的是另一條路徑:一個從設計之初便高度去中心化、幾乎只有 Data Plane 的系統。在這樣的架構下,節點之間對等、沒有全域協調者,原本也刻意避免強一致的集中決策。因此,當 Cassandra 試圖引入跨 partition 的 ACID 交易時,便無法簡單地「加上一個 Control Plane」來解決問題。
Accord 正是在這個背景下誕生的。它不是替 Cassandra 補上一個傳統意義的控制層,而是選擇在 Data Plane 內部,直接對交易之間的依賴關係與提交條件進行 leaderless 的共識協調。換言之,Accord 並沒有改變 Cassandra 去中心化的本質,而是重新定義了「在沒有 Control Plane 的世界裡,consensus 能為誰服務」。
從這個角度來看,consensus 並不是 Control Plane 的專利。它真正回答的問題始終只有一個:在分布式系統裡,哪些狀態須被一致地決定,以及為了這個目標,我們願意付出多少代價。







