題目與適用情境
行動裝置會回報購買事件,欄位至少包含 eventid、eventtime、userid,以及已換算為統一幣別的 amount。網路重試可能產生重複事件,離線裝置會在數小時後補送,不同分割區也不會維持全域順序。請依 eventtime 計算 UTC 自然小時營收,尖峰輸入為每秒 2 萬筆事件。
業務希望目前小時的估算值在 1 分鐘內可見,watermark 越過視窗結束時間後產生準最終結果,並在視窗結束後的 24 小時內接受修正。超過 24 小時才到達的資料不能默默丟棄,需要進入稽核與離線對帳。以上吞吐量、時效與修正期間都是本題輸入,並非任何串流處理引擎的效能承諾。
本題假設 event_id 由事件產生端穩定建立,同一 ID 的合法重試具有相同業務內容;原始事件會保存在可重播儲存中。範圍包含時間語意、watermark、視窗觸發、去重、結果更新、狀態容量、復原與對帳,不展開訊息中介軟體選型及多幣別匯率計算。核心能力屬於資料工程:在完整性、延遲與狀態成本之間建立可驗證的資料契約。
面試官考察重點
第一個訊號是能否分清楚三種時間。eventtime 決定事件屬於哪個業務小時;processingtime 表示引擎何時看到事件,可用於每分鐘提早更新;watermark 表示引擎對事件時間進度的估計。若直接依 processing time 切視窗,同一批歷史重播會因執行時間不同而得到不同結果。
第二個訊號是不把 watermark 當成絕對保證。它是「先前事件大概已經到齊」的進度邊界。watermark 推進得快,結果及時,但更多事件會變成 late;推進得慢,完整性提高,卻會延後視窗關閉並保留更多狀態。好的回答會使用真實到達延遲分布與業務修正 SLO 選擇策略,而非背誦固定的 5 分鐘或 1 小時。
第三個訊號是分開 watermark、allowed lateness 與去重保留期。watermark 決定何時觸發準最終結果;允許延遲決定視窗狀態還能保留並修正多久;去重狀態必須涵蓋可能出現重複副本的時間跨度。三個值可能相關,但不是同一個設定。
最後要看輸出能否收斂。延遲事件會使同一視窗多次輸出,下游若只追加每次快照就會重複累加。輸出必須依視窗鍵進行冪等 upsert,並帶版本或最終性標記。檢查點只保護運算子狀態;若 sink 無法和檢查點協同提交,還需要交易寫入或冪等版本覆寫。
回答前需要釐清的問題
- 業務依發生時間還是到達時間統計? 本題依購買發生的
event_time。只有監控「系統此刻處理多少請求」時,processing time 才可能是正確的業務時間。 - 1 分鐘內可見的是估算值還是最終值? 本題允許估算值,因此使用 processing-time early trigger;若每次顯示都必須完整,就只能等待更久或改用批次處理。
- 24 小時後還能否修改歷史報表? 本題將 24 小時定義為串流任務的自動修正期間,逾期資料進入對帳。若法規或結算要求任何延遲都必須改帳,就不能把狀態清理等同於資料終局。
- 重複事件能否依 ID 識別? 本題有穩定的
event_id。若只能依使用者、金額與時間猜測,誤刪合法購買與漏掉重複都無法避免,必須先修正產生端契約。 - 同一 ID 的內容不一致怎麼辦? 不選擇「先到」或「後到」覆寫;記錄 payload 指紋衝突並隔離,因為這表示產生端違反冪等契約。
- 下游接受快照、增量還是撤回訊息? 本題輸出視窗完整快照,並依
(window_start, dimensions)upsert。只支援 append 的 sink 需要改成帶版本的變更日誌,再由讀取層選擇最新版本。 - 事件時間是否可信? 遠未來、早於資料保留期或時區不合法的時間戳記要先隔離。錯誤的未來時間若參與最大事件時間計算,可能使 watermark 錯誤地推得太快。
30 秒回答架構
「我會依 eventtime 分 UTC 小時視窗,每個活躍來源分割區產生 watermark,下游取最小安全進度。processing-time trigger 每分鐘輸出估算,watermark 越過視窗末端時輸出準最終結果,狀態再保留 24 小時接收延遲修正。事件依 eventid 和 payload 指紋去重,結果依視窗鍵與 revision upsert。逾期事件進入 side output 與每日批次對帳;復原依賴可重播來源、檢查點以及交易或冪等 sink。」
分步深入解答
先定義結果契約。視窗採用 [10:00, 11:00) 這種左閉右開區間,視窗鍵至少包含 window_start 與報表維度。每筆輸出都是該視窗截至目前版本的完整營收快照:
HourlyRevenue {
window_start
window_end
revenue
revision
result_state // EARLY | ON_TIME | FINAL
}同一視窗的 revision 單調遞增,sink 只接受較大的版本。EARLY 表示一分鐘更新,不承諾完整;ON_TIME 表示 watermark 已越過視窗末端;FINAL 表示 24 小時自動修正期已結束。這裡的 FINAL 是營運契約,不代表世界上不會再出現資料,逾期事件仍由對帳流程處理。
時間策略從限制推導。每個活躍來源分割區提取通過驗證的 event_time,常見的有界亂序策略可以寫成:
partition_watermark = max_valid_event_time_seen - out_of_order_bound
operator_watermark = min(active_partition_watermarks)outoforderbound 應來自過去一段時間的到達延遲分布、可接受的 late 比例與 ONTIME 時效目標。依分割區產生 watermark,能避免快分割區替慢分割區宣告完成;多輸入運算子取最小進度,避免越過仍可能產生舊事件的輸入。空閒分割區要在確認一段時間沒有資料後標記為 idle,否則會使全域 watermark 永久停住;門檻過短時,分割區恢復後的舊事件會變成 late,因此仍需 side output。
視窗使用兩類觸發。processing-time early trigger 每分鐘輸出一次目前的完整快照,滿足可見性;watermark 越過 windowend 時輸出 ONTIME 版本。視窗狀態保留到 watermark 越過 window_end + 24h,期間每個合法延遲事件都會更新彙總並產生更高 revision。到達清理點後輸出 FINAL 並釋放視窗狀態。Beam 的 trigger 與 accumulation 模型說明了為什麼「何時輸出」與「輸出本輪增量或累積快照」必須分別定義;本題選擇累積快照加 upsert,避免下游自行拼接多個 pane。
事件進入彙總前依 eventid 去重。狀態保存 eventid → (eventtime, payloadfingerprint):首次出現就放行;相同指紋的重複副本丟棄;同 ID 不同指紋寫入衝突串流並告警。去重狀態的保留期不能只看視窗大小,而要涵蓋上游可能重播重複副本的最長時間。Spark 的 watermark 去重語意也明確要求延遲門檻涵蓋同一事件最早與最晚副本的時間差;太早清除後,延遲的重複事件會再次被計入。
狀態成本必須估算。若尖峰每秒 2 萬筆事件持續 24 小時、最壞情況全部為不同 ID,需要同時記住 20,000 × 86,400 = 1,728,000,000 個 ID。這個上界即使每筆邏輯負載只按 40 位元組估算,也約為 69.12 GB;引擎物件、索引、狀態後端、檢查點與副本會進一步增加實際用量。因此狀態要依 key 分片並使用增量檢查點,保留期要由重播契約決定;若 99% 的重複只在更短期間內出現,可以採用短期串流去重搭配 sink 業務鍵與離線對帳,但必須量化超過短期的重複風險,不能把風險藏在 TTL 之後。
輸出鏈路要能處理重播。理想情況是 sink 參與檢查點交易,使「消費位移、運算子狀態、視窗結果」一起提交。若做不到,使用 (window_key, revision) 冪等 upsert:崩潰後相同或更舊 revision 的重播不會覆寫新結果。只依賴訊息系統的 exactly-once 標籤仍不夠,因為外部資料庫寫入可能發生在檢查點確認之前;端到端語意取決於來源、狀態和 sink 的共同提交邊界。
超過 24 小時的事件進入 too-late side output,並繼續保存在原始日誌。每日批次依 event_time 重算受影響視窗,使用相同的 ID 去重與金額規則,將結果與串流 FINAL 快照比較。差異超過門檻時產生更高 revision,或進入財務審核。批次任務必須依視窗原子取代或以版本 upsert,不能在既有總額上再加一次,否則重播批次任務會重複計數。
用最小序列驗證語意。視窗 [10:00, 11:00) 收到 A=100、B=50,之後收到 A 的相同副本;去重後 ON_TIME 結果是 150。watermark 越過 11:00 後,C=20 在允許延遲期間內到達,視窗更新為更高 revision 的 170。D 在 watermark 越過清理點後到達,不直接修改已清除狀態,而是進入對帳串流。若 A 的第二份 payload 金額變成 120,它必須進入衝突串流,不能把結果改成 170 或 190。
驗證矩陣還應涵蓋:不同分割區亂序;一個分割區空閒造成 watermark 停滯;未來時間戳記被隔離;watermark 前後的邊界時間;程序在寫入 sink 前後崩潰;從檢查點復原並重播;同一視窗的多個 revision 亂序到達 sink;批次對帳重複執行。線上監控事件到達延遲的 p50/p95/p99 與長尾、各運算子的 current watermark 和 lag、early/on-time/late/too-late 計數及金額、去重命中與指紋衝突、狀態位元組數、檢查點時間、sink 版本拒絕數與批流差異。
高品質示範回答
「我會先把 1 分鐘內的數值定義為估算,24 小時定義為自動修正期間。購買歸屬由 event_time 決定,processing time 只負責每分鐘提前觸發。每個活躍輸入分割區依通過驗證的最大事件時間減去亂序容忍量產生 watermark,下游取最慢活躍輸入的進度;空閒分割區要明確標記,未來異常時間戳記先隔離。
資料先依 eventid 去重。我會保存事件時間和 payload 指紋:相同副本只計算一次,同 ID 不同內容進入衝突串流。接著進入自然小時視窗,每分鐘輸出 EARLY 完整快照,watermark 越過視窗末端後輸出 ONTIME,狀態繼續保留 24 小時;期間延遲事件會更新總額並增加 revision,清理前輸出 FINAL。
sink 依視窗鍵與 revision upsert,不把多次快照當成增量累加。來源可重播、運算子狀態隨檢查點復原;sink 若不能參與檢查點交易,就靠版本寫入保證復原重播不會覆寫新結果。超過 24 小時的事件進入 side output,每日批次從原始日誌重算並比對 FINAL 結果。
容量方面,若每秒 2 萬筆的尖峰持續 24 小時且全部唯一,要保留 17.28 億個去重 ID;每筆僅以 40 位元組計算也有 69.12 GB 邏輯狀態上界,因此我要用真實延遲分布決定保留期,並監控狀態與檢查點成本。最後以重複、亂序、延遲、未來時間戳記、空閒分割區和崩潰復原做故障注入,驗證視窗最終值與對帳結果一致。」
常見錯誤
- 依 processing time 切業務視窗 → 同一事件在即時執行與歷史重播中可能進入不同小時 → 用 event time 決定歸屬,只用 processing time 觸發早期結果。
- 把 watermark 說成「之前的資料絕不會再來」 → watermark 通常是啟發式進度,舊事件仍可能到達 → 定義 allowed lateness、too-late side output 與對帳策略。
- watermark、允許延遲與去重 TTL 使用同一個值且沒有依據 → 三者分別控制觸發、視窗狀態與重複識別,錯誤綁定會遺失修正或重複計數 → 從時效 SLO、修正期間與上游重播契約分別推導。
- 每次觸發都向下游追加總額 → EARLY、ON_TIME 與 late firing 會被重複相加 → 輸出完整快照並依視窗鍵與 revision upsert,或明確使用可撤回增量協定。
- 用全域最大事件時間直接推進 watermark → 快分割區或異常未來時間戳記會使慢分割區資料過早變 late → 依分割區產生進度、取活躍輸入最小值,並驗證時間戳記。
- 讓永久不動的空閒分割區參與最小值 → watermark 停止,視窗與去重狀態無法清理 → 設定可觀測的 idleness 策略,並處理恢復分割區的延遲資料。
- 只保存 event ID,不檢查內容 → 產生端重複使用 ID 時,錯誤會被默默吞掉 → 同時保存 payload 指紋,把衝突隔離並告警。
- 只說啟用了 exactly-once → 外部 sink 可能不在檢查點提交邊界內 → 核對端到端提交路徑,使用交易 sink 或版本化冪等寫入。
- 狀態清理後直接丟棄逾期事件 → 串流任務看似穩定,財務結果卻無法解釋 → 保存原始事件,建立 side output、批次重算與差異審核。
追問與應對
追問一:watermark 的亂序容忍量應該設定多少?
從生產資料計算 processingtime - eventtime 的分布,並依來源、用戶端版本與地區分層。先確定 ON_TIME 結果最晚可以等多久,以及允許進入修正串流的比例,再選擇能滿足兩者的分位點。上線後持續觀察 p95、p99 與長尾;延遲分布改變時走設定發布,不能自動追隨單一異常值。watermark 只決定準最終觸發,24 小時修正期間仍負責更長尾的資料。
追問二:一個 Kafka 分割區長時間沒有事件,為什麼會卡住視窗?
多輸入運算子的安全進度由最小輸入 watermark 決定;空閒分割區不推進時,最小值會保持不變。確認該分割區在門檻內沒有事件後,將它標記為 idle,使其暫時不參與最小值。門檻不能過短,因為恢復後的舊事件可能已落後於新 watermark;這部分要進入允許延遲或 side output,並單獨監控 idle 切換頻率。
追問三:為什麼去重狀態保留 24 小時仍不能承諾永不重複?
24 小時只涵蓋本題的自動修正期間。上游若在第 25 小時重播同一 ID,串流狀態已刪除,它可能再次進入彙總。永久不重複需要更長久的業務唯一索引、可查詢的事件登記表或離線全量去重;這些方案會增加儲存與寫入成本。答案要把保證寫成「在已宣告的重播期間內去重」,再用對帳處理期間外資料。
追問四:下游資料庫只支援 INSERT,不能 upsert 怎麼辦?
將每次結果寫成不可變變更日誌,鍵中包含視窗與 revision;讀取層用每個視窗的最大 revision 產生目前視圖。消費者必須依結果狀態區分完整快照與增量,不能將所有版本加總。若查詢成本無法接受,再非同步壓實到支援原子取代的服務表;原始版本日誌仍用於稽核與復原。
追問五:如何證明故障復原不會多算或少算?
在固定輸入序列上記錄預期的去重集合與各視窗 revision,接著分別在「來源已讀但狀態未檢查點」「狀態已檢查點但 sink 未確認」「sink 已寫入但檢查點未確認」三個位置終止任務。復原後重播相同輸入,斷言同一 event ID 只貢獻一次、sink 最後只保留最大 revision、最終視窗值與離線重算相同。沒有這三個提交邊界的故障注入,只看到任務顯示 RUNNING 不能證明端到端正確。