2023 年 Figma 的每日資料管道從凌晨開始跑:對每張資料表做一次完整 dump、複製到 Snowflake、覆寫舊版。一張資料表 1 億 row、改了 100 row,pipeline 還是搬了完整 1 億 row。某天工程師發現整個 daily sync 花了超過 6 小時,分析師早上 9 點打開 dashboard 仍是「前天」的數字——而 Figma 為這條每日複製鏈付了多個專屬 RDS replica、年費以百萬美元計。
Figma 的資料管道升級記——從 6 小時 cron 到分鐘級 CDC 的三年路
這篇是一條資料管道從每日全表 dump 走到分鐘級 CDC 的三年路。技術選型是常見的——PostgreSQL WAL、Kafka、S3 snapshot、Snowflake——但 Figma 把每一步銜接的失敗模式都用一個額外設計補上。關鍵不在組件清單,在 snapshot 與 stream 之間怎麼對齊、incremental merge 怎麼冪等、以及離線怎麼證明資料是對的——這三件事決定了一條 CDC 管道能不能撐到三年後仍可信。
下面這個互動的 scroll-driven explanation,把 Figma 從 2020 年第一條 cron job、到 2023 年 6 小時 dump 的痛點、到 2024 年 CDC 切換、到 2025 年 bootstrap 自動化、到 2026 年 cell-by-cell audit 的五個階段並列。每個階段的左欄是當時的描述、右欄的圖會隨著捲動切到當階段的狀態。讀者可以把它當作這條三年敘事的脊樑。
2020起點:一條 cron job
Figma 的資料倉是 Snowflake,來源是 production PostgreSQL(Amazon RDS)。最初的同步只是一個 cron job——午夜對每張表做 SELECT *、寫進 S3、再 load 進 Snowflake。當時的資料量還小,6 張表、每張幾百萬 row、半小時跑完。
2023痛點:6 小時與百萬美元帳單
三年後,要同步的表從 6 張長到上百張,最大的表破 1 億 row。daily sync 變成 6+ 小時,最大的幾張表要好幾天。為了不讓 dump 壓垮 production,Figma 開了多個專屬 RDS read replica 跑 export——這條 replica 線的年費以百萬美元計。分析師早上看到的仍是「前天」的 dashboard。
30+ 小時端到端延遲;$M 級 replica 帳單。2024切換:CDC + Kafka + 增量 merge
第一階段重建:在 PostgreSQL 上啟用 logical replication,把 WAL 變更事件以 CDC 的形式寫到 Kafka——每張表一個 topic、下游獨立消費。Snowflake 端寫 stored procedure,每隔 N 小時把 Kafka 上累積的 delta 與 production table 做 incremental merge——N 預設 3 小時,per-table 可調。CDC 的 read 成本接近零,因為它讀的是 WAL,不是線上查詢。
3 小時預設刷新;billing 等關鍵表降到 30 分鐘。2025bootstrap:offset 與 snapshot 怎麼對齊
新表第一次接入時,必須先把現有完整內容搬到 Snowflake、然後從某個 CDC offset 開始 apply stream。兩者必須在時間上「重疊」否則會漏資料。Figma 的解法:先在 Kafka 上記下「現在的 offset O_start」,再立刻觸發 RDS snapshot 到 S3。snapshot 完成後可能落在 2 小時之後——但因為 O_start 是 snapshot 開始之前的 offset,stream 從 O_start replay 必然覆蓋 snapshot 過程中發生的所有寫入。重疊段是冪等的,因為每筆 CDC 事件帶 primary key、merge 是 upsert。
offset 在 snapshot 前記下 → 重疊保證無漏。2026audit:cell-by-cell 重做
CDC 管道有個經典失敗模式——某個 stored procedure bug 或 schema 漂移導致 delta 漏掉特定 row,整體 row count 看起來對,但個別 cell 的值靜默偏離。Figma 不靠 row count 抽樣:每週對所有表做一次獨立的 re-bootstrap(snapshot → 匯入到 audit schema),然後跑 cell-by-cell diff。發現偏差時自動 alert 並列出具體偏離的 row。staging 環境跑這套 audit 時抓到過一次至少會造成 20 分鐘全站事故的失敗模式——在 production 觸發之前就攔下。
每週 audit;零事故上線。互動圖表
Figma 的 CDC 管道把 PostgreSQL WAL 直接接上 Kafka,省去 replica 層和全表 dump,同步延遲從小時降到分鐘。
從 cron 到 6 小時
Figma 在 2020 年的資料同步是典型初創公司形狀:production PostgreSQL 跑在 Amazon RDS、有人寫了一個 cron job 在午夜跑、把每張需要分析的表完整 dump 到 S3、再 load 進 Snowflake。當時是 6 張表、每張幾百萬 row,半小時跑完。沒有 Kafka、沒有 CDC,幾百行 Python 加一個 cron entry 就上線。
但這套設計從第一天就有一個結構性問題:dump 不在乎變動量。一張表昨天到今天只改了 100 row,pipeline 仍然會完整搬完整張表。資料量小時這個浪費沒人在意;資料量大時它會把整個系統壓垮。
到了 2023 年,要同步的表從 6 張長到上百張、最大的表破 1 億 row。daily sync 從 30 分鐘膨脹到 6+ 小時,最大的幾張表「要花好幾天才能跑完一輪」——分析師早上 9 點打開 dashboard 時看到的是「前天甚至大前天」的數字。對 production PostgreSQL 做 SELECT * 會把整張表掃過,這對線上服務不可接受,所以 Figma 開了多個專屬 RDS read replica 隔離 export,年費以「百萬美元計」累積(ByteByteGo 原文:「costing Figma millions of dollars a year」)。第三個後果是「不能再上新表」——當每張表的 dump 動輒花一整天 cron 視窗,工程師開始拒絕分析師提的新同步請求。
2023 年中 Figma 決定重做。結論明確:cron dump 的問題不是 tuning 可以解決的,因為「搬全表」這個前提本身就錯。要解的是「只搬變更」——這就是 CDC(Change Data Capture)的形狀:讀 PostgreSQL 的 WAL、把每筆變更事件流出來、下游按 primary key 做增量 upsert。CDC 在 2023 年已經是大型資料平台的事實默認,Figma 沒有選擇 pure off-the-shelf 的路徑——ByteByteGo 報導只描述概念元件、未明說具體 connector,通常意味著內部對既有工具做了相當程度的客製。CDC 的「上手成本」不在 connector,而在 schema 變更怎麼處理、bootstrap 怎麼對齊、audit 怎麼跑——這幾件事每家公司都得自己做一次。
那個夏天的另一個觀察:當資料工程團隊向財務說明「要花一個季度做這個重構」時,手上的數字不是「延遲改善 X 倍」這種抽象指標,而是「關掉這些 RDS replica、年費省下 $M」這種帳面上立刻可見的支出削減。成本驅動的工程投入往往比延遲驅動的更容易過內部審批——因為前者直接對應到財報。
把舊架構與新架構的「形狀」並排看,差異最一目了然。下面這張並排圖左半邊是 2023 年的 cron-dump 架構——多個專屬 RDS replica 攔在 primary 與 Snowflake 之間、橙色的線是「每天一次的全表搬運」;右半邊是 2024 年切換後的 CDC 架構——一條 WAL 串接 Kafka、再以 delta 進 Snowflake,replica 那一整層消失了。拖動中間的把手可以漸進對比兩種拓撲——重點不是元件數量,而是「成本集中的位置」整個被移動了。
左半(2023):成本集中在「replica 那一層」——多份 PostgreSQL 副本 + 全表 dump 的橙色…
2024 年 CDC 切換移除 PostgreSQL replica 層,WAL 直接接 Kafka delta 流——維運成本與延遲同時消失。
CDC 切換與 bootstrap 怎麼對齊
2024 年的重建是分階段的,不是一次切換。第一階段:對所有需要同步的表啟用 PostgreSQL logical replication,這是 PostgreSQL 9.4 之後提供的功能、可以把 WAL 上的變更事件透過 publication / subscription 模型流出去。CDC 讀的是 WAL,不是線上查詢,所以對 primary 的負擔接近零——這個性質讓 Figma 可以把 CDC 直接掛在 primary 上、不再需要專屬 replica 跑 export。
第二階段:每張表配一個 Kafka topic,所有 CDC 事件按 primary key 分區寫入。Kafka 在這裡的角色是「order-preserving、持久化的 delta 緩衝」——它接收 WAL 流、保留可重放的歷史、給下游一個穩定的 offset 概念。為什麼一張表配一個 topic 而不是所有表共用一個 topic:因為 per-table topic 讓下游可以獨立消費、獨立調 commit 頻率、獨立暫停——一張表的 schema 變更不會卡住其他表的 sync。
第三階段:Snowflake 端寫 stored procedure,每隔 N 小時從 Kafka 上對應的 topic 拉取累積的 delta、對 production table 做 incremental merge。預設 N 是 3 小時,per-table 可調——billing 等對新鮮度敏感的表配 30 分鐘、ad-hoc 觸發走 CLI 工具。Merge 的形式是 Snowflake 的 MERGE statement,按 primary key 做 upsert:
MERGE INTO prod.table_X target
USING (
SELECT * FROM stage.table_X_delta
WHERE offset > last_committed_offset
) source
ON target.pk = source.pk
WHEN MATCHED AND source.op = 'DELETE' THEN DELETE
WHEN MATCHED AND source.op = 'UPDATE' THEN UPDATE SET ...
WHEN NOT MATCHED AND source.op = 'INSERT' THEN INSERT ...;
// 每個 cycle commit 後更新 last_committed_offset
這套 incremental merge 的關鍵性質是冪等:同一筆 delta event 被 apply 兩次、結果與 apply 一次一樣。CDC 管道最怕的不是「漏一筆」、而是「不知道有沒有漏」——冪等讓 retry 變成安全的工具,把容錯模型從「複雜的兩階段提交」簡化為「從上一個 checkpoint 重做」。merge cycle 因網路抖動或 Snowflake warehouse 重啟中斷時,下一次 cycle 從 last_committed_offset 重新開始永遠安全。
Snowflake 的 separated compute/storage 模型在這裡有個搭配優勢:warehouse 在不需要時自動 suspend、merge cycle 觸發時 auto-resume——per-table 可調到分鐘級在配置上很彈性,但短週期的 merge 不會讓 warehouse 長時間 idle、帳單不會線性放大。
第四階段也是最棘手的:bootstrap。前面三階段處理「穩態」——CDC 已經在跑、delta 在 Kafka 上、merge 在 Snowflake 上。但是當一張新表第一次接入 CDC 系統時呢?這張表已經有 5000 萬 row 存在於 PostgreSQL,CDC 流只能從「啟用之後」的變更開始——這 5000 萬 row 怎麼進 Snowflake?
Figma 的解法是 Amazon RDS 的 snapshot-to-S3 export 功能。流程是這樣的:
// 1. 先在 CDC stream 上記下 offset
O_start := current_offset(kafka_topic_X)
// 2. 立即觸發 RDS snapshot export 到 S3
trigger_snapshot_export(table_X, S3_bucket)
// snapshot 完成可能要幾十分鐘到幾小時
wait_until(snapshot_complete)
// 3. 把 S3 上的 snapshot 批次匯入 Snowflake
load_into_snowflake(S3_bucket, audit_table_X)
// 4. 從 O_start 開始 apply stream
apply_cdc_stream(kafka_topic_X, from_offset=O_start)
// stream 會 replay snapshot 期間的所有變更——重複 apply 被冪等吸收
這套順序的關鍵:先記 offset、再 snapshot。為什麼?因為 O_start 是 snapshot 開始之前的時間點,snapshot 完成時 RDS 提交的事務必然涵蓋了所有 offset ≤ O_start 的寫入(snapshot 是 transaction-consistent 的,內部使用 RDS 的 MVCC 快照機制)。stream 從 O_start 開始 replay,會覆蓋 snapshot 期間的任何寫入——包括 snapshot 開始到完成之間 RDS 上發生的所有變更。因為 merge 是冪等的,重複 apply 不會造成髒資料。
對比常見的「先做 snapshot、snapshot 完成後再記 offset」的順序:這種順序會在 snapshot 完成的瞬間與「開始記 offset」之間留一個 race window——snapshot 過程中 RDS 上發生的寫入可能既不在 snapshot 內、也不在 stream 內,於是就漏掉了。Figma 用「先 offset、後 snapshot」的順序把這個 race window 在設計階段消掉,不靠人類盯著時間戳對齊。
2024 到 2025 年之間,Figma 把這套 bootstrap 流程做成 self-serve CLI——一條命令、指定表名,背後的 controller 自動觸發 snapshot、設定 Kafka topic、建 Snowflake stored procedure、啟動 merge cycle。原本「每張表 1-2 天人工 bootstrap」變成「幾小時自動完成」,這對能搬的表規模有複利效應。底層的「執行」與上層的「調度」分離——bootstrap 邏輯與 scheduling 策略可以獨立演化。整條 bootstrap 路徑 RDS snapshot → S3 → Snowflake COPY INTO,沒有任何一步要動到 production PostgreSQL。
audit:cell-by-cell 與 zero-downtime re-bootstrap
到 2025 年底,Figma 的 CDC pipeline 已經在 production 跑了一年多——延遲從 30+ 小時降到 3 小時、最大表能跑下來、replica 帳單顯著下降。但團隊內部有一個揮之不去的擔憂:怎麼證明資料是對的?
CDC 管道有一個經典的、難以察覺的失敗模式:靜默偏離(silent drift)。某個 stored procedure 的 corner case、某次 schema 變更時的順序錯誤、某段時間 Kafka topic 的 retention 配置漂移——這些都可能導致少量 row 在 Snowflake 上的值與 PostgreSQL 不一致。整體 row count 看起來對,因為大部分 row 還是對的;只有那些靜默偏離的 row 在分析報表上產生詭異的數字,往往要好幾週後才有分析師察覺。
傳統 ETL 圈用 row count 抽樣比對來捕捉這類問題,但 row count 只能抓粗暴的失敗(一整批 row 漏掉、整個 partition 沒寫入)。對「100 萬 row 裡有 17 row 的某個 column 值錯了」這種情境,row count 完全失明。下面這張並列圖把同一張表交給兩種 audit——左欄看 row count 通過、右欄按 cell 比對抓出三格靜默偏離。Figma 決定不靠抽樣,他們每週對所有同步的表做一次完整的 re-bootstrap:
同一張 48-cell 的表交給兩種 audit
row count 對「總數對、內容錯」完全失明;cell-by-cell diff 直接揭示哪個 cell 偏離、Figma 選擇不依賴抽樣的原因。
// 每週對每張表跑一次
weekly_audit(table_X):
// 走獨立的 snapshot 路徑——不重用 CDC pipeline
snapshot := rds_export_snapshot(table_X) → S3
audit.table_X := load_into_snowflake(snapshot)
// cell-by-cell diff
diff := SELECT * FROM prod.table_X p
FULL OUTER JOIN audit.table_X a ON p.pk = a.pk
WHERE p IS DISTINCT FROM a
if diff.rows > 0:
alert(diff.rows)
log(diff)
這套 audit 在工程量上不便宜——每週做一次完整 snapshot 並 cell-by-cell diff,等於把 CDC 之前的「全表 dump」這件事重新跑一次,只是頻率從每天降到每週。但這是「資料正確性」的唯一硬保證:因為 audit 走的是完全獨立的路徑(snapshot → S3 → Snowflake),它與 CDC pipeline 共享的代碼面積接近零;CDC pipeline 的任何 bug 不會影響 audit 的結果——這種「獨立路徑驗證」是分散式系統裡證明正確性的標準手段。
2026 年初 audit 系統在 staging 環境抓到過一次嚴重的 silent drift——某個 schema 變更觸發了 stored procedure 的 corner case、導致一張關鍵表的部分 row 在 merge 過程中被錯誤地 update。ByteByteGo 原文描述這次發現「would have triggered a site-wide outage lasting at least twenty minutes」——staging 的 audit 在發布前就攔下了它。audit 的 ROI 在第一次攔截 silent drift 的瞬間就回本:每週完整 snapshot + cell-by-cell diff 的雲端與 compute 成本,遠小於「一次 silent drift 進 production 後的 cleanup 成本」。
audit 還有一個側面值得記下:re-bootstrap 過程本身被做成 zero-downtime 的。傳統的「重建表」流程是「drop + reload」——這意味著從 drop 到 reload 完成之間,下游讀到的是空表或半填的表。Figma 的做法是用「版本化 artifact + atomic view swap」:
// audit table 用版本後綴避免衝突
audit.table_X_v_20260518 := load_snapshot()
// diff 與 alert 都針對這個版本
run_diff(audit.table_X_v_20260518, prod.table_X)
// 完成後,atomic 切換 audit view
CREATE OR REPLACE VIEW audit.table_X AS
SELECT * FROM audit.table_X_v_20260518;
// 舊版本保留 N 週,作為回滾或調查證據
這套版本化讓 audit 對下游分析任務透明——任何讀 audit schema 的下游 query 看到的永遠是一個一致的版本,view swap 是 atomic 的、不會出現「query 跑到一半 audit 表被 drop」的情境。
兩個關鍵的設計決策值得獨立記下。其一,「全表 cell-by-cell」而非「抽樣比對」:抽樣比對成本低 100×、但對非均勻分布的 drift 完全失明(drift 若集中在特定 partition、命中率可能是零)。Figma 堅持完整 diff,理由是「無法事先知道 drift 會集中在哪裡」。其二,用 PostgreSQL snapshot 而非「重跑 CDC pipeline」做 audit 來源——如果 audit 用 CDC 的歷史 event 重建,它與 production CDC pipeline 共享 Kafka topic 與 merge logic、CDC 的 bug 會以相同方式污染 audit。走 RDS snapshot → S3 → Snowflake COPY 的獨立路徑只依賴 RDS 自己的事務一致性,與 CDC pipeline 共享的代碼面積接近零——這是 audit 的「diversity」性質。
三年的變動軌跡
下面這張資料表把每個階段的關鍵數字並列:原本的 cron 系統、CDC 過渡期、與 2026 年的穩定狀態。讀者可以橫向掃過、感受每一個指標的量級變化。
| 維度 | 2020-2023 · cron dump | 2024 · CDC 切換 | 2026 · 穩態 + audit | delta |
|---|---|---|---|---|
| 端到端延遲 | 30+ 小時 | ~3 小時 | 分鐘級(per-table) | ~600× 改善 |
| 同步成本 | $M / 年(專屬 replica) | 顯著下降 | ~0(CDC 跑 WAL) | multi-million 級節省 |
| 最大可搬表規模 | 上限約 1 億 row | 10× 放大 | 10×+ 放大 | 10× 放寬 |
| 新表接入摩擦 | 1-2 天人工 | 幾小時半自動 | CLI 一條命令 | 自助化 |
| 資料正確性保證 | row count 抽樣 | row count 抽樣 | 每週 cell-by-cell | silent drift 可被抓 |
| primary 負擔 | 0(透過 replica) | ~0(WAL 讀) | ~0(WAL 讀) | 不變 |
| 事故率(上線後) | n/a | ~0 major | 0 major(audit 攔下) | staging 攔下 20 min 級事故 |
每一列是一個營運維度——上面三欄是三個時間切片、最右欄是新舊對比的 delta
Figma 的管道在 CDC 切換後同步延遲從小時級壓縮到分鐘級、且不再隨表規模發散——資料新鮮度與表大小解耦。
把上面這幾個維度合起來看:CDC 切換不是單一指標的改善、而是一條多維度的曲線整體位移。最值得記的是「延遲與成本同時下降」——傳統 ETL 圈的直覺是「想要更低延遲就要付更高成本」(更頻繁的 cron、更多 replica),但 CDC 把這條 trade-off 重塑了:因為 CDC 讀 WAL 的負擔接近零,更頻繁的 sync 不會線性增加成本。Figma 從「6 小時跑一次、付 $M」變成「分鐘級可調、近乎免費」——這是架構變更帶來的非線性收益。
除了維度層面的比較,把延遲本身的軌跡按時間畫出來、用 log scale 拉開三個世代的量級差,這條曲線最能說明「CDC 切換」是怎樣一個拐點。下面這張 chart 把「每張表的同步延遲」按年份排列——兩條線分別是「最常見表」與「最大表」、後者的曲線變化更戲劇。
X 軸是年份、Y 軸是同步延遲(log scale,分鐘)
2024 年 CDC 切換是延遲曲線的拐點:之後最大表與最常見表的同步延遲都壓在分鐘級、不再隨規模發散。
這張 chart 用 log scale 是必要的——線性 Y 會讓 2023 年的 30 小時點完全淹掉 2026 年的 5 分鐘,視覺上的拐點就看不出來了。log 之後,兩個關鍵的特徵浮現:第一,2022 → 2023 年的曲線是「指數惡化」——資料量成長、cron 模型不調整、延遲呈現非線性膨脹;第二,2024 → 2025 年的兩條曲線開始「收斂」——因為 CDC 模型下延遲與表規模脫鉤、最大表不再是延遲瓶頸。
第三個值得注意的特徵:2026 年兩條線完全重疊在 5 分鐘左右——這不是測量誤差,而是「per-table 可調」的副作用。每張表的 sync 頻率由 owning team 在配置裡指定,跟表規模無關。這個性質本身就是 CDC 架構帶給 Figma 的最大組織槓桿——資料工程團隊不再是新鮮度的單點瓶頸、每個 owning team 可以根據自己的 SLA 自己決定。
那「per-table 可調」具體調的是什麼?是 merge cycle 的間隔 N。預設 3 小時、billing 表壓到 30 分鐘、ad-hoc 觸發走到 5 分鐘——但壓得越短,Snowflake warehouse 一天要被叫醒越多次。下面這個 slider 讓讀者直接把 N 從 6 小時掃到 1 分鐘,看「資料新鮮度」與「每日 warehouse 計時」這兩條曲線怎麼一起動。新鮮度大致是 N 的一半(平均等待)、最壞為 N;warehouse 計時是「每天 N 個 cycle × 每 cycle 啟動成本」——當 N 很短時,啟動 overhead 主導;當 N 很長時,cycle 數少但每 cycle 等的資料多。中間有個甜蜜帶。
沙金色:「平均資料新鮮度」≈ N/2(最壞為 N)
合併週期 N 太短時 30 秒的 warehouse 啟動 overhead 主導成本;N 太長時單次處理時間隨累積 delta 線性增長。
把 slider 拖到右半邊(N ≥ 30 min)會看到計時曲線變得很平——這是 Figma 大多數表的工作區,每日 warehouse 時間在分鐘級、新鮮度足夠分析師用。拖到左端(N ≤ 5 min)計時曲線急速上揚——這就是「為什麼不所有表都用 5 分鐘」的答案:billing 表願意付這個代價、long-tail 報表沒必要。N 不是要被最小化的優化目標、而是要被「按表分」的配置維度。
把同一條故事的最終形狀畫成可互動的元件圖、讓讀者點選任一節點看它的職責邊界——這是用「空間」去印證上面用「時間」描繪的同一條敘事。下面這張是 2026 年穩態的 CDC 架構,五個主要元件、每個都可以點選展開它的不變式。讀完這張圖,讀者應該能回答「哪個元件擁有 offset 重疊的不變式?」、「哪個元件擁有 cell-by-cell 比對的責任?」這兩個問題——這也是這套設計的兩個關鍵保護點。
PostgreSQL · 變更事件的權威來源
所有寫入由 application server 直接送進 primary。logical replication 把 WAL 透過 publication / subscription 模型輸出——CDC consumer 讀的是已 commit 的事務,順序由 WAL 保證。primary 自己不知道下游有沒有 CDC consumer——這就是 logical replication 的解耦點。
不變式:WAL 是 transaction-ordered append-only log;每筆事件帶 LSN(log sequence number)。Kafka · offset 重疊的不變式之地
每張表一個 topic、按 primary key 分區。Kafka 持久化 WAL stream,給下游一個穩定的 offset 概念。bootstrap 流程的關鍵「先記 O_start、再 snapshot」這個動作的實現點就在 Kafka——O_start 是 Kafka 的 offset、不是 PostgreSQL 的 LSN,因為 bootstrap 流程之後 replay 的是 Kafka 上的事件。
不變式:consumer 從 O_start replay → 接收到所有 offset ≥ O_start 的事件、且順序保證。Snowflake merge · 增量 upsert 與冪等
每 N 小時跑一次 stored procedure:從 Kafka 對應 topic 拉 delta、跟 production table 做 MERGE upsert、推進 last_committed_offset。N 預設 3 小時、per-table 可調到 30 分鐘或更低。冪等的 key:每筆 delta event 帶 primary key 且 op 屬於 INSERT/UPDATE/DELETE,重複 apply 結果一致。
不變式:merge(delta, target) 在相同 input 下結果相同;retry-safe。snapshot 路徑 · bootstrap 的對齊保證
當新表第一次接入或需要重新對齊時走這條路徑。順序:在 Kafka 上記下 O_start → 觸發 RDS snapshot 到 S3 → snapshot 完成後匯入 Snowflake → 從 O_start 開始 apply Kafka stream。關鍵:O_start 在 snapshot 開始之前,snapshot 必然涵蓋 ≤ O_start 的所有寫入;stream replay 覆蓋 snapshot 期間的所有寫入;重複 apply 被 merge 的冪等性吸收。
不變式:snapshot.committed ⊇ events.offset ≤ O_start。audit · 獨立路徑的正確性保證
每週走完全獨立的 snapshot → S3 → audit schema 流程、在時間對齊的點上對 prod 跑 cell-by-cell diff。獨立路徑的意義:audit pipeline 與 CDC pipeline 共享的代碼面積接近零——CDC 的任何 bug 不會影響 audit 的判斷。發現偏差自動 alert + 列出具體偏離 row;audit 完成後 atomic 切換 view、保留舊版本作回滾證據。
不變式:diff(prod.X, audit.X) 為空 → CDC pipeline 對 X 的處理正確。三條主要路徑:實線是 CDC stream、虛沙金線是 bootstrap snapshot、虛紅線是 audit r…
CDC stream、bootstrap、audit 三條路徑各自獨立——audit 走 snapshot 機制,不依賴 CDC pipeline。
這張圖讓兩個容易被忽略的設計點凸顯出來。第一是 Kafka 在 bootstrap 流程中的角色——很多人寫 CDC 都以為 Kafka 只是「delta 流的緩衝」,但 Figma 把 Kafka 的 offset 拿來當 bootstrap 對齊的錨點。這個用法把「snapshot 與 stream 對齊」這個分散式系統的經典難題,從「時間戳對齊」轉成「offset 對齊」——offset 是嚴格遞增的整數,比時間戳容易推理。
第二是 audit 的獨立性。audit schema 與 CDC pipeline 共享的元件只有 PostgreSQL(資料來源)與 RDS snapshot 機制(搬資料的工具)——下游的 audit table、diff query、alerting 都是獨立的代碼路徑。這意味著 CDC pipeline 的任何 bug(merge stored procedure 的 corner case、Kafka consumer 的 offset 漂移、Snowflake 的版本不一致)不會也以同樣的方式影響 audit;audit 與 CDC 不會「相同地壞掉」。這是分散式系統裡證明正確性的標準形狀,名字叫「diverse redundancy」——用兩條足夠不同的路徑算同一個結果,比對它們。
從這條三年路能學到什麼
對任何維護資料管道的團隊,這條三年路有幾個直接可拿走的設計選擇。
第一,只要管道夠大,CDC 比定期 dump 划算的點來得很快。CDC 模型的根本好處不是「更新更頻繁」、而是「成本與規模脫鉤」——CDC 讀 WAL 的負擔幾乎不隨表規模變化,這改變了「sync 頻率與成本的 trade-off」。傳統直覺認為「想要更新更頻繁就要付更多錢」,但 CDC 切換證明這個 trade-off 可以被架構重塑:刪掉專屬 replica(成本下降)+ 接近實時的 delta sync(延遲下降)。同時改善兩個本來 trade-off 的指標,通常意味著之前的設計選擇本身就是錯的、不是 tuning 的問題。
第二,bootstrap 是 CDC 管道每次新表上線時都要重做的——把它做成 self-serve 工具流程,不要人工對 offset。「先 offset、後 snapshot」的順序把 race window 在設計階段消掉,這個 pattern 不只用在 CDC 上:任何「啟動一個 stream 並 backfill 歷史資料」的場景——日誌聚合、事件溯源、materialized view 重建——都面對同一個問題。
第三,資料正確性的審計不能靠 row count、要靠獨立路徑重做的 byte-level diff。row count 抽樣只能抓粗暴失敗、對 silent drift 失明。Figma 用 RDS snapshot 把每週完整 re-bootstrap 的成本降到「可接受但不便宜」、再用 atomic view swap 做成 zero-downtime——這套組合讓 audit 從「理論上應該做」變成「實際上每週在跑」。staging 攔下一次 20+ 分鐘級事故就證明了這個投入的價值。
第四,per-table 配置是組織槓桿。cron dump 時代資料平台團隊是新鮮度的單點瓶頸;CDC 切換後 per-table 頻率讓 owning team 根據自己的 SLA 自己決定——billing 30 分鐘、metrics 5 分鐘、long-tail 表 6 小時。資料平台從「每天接到加速請求」變成「平台、user 自己調」,這跟雲端基礎建設 self-serve 化的曲線是同一條。
Take-away:CDC 管道的設計不在組件清單(Kafka、Snowflake、S3 大家都有),而在三件事——bootstrap 與 stream 的 offset 怎麼重疊銜接、incremental merge 怎麼冪等、以及靜默偏離怎麼用獨立路徑 re-bootstrap 的 cell-by-cell diff 抓出來。這三件事決定了一條 CDC pipeline 在三年後仍然可信。