後端工程師必備: 排程任務的處理機制練習 (12/01 補完)

後端工程師必備: 排程任務的處理機制練習 (12/01 補完)

摘要提示

  • 問題本質: 僅能用資料庫輪詢實作「預約時間執行」的任務排程,需兼顧精確度、成本與高可用。
  • 時間與成本取捨: 提升輪詢頻率可降延遲但暴增 DB 負載,需在兩者間取得平衡。
  • 明確評量: 以成本分數(COST)與精確度分數(EFFICIENT)量化方案優劣,並施以 HA 測試。
  • 約束條件: 有最小準備時間(MinPrepare)與最大允許延遲(MaxDelay);任務不得重複執行。
  • 測試設計: 10 分鐘持續產生多型態負載(含尖峰批次與隨機流),並以 5 個實例並跑。
  • 基礎介面: 透過 JobsRepo 統一操作 DB(查清單/查單筆/鎖定/執行),禁止旁路存取。
  • 參考解法: 範例解法為最低標準;多數優化聚焦「提前鎖定」與「降低碰撞」兩大方向。
  • PR 實戰觀察: 各家在 COST 與 EFFICIENT間取向不同:有的極省 DB,有的極追延遲。
  • 作者示範: 加入隨機漂移的「提前鎖定」與最佳化工作執行緒數,兼顧精準與可關機性。
  • 關於造輪子: 不求凡事自製,但理解機制可做更準確的取捨、整合與在地最佳化。

全文重點

本文以「只能靠輪詢的資料庫排程」為題,帶領讀者在受限環境下設計可用且可量化優化的排程服務。問題核心在於 Web 應用的 Request/Response 天性不利於「定時觸發」;若直接靠高頻輪詢可達秒級精度,卻會大幅提高 DB 成本。因此作者將題目收斂為:在 DB 維護任務表、僅以輪詢啟動工作,同時須兼顧高可用與不可重複執行,並滿足兩個時間約束(MinPrepareTime、MaxDelayTime)。

評估標準分為兩面向:一是 DB 成本(查清單次數、鎖定失敗次數、查單筆次數加權相加);二是執行精確度(實際延遲的平均與標準差)。測試環境會以 10 分鐘持續丟任務,包含固定頻率、突發批量與隨機模式,並以 5 個實例並行,過程中還有中途關閉部分實例的 HA 測試。合格需在時間約束內完成全部任務、無重複執行與延遲超標。

開發面限定透過 JobsRepo 操作 DB:GetReadyJobs(可帶 duration 預抓)、GetJob、AcquireJobLock、ProcessLockedJob。示範的最低標解法展示傳統「掃清單→嘗試鎖定→執行→無則睡眠」迴圈,其延遲與成本皆非最佳,目的在於提供起點。作者亦提供測試程式自動建立資料、產生多型態負載並產出統計。

在「Solution & Review」部分,作者彙整各位 PR 的分數與行為特徵,逐一點評策略與程式細節。常見優化有:在預期執行時間「稍早鎖定」以降低延遲、先查單筆再鎖以減少鎖定失敗成本、利用生產者/消費者模型與 BlockingCollection/Channel 平衡流量;也見到刻意引入隨機性以避免多實例「同時撞鎖」的碰撞。問題點則多出在忙等(busy waiting)耗 CPU、關機流程未妥善(造成例外)或「偷跑」到預期時間之前即執行。

作者示範方案以「把延遲壓到極致」為目標:採固定上限的處理執行緒數(實測最佳為 10)、在預約時間前 300–1700ms 間隨機提前鎖定,既降低碰撞又保留可在短時間內優雅關機;若時間尚早就以可取消的延遲等待,避免過度提前鎖造成長時間無法下線。最終在 EFFICIENT 分數名列前茅,同時 COST 也維持在良好水準。

結語強調:不必凡事自造,但理解機制讓你能更準確比較既有解法、評估與整合,並在特定情境(如 SaaS 分層、基礎設施權限與安全)中做出合理的邊界選擇。藉由本練習的量化方式,你能在受限前提下科學地優化排程機制,進而累積面對實務異常與高可用場景的能力。

段落重點

問題定義

