題幹與適用情境
一個 Kafka Topic 有 24 個分割區,尖峰寫入 12 萬筆/秒。某租戶產生 45% 的流量,Producer 依 tenantid 分割,結果一個分割區持續積壓,其他 Consumer 大多閒置。單一 Consumer 對目前處理邏輯可穩定處理 8000 筆/秒。業務真正要求的是同一 orderid 內有序,不要求整個租戶的事件彼此有序;事故期間不能增加分割區數。
請說明你會如何證明問題來自 key 傾斜,而非 Consumer、Broker 或下游故障;如何在當天降低積壓成長;如何重新設計分割 key 與遷移路徑;以及非同步並行處理時如何安全提交 offset。最後提出能證明修復有效的指標與故障測試。
這是一道資料工程與串流平台除錯題。目前公開的 Kafka 面試材料直接使用「一個分割區很熱、負責它的 Consumer 持續落後、其他 Consumer 閒置、暫時不能改變分割區數」的情境,要求候選人同時處理 Producer 分割策略、Consumer 並行度與順序限制。Apache Kafka 文件說明,帶 key 的紀錄預設依 key 的雜湊選擇分割區;語意分割可以保留局部順序與資料區域性,但也會把同一個 key 的負載集中到同一分割區。華為雲的 Kafka 實務文件也明確指出,一個分割區同一時間只能由一個 Consumer 消費,臨時新增分割區也很難快速消化既有積壓。
題目中的吞吐、占比、Consumer 能力與順序範圍都是供推導使用的面試假設,不代表任何特定公司的正式環境資料。
面試官評估重點
第一項是能否從總量指標下鑽到分割區指標。整個 Topic 的 lag、Consumer 平均 CPU 或 Consumer 數量,都可能掩蓋單一熱分割區。強回答會同時檢查每個分割區的寫入速率、消費速率、lag 斜率、leader 所在 Broker、訊息 key 分布與下游耗時,再依時間軸判斷是 Producer 突然傾斜、單一 Consumer 變慢、頻繁重新平衡,或 Broker 資源異常。
第二項是能否先算出問題規模。熱租戶寫入約為:
120,000 × 45% = 54,000 筆/秒若單一分割區對應的 Consumer 只能處理 8000 筆/秒,熱分割區的積壓成長約為:
54,000 - 8,000 = 46,000 筆/秒
46,000 × 600 = 27,600,000 筆/10 分鐘這表示「再加幾個一般 Consumer」不會改變熱分割區的消費上限,也表示只調整警示門檻無法止損。
第三項是能否辨識真正的順序不變量。目前的 key 把順序範圍擴大到租戶,但業務只要求訂單內有序。把 key 改為 order_id 可以提高基數並分散新訂單,不過必須先回答:同一訂單會不會在遷移時跨兩個分割區或兩個 Topic?若會,直接改 key 會破壞順序。
第四項是 offset 正確性。把一個分割區的資料交給工作執行緒池後,完成順序可能與 offset 順序不同。若 offset 105 已完成而 104 尚未完成,提交 105 會在程序崩潰後跳過 104。強回答會維護「最大連續完成 offset」,只提交所有較小 offset 都完成的位置,並讓下游寫入具備冪等性以承受重播。
第五項是方案邊界。更多分割區只能增加可用並行槽,不能拆開一個仍被固定 key 綁定的超大實體;更多 Consumer 也不能讓傳統消費群組內兩個 Consumer 同時消費同一分割區。如果單一訂單本身長期超過單分割區安全能力,又必須嚴格有序,就要最佳化串行路徑、限流,或重新定義可獨立處理的業務序列。
回答前需要釐清的問題
- 順序究竟要依租戶、訂單,還是單一事件流保證? 若必須租戶內全序,就不能直接把一個租戶拆到多個分割區;若只需訂單內有序,
order_id才是較合適的分割邊界。 - 45% 指的是事件數、位元組數,還是處理成本? 一筆大訊息或一次昂貴的下游寫入,可能讓事件數看似均勻但處理成本傾斜。診斷必須同時看 records、bytes 與處理時間。
- 積壓來自寫入突增,還是消費能力下降? 若熱分割區輸入一直是 5.4 萬筆/秒,根因是 key 傾斜與容量;若輸入不變但消費突然從 8000 降到 2000,應先檢查下游、GC、網路、磁碟與重新平衡。
- Consumer 寫到哪裡? Kafka 到 Kafka 可以考慮用交易一起提交輸出與輸入 offset;寫資料庫、物件儲存或外部 API 時,通常採至少一次處理,再用業務冪等 key 或
topic-partition-offset去重。 - 訂單生命週期有多長? 若訂單能在短時間內關閉,可讓舊訂單留在舊路由、新訂單進入新路由;若訂單長期存在,需要明確的遷移屏障、序號或路由狀態。
- 事故期間允許限流或降級嗎? 若可對熱租戶啟用配額、延後非關鍵事件或合併可壓縮更新,止損速度會快於程式與分割區遷移。
- 現有 Consumer 是否經常超過
max.poll.interval.ms? 若 poll 執行緒被重處理阻塞,重新平衡會進一步放大 lag;此時必須先拆開 poll 與處理,並限制在途工作。
30 秒回答框架
「我會先把總 lag 拆成分割區層級的寫入、消費與 lag 斜率,再對照 key 頻率、訊息位元組、處理耗時、重新平衡紀錄與分割區 leader 的 Broker 指標,證明是 tenantid 傾斜。依題目數據,熱分割區每秒進入 5.4 萬筆、只能處理 8000 筆,積壓每秒增加 4.6 萬筆,增加一般 Consumer 不會幫助同一分割區。當天我會先對熱租戶限流或降級,把熱分割區隔離到專用 instance,並在只需訂單內有序的前提下,依訂單串行、跨訂單並行;offset 只提交到最大連續完成位置,下游保持冪等。長期把新訂單遷移到依 orderid 分割的新 Topic,舊訂單留在舊路由直到結束,避免一個訂單跨路由亂序。最後用傾斜壓測驗證每分割區吞吐、lag 斜率、端到端 p99、重複與亂序,並執行 Consumer 崩潰和重新平衡測試。」
分步深入解答
第一步:先證明是哪一層形成熱點
把觀察視窗固定在同一段尖峰時間,依序對齊以下指標:
- 每個分割區的 Producer records/s、bytes/s 與最大 offset 成長;
- 每個分割區的消費 records/s、目前 offset 與 lag 斜率;
- key 的頻率、位元組數與估算處理成本分布;
- 熱分割區 leader 所在 Broker 的 CPU、網路、磁碟等待與請求延遲;
- 對應 Consumer 的 poll 間隔、批次大小、處理耗時、GC、錯誤與重新平衡紀錄;
- 下游資料庫、儲存或 API 的分割區層級耗時與限流。
判斷規則很直接:若熱分割區輸入約 5.4 萬筆/秒、其他 23 個分割區合計約 6.6 萬筆/秒,那麼其他分割區在均勻假設下平均只有約 2870 筆/秒。單一 Consumer 的 8000 筆/秒足以處理一般分割區,卻遠低於熱分割區輸入,key 傾斜已能解釋「一個落後、其餘閒置」。若熱分割區輸入沒有異常,消費速率卻與下游延遲同步惡化,就不能誤診為分割 key 問題。
也要檢查 Broker 熱點。一個分割區的 leader 集中到已過載的 Broker 時,寫入與拉取都可能變慢。移動 leader 或重新平衡 replica 可以排除 Broker 放置問題,但不會改變同一個 tenant_id 仍落到一個分割區的事實。
第二步:先降低積壓斜率,再討論清空積壓
事故處理的第一個目標是讓:
熱分割區輸入速率 ≤ 熱分割區安全處理速率最快的槓桿通常在入口。對熱租戶啟用有上限的配額;延後可重建的分析事件;對只需保留最新狀態的更新進行合併;把非關鍵工作寫入獨立降級路徑。每種降級都要標明遺失、延遲或重播語意,不能把靜默丟訊息當成限流。
Consumer 端可以在受控維護時段停止原消費群組,再用互斥且不重疊的明確分配,讓一台資源充足的 instance 只負責熱分割區,其餘 instance 負責其他分割區。不能直接啟動第二個一般消費群組來「幫忙」,因為它會獨立讀取完整資料並產生重複業務效果。隔離能避免熱分割區拖慢同一 instance 上的一般分割區,但單執行緒處理能力仍只有 8000 筆/秒。
若業務只要求訂單內有序,可在熱分割區 Consumer 內依 order_id 分派:同一訂單進入同一串行佇列,不同訂單交給有界工作池並行處理。工作池必須有在途上限;達到上限時暫停拉取或縮小批次,避免把 Kafka 積壓搬成程序記憶體積壓。poll 迴圈不能被長任務阻塞,否則超過 max.poll.interval.ms 會觸發重新平衡並造成更大停頓。
第三步:用連續完成水位提交 offset
分割區內並行會打亂「完成順序」,但不能打亂「可提交順序」。可以為每個分割區維護以下狀態:
nextCommitOffset = 最小尚未完成的 offset
completed = 已完成但前方仍有缺口的 offset 集合
工作完成(offset):
將 offset 加入 completed
當 completed 包含 nextCommitOffset:
從 completed 移除 nextCommitOffset
nextCommitOffset 加 1
提交 nextCommitOffsetKafka 提交的是下一筆要讀取的位置,因此只有 104、105、106 都完成後,才能把提交位置推進到 107。若 105 先完成、104 仍在重試,提交位置必須停在 104。程序在提交前崩潰會重播已完成但尚未提交的訊息,因此資料庫寫入應使用 event_id 或業務唯一 key 做冪等 upsert;缺少業務 key 時,可以用 topic-partition-offset 作為去重識別。
不要把 offset 提交與外部副作用說成天然原子。Kafka 到 Kafka 的處理可以把輸出紀錄與消費 offset 放進同一 Kafka 交易;寫外部資料庫時,更常見的邊界是至少一次消費加冪等寫入,或把寫入與去重紀錄放在同一個資料庫交易中。
第四步:把分割邊界縮小到真實順序域
目前 24 個分割區的平均容量未必不足。若每分割區安全能力為 8000 筆/秒,並把目標使用率限制在 70%,每分割區規劃能力約為 5600 筆/秒:
120,000 ÷ 5,600 ≈ 21.4平均分布時,24 個分割區可以涵蓋題目中的尖峰,並保留少量餘裕。失敗來自 45% 的流量被一個低基數 key 固定到同一分割區,而非分割區總數本身。長期 key 應符合三個條件:對應最小必要順序域、基數足夠高、在業務尖峰下分布可預測。此處自然候選是 orderid,或在確有租戶區域性需求時使用 (tenantid, order_id) 的穩定編碼。
受控加鹽只適合原 key 內可以亂序,或下游能重新排序的情境。若必須保證訂單內順序,不應對同一訂單隨機加鹽,否則同一訂單會跨分割區並行抵達。若單一訂單就超過單分割區能力,提高 key 基數也無效,只能最佳化該訂單的串行路徑、限制其速率,或把業務協定拆成帶明確相依關係的獨立序列。
第五步:透過版本化路由安全遷移
直接在原 Topic 增加分割區並立即更換 key 有兩個風險:預設雜湊映射會隨分割區數改變,既有 key 可能移動;同一訂單在切換點前後的事件可能進入不同分割區,而 Kafka 無法提供跨分割區順序。
較穩妥的方案是建立依 order_id 分割的新 Topic,並替 Producer 路由加上版本:
- 切換點之後建立的新訂單全部進入新 Topic;
- 切換前已存在的訂單繼續使用舊 Topic 與舊 key,直到訂單關閉;
- 所有 Producer 依同一份訂單路由狀態送出,不能各自用本機時間猜測;
- Consumer 同時處理兩條路由,但每個訂單在任一時刻只屬於其中一條;
- 舊訂單清空並達到保留期後,再關閉舊 Topic。
若訂單不能自然結束,就為單一訂單建立遷移屏障:暫停該訂單的新事件,等待舊路由消費到已記錄的最後序號或 offset,更新路由版本,再恢復送出。需要零停頓時,可以攜帶單調序號並在下游合併,但這會引入快取、逾時與缺口復原,只有業務確實需要才值得採用。
第六步:用傾斜流量與故障驗證方案
驗證不能只看總吞吐,至少要涵蓋:
- 45% 流量來自單一租戶、訂單分布接近真實尖峰的壓測;
- 每分割區輸入、消費、lag 斜率與最大值;
- 端到端 p50、p95、p99,以及積壓清空預估時間;
- 工作池在途數、最老工作年齡、重試與 dead-letter 數量;
- 重複寫入率、同訂單亂序數與冪等衝突數;
- Consumer 在 offset 缺口存在時崩潰並重新啟動;
- 處理時間超過預期時是否觸發重新平衡;
- 新舊 Topic 路由交界處,同一訂單是否只出現一條路徑。
通過條件應包含穩定性,而非只有短時尖峰:熱分割區 lag 斜率在持續尖峰下不再為正;Consumer 崩潰後允許重播但不遺失業務效果;同一訂單沒有亂序;新訂單均勻分布;舊訂單能依計畫排空。若修復後總吞吐提高,但一個大訂單仍反覆形成熱點,表示順序域或業務限流問題尚未解決。
高品質示範回答
「我不會先加 Consumer,因為題目已說明一個分割區落後而其他 Consumer 閒置。我要先用分割區層級 records/s、bytes/s、lag 斜率與 key 頻率證明傾斜,同時排除熱分割區 leader 所在 Broker、Consumer 重新平衡與下游耗時異常。
依題目數據,熱租戶每秒產生 5.4 萬筆。單一分割區 Consumer 只能處理 8000 筆,因此 lag 每秒增加約 4.6 萬筆,十分鐘就是 2760 萬筆。剩餘 55% 流量若大致分散在其他 23 個分割區,平均每個分割區約 2870 筆/秒,正好解釋為什麼其他 Consumer 有餘裕。傳統消費群組中,一個分割區同一時間只交給一個 Consumer,再增加一般 instance 不會提高這個分割區的吞吐。
當天我會先降低輸入斜率:對熱租戶啟用明確配額,把可延後或可合併的非關鍵事件移到降級路徑。接著在受控時段把熱分割區隔離給專用 instance,避免它拖慢一般分割區。因為業務只要求訂單內有序,我會讓 Consumer 依 orderid 分派到有界工作池:同一訂單串行、不同訂單並行。offset 不依最快完成的工作提交,而是維護最大連續完成水位;任何前方缺口未完成時都不越過它。崩潰會重播未提交訊息,所以下游用 eventid 或業務唯一 key 做冪等寫入。
長期我不會把『加分割區』當成唯一修復。以 70% 目標使用率計算,單分割區規劃能力約 5600 筆/秒,24 個分割區平均可以涵蓋 12 萬筆/秒;真正問題是 tenantid 把 45% 流量鎖在一個分割區。我會建立依 orderid 分割的新 Topic。切換後建立的新訂單進入新 Topic,既有訂單繼續走舊路由直到結束,路由版本由共享狀態決定,確保一個訂單不會跨兩個 Topic。
上線前我會用相同的 45% 租戶傾斜進行壓測,觀察每分割區吞吐、lag 斜率、端到端 p99、重複與亂序。再刻意讓 Consumer 在多個 offset 亂序完成時崩潰,確認重新啟動後只發生冪等重播、不遺失資料;觸發重新平衡並檢查復原時間;最後驗證新舊路由邊界上的訂單只走一條路徑。如果單一訂單本身超過單分割區能力,我會明確告訴面試官,繼續加 Consumer 或分割區都解決不了,必須最佳化串行處理、限流,或重新定義可獨立排序的業務單元。」
常見錯誤
- 只看 Topic 總 lag → 平均值掩蓋單分割區輸入與消費斜率 → 同時畫出每分割區 records、bytes、lag 與 key 分布。
- 看到 lag 就增加 Consumer → 一個分割區在傳統消費群組內同一時間只能由一個 Consumer 處理 → 先判斷分割區數、分配與單分割區能力,再決定擴充。
- 立刻增加分割區 → 舊積壓不會自動搬移,預設 key 映射還可能改變 → 先止損,再使用版本化 Topic 與明確遷移邊界。
- 隨機替熱 key 加鹽 → 同一訂單可能跨分割區並亂序 → 只在允許亂序時加鹽;本題依
order_id保留真實順序域。 - 執行緒池工作完成就提交對應 offset → 較小 offset 尚未完成時,會在崩潰後被跳過 → 只提交最大連續完成水位。
- 聲稱提交 offset 後就是恰好一次 → 外部資料庫寫入與 Kafka offset 通常不在同一交易 → 說明至少一次、冪等 key 與交易邊界。
- 用第二個一般消費群組幫助熱分割區 → 第二組會獨立消費完整 Topic,造成重複業務效果 → 採用互斥的明確分配或受控的新處理架構。
- 只測均勻流量 → 平均值通過不能證明熱 key 已消失 → 重播真實或合成的傾斜分布,並檢查最大分割區而非平均分割區。
- 忽略單一實體上限 → 嚴格有序的單一實體無法由多個分割區並行處理 → 明確順序、限流與串行路徑之間的硬邊界。
追問與應對
追問一:為什麼不直接把分割區數從 24 增加到 48?
新增分割區能增加未來的並行槽,卻不會把舊分割區的既有積壓自動拆開,也不會讓同一個 tenant_id 同時落到多個分割區。預設 key 雜湊在分割區數變更後還可能把既有 key 映射到新分割區,讓同一訂單在切換前後跨分割區。應先修正順序域與遷移路徑,再依壓測決定是否需要更多分割區。
追問二:分割區內並行後,如何避免記憶體無限成長?
為每個分割區設定最大在途紀錄數與最大未提交 offset 視窗。達到上限時暫停該分割區拉取或減少每次交給工作池的紀錄,同時讓 poll 與處理解耦,維持必要的消費群組活性。監控最老未完成 offset 的年齡;單一訂單持續阻塞時,將它隔離、重試或轉入人工處理,不能讓後續所有 offset 永久卡住。
追問三:如果下游資料庫不支援冪等 upsert,該怎麼辦?
在同一個資料庫交易中先寫入去重表,唯一 key 使用業務 event_id 或 topic-partition-offset,插入成功後再執行副作用;唯一 key 衝突代表該事件已處理。若副作用發生在外部且無法交易化的 API,使用帶冪等 key 的介面、outbox 或可查詢的操作狀態。無法建立冪等邊界時,就不能承諾崩潰重播不會產生重複效果。
追問四:如果業務後來要求同一租戶內嚴格有序呢?
此時 tenant_id 就是不可拆分的順序域,依訂單並行不再合法。單租戶輸入超過單分割區能力時,只能提升該串行處理路徑、限制租戶速率,或與業務方重新定義哪些事件可以獨立排序。可以建立帶全域序號的分片與下游合併器,但它把排序等待、缺口復原與可用性成本轉移到消費端,不能當成免費擴充。
追問五:如何估算清空 2760 萬筆積壓需要多久?
先讓輸入低於處理能力。若限流後熱分割區輸入降到 3000 筆/秒,而最佳化後的處理能力達到 1.2 萬筆/秒,淨清空速度是 9000 筆/秒:
27,600,000 ÷ 9,000 ≈ 3,067 秒 ≈ 51 分鐘這仍是基於穩定速率的估算。實際計畫還要加入重試、下游限流、訊息大小差異與安全餘裕,並持續用 lag 斜率更新預估完成時間。
追問六:遷移期間如何證明沒有訂單跨新舊 Topic?
替每個訂單保存唯一的 partitioning_version,所有 Producer 送出前讀取或快取帶版本的路由紀錄;版本只有在遷移屏障滿足後才能改變。Consumer 記錄訂單首次出現的 Topic 與版本,發現同一訂單同時出現在兩條活動路徑時立即警示並停止自動推進。壓測與灰度期間還要核對 Producer 紀錄、兩側 offset 與下游訂單序列,不能只看總訊息數相等。