題目與適用情境
設計一套供多個業務團隊使用的分散式訊息佇列。穩態入口為每秒 100 萬則、平均每則 1 KB, 流量尖峰可持續 15 分鐘達到每秒 300 萬則。訊息預設保留 24 小時,生產者需要批次發布, 消費者以群組拉取、提交進度,並在保留期內重播。相同業務鍵的訊息需要局部有序;不同鍵可平行處理。 一般訊息採至少一次遞送,發布確認 p99 目標小於 50 毫秒。每個分割區跨三個可用區保存三份副本, 失去一個可用區時不能遺失已確認的訊息。
單則訊息通常很小,但介面允許最大 100 MB 的業務酬載。大型酬載不應反覆通過 Broker 日誌、 副本同步與消費者緩衝區;本題先把它寫入物件儲存,佇列只保存不可變參照、大小與校驗和。 題目中的吞吐量、延遲、保留期、門檻與副本數都是面試假設,必須在目標硬體上壓測,不代表任何產品承諾。
2026 年 5 月發布的中文系統設計文章直接討論每日百億則、尖峰百萬 QPS 的訊息佇列設計;PracHub 在 2026 年 6 月更新的公開題目要求涵蓋 API、消費者群組、offset、分割區、副本、大型酬載與多租戶隔離。 兩份材料證明這是一道目前仍可查證的系統設計題。公司歸屬只有單一頁面陳述,因此本文不設定公司名稱。
面試官考察重點
第一,候選人是否先定義遞送契約。發布成功、Broker 持久化、消費者取得訊息、業務副作用完成與 消費進度提交是五個不同邊界。把它們都稱為「訊息已成功」會隱藏兩類故障窗口:生產者沒收到確認時 可能重複發布,消費者完成副作用但尚未提交進度時可能重複消費。
第二,分割區、順序與擴充是否來自同一套推導。相同鍵進入同一分割區,才能保留該鍵的日誌順序;一個 分割區同時限制寫入吞吐量與同組消費平行度。增加分割區提高平行度,卻無法拆開持續超載的熱鍵,也不會 提供全域順序。
第三,副本確認是否真的涵蓋故障。強回答會說明何時能向生產者確認、哪些副本能參與新 Leader 選舉、 舊 Leader 復原後如何由 epoch 隔離。只說「三副本自動切換」無法證明已確認訊息在故障後仍然存在。
第四,消費進度與業務結果能否形成閉環。先提交 offset 再處理可能漏掉業務結果;先處理再提交會在當機時 重播。至少一次的正確落點是後者,再由穩定訊息 ID、業務冪等鍵、唯一限制或版本條件吸收重複。Broker 內部交易只能涵蓋參與同一交易的資源,無法自動讓外部付款、郵件或資料庫取得 exactly-once。
最後,佇列是否能在真實壓力下守住邊界。候選人需要計算 24 小時儲存與尖峰積壓,處理大型訊息、熱分割區、 慢速消費、毒訊息、磁碟已滿與租戶噪聲,並以故障注入證明「已確認不遺失、未提交不跳過、舊擁有者不能 推進進度」。
回答前需要釐清的問題
- 要設計保留日誌,還是領取後刪除的工作佇列? 本題選擇保留日誌,因為需要多個消費者群組與重播。
若每項工作只需一個 Worker 領取且不必重播歷史,具有可見性逾時的租約佇列會更簡單。
- 順序範圍是什麼? 只保證同一主題、分割區內的追加順序,相同業務鍵固定到一個分割區。不要求跨鍵、
跨分割區或跨主題全域有序;全域有序會把吞吐量壓到單一序列日誌。
- 發布確認代表什麼? 三份副本中有兩份已持久化記錄,而且控制面仍認可目前 Leader epoch 後才確認。
少於兩份可用副本時,拒絕需要強持久性的寫入。
- 消費語意是什麼? 預設採「處理成功後提交下一個 offset」的至少一次語意。外部副作用必須支援冪等
或對帳。允許遺失但不能重複的低價值情境,才可先提交再處理。
- 消費者需要任意重播嗎? 可在 24 小時保留窗口內依 offset 或時間重設消費者群組;超過保留邊界只能
從長期封存還原,若沒有封存就明確失敗。
- 毒訊息能否跳過? 一般主題允許有限重試後進入死信主題。嚴格依鍵有序的主題無法無代價跳過;
應暫停該鍵或分割區並修復,否則後續訊息可能越過失敗訊息。
- 100 MB 酬載必須內嵌嗎? 不需要。假設 256 KiB 以內可內嵌,更大的酬載以物件參照傳遞。
門檻由壓測與成本決定,100 MB 是物件酬載上限。
- 跨區域需求是什麼? 主方案為單一區域、三個可用區。跨區域災難復原採非同步複寫,無法同時承諾
零資料遺失與本地寫入延遲;若業務要求跨區域零 RPO,確認路徑與延遲預算都要重做。
- 租戶要隔離到什麼程度? 預設共享 Broker,但依租戶限制入口、出口、儲存、分割區與連線。超大型或
強法遵租戶可放入獨立 Broker 池,控制面與協定維持一致。
30 秒回答架構
「我會先把它定義成可保留、可重播的分割區日誌。生產者依業務鍵路由到分割區 Leader,批次寫入後由 三個可用區中的兩份副本持久化再確認;同鍵有序,全域不保序。消費者依群組獨占分割區,拉取後先完成 業務結果,再提交下一個 offset,因此系統是至少一次,外部副作用要用訊息 ID 做冪等。穩態入口約 1 GB/s、每日 86.4 TB 邏輯資料,三副本約 259.2 TB;15 分鐘三倍尖峰相對穩態消費能力多出約 1.8 TB backlog。超過 256 KiB 的酬載放物件儲存,佇列只傳參照與校驗和。最後用配額、公平排程、 lag 警示、Leader 故障、確認回應遺失與消費者當機測試,證明訊息不會靜默遺失且重複可控。」
分步深入解答
第一步:先寫 API 與不變量,再畫元件。
核心介面只公開主題、發布、拉取、提交與重設進度:
POST /v1/topics
POST /v1/topics/{topic}/messages:publish
POST /v1/groups/{group}/messages:fetch
POST /v1/groups/{group}/offsets:commit
POST /v1/groups/{group}/offsets:reset發布請求包含 tenantid 的認證上下文、主題、選填 messagekey、穩定 message_id、酬載或物件參照, 以及生產者 epoch 與分割區內序號。批次回應傳回每則訊息的分割區、offset 與提交狀態。拉取請求帶消費者 群組、分割區所有權 generation、起始 offset、最大位元組與長輪詢時間;提交請求寫入「下一則要讀取的 offset」。
設計要守住四個不變量:已確認記錄在一個可用區故障後仍可讀;消費者只能看見已提交前綴;同一 Leader epoch 內 offset 單調增加;過期 generation 的消費者不能提交進度或繼續寫入結果。介面中的 messageid 用於業務去重,producerid + epoch + sequence 用於 Broker 辨識同一次發布重試。
第二步:分開控制面與資料面。
控制面:租戶與 ACL、主題設定、分割區配置、副本成員、Leader epoch、配額
資料面:
Producer -> metadata cache -> partition leader -> follower replicas
Consumer group -> group coordinator -> partition leaders -> business sink小型一致性叢集保存主題與分割區中繼資料,為每次 Leader 任期分配遞增 epoch。它不承載訊息正文。 Broker 負責追加日誌、複寫、讀取與保留。用戶端快取分割區 Leader,遇到舊 epoch 或非 Leader 回應後 重新整理中繼資料。這個拆分讓訊息吞吐量不經過單一代理,也讓控制面故障期間既有分割區能在有限租約內 繼續服務;建立主題與遷移分割區可以暫時失敗,不能用過期中繼資料任意選主。
第三步:以分割區日誌取得吞吐量、重播與局部順序。
每個分割區是一組只追加的 segment,記錄包含:
MessageEnvelope {
tenant_id, topic, partition, offset
message_id, message_key, producer_id, producer_epoch, sequence
created_at, headers, payload_or_ref, payload_size, checksum
}活躍 segment 依序追加,稀疏 offset 索引定位讀取位置,關閉的 segment 依時間或大小捲動。消費者依 offset 批次拉取,Broker 可利用順序 I/O、page cache 與批次網路傳輸。24 小時到期後以 segment 為 單位刪除;正被合法重播或上傳到分層儲存的 segment 受參照保護,避免保留工作與讀取競爭。
分割區鍵採用可信租戶、主題與業務鍵的雜湊。具有相同業務鍵的訊息固定到同一分割區;無鍵訊息可輪詢或 使用黏性批次分配。只依租戶分割會形成大型租戶熱點,完全隨機則會失去業務鍵順序。增加分割區只影響未來 訊息;若改變雜湊取模,相同鍵可能跨越新舊分割區。需要穩定順序時使用虛擬分片到實體分割區的映射,並在 遷移時停止該虛擬分片、記錄切換 offset、排空舊擁有者,再以新 epoch 恢復。
第四步:讓副本確認與選主共用同一提交定義。
每個分割區有三份跨可用區副本。Leader 為記錄分配 offset,將批次寫入本機持久日誌並平行複寫;任意 兩份副本持久化後推進 commit_watermark,再向生產者確認。消費者只讀取小於該水位的 offset。 Leader 故障時,控制面只從具有已提交前綴的副本中選出新 Leader,並遞增 epoch。舊 Leader 復原後先 截斷未提交尾端、追上新 Leader,不能繼續接受舊 epoch 請求。
這項選擇能承受一個可用區故障。故障後只剩兩份副本,寫入仍需兩者確認,延遲與容量會下降;其中任何一份 再次失效時,強持久主題停止確認新寫入,直到補足副本。若為了可用性強行選出落後副本,已確認不遺失的 承諾就會失效。網路分割期間只有具備提交多數的一側能寫入,另一側必須被 fencing。
生產者在「已提交但確認回應遺失」時會重試。Broker 以生產者 epoch 與單調 sequence 在分割區內去重, 傳回原 offset;舊 epoch 的殭屍生產者會被拒絕。此機制只去除發布重試造成的日誌重複。兩個獨立業務請求 誤用不同 ID,或消費者重複執行外部副作用,仍需業務層處理。
第五步:把消費者群組所有權、進度與業務結果連起來。
同一消費者群組內,一個分割區同時只分配給一個成員。Group coordinator 維護成員、租約、generation 與分配;成員逾時或擴縮容時產生新 generation,舊成員的 fetch 與 commit 會被拒絕。漸進式重新平衡 可以只移動必要分割區,減少整組停頓,但消費者仍要在撤銷所有權前停止取得新訊息並提交已完成進度。
預設處理順序是:拉取批次、執行冪等業務寫入,成功後提交下一個 offset。消費者若在業務提交後、offset 提交前當機,接手者會重播已完成訊息,因此形成至少一次。穩定 message_id 可進入目標資料表唯一限制、 冪等記錄,或與業務狀態在同一資料庫交易中提交。若輸出仍寫回同一訊息系統,可把輸出記錄與輸入進度放入 一個 Broker 交易;外部資料庫、付款或郵件不參與該交易時,仍需冪等、狀態查詢或對帳。
進度依 (tenant, group, topic, partition) 存入複寫的中繼資料日誌,並攜帶 generation。監控同時顯示 logendoffset - committed_offset 與最老未處理訊息年齡。只看則數會誤判大小差異明顯的 backlog, 還要記錄落後位元組與依目前淨消費速率估算的追趕時間。
第六步:明確重試、死信與順序的衝突。
暫時性網路與限流錯誤進入帶抖動的延遲重試主題;確定無法處理的 schema、權限或業務驗證錯誤不做盲目 重試。重試訊息保留原 message_id、來源主題、分割區、offset、首次時間、嘗試次數與錯誤分類。超過 次數或業務期限後進入死信主題,發出警示並允許受控 redrive;redrive 仍使用原 ID,避免繞過冪等。
把失敗訊息移走會讓後續訊息先完成,因此與嚴格同鍵順序衝突。訂單狀態必須嚴格演進時,可以暫停該鍵, 把該鍵後續訊息暫存到獨立有序通道,修復後從原 offset 恢復;最簡單但影響較大的方案是暫停整個分割區。 若業務接受最終狀態依版本收斂,可讓後續訊息繼續,並由目標拒絕舊版本。選擇必須寫進主題契約,不能同時 承諾「毒訊息不阻塞」與「絕不越序」。
第七步:讓大型訊息走物件參照,並關閉垃圾回收競態。
本題把 256 KiB 設為內嵌門檻。更大酬載先透過短期上傳憑證寫入物件儲存,使用不可變物件鍵並記錄大小、 內容雜湊與加密資訊;上傳成功後才發布參照訊息。消費者取得訊息後讀取物件並驗證雜湊。Broker 的副本 日誌只複寫小型 envelope,避免一則 100 MB 訊息占滿網路緩衝區、複寫批次與消費者記憶體。
上傳成功但參照未發布會留下孤兒物件,由上傳工作階段 TTL 清理。參照已提交後,物件保留期必須涵蓋訊息 保留、合法重播、死信與安全餘裕;刪除工作先掃描仍受保護的參照,再延遲刪除。物件讀取失敗時訊息保持 未提交並重試,不能確認消費後才發現正文永久缺失。大型訊息另設每租戶位元組率、同時下載與儲存配額, 因為只按訊息則數限流會失真。
第八步:用容量反推分割區、磁碟與追趕餘裕。
依十進位 1 KB 計算,穩態邏輯入口為:
1,000,000 messages/s × 1,000 bytes = 1 GB/s
1 GB/s × 86,400 s = 86.4 TB/day
三副本寫入量下界 = 86.4 × 3 = 259.2 TB/day15 分鐘內總入口為 3 GB/s × 900 = 2.7 TB。若消費者只能維持穩態 1 GB/s,新增 backlog 為:
(3 GB/s - 1 GB/s) × 900 s = 1.8 TB尖峰結束後,若消費者可持續處理 1.5 GB/s,而新入口仍是 1 GB/s,淨追趕速度為 0.5 GB/s,清空 1.8 TB 理論上需要 3,600 秒,也就是約 1 小時。實際還要計入副本復原、批次開銷、壓縮率、索引、 檔案系統餘裕與大型訊息物件儲存,因此這些數字是容量下界。
分割區數同時受位元組率與訊息率限制。假設在三副本與目標 p99 下,壓測得到單一分割區可持續 40 MB/s 與每秒 4 萬則,那麼尖峰兩個維度都至少需要 75 個分割區;再保留 50% 的故障與重新平衡餘裕,至少約 113 個,實際可選擇 128 個。此單分割區能力是面試壓測假設,硬體、批次大小、確認策略或訊息大小改變後 都必須重測,不能把 128 當成通用答案。
第九步:實作背壓、多租戶隔離與可驗證維運。
消費者採長輪詢,並用 max_bytes 與在途批次數控制自己的速率。Broker 磁碟水位升高時先阻止建立新 分割區、降低低優先租戶突發額度,再拒絕超過配額的發布並傳回可重試訊號;無限記憶體佇列與無限重試只會 把壅塞變成程序崩潰。生產者使用有上限的批次緩衝、截止時間與帶抖動退避,不能在 Broker 故障時造成 同步重試風暴。
租戶身分來自認證憑證,不能信任訊息正文。入口依訊息數與位元組限流,出口依 fetch 位元組與請求 CPU 公平排程,並限制儲存、分割區數、消費者群組數、連線、在途請求與大型物件。分割區配置避免同一租戶所有 副本或所有熱分割區集中在少數 Broker。超大型租戶遷移到獨立池,小型租戶共享池仍保留逐租戶計量與拒絕指標。
關鍵指標包括發布確認 p50/p95/p99、錯誤與未知結果,依分割區的入口位元組、Leader 與副本 lag、 commit_watermark、磁碟水位、熱鍵、消費者群組已提交 offset、消費 lag 與最老年齡、重新平衡次數、 重試與死信、物件孤兒與讀取 失敗,以及依租戶的限流與公平性。端到端 canary 發布穩定 ID,完成冪等寫入後提交 offset,再核對 Broker、消費者群組與業務結果。
故障矩陣至少涵蓋:Leader 在複寫前、提交後與回傳確認前當機;一個可用區失效;網路分割;磁碟已滿; 過期 Leader 復原;熱鍵;15 分鐘三倍尖峰;消費者在業務提交前後當機;重新平衡時舊成員繼續提交; 毒訊息;物件上傳成功但發布失敗;物件讀取故障與死信 redrive。驗收斷言是已確認訊息不遺失、未提交訊息 不越過、同鍵順序符合主題契約、舊 generation 無法推進進度,而且所有重複、拒絕與丟棄都有可歸因指標。
高品質示範回答
「我會先確認這是一套需要多個消費者群組與 24 小時重播的保留日誌服務。主題切成分割區,相同業務鍵 固定到同一分割區,所以順序保證只到鍵與分割區;不同分割區平行處理。生產者從控制面取得 Leader 中繼資料後直接批次寫入,每個分割區三份副本跨可用區配置,兩份副本持久化後才推進 commit_watermark 並回傳確認。新 Leader 只能從包含已提交前綴的副本中選出,epoch 會拒絕舊 Leader 與舊生產者。
儲存採用只追加 segment 與稀疏 offset 索引,消費者依群組獨占分割區並長輪詢。它先冪等提交業務結果, 再提交下一個 offset;當機窗口會重播,但不會靜默跳過。生產端以 producer epoch 與 sequence 去除確認 遺失造成的發布重試,消費端以穩定 message ID、唯一限制或版本條件吸收副作用重複。我只會在輸入進度與 輸出都屬於同一 Broker 交易時談端到端交易,外部系統仍需冪等或對帳。
容量方面,穩態是 1 GB/s 與每日 86.4 TB 邏輯資料,三副本寫入下界約 259.2 TB。15 分鐘三倍尖峰在 消費能力維持穩態時會多積壓 1.8 TB;復原後若有 0.5 GB/s 淨追趕能力,理論清空約一小時。分割區數取 訊息率與位元組率計算結果的較大值,再保留故障餘裕,並以實際硬體壓測校準。
超過 256 KiB 的正文先寫物件儲存,佇列只保存不可變參照、大小與校驗和;孤兒上傳依工作階段 TTL 清理, 已提交參照的物件保留到訊息、重播與死信窗口之後。重試保留原訊息 ID。嚴格有序主題遇到毒訊息時暫停 該鍵或分割區,因為直接送入死信會讓後續訊息越序。
最後我會依租戶限制入口、出口、儲存、分割區與大型物件,把熱租戶隔離到獨立池,並監控確認延遲、未知 發布、副本 lag、磁碟水位、消費最老年齡、熱鍵與死信。故障測試會涵蓋確認回應遺失、Leader 提交前後 當機、可用區故障、舊成員提交進度、消費者業務寫入後當機及物件儲存故障,逐項驗證確認邊界。」
常見錯誤
- 錯誤表現:只畫 Producer、Kafka、Consumer → 失敗原因:元件名稱沒有定義確認、進度、順序與故障邊界 → 修正方法:先寫遞送契約與四個不變量,再讓元件逐項對應。
- 錯誤表現:承諾全域有序並繼續水平擴充 → 失敗原因:全域順序需要單一序列決定點,分割區平行會失去該順序 → 修正方法:把順序縮小到業務鍵與分割區,明確熱鍵上限。
- 錯誤表現:Leader 本機落盤就確認 → 失敗原因:Leader 所在可用區故障可能帶走唯一持久副本 → 修正方法:跨可用區提交多數持久化後確認,只從擁有已提交前綴的副本選主。
- 錯誤表現:消費者收到訊息立刻提交 offset → 失敗原因:提交後當機會讓業務結果永久缺失 → 修正方法:先冪等提交業務結果,再提交下一個 offset,接受可控重播。
- 錯誤表現:把 Broker exactly-once 等同外部副作用 exactly-once → 失敗原因:外部系統不參與 Broker 交易,確認遺失仍有重複窗口 → 修正方法:使用業務冪等鍵、唯一限制、版本條件或對帳。
- 錯誤表現:失敗訊息一律立即送死信 → 失敗原因:後續同鍵訊息可能越過它,破壞狀態順序 → 修正方法:依主題契約選擇暫停鍵、暫停分割區或允許版本收斂。
- 錯誤表現:100 MB 酬載直接寫入 Broker 日誌 → 失敗原因:少數大型訊息會占滿複寫、緩衝與拉取批次 → 修正方法:物件儲存保存正文,日誌保存參照、大小與校驗和。
- 錯誤表現:只按訊息則數進行限流與容量規劃 → 失敗原因:1 KB 與 100 MB 訊息的網路、磁碟及記憶體成本差距極大 → 修正方法:同時依則數、位元組、在途批次與物件並行數計量。
- 錯誤表現:增加消費者就能消除所有 lag → 失敗原因:同組每個分割區同時只有一個擁有者,熱鍵仍受單分割區序列路徑限制 → 修正方法:先定位分割區與鍵分布,再決定擴分割區、拆業務鍵或限流。
- 錯誤表現:只監控 Broker 存活 → 失敗原因:存活的叢集仍可能出現副本落後、磁碟耗盡、舊進度與死信增加 → 修正方法:監控分段延遲、提交前綴、最老訊息、追趕時間與端到端 canary。
追問與應對
追問一:需要跨區域零資料遺失,同時維持 50 毫秒 p99,該怎麼做?
跨區域同步確認會把廣域往返時間放入發布路徑,50 毫秒目標是否可達取決於區域距離與網路尾端延遲。先讓 業務在零 RPO 與本地低延遲之間排序。零 RPO 較重要時,寫入必須等待遠端提交多數,並重新制定延遲 SLO; 低延遲較重要時採區域內同步、跨區域非同步,明確故障時可能遺失尚未複寫的尾端。雙主還要解決相同鍵的 歸屬與衝突,通常讓一個主題或鍵範圍只有一個主要區域更容易維持順序。
追問二:一個租戶的單一業務鍵達到每秒 20 萬則,增加到 128 個分割區為何沒有改善?
相同鍵必須落到同一分割區才能維持順序,因此它仍受單一分割區每秒約 4 萬則的壓測上限限制。可選動作只有 最佳化單分割區路徑、對該租戶限流,或重新定義獨立順序域,例如把一個帳戶拆成互不影響的子實體鍵。若 業務堅持該鍵全序,系統應拒絕超過序列容量的承諾;把同一鍵隨機打散只會把容量問題換成亂序問題。
追問三:消費者已經扣款,隨後在提交 offset 前當機,如何避免重複扣款?
使用 message_id 或業務操作 ID 作為付款冪等鍵。付款方若支援冪等介面,重播繼續使用同一鍵並查詢原 結果;若只控制本機資料庫,在同一交易中寫入業務狀態與已處理訊息唯一記錄,再由 outbox 推進外部動作。 外部系統沒有冪等或查詢能力時,只能記錄 UNKNOWN、對帳與人工補償,不能靠提前提交 offset 隱藏風險。
追問四:毒訊息進入死信後,如何安全 redrive?
先修復消費者或資料,再凍結 redrive 範圍,保留原訊息 ID、來源 offset、首次時間與嘗試歷程。影子消費 驗證新版本可處理後,依租戶與分割區限速重播,目標冪等規則繼續生效。嚴格有序主題還要暫停相關鍵的後續 訊息,從失敗 offset 依序恢復;若順序已被允許打破,目標使用業務版本拒絕舊狀態。redrive 不能產生新 ID 來繞過去重,也不能在正常流量尖峰無限制灌回主佇列。
追問五:保留期從 24 小時改成 30 天,架構哪裡先改變?
依目前穩態,30 天邏輯資料約為 86.4 × 30 = 2.592 PB,三副本全放 Broker 本機磁碟的下界約為 7.776 PB,成本與復原時間都會成為瓶頸。保留活躍與近期 segment 在 Broker,已關閉且校驗完成的 segment 上傳物件儲存;中繼資料記錄物件位置與校驗和,歷史 fetch 經快取或讀取代理回傳。刪除、重播、 壓實與物件生命週期必須共用同一保留狀態機,避免本機 segment 已刪而遠端物件尚未可讀。