以資料庫中的排程表為唯一真相,僅允許「輪詢」得知任務變更,不可依賴 DB 主動通知。任務欄位含建立、預定執行、實際執行與狀態。天真的做法是固定頻率掃描 runat<now 的任務,但會在「秒級精度」與「DB 負載」間拉扯:頻率越高越準、成本越高;頻率變慢則延遲飆升。題目設定即要求在此限制下探索更聰明的輪詢策略。

需求定義

目標四項:盡量減少 DB 額外負擔、啟動時間要準(平均延遲低且波動小)、支援多實例高可用、同一任務只能執行一次。約束兩項:插入任務需預留最小準備時間(如 10 秒),啟動必須在最大允許延遲內(如 30 秒)。換言之,10:30 任務最晚 10:29:50 寫入,並須在 10:30:30 之前開始執行。

評量指標

成本分數(越低越好):查清單次數×100+鎖定失敗×10+查單筆×1。精確度分數(越低越好):延遲平均+延遲標準差。另定合格條件:全部完成、每筆皆滿足 MinPrepareTime 與 MaxDelayTime。配合時間軸示意:定期掃描造成各任務延遲差異,取其平均與標準差即為效率評分。

測試方式

以工具持續 10 分鐘造資料(含固定頻率、突發批量與隨機間隔),並行啟動 5 個實例。可靠度測試會隨機中止 3 個實例再檢核合格條件。正式評分則在同樣併發下計算 COST 與 EFFICIENT。若多方案接近,會測 1–10 個實例找出最佳並行數。HA 測試在 Windows 環境以 GenericHost 控制 Start/Stop 模擬關機行為。

開發方式

提供 JobsRepo 作為唯一 DB 出入口:GetReadyJobs(可預抓 duration)、GetJob、AcquireJobLock、ProcessLockedJob(含延遲與單進程併行上限)。附基準示例(最低標)與完整測試程式:先清 DB,10 秒後開始,持續 10 分鐘混合型態注入任務,結束後再等 30 秒收尾並生成統計。限制使用 LocalDB/SQL Server,並附 SQL 腳本與連線字串。

處理結果

示範解法在 10 分鐘情境下共 1726 筆任務,平均延遲約 4.3 秒、標準差約 2.86 秒;全部完成但 46 筆逾 MaxDelay。COST 約 119,360,EFFICIENT 約 7201.5。此成績作為低標參考,說明純「固定掃描→嘗試鎖定→執行→無則睡眠」的簡單輪詢難以同時兼顧低延遲與低成本,亦為後續優化提供基線。

寫在最後

此題聚焦務實的系統設計與量化優化,讓團隊在可控的 PoC 環境中專注於機制設計而非框架雜訊。藉由共同的測試與評分標準,能科學比較不同作法的差異,促進工程師對取捨、整合與高可用設計的敏感度。作者亦邀請讀者提交 PR,並承諾公開以一致腳本評測與回饋。

Solution & Review(整體)

收錄 7 份 PR,統一以 5 個實例為主比,另附 1–10 實例數據供觀察延展性;HA 測試用 GenericHost 模擬反覆啟停。重點觀察欄位含鎖定失敗、清單查詢、延遲均值/標準差、是否提前鎖定/提前執行。總體結論:最佳 EFFICIENT 來自作者方案;最低 COST 由 jwchen-dev 奪得;亦有因「提前執行」違規而失格之例。

PR1, HankDemo

以 ThreadPool 啟動 5 個 worker,主線程每 5 秒預抓 15 秒內任務並平均分配至各自佇列;worker 忙等至時間到再執行。優點:結構清楚、先查單筆再鎖定可降 COST。缺點:忙等耗 CPU、固定分配無負載均衡、預抓區間與排序易造成不必要延遲,關機流程在 worker 端不夠優雅。EFFICIENT 表現有限、COST 可入 300% 內。

PR2, JolinDemo

以 BlockingCollection 實作生產者/消費者,FetchJob 預抓 10 秒內任務,ExecuteBody 到點再鎖定+執行;以 WaitHandle 協調關機,非同步控制細膩。優點:多執行緒與關機處理完善。可再優化:先查單筆再鎖、適度提前鎖定以壓延遲。最終 COST 與 EFFICIENT 未入 300% 之內,主因未採更進一步的「降鎖碰撞+偷跑」策略。

