題幹與適用場景
公司有 20 個 PostgreSQL OLTP 資料庫、200 張需要同步的資料表,以及 8 TB 現有資料。來源平均每秒產生 2 萬筆已提交資料列變更,尖峰為 6 萬筆,編碼後的單筆變更平均約 1 KB。資料要進入分析用資料倉儲或湖倉,從來源交易提交到整理層可查詢的 p99 延遲不得超過 2 分鐘;初始同步必須在 72 小時內完成,期間不能停止業務寫入;快照與 CDC 對來源寫入 p99 造成的退化不得超過 5%。
管線要處理新增、更新、刪除與主鍵值變更,保證同一來源中同一資料列的變更順序,採用至少一次傳遞,並支援 Schema 演進、依資料表或主鍵範圍重播、來源與目標故障復原,以及能證明資料一致的對帳。耐久變更串流保留 7 天原始事件,較長歷史另存到低成本物件儲存。題目中的資料庫數量、吞吐、大小、時效與資源預算都是面試假設,不代表任何工具的效能承諾。
這道題雖然包含訊息系統與多個元件,核心能力仍是資料工程:如何讓存量快照與持續增量無缺口銜接,如何定義可復原的位置、順序與冪等邊界,以及如何證明目標資料沒有漏失或靜默錯誤。
面試官考察點
第一個訊號是能否先定義正確性,再畫元件。普通回答會從「Debezium 加 Kafka」開始;強回答會先寫出不變量:已提交變更不能消失,同一資料列要按來源順序收斂,重複事件不能改變最終狀態,刪除必須傳到目標,快照與增量交界不能有空洞,復原位置不確定時必須停止,不能靜默繼續。
第二個訊號是理解初始快照不等於「先匯出全部資料表,再開啟 CDC」。8 TB 匯出期間仍持續寫入;若匯出完成後才讀 WAL,中間變更可能已被回收。可靠方案要先建立可復原的日誌位置與保留機制,取得一致快照,再從對應位置持續讀取已提交變更。連接器可以封裝實作,但候選人仍須說清楚為何銜接沒有缺口。
第三個訊號是把至少一次傳遞落實到 sink。連接器可能在事件寫入耐久串流後、記錄來源 offset 前當機;目標批次可能已提交,但確認回應遺失而重試。說「訊息佇列 exactly-once」不會自動涵蓋外部資料倉儲。強回答會為每個事件攜帶來源識別、來源位置與交易內順序,並只在事件版本比該資料列已記錄版本更新時套用。
第四個訊號是看見 PostgreSQL 複寫槽的兩面性。複寫槽讓連接器從已保存的 LSN 復原,也會在連接器落後時保留 WAL。若不監控保留位元組與磁碟餘量,目標故障最後可能填滿來源磁碟。若複寫槽被刪除後重建,新槽無法提供舊位置;此時必須停止、對帳或重新快照,不能從「現在」繼續並宣稱沒有漏資料。
最後要看答案是否涵蓋真實資料語意。業務 DELETE 事件與供日誌壓實使用的 tombstone 不同;主鍵值修改通常表現為舊 key 刪除與新 key 建立;不同來源資料庫的 LSN 不能比較;若需要跨多列維持交易原子可見,必須使用交易邊界中繼資料,並接受額外緩衝、延遲與復原複雜度。
回答前需要澄清的問題
- 目標需要目前狀態、完整變更歷史,還是兩者都要? 目前狀態適合依主鍵 MERGE;稽核與重播還需要不可變原始變更層。只保留最終狀態無法重建過去。
- 兩分鐘時效從哪裡量到哪裡? 本題從來源交易提交量到整理層可查詢。若只要求原始事件進入訊息系統,兩分鐘預算會寬鬆許多。
- 是否要求跨表交易原子可見? 本題預設保證同一列有序,資料倉儲允許同一交易中的多列短暫分批可見。財務情境若要求整筆交易原子出現,就要依 BEGIN/END 組裝。
- 每張資料表都有穩定主鍵嗎? 沒有主鍵時,UPDATE 與 DELETE 的定位和去重更困難。應先補業務鍵,或明確接受完整舊列、代理鍵與較高儲存成本。
- 來源故障轉移是否保留邏輯複寫槽? 若無法保證,復原設計要包含暫停寫入、核對最後 LSN、重建槽與對帳;不能把主機切換視為一般重新連線。
- 哪些 Schema 變更可以自動通過? 本題只自動接受新增可空欄位等相容變更;刪除、重新命名、型別縮窄與主鍵變更採受控遷移。
- 72 小時快照期限與 5% 來源預算哪個更硬? 8 TB 在 72 小時內讀完,需要約 30.9 MB/s 的聚合有效吞吐。若壓測顯示會超出來源預算,就延長期限、使用語意相容的副本,或分階段縮小範圍。
- 目標停機多久仍要靠線上串流復原? 本題要求承受 30 分鐘尖峰停機;更長故障由 7 天耐久串流與物件儲存重播處理。
30 秒回答框架
「我會先把正確性定義為:快照與增量無缺口、同一來源與主鍵按來源位置收斂、重複可安全重播、刪除會傳播、複寫槽遺失時失敗關閉。每個 PostgreSQL 來源用邏輯解碼讀 WAL,先取得一致快照並從對應 LSN 接續,再把事件依來源、資料表與主鍵分割寫入耐久串流。原始層不可變保存,整理層按主鍵與來源版本冪等 MERGE。Schema 先驗證相容性,刪除與 key 變更顯式處理。維運上監控提交到目標的分段延遲、LSN lag、複寫槽保留 WAL、快照進度與對帳差異,再用當機、槽遺失、Schema 變更與目標停機做故障注入。」
分步驟深入解答
第一步:用容量估算決定緩衝、快照與復原預算
平均負載為:
20,000 筆/秒 × 1 KB ≈ 20 MB/s
20,000 × 86,400 = 1,728,000,000 筆/天
20 MB/s × 86,400 ≈ 1.728 TB/天尖峰入口約為 60 MB/s。若目標端在尖峰停止 30 分鐘,原始積壓約為:
60 MB/s × 1,800 秒 = 108 GB7 天按平均負載保留約 12.096 TB 原始資料,還未計入副本、索引與編碼開銷。訊息系統、物件儲存與網路要按尖峰和復原追趕速率規劃,不能只看 20 MB/s 平均值。目標恢復後若只能以尖峰同速處理,就永遠清不掉 108 GB 積壓;需要額外消費餘量,或暫時放寬整理層時效。
8 TB 快照要在 72 小時內完成,聚合有效讀取量至少約為:
8 TB ÷ 72 小時 ≈ 30.9 MB/s這只是下界,尚未包含掃描放大、序列化、網路重試與目標寫入。先用接近正式環境的資料壓測分塊快照,再依來源 I/O、快取行為、複寫延遲與 p99 動態限速。期限與 5% 預算衝突時,要調整期限或資料來源,不能讓初始同步拖垮 OLTP。
第二步:讓每個元件對應一項正確性或容量需求
主要資料流可以寫成:
PostgreSQL WAL / logical slot
→ source connector
→ durable change stream keyed by source + table + primary key
→ immutable raw archive
→ schema validation and light normalization
→ sink staging tables
→ idempotent MERGE / DELETE into curated tables
→ warehouse and lakehouse consumers每個來源資料庫使用獨立的來源識別、連接器與複寫槽,避免一個故障域拖住全部來源。耐久串流吸收尖峰、解耦多個消費者並提供短期重播;物件儲存保留較長歷史。CDC 處理層只做 Schema 驗證、事件標準化與路由,避免在主路徑加入無法重播的外部查詢。資料倉儲先寫 staging,再用小批次原子 MERGE,兼顧兩分鐘 SLA 與欄式倉儲寫入效率。
分割鍵使用 (sourceid, tableid, primary_key) 的穩定編碼,讓同一資料列進入同一個有序分割區。它不提供全域順序,也不需要提供。20 個來源資料庫各有獨立 LSN 序列,數值大小沒有跨來源意義。
第三步:完成不停寫的初始快照
邏輯順序是:
- 為來源建立邏輯複寫槽,讓必要 WAL 不會在連接器讀取前被回收。
- 取得一致快照與其對應的來源日誌位置。
- 依資料表與主鍵範圍分塊讀取快照,將目前資料列發成
READ事件。 - 從與快照匹配的位置持續讀取已提交的 INSERT、UPDATE 與 DELETE。
- 目標依來源版本收斂,允許故障復原造成的邊界重播,不允許位置空洞。
連接器可能實作全量一致快照或增量快照視窗。答案不應自行發明「先記 LSN,再跑一般 SELECT」協定,因為隔離層級、長交易與並行寫入會讓細節失真。應依賴經過驗證的連接器語意,並用同一主鍵在快照期間反覆更新、刪除與重建的測試證明結果。
快照依主鍵範圍切塊並持久化進度。較小區塊能降低長交易、快取污染與失敗重做範圍;過小則會放大查詢和排程成本。每個來源獨立限速,先用小表建立端到端信心,再推進大表。快照期間仍要持續讀 WAL,否則複寫槽保留量會一直成長。若所選連接器不支援增量快照期間的 Schema 變更,就凍結該表 DDL 或暫停快照。
第四步:定義事件、順序與冪等寫入
標準事件至少包含:
ChangeEvent {
event_id
source_id
table_id
primary_key
operation // READ | CREATE | UPDATE | DELETE
before
after
source_lsn
transaction_id
transaction_order
source_commit_time
schema_version
captured_at
}同一來源內,sourcelsn 加上交易內順序用來判斷事件先後;版本鍵必須包含 sourceid,因為不同資料庫的 LSN 沒有共同座標。目標狀態表為每個 (sourceid, tableid, primary_key) 保存最後套用的來源版本。收到相同或較舊版本時直接確認而不重複修改;收到更新版本時,在同一目標交易中更新業務資料列與已套用版本。
主要至少一次故障視窗因此可控:
- 事件已進入耐久串流,但來源 offset 尚未記錄:復原後重發,目標忽略相同版本。
- 目標 MERGE 已提交,但確認遺失:批次重播,目標仍收斂到相同狀態。
- 消費者在批次中途當機:已提交資料列安全重播,未提交資料列重新處理。
如果目標要求跨多列維持來源交易原子性,啟用交易邊界中繼資料,依 transaction_id 緩衝到 END,再一起提交完整交易。大型交易會占用更多記憶體並增加尾端延遲,逾時與復原邏輯也必須判斷交易是否完整。一般分析倉儲只需要資料列層級最終一致時,不應在沒有真實需求下承擔這層複雜度。
第五步:正確處理刪除、主鍵變更與 Schema 演進
DELETE 事件必須攜帶足夠的 key,讓整理層選擇硬刪除、設定 is_deleted,或保存歷史。刪除事件後的 tombstone 主要服務日誌壓實;它不能取代業務刪除事件,中介層也不能在事件到達目標前丟掉 DELETE。
主鍵值改變時,常見 CDC 語意是「刪除舊 key,再建立新 key」。目標必須同時套用兩筆事件,否則舊 key 會留下幽靈資料列。修改主鍵定義比修改值更危險,需要唯讀或暫停寫入視窗、排空連接器、更新 Schema 後再恢復,因為事件 key 結構在切換期間可能不一致。
Schema 採用明確相容策略:
- 新增可空欄位:註冊新版本,舊消費者忽略未知欄位,整理表先擴充再開始寫入。
- 刪除或重新命名欄位:先加入並填寫新欄位,遷移所有消費者,再移除舊欄位。
- 型別放寬:確認目標相容後升級;型別縮窄或語意改變時隔離不相容事件。
- 主鍵變更:當成獨立遷移,不交給自動 Schema 演進靜默處理。
不相容事件進入隔離區並告警。只有事件已可靠保存、可以重播且處置責任明確後,主串流 offset 才能前進。直接跳過錯誤 Schema 會製造看不見的資料缺口。
第六步:把復原、重播與對帳設計成正常路徑
連接器復原需要「已持久化 offset」與「仍包含對應歷史的複寫槽」同時成立。監控 restartlsn、confirmedflush_lsn、目前 WAL 位置、保留位元組、成長速度與磁碟耗盡剩餘時間。目標停機時,應讓耐久串流吸收積壓,避免連接器長時間停止確認而讓來源無限保留 WAL。
如果複寫槽遺失、儲存 offset 落後於槽可提供的位置,或必要 WAL 已消失,就要失敗關閉。停止該來源的整理層發布,記錄最後可信位置,重新快照受影響資料表,再按範圍對帳。新複寫槽只能捕捉建立後的變更,無法證明更早區間完整。
重播使用獨立的 replayjobid、資料表與主鍵範圍,以及來源時間邊界,從不可變原始層重新產生 staging 資料。即時與重播都套用相同來源版本 MERGE 規則,避免舊回填覆蓋新狀態。轉換邏輯改變時,先建立新整理版本或影子表,比直接覆蓋正式資料更容易比較與回滾。
對帳至少包含四層:
- 來源提交位置到連接器、耐久串流與目標套用位置的連續性;
- 依資料表、日期與主鍵桶比較資料列數、刪除數與校驗摘要;
- 抽樣讀取具體主鍵,比對來源目前狀態與目標最新狀態;
- 定期寫入可辨識的 canary 交易,驗證 INSERT、UPDATE 與 DELETE 都在 SLA 內出現。
線上指標要把總延遲拆成來源提交到擷取、擷取到串流、串流到 staging、staging 到整理層四段;同時監控每個來源的事件速率、LSN lag、複寫槽 WAL、快照區塊進度、重複或過期版本、Schema 隔離量、目標 MERGE 失敗、對帳差異與預估追趕時間。
第七步:說明替代方案的邊界
依 updated_at 輪詢實作簡單,適合低寫入、分鐘級時效且允許補掃的資料表,但會增加查詢負載、容易漏掉硬刪除,也必須處理相同時間戳與時鐘精度。觸發器可以把刪除寫入稽核表,卻會增加來源寫入路徑成本、部署耦合與故障面。對本題繁忙的 OLTP,日誌型 CDC 是較合適的預設選擇。
Outbox 解決的是服務在同一資料庫交易中寫入業務狀態與領域事件,避免資料庫提交成功但訊息傳送失敗。它適合傳播經過業務定義的事件;不會自動取代 200 張表的通用資料列同步。兩者可以共存:服務整合消費 outbox,分析與稽核消費 CDC。
高品質示範回答
「我會先把保證寫清楚:所有仍存在於 WAL 或耐久串流的已提交變更都不能遺失,同一來源的同一主鍵依來源位置收斂,重複重播不改變最終結果,刪除一定到達目標;如果複寫槽與歷史位置不再完整,就停止發布並重新快照,不能靜默從最新位置繼續。
容量上,平均入口約 20 MB/s、每天 17.28 億筆與 1.728 TB;尖峰 60 MB/s,目標停機 30 分鐘會積壓約 108 GB。7 天原始保留約 12.096 TB。8 TB 快照要在 72 小時完成,平均有效讀取至少 30.9 MB/s,所以我要先壓測,再依每個來源的 p99、I/O 與 WAL 保留量限速。
架構上,每個 PostgreSQL 來源使用獨立邏輯複寫槽與連接器,先做一致快照,再從對應 LSN 接續已提交變更。事件依來源、資料表與主鍵分割寫入耐久串流,同時保存到不可變原始層。輕量處理只做 Schema 驗證與標準化,資料倉儲先寫 staging,再依主鍵與來源版本做冪等 MERGE 或 DELETE。不同來源的 LSN 不比較;同一交易內用交易順序補充 LSN。
初始快照依主鍵範圍分塊、持久化進度並動態限速。快照期間連接器繼續讀 WAL,目標用版本規則吸收邊界重播。事件攜帶 operation、before、after、來源 LSN、交易 ID、交易順序與 Schema 版本。目標只套用比該列已記錄版本更新的事件,因此連接器重發、目標確認遺失與消費者當機都能安全收斂。
刪除事件完整傳到整理層,tombstone 只用於日誌壓實;主鍵值改變依舊 key 刪除與新 key 建立處理。新增可空欄位可以相容演進,刪除、重新命名、型別縮窄與主鍵變更走受控遷移。不相容事件隔離且可重播,不能直接跳過。
維運上我會監控分段延遲、每個來源的 LSN lag、複寫槽保留 WAL 與磁碟餘量、快照進度、Schema 隔離、目標失敗與對帳差異。複寫槽遺失時失敗關閉並重新快照受影響範圍。最後用快照期間並行更新、重複傳遞、刪除、主鍵變更、連接器當機、目標回應遺失、30 分鐘目標停機、槽遺失與破壞性 Schema 變更做故障注入,再用資料列數、主鍵桶校驗和 canary 交易證明沒有漏資料,也沒有舊事件覆蓋新狀態。」
常見錯誤
- 先匯出所有資料表,完成後才開啟 CDC → 匯出期間的 WAL 可能被回收,快照與增量之間形成資料缺口 → 先建立日誌保留與一致快照位置,再從匹配位置接續。
- 只說「使用 Debezium 和 Kafka」 → 工具名稱沒有定義延遲、順序、復原與目標冪等邊界 → 先寫正確性不變量與容量,再讓元件逐項對應。
- 把不同資料庫的 LSN 當成全域版本比較 → 每個來源有獨立日誌座標,數值大小沒有跨來源意義 → 版本鍵包含
source_id,只在同一來源序列判斷先後。 - 宣稱訊息系統 exactly-once 就讓資料倉儲 exactly-once → 外部 MERGE 可能不在訊息交易內,確認遺失仍會重播 → 目標依主鍵與來源版本冪等套用。
- 連接器停滯時只等待復原 → 複寫槽會持續保留 WAL,可能填滿來源磁碟 → 監控保留位元組與剩餘時間,用耐久串流隔離目標故障並設定止損門檻。
- 複寫槽重建後從最新位置繼續 → 新槽沒有舊歷史,可能靜默漏失資料 → 失敗關閉,重新快照並對帳受影響範圍。
- 把 tombstone 當成唯一刪除語意 → 日誌壓實標記不能取代攜帶業務 key 的 DELETE → 完整傳遞刪除事件,再按目標需求硬刪、軟刪或保留歷史。
- 所有 Schema 變更都自動通過 → 刪除、型別縮窄與主鍵變更會破壞消費者或事件 key → 定義相容矩陣,隔離不相容事件,分階段遷移破壞性變更。
- 快照只追求 72 小時完成 → 大量掃描可能破壞來源 p99、快取與複寫 → 以 30.9 MB/s 下界壓測、動態限速,優先保護來源預算。
- 只比較來源與目標總資料列數 → 更新錯位、漏刪與互相抵銷的錯誤都可能被掩蓋 → 依主鍵桶比較計數與摘要,再抽樣逐列並注入 canary。
追問及應對
追問一:初始快照期間同一列連續更新與刪除,如何證明舊快照值不會覆蓋新狀態?
依賴連接器經驗證的一致快照與日誌銜接語意,不用應用程式猜測時序。目標為每列保存來源版本,只接受更新版本,因此重播的 READ 不能覆蓋較新的 UPDATE 或 DELETE。測試時讓同一主鍵在快照區塊前後反覆更新、刪除與重建,再斷言來源與目標最終一致,且來源位置連續。
追問二:目標資料倉儲停機 30 分鐘後,如何估算復原時間?
尖峰積壓約為 108 GB。復原時間取決於「恢復後處理速率減去仍持續進入的即時流量」。若即時流量回到 20 MB/s 平均值,而消費者可持續處理 80 MB/s,淨追趕速率約 60 MB/s,理論清空時間約 30 分鐘,還要加上 MERGE 放大與安全餘量。若處理能力只等於即時入口,積壓永遠不會下降。
追問三:業務要求同一來源交易中的多張表同時可見,方案怎麼改?
啟用交易邊界中繼資料,依交易 ID 收集事件,收到完整 END 邊界後,才把整筆交易一次寫入 staging 並原子發布。大型交易要落磁碟緩衝,定義逾時與人工處置,復原時從持久化組裝狀態繼續。這會增加尾端延遲與狀態成本,因此要先確認短暫跨表不一致真的不可接受。
追問四:複寫槽遺失,但耐久串流仍有 7 天資料,是否一定要全量重做?
先確認可能缺口。若耐久串流可以證明完整包含最後可信 LSN 之後的所有變更,就能透過重播與對帳恢復連續,不必全量快照。若槽在事件進入串流前就遺失,或無法證明缺口起點,就重新快照受影響的資料表或主鍵範圍。判斷依據是可證明的連續性,不是重做成本。
追問五:為什麼不能只用 outbox 取代整套 CDC?
Outbox 很適合由服務明確發布訂單已建立、付款已完成等領域事件,並在同一交易中寫業務狀態與事件。它需要應用程式主動參與,也只包含選擇發布的業務事實。本題要把 200 張表的新增、更新與刪除同步到分析系統,仍需要通用資料列 CDC;兩種模式可以服務不同消費者並存。