具代表性的面試主題

後端面試:如何設計有界 Worker Pool?

後端中等
OfferCC 編輯團隊發佈 更新

題幹

為後端服務設計有界 Worker Pool。請說明佇列容量、接納或拒絕策略、取消、重試歸屬、結果關聯、指標與優雅關閉。

題目與範圍

為後端服務設計 Worker Pool:接收任務、限制同時執行數量,並在關閉時不靜默遺失已接收任務。說明佇列容量、接納或拒絕、取消、重試歸屬、結果關聯、指標和優雅關閉。

假設任務彼此獨立,可能呼叫下游 API。這裡討論行程內元件;若任務必須跨行程故障復原,應在更外層設計持久化佇列。

面試官考察什麼

面試官會看你是否給出明確的並發上限、有界記憶體模型和過載策略,也會追問取消如何到達 Worker、重試是否放大流量,以及關閉時如何區分排隊、執行中、完成和拒絕的任務。

作答前需要確認的問題

  • 行程崩潰時允許遺失任務嗎?不允許時先放入持久化訊息系統。
  • 任務是否冪等,是否可以安全重試?
  • 下游限流、平均耗時和尾延遲目標是什麼?
  • 佇列滿時是短暫阻塞、回傳 429,還是優先拒絕低優先級任務?
  • 取消只針對未開始任務,還是任務可以協作式中斷 I/O?

30 秒回答框架

「我會用固定數量的 Worker 和有限佇列實作有界提交。容量耗盡時回傳明確的過載結果;context 或取消訊號讓排隊任務退出,也讓執行中的任務協作式停止。每個任務有 ID 和終態,重試次數受限並由單一層負責。指標包括佇列深度、等待時間、活躍 Worker、拒絕、耗時和取消。關閉先停止接納,再按策略處理排隊任務,在截止時間內排空已接收任務,並回報未完成任務。」

分步深入

第一步:定義狀態機

使用 submitted → queued → running → succeeded|failed|cancelled。佇列滿時回傳 rejected,不要讓呼叫方無限等待。若呼叫方需要行程崩潰後復原,狀態必須持久化到行程外。

第二步:同時限制 Worker 與佇列

選擇 W 個 Worker 和容量為 Q 的佇列;記憶體上限約為 Q 個任務載荷加 Worker 堆疊。不要為每個請求建立執行緒或 goroutine。Python 的 ThreadPoolExecutor 提供 max_workers,但提交量無界時仍需要應用層的有界接納策略。

第三步:選擇接納和背壓

可以非阻塞提交、限時等待或按優先級拒絕。回傳穩定的過載碼,只有確認安全時才給 Retry-After。滿佇列上無限等待可能卡住請求鏈路;無界佇列則會把過載變成延遲和記憶體增長。

text
submit(job, deadline):
    if stopping or deadline expired: return REJECTED
    if queue.try_push(job): return ACCEPTED(job.id)
    if policy == WAIT and wait_until(deadline) and queue.try_push(job):
        return ACCEPTED(job.id)
    return OVERLOADED

第四步:實作協作式取消

把取消 context 傳給任務。尚未開始的任務可以移除;執行中的任務要在安全點檢查取消,並把訊號傳給下游客戶端。執行緒通常不能被安全強殺,因此要定義不可中斷 I/O 的「已取消」語義。

第五步:讓重試只有一個歸屬層

選擇由 Pool、任務處理器或持久化佇列中的一層調度重試。限制次數,使用帶抖動的指數退避,並區分可重試和永久錯誤。否則多層逾時會對同一依賴製造重試風暴。

第六步:關聯結果和錯誤

回傳任務 ID 或 future,不要共享可變結果槽。記錄原始錯誤、嘗試次數和終態。若採用輪詢,要說明結果保留時間和授權;若採用等待,要說明呼叫方斷線後任務怎麼處理。

第七步:設計優雅關閉

關閉時先停止接納,再按策略處理排隊任務,讓執行中的任務在截止時間前排空。Go 的流水線指南透過 done 訊號傳播取消;Worker Pool 也應遵循同樣原則。逾時後要回報未完成任務以便重放,只有契約明確時才能標記為放棄。

第八步:觀測真正的瓶頸

記錄佇列深度和年齡、活躍 Worker、利用率、接納/拒絕/取消數量、執行耗時、重試次數和下游錯誤。告警應關注持續佇列年齡與拒絕率,而不只看 CPU。W 應依據下游連線池、外部限流或 CPU 容量設定。

權衡與邊界

權衡一:固定 Worker 還是動態擴縮

固定數量容易預測並發並保護依賴。動態擴縮可以提高吞吐,但必須有全局硬上限,並統計每個副本的並發,否則各實例獨立擴張會壓垮下游。

權衡二:佇列滿時拒絕還是等待

拒絕能快速回饋並保護延遲;等待可以吸收短峰值,但必須有截止時間,不能長期佔用稀缺請求執行緒。

權衡三:行程內還是持久化佇列

行程內 Pool 延遲低、實作簡單。持久化 Broker 增加復原、重放能力和運維成本。若任務要跨部署、崩潰或多實例路由存活,應選持久化方案。

失敗演練與演進計畫

演練一:下游故障

讓任務都慢速失敗,觀察佇列年齡、拒絕、逾時和有界重試,確認依賴不健康時並發不會繼續膨脹。

演練二:大規模取消

提交 10,000 個任務,執行前取消一半,並在排空期間停止服務。確認排隊取消任務不會執行,已接收的執行中任務最終都有可回報狀態。

演練三:副本滾動發布

滾動發布期間同時執行兩個版本,確認每個實例遵守本地上限;需要系統級上限時,再驗證持久化佇列或全局限流器。

常見錯誤與追問

錯誤一:無界緩衝

無界佇列只是在隱藏過載,最終會表現為記憶體或延遲崩潰。容量和拒絕策略必須可觀測。

錯誤二:重複重試

HTTP 客戶端和任務處理器同時重試會相乘。指定單一重試歸屬,並傳遞嘗試次數。

錯誤三:把取消當作強殺執行緒

多數執行環境不能安全強殺任意任務。使用協作式檢查、可取消 I/O 和明確的放棄策略。

錯誤四:排空前不停止接納

新任務會讓佇列永遠非空。等待排空截止時間前先關閉接納。

錯誤五:忽略副本級並發

10 個副本各有 20 個 Worker 就是 200 個並發呼叫。要說明上限是本地、分片還是全局協調。

錯誤六:沒有結果保留策略

future 和輪詢記錄都需要過期、授權和失敗路徑,否則已接收任務會造成無界儲存。

公開來源

同類題目