PR3, JWDemo

亦採 BlockingCollection,worker 延遲至時間點後執行,並於鎖定前先查單筆降低 COST;在主迴圈加入隨機等待以錯開多實例同時掃描,減輕鎖碰撞。單實例下 EFFICIENT 普普,但多實例愈多表現愈佳,COST 名列前段。小瑕疵:關機未完整 await worker 收尾。整體在「省成本」策略上見效明顯。

PR4, LeviDemo

自製 SimpleThreadPool 與佇列分派,流程完整但在執行端缺少「未到時間不執行」的檢查,導致提前執行而 HA 失敗、EFFICIENT 為負遭淘汰。若修正時間判斷,池化與分派結構仍具參考價值,但需避免忙等與確保正確的時間門檻控制。

PR5, BorisDemo

抓清單即嘗試「提前鎖定」後放入 BlockingCollection;worker 僅等待至時點並執行。此策略大幅降低鎖碰撞與延遲,COST 排名佳、EFFICIENT 亦名列前茅。風險在關機:若提前鎖太早需確保隊列清空才能優雅下線;程式中關機偶有例外訊息(不影響成績),可能源於等待與取消協調未盡完備。

PR6, JulianDemo

使用 Channel 取代 BlockingCollection,理論上更適合 async 工作;實作上建立多個單容量 channel 並輪詢寫入,使得實際並行度受限、效益未完全釋放。COST 尚可,EFFICIENT 未入 300%。HA 測試偶見取消例外訊息。若收斂為單一 channel 並強化消費端並行與關機協調,潛力仍大。

PR7, AndyDemo

在抓清單階段即先查單筆、提前鎖定並等待至時點才入隊,worker 只負責執行;此法兼顧「降 COST」與「保 EFFICIENT」,表現均衡。缺點是前段動作較集中,遇同時段多任務時需靠增加實例數消化。整體程式精煉、關鍵邏輯清楚,分數在兩面向皆入榜。

示範專案

作者自解法以「極致壓低 EFFICIENT」為策略:實測以 10 條處理緒最佳;在預約時間前 300–1700ms 隨機「提前鎖定」,既降低多實例撞鎖,又保證可在 ≲1.7 秒內優雅關機;若距時點尚早則以可取消延遲等待;入隊後由 worker 立即 ProcessLockedJob。結果 EFFICIENT 最佳,COST 亦在前段,證明「受限輪詢下仍可逼近秒以下延遲」。

結論

非凡事自造,但理解機制能讓你:更精準地評估與整合現成方案;在 SaaS 與基礎設施邊界、權限與安全上做正確切分;在受限前提下藉量化指標持續優化。此練習展示:僅靠輪詢也能透過「提前鎖定+隨機漂移+並行與關機協調」把延遲與成本壓至可用水位。面向實務,這種能力將直接轉化為對極端與異常情境的韌性。

資訊整理

知識架構圖

  1. 前置知識:
    • 基礎資料庫操作與索引設計(特別是時間欄位查詢)
    • 多執行緒/非同步程式設計(Thread/Task、同步原語、阻塞/非阻塞)
    • .NET Generic Host/BackgroundService 的生命週期與優雅關閉
    • 分散式協調的基本觀念(鎖定、去重、冪等)
    • 度量與監控(平均值/標準差、操作計數)
  2. 核心概念:
    • 以 DB 為真相來源的排程:僅能輪詢(polling),無通知機制
    • 成本與精確度權衡:Query/Lock 成本 vs 延遲(平均與抖動)
    • 去重與一致性:AcquireJobLock→ProcessLockedJob(確保一次且僅一次)
    • 視窗式預抓(look-ahead window):MinPrepareTime 為基準,控制掃描頻率
    • 高可用與關機語義:多實例競爭、提前鎖與優雅停機的取捨
  3. 技術依賴:
    • SQL Server + Dapper(資料訪問層 JobsRepo)
    • .NET Core Generic Host + BackgroundService(常駐服務與關機鉤子)
    • 併發容器:BlockingCollection / Channel(生產者-消費者)
    • 同步原語:CancellationToken、WaitHandle、Semaphore(限流/關機)
    • 計時/隨機:Task.Delay/Thread.Sleep、Random(抖動、退避)
  4. 應用場景:
    • Web/SaaS 需精準到秒級的「預約任務」執行(如寄信、扣款、通知)
    • 無法使用 MQ 延遲訊息或 DB 通知服務的環境
    • 系統需要橫向擴展與高可用,但又要降低 DB 壓力的場合
    • 需可觀測、可度量、可灰度調整(執行緒數/實例數)的排程服務

