串流:雙寫的陷阱、CDC,與流表二象性

· tech

#distributed-systems#book-notes#streaming

📑 目錄

批次處理「已經齊了」的資料;串流處理「一直來」的資料。這章很多地基我在別處鋪過了:log 與 offset 在 Kafka 系列、投遞保證在 delivery 那篇、視窗與 event time 在 Spark Streaming——都不重複。DDIA Ch11 真正的獨門,是三個更根本的觀念:為什麼「分頭寫兩份」注定出事、怎麼讓資料庫自己變成事件的源頭(CDC)、以及「流」和「表」其實是同一個東西的兩面。

雙寫的陷阱:同一份資料,分頭寫進三個系統

真實系統裡,同一份資料常要同時存在好幾個地方:DB 是主存放、Redis 是快取、Elasticsearch 是搜尋索引。最直覺的做法是應用程式自己分頭寫三份(dual write)——而這正是最常見的沉默資料事故的源頭:

✗ 雙寫:應用自己寫三份 應用程式 DB 快取 搜尋索引 病一:寫到一半當掉 → 有的寫了有的沒寫 沒有交易能跨三個系統回滾 病二:並行寫抵達順序不同 DB 收到先A後B、快取先B後A → 收斂到不同值 → 三個系統永久分歧,無聲無息 ✓ log 先行:只寫一個地方 應用程式 只寫這裡 一條有順序的 log(source of truth) DB 快取 搜尋索引 全部照「同一順序」消費 → 順序一致、掉了可重放 下游全是 follower,最終收斂到同一狀態 一份資料要進 N 個系統?選一個當 source of truth,其他全部當 follower
雙寫的兩個病無藥可醫:部分失敗(寫完 DB 應用當掉,快取沒跟上——跨系統沒有交易能回滾)與亂序(兩個並行寫抵達三個系統的順序不同,各自收斂到不同值)——三個系統永久分歧,而且無聲無息Log 先行把問題結構性地消滅:只寫一個地方(一條有順序的 log),所有下游照同一順序消費——順序一致、掉了從 offset 重放。這其實就是 複製那章的 leader–follower,推廣到「異質系統之間」:選一個 source of truth,其他全部當 follower

CDC:讓資料庫自己變成事件源頭

「log 先行」聽起來要改寫整個應用——但有個聰明的捷徑:資料庫本來就有一條寫入順序的 log(WAL / binlog,複製 follower 就是靠它同步的)。CDC(change data capture)就是把這條內部的複製 log 接出來、變成人人可訂閱的事件流——Debezium 之類的工具偽裝成一個 replication follower,把每筆變更寫進 Kafka。應用程式一行不用改、照常寫 DB;快取、索引、數倉全部改吃這條流。DB 仍是 source of truth,但它的每一次心跳,全世界都聽得見——這也是現代資料平台把 OLTP 資料餵進分析側的主流姿勢。

流表二象性:表是流的積分,流是表的微分

這章最漂亮的觀念,是流(stream)和表(table)是同一個東西的兩面:

流:一條 changelog ① k1=a ② k2=x ③ k1=b(蓋掉 a) ④ k2=∅(刪除) 每一筆「改變」都是一個事件,依序排好 摺疊到現在(套用每筆) 每次改動發一筆(changelog) 表:當下的狀態 k1 = b (k2 已刪) 同一份資訊,凍結在「現在」 log compaction=只留每個 key 最後一筆(能重建表的最小流)· materialized view=一直在摺疊的表
是「每一次改變」的序列;把它從頭到尾摺疊(依序套用),就得到——當下的狀態。反過來,把表的每次改動發成一筆事件,就還原出流。工程師版的說法:表是流的積分,流是表的微分。一堆你見過的東西是它的化身:log compaction(只留每 key 最後一筆=能重建表的最小流)、materialized view(一張持續在摺疊的表)、複製串流(把 leader 的表變回流、傳給 follower 再摺回表)。狀態機複製其實也是它:log 是流、每台節點的狀態是表

這個等價的實用後果:你可以永遠保留「流」,把「表」當成隨時可拋、隨時可重建的衍生品。 快取壞了?從 log 重摺一次。想加一個新的搜尋索引?從 log 的開頭重放一遍,新 follower 就長出來了。批次那章的「人為容錯」——輸入不可變、錯了重跑——被 log 原封不動帶進了串流世界:只要 log 還在,一切狀態都只是快取。

反思

「誰是 source of truth?」——一題問倒九成的資料架構

雙寫那張圖,是我工作裡見過最多次的事故原型:DB 和快取不一致、ES 索引跟主庫對不上、數倉數字跟線上差一截——追到根,幾乎都是某個地方在分頭寫兩份,而沒有人是誰的 follower。所以我現在看任何資料架構,第一個問題永遠是:這份資料的 source of truth 是誰?其他副本是「照同一條有序 log 跟隨」,還是「各寫各的、祈禱一致」? 是後者,就只是還沒出事。而 CDC 之所以優雅,是它不要求你改寫應用——它把資料庫既有的複製機制,從內部設施升級成公共接口,讓「加一個 follower」從大工程變成訂閱一條流。

「表是流的積分」——第三個看懂一票的等價

這系列我收集到第三個「一個等價、看懂一票」了:共識=一條大家同意的 log批次=不可變輸入的純函數、現在表=流的摺疊。它一下子把散落的東西串起來:Kafka 的 compacted topic 為什麼能當 KTable 的底、materialized view 為什麼叫「物化」(把流凍成表)、Redis 複製為什麼傳的是命令流而不是整份資料、Kafka Streams 的 state store 為什麼敢放本地(反正 changelog 在 Kafka,掉了重摺)。連 Medallion 都能用它重新敘述:Bronze 是流的存檔,Silver/Gold 是不同深度的摺疊。抽象的等價關係,是知識最高的壓縮率。

「只要 log 還在,一切狀態都只是快取」——這句話值一個架構

寫完這篇,我想把這個系列裡最有殺傷力的一句話單獨拎出來:把不可變的 log 當唯一的真相,把所有狀態(快取、索引、報表、甚至資料庫本身)當成可重建的衍生品。 它把「壞了怎麼辦」這個最難的問題,降維成「重放一次 log」;把「想加新視圖」從遷移專案,降維成「起一個新 consumer 從頭讀」。這正是 log vs queue 那條軸的最終回報——當年選了「留著」而不是「拿走」,今天才有資格說「一切皆可重建」。當然它不免費:log 要留多久、schema 要能演化、重放要冪等,全是要繳的稅。但作為架構的預設傾向,我已經完全站在 log 這邊。下一篇是全書終章:把這些拼圖兜成 Kleppmann 對資料系統未來的想像。