《Designing Data-Intensive Applications(DDIA)》Podcast 準備稿:串流處理
書名: 設計資料密集型應用 Designing Data-Intensive Applications(The Big Ideas Behind Reliable, Scalable, and Maintainable Systems) 作者: Martin Kleppmann 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|DDIA.Kafka、CDC 與事件溯源:當資料變成永不停止的河流 (11/12) 涵蓋範圍: 第12章 串流處理(訊息代理 vs 日誌式代理、CDC、Event Sourcing、狀態與不可變性、事件時間 vs 處理時間、串流 join、容錯與 exactly-once)
背景速覽
上一集批次處理的輸入是有界的(會讀完、會結束);這一集進入無界的世界——串流處理。現實中大量資料源源不絕:使用者昨天在產生、今天在產生、明天還會繼續。批次處理只能人為切成時間區段(每天/每小時跑一次),導致延遲可達數小時。串流處理的核心思想是:放棄固定時間切片,事件一發生就立即處理。 這集是 DDIA 後半最精彩的一章——它會把「資料庫」和「訊息系統」這兩個看似無關的東西,揭示為同一枚硬幣的兩面。
一句話重點
串流的世界觀顛覆了我們對資料庫的認知——資料庫儲存的「當前狀態」,其實只是「事件流隨時間累積」的結果(狀態是事件的積分、變更流是狀態的微分);於是「真相是日誌、資料庫只是日誌的快取」,而 Kafka 這類日誌式訊息代理結合了資料庫的持久性與訊息系統的低延遲,加上 CDC 和事件溯源,讓你能用一份不可變的事件日誌,持續餵養搜尋索引、快取、分析系統——甚至從頭重建全新的視圖。
值得討論的重點
1. 兩類訊息代理:暫態的傳統代理 vs 持久的日誌式代理
事件由生產者產生一次、可能被多個消費者處理,歸入同一個主題(topic)。兩個關鍵設計問題:生產快於消費怎麼辦(丟棄/緩衝/背壓)、節點掛了訊息會丟嗎(要不要寫磁碟)。
- 傳統訊息代理(RabbitMQ、ActiveMQ,遵循 JMS/AMQP):心智模型是暫態的——訊息投遞確認後就刪除。消費是破壞性的,無法重新執行、新消費者讀不到歷史。多消費者有兩種模式:負載平衡(每則訊息給一個消費者)、扇出(每則訊息給所有消費者)。重新投遞會導致訊息亂序。
- 日誌式訊息代理(Kafka、Kinesis):結合資料庫的持久儲存與訊息系統的低延遲。生產者追加到日誌末尾、消費者循序讀、日誌可分區提升吞吐、每則訊息有單調遞增的 offset。天然支援扇出(讀不刪除)、消費者進度只需記一個 offset(像資料庫複製的 log sequence number——代理是 leader、消費者是 follower)。
2. 資料庫與串流的深層連結:複製日誌本身就是事件流
訊息代理和資料庫看似兩類工具,但連結比表面深——資料庫的複製日誌,本質就是一串寫入事件。狀態機複製原理告訴我們:每個副本以相同順序處理相同事件,就會達到相同狀態。
異質系統同步的陷阱:同時用 OLTP 資料庫+快取+搜尋索引時,雙重寫入(dual writes) 有嚴重問題——競態條件(兩客戶端並發更新,資料庫最終是 B、索引最終是 A,永久不一致)、部分失敗(一個成功一個失敗)。真正的解法:讓一個系統當唯一的 leader,其他系統作為 follower 從它衍生資料。
3. CDC 與 Event Sourcing:兩種「以事件為中心」的做法
- CDC(變更資料捕獲):觀察資料庫的所有寫入變更、提取成可複製到其他系統的串流。本質是讓資料庫當 leader、其他系統當 follower。實作靠解析複製日誌(Debezium 解析 MySQL binlog、Bottled Water 解析 PostgreSQL WAL)。應用程式甚至不需知道 CDC 存在。
- Event Sourcing(事件溯源):源自 DDD 社群,運作在更高的抽象層——應用邏輯明確建構在不可變事件日誌之上,事件反映使用者意圖(「學生取消選課」)而非底層狀態變化(「刪一筆記錄、加一筆記錄」)。需要完整事件歷史來重建狀態(日誌壓縮不適用,因為事件不互相覆蓋)。命令(command,可被拒絕)驗證通過後才成為事件(event,既定事實、不可拒絕)。
4. 狀態、串流與不可變性:本集最深的洞見
我們以為資料庫存的是「當前狀態」,但狀態只是事件隨時間累積的結果——帳戶餘額是借貸事件的結果、可用座位是預訂事件的結果。用數學類比:應用狀態是事件流對時間的積分,變更流是狀態對時間的微分。
Pat Helland 的名言:「交易日誌記錄了資料庫的所有變更……資料庫的內容只是日誌中最新值的快取。真相是日誌,資料庫是日誌子集的快取。」
不可變事件的優勢:可審計性(像會計帳本,錯誤不擦除而是加一筆沖正)、捕獲更多資訊(加入購物車又移除——從訂單角度抵消,但對分析是寶貴的意圖)、多重視圖(同一份日誌衍生多個讀取最佳化視圖——這就是 CQRS,讀寫分離,讓正規化爭論變得不重要)。
5. 串流處理的三大難題:時間、join、容錯
- 時間推理:事件時間 vs 處理時間。用處理時間分窗會出錯(消費者重啟後處理積壓事件會產生虛假流量尖峰)。「星際大戰電影上映順序不等於故事時間線」的類比很傳神。難題:何時宣告視窗完成(永遠無法確定收齊了沒)、落後者事件(straggler)怎麼辦(忽略 or 發布修正)、該用誰的時鐘(行動裝置離線緩衝→記三個時間戳估算偏移)。四種視窗:滾動、跳躍、滑動、會話。
- 串流 join:stream-stream(視窗 join,如搜尋與點擊關聯)、stream-table(串流增強,用 CDC 保持本地副本同步)、table-table(物化視圖維護,如 Twitter 時間軸——乘積法則
(u*v)' = u'v + uv')。事件順序很重要,可能變非確定性。 - 容錯與 exactly-once:串流永不結束,無法等「完成」才讓輸出可見。微批次(Spark Streaming)、檢查點(Flink)、原子提交、冪等性(即使重試效果不變,例如附上 Kafka offset 判斷是否已套用)。目標是讓輸出看起來像每筆輸入「恰好被處理一次」。
注意事項
⚠️ 「exactly-once」是個容易誤導的詞。 它的真正意思不是「訊息真的只送一次」(網路會重傳),而是「安排計算,使最終效果與沒有故障時相同」——即使實際重試了。而且微批次/檢查點只在框架內部提供 exactly-once,一旦輸出離開框架(寫外部資料庫、發 email),失敗重試仍會導致外部副作用重複。所以真正的 exactly-once 要靠冪等性或端到端去重(第12集的核心)。別讓聽眾以為勾個選項就萬事大吉。
⚠️ Event Sourcing 很美,但不是免費的、也不是萬用的。 它需要保留完整事件歷史(日誌壓縮不適用)、更新率高且資料集小時歷史會膨脹、隱私法規可能要求真正刪除(而非追加「標記刪除」事件——而真正刪除在分散式系統裡出人意料地難,副本散在儲存引擎/檔案系統/SSD/備份各處)。提醒聽眾:Event Sourcing 適合「意圖比狀態更有價值、需要審計」的領域,不是所有系統都該上。
⚠️ 日誌式代理(Kafka)和傳統代理(RabbitMQ)各有所長,別無腦選 Kafka。 Kafka 適合需要保序、可重讀歷史、高吞吐的串流場景;但 JMS/AMQP 式適合「任務佇列風格的非同步 RPC」——訊息處理昂貴、需要逐則平行化、順序不重要時,傳統代理的細粒度負載平衡更合適(Kafka 的粒度是整個分區,有隊頭阻塞問題)。工具選擇要看場景,不是看名氣。
專家補充
💡 「真相是日誌、資料庫是快取」是會改變你世界觀的一句話。 我們習慣把資料庫當成「真相的所在」,但 Event Sourcing 翻轉了這個關係——不可變的事件日誌才是真相,當前狀態只是從中衍生、可隨時重建的快取。這個視角一旦裝進腦袋,你看待整個資料系統的方式都不一樣了:bug 寫壞了狀態?沒關係,修好程式碼、從日誌重放。想要新的讀取視圖?從日誌重建一個。這是 DDIA 後半最具顛覆性的思想,務必經營好這個「啊哈」時刻。
💡 CDC 是「打通資料孤島」的當代利器。 在微服務時代,資料散落在各服務的資料庫裡,怎麼讓搜尋、分析、快取系統都拿到最新資料又不搞出雙重寫入的不一致?CDC(Debezium + Kafka)是現在的主流答案——讓資料庫的 binlog 變成事件流,餵養所有下游。這個「資料庫變更即事件流」的思想,把第3集的儲存引擎、第5集的複製日誌、和這集的串流,全部串成一條線。
💡 積分/微分的類比是 Kleppmann 教學功力的巔峰。 「狀態是事件流的積分、變更流是狀態的微分」——用一個高中數學概念,把「狀態與事件的關係」講得無比清晰。table-table join 用乘積法則 (u*v)' = u'v + uv' 推導 Twitter 時間軸的維護,更是把數學直覺用到了極致。這種「用熟悉的數學照亮陌生的系統概念」的能力,是這本書封神的原因之一。
討論問題
🎙️ 開場鉤子:「你的銀行帳戶餘額是多少?這個『當前狀態』,其實只是你所有存款和提款事件累積起來的結果。如果我把這個想法推到極致——資料庫存的『現在』,根本不是真相,真相是那一串永不改變的事件日誌。今天,我們顛覆你對資料庫的認知。」
🎙️ 自問自答:「同時用資料庫和搜尋索引時,為什麼不能『應用程式同時寫兩邊』就好?」回答:因為兩個客戶端並發寫入時,請求交錯會讓資料庫最終是 B、索引最終是 A,永久不一致——這就是雙重寫入的陷阱。真正的解法是讓一個系統當 leader,其他從它衍生。
🎙️ 帶走的一題:「想一下你系統裡那些被覆寫、被刪除的資料——使用者加入購物車又移除、改了又改的設定。如果你只存『最終狀態』,你正在丟掉大量寶貴的『意圖』資訊。如果改存不可變的事件日誌,你會獲得什麼?」
更大範圍關聯
- 批次與串流的統一:上一集(批次,有界輸入)和這集(串流,無界輸入)共享大量原則(分區、容錯、join)。第12集會講它們最終如何融合——Spark 用微批次在批次引擎上做串流、Flink 在串流引擎上做批次。串流就是「把批次推到無界」,理解了批次,串流只是多了「時間」和「永不結束」兩個維度。
- 不可變性暗線的高潮:第10集的「不可變批次輸入」、第4集的「資料活得比程式碼久」、這集的「不可變事件日誌」——DDIA 後半這條暗線在這裡達到高潮。「不可變性是可靠資料系統的基石」這個信念,將在第12集(資料系統的未來)收束成完整的架構哲學。
- 與系統設計實務:Kafka、CDC、事件驅動架構是當代後端的熱門技能。DDIA 這集給你的是底層原理——你才懂為什麼用 Kafka 而非 RabbitMQ、CDC 怎麼避免雙重寫入、事件溯源解決什麼問題。能講出「真相是日誌」的人,設計事件驅動系統時有完全不同的高度。(見 [軟體工程領域切分藍圖] §5「軟體架構演進」)
錄製建議
- 建議時長:約 26–28 分鐘(內容豐富,僅次於第9集)。配比:兩類訊息代理約 6 分;資料庫與串流的連結+雙重寫入陷阱約 4 分;CDC 與 Event Sourcing 約 6 分;狀態/串流/不可變性(含積分微分類比)約 6 分(核心高潮);三大難題(時間/join/容錯)約 6 分。
- 討論策略:本集的情感與智識高峰是「真相是日誌、資料庫是快取」——一定要用「你的銀行餘額其實是事件累積的結果」這種貼身例子鋪陳,再揭示這個顛覆性結論。積分/微分的類比要慢慢講、講到聽眾「喔——」。三大難題(尤其事件時間 vs 處理時間)用星際大戰上映順序的類比化解抽象感。
- 節奏:這集概念密度高,建議用「訊息代理 → 資料庫即串流 → 不可變性哲學 → 實務難題」這個由淺入深的階梯。前半建立工具認知,中段提升到世界觀(不可變性),後段回到實戰難題。結尾預告終章:「我們走過了資料的儲存、分散、共識、批次與串流——最後一集,Kleppmann 要把這一切縫合起來,談資料系統的未來,以及一個工程師很少談、卻無比重要的問題:我們該如何用這些技術,做正確的事。」