學習路徑建議

  1. 入門者路徑:
    • 了解問題與限制:僅能輪詢、MinPrepareTime/MaxDelayTime 的 SLA
    • 實作最小可行解:每固定間隔查 GetReadyJobs→AcquireJobLock→Process
    • 加入度量:記錄查詢次數/鎖定成功失敗、延遲平均與標準差
  2. 進階者路徑:
    • 引入生產者-消費者模型(BlockingCollection/Channel)分離抓取與執行
    • 視窗式預抓與自適應掃描間隔(依上次耗時/剩餘 window 動態調整)
    • 降低成本:Acquire 前先 GetJob 狀態判斷、控制 Query/Lock 的比例
    • 精準時間控制:避免 busy-wait,改用高精度等待;引入微量「提早鎖」與抖動
    • 高可用與優雅關閉:在停止時清空佇列/完成已鎖任務,確保不重複不遺漏
  3. 實戰路徑:
    • 以 JobsRepo 為唯一 DB 介面,實作可配置的 worker(執行緒數/提早鎖毫秒數/抖動範圍)
    • 建立測試矩陣(instances=1..N)跑基準測試與 HA 測試,收集分數
    • 逐步調參:look-ahead 視窗、掃描頻率、提早鎖時間與抖動、併行度
    • 監控與告警:延遲超標數、標準差變化、Acquire 失敗率、Query 次數突增
    • 風險管控:確保提前鎖與關機時限的上界,避免鎖住但無人處理

關鍵要點清單

  • 輪詢視窗(Look-ahead Window):以 MinPrepareTime 設定預抓視窗,避免掃描過密造成 DB 壓力 (優先級: 高)
  • 成本/精確度指標:以 Query/Lock/JobQuery 計分與延遲平均+標準差評估優劣 (優先級: 高)
  • 一次且僅一次:AcquireJobLock→ProcessLockedJob 的嚴格順序與冪等思維 (優先級: 高)
  • 提前鎖與抖動(Jitter):在執行前短時間先鎖,加入隨機抖動降低碰撞與成本 (優先級: 高)
  • 精準等待:避免 busy-wait,使用 Task.Delay/等待至 RunAt,控制毫秒級精度 (優先級: 高)
  • 生產者-消費者:用 BlockingCollection/Channel 隔離抓取與執行、提升吞吐 (優先級: 高)
  • 優雅關機:Generic Host + CancellationToken,確保已鎖任務能完成或安全釋放 (優先級: 高)
  • 降低鎖嘗試:Acquire 前先 GetJob 確認狀態,減少昂貴鎖定失敗次數 (優先級: 中)
  • 動態節流:依抓取耗時/負載調整掃描間隔,避免齊步效應造成尖峰 (優先級: 中)
  • 併行度調參:測試求得最佳執行緒數/實例數,平衡成本與延遲 (優先級: 中)
  • 高可用與去重:多實例競爭不重複,並處理實例中斷的恢復策略 (優先級: 中)
  • 指標監控:延遲超標數、Acquire 失敗率、Query 次數、平均/標準差趨勢 (優先級: 中)
  • 資料庫索引:RunAt、State 的複合索引以支援高效掃描 (優先級: 中)
  • 瞬時尖峰處理:對同時多筆同時刻任務,確保有足夠併發與隊列容量 (優先級: 中)
  • 風險控制:提前鎖時間設上限,避免在停機時長時間卡住或遺漏 (優先級: 低)





Facebook Pages

AI Synthesis Contents

Edit Post (Pull Request)

Post Directory