回到部落格
2026年3月22日4 分鐘閱讀

系統設計05:任務排程系統(Task Scheduling System)

用五維度分析法拆解任務排程系統,從內部系統 MVP 到進階 Cron 與 DAG 架構,掌握非同步執行、狀態機與資源調度的關鍵取捨。

《從 Web 到 AI-Native:一個新鮮人的系統設計五維度推演筆記》 系列學習筆記系統設計

前言

無論是定期備份資料庫、發送電子報,還是執行龐大的資料管線(Data Pipeline),背後都少不了一套穩定的任務排程系統。

這類系統看似只是把任務丟給 Worker 跑完就好,實際上最難的地方在於:狀態管理、故障判定、重試策略與資源分配。

本篇以企業內部系統(約 1 萬名使用者)為背景,透過五維度分析法,從 MVP 走到進階架構,拆解每一步的技術取捨。

這篇你可以帶走什麼

  • 任務排程系統的最小可行邊界與狀態機設計
  • 非同步解耦架構在低 QPS 場景的實務取捨
  • 為什麼不是所有系統都需要 MQ,以及資料庫輪詢何時更划算
  • 面對資源枯竭與不準確資源申報時,如何提升叢集利用率

維度一、可直接觀察的事實:系統邊界與核心行為

先看這題的真實邊界。

系統背景:

  • 企業內部系統(Internal System)
  • 使用者約 10,000 人
  • 每人每天提交約 100 個任務

雖然任務數看起來很多,但換算成平均讀寫 QPS 並不高:

  • 均值通常不到 10
  • 尖峰約 50 到 500

這意味著資料庫不是主要效能瓶頸,瓶頸更可能在排程策略與執行資源。

任務型態可分兩類:

  • 跑完即結束的短暫任務(Script / Task)
  • 常駐型服務(Long-running Service)

資料模型至少要包含:

  • 可執行二進位檔案位置(例如放在 AWS S3)
  • 任務元資料(Task ID、Owner、Input/Output Path、建立時間、Retry 次數)

而核心中的核心是狀態機(State Machine):

  • Ready -> Waiting -> Running -> Success / Failed
  • 若 Failed 且重試次數低於閾值,回到 Waiting 重試
  • 超過重試上限,進入 Final Failure

重點:任務排程系統的穩定性,最終取決於狀態機是否完整、可恢復、可追蹤。

維度二、條件檢查:非同步執行的最佳解與邊界

一般情況下的最佳解

為了避免使用者提交任務後被同步阻塞,經典做法是非同步解耦:

  • 使用者提交任務到 Database
  • Informer / Message Queue 負責派發
  • Worker Pool 負責實際執行

這能把 API 響應時間與任務執行時間拆開,讓系統更穩定。

邊界條件(資源枯竭)

當大量 CPU / 記憶體重任務同時湧入,Worker 不足就會出現資源排擠。

理論上可水平擴展(Scale-out),但實務上常受預算限制(Cost Down)而無法無限加機器。此時原本派發邏輯會因運算資源不足而卡死。

重點:非同步只能解耦等待時間,不能憑空創造算力;資源治理策略必須一起設計。

維度三、反證檢查:跳脫直覺的架構盲點

這題有兩個常見直覺陷阱。

盲點一:濫用 Message Queue(MQ)

直覺上,一談非同步就想放 MQ(RabbitMQ / Kafka)。但在低 QPS 內部系統中,任務先寫 DB 又寫 MQ,會帶來雙寫不一致(Dual-write Inconsistency)風險。

在這種場景,直接把資料庫當簡化版佇列通常更務實:

  • 透過 SQL 輪詢(Polling,例如 SELECT * WHERE status=Running AND ...)即可,減少維護一套中介軟體的複雜度。

盲點二:Worker 回報機制設計錯誤(Push vs. Pull)

若只讓 Worker 執行完再主動打 API 回報(這稱為 Pull 模型,Fire-and-forget),一旦 Worker 當機,系統可能永遠不知道任務已死亡。

更穩健的實務是混合模型(引入 Sidecar 模式):

  • Informer 推送任務給 Worker
  • Worker 旁掛 Sidecar
  • Sidecar 每 60 秒送一次 Heartbeat
  • 若 180 秒未收到心跳,系統判定 Worker 失效,將任務轉 Failed 並觸發重試

維度四、不確定性分析:資源調度的假設與優化

先區分事實與推論。

  • 觀察事實:使用者提交任務時會宣告 CPU 與記憶體需求
  • 經驗推論:使用者通常會高估需求(例如實際 1GB,申請 3GB)

結果是叢集看似滿載,實際利用率卻很低。

核心假設是:部分任務對時間不敏感。

因此可採用超額配置(Over-provisioning)與混合部署:

  • 對常駐服務保證資源
  • 對短暫且可搶占任務(Preemptible Task,如深夜備份)在資源緊張時壓縮 CPU 或延後執行

這能在不大幅擴機的前提下,提高整體叢集效能。

維度五、最終結論與行動建議

系統演進可以分三層:

  • 基礎層:以關聯式資料庫作為唯一事實來源(Single Source of Truth),實作任務狀態機
  • 調度層:使用 Informer + Sidecar,管理 Worker 生死、心跳、失敗轉移與重試
  • 進階層:當要支援 定時排程任務 (Cron) 或 相依性任務 (DAG),不要塞進基礎 Informer,改在上層額外封裝獨立的微服務

對進階能力可進一步拆分:

  • Cron Service:維護 Priority Queue 處理時間與觸發
  • DAG Service:在記憶體內做拓撲排序(Topological Sort),算好順序後再把單一任務丟給基礎層

破關路線圖(信心度 90%)

  • MVP 先把狀態機與重試邏輯做完整,確保任務可恢復
  • 在低 QPS 場景優先採 DB 輪詢,避免過早導入 MQ 複雜度
  • 用 Sidecar 心跳補上 Worker 失聯偵測,避免靜默失敗
  • 針對資源調度加入可搶占任務與資源壓縮策略
  • Cron 與 DAG 需求成熟後再外掛獨立服務,維持基礎調度層簡潔

這套演進邏輯與 Google / AWS 內部資源排程系統的實戰思路高度一致。

脆弱點提示

如果場景改成金融交易等級的高頻微秒任務,這套依賴資料庫輪詢與 Sidecar 心跳的機制會出現無法接受的延遲。

此時架構需大幅重構為:

  • In-memory 分散式排程器
  • 強一致性事件溯源(Event Sourcing)

否則無法滿足極低延遲與高一致性要求。


一句話總結

任務排程系統的核心不在「把任務跑起來」,而在用狀態機、故障偵測與資源調度,把不穩定的執行環境變成可預測的系統行為。