前言
無論是定期備份資料庫、發送電子報,還是執行龐大的資料管線(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)
否則無法滿足極低延遲與高一致性要求。
一句話總結
任務排程系統的核心不在「把任務跑起來」,而在用狀態機、故障偵測與資源調度,把不穩定的執行環境變成可預測的系統行為。