題幹與適用場景
請設計一個工作流程引擎:客戶可定義 20 步以內的業務流程,其中一步需要人工核准,流程可能等待數天;活動會失敗、重複投遞或執行後回應遺失。系統需要支援故障恢復、逾時、取消、稽核與版本升級。請說明狀態模型、排程、冪等、回呼安全、擴充性與取捨。
這是一道系統設計題,適合後端、平台與基礎設施職位。重點是持久化執行語意,而不是畫出簡單佇列。題目中的 20 步與數天等待是面試假設;應先確認吞吐、延遲、租戶隔離、資料敏感度、恢復目標與合規留存。
面試官考察點
面試官會觀察候選人是否區分工作流程狀態、活動執行與外部副作用,是否承認活動通常可能至少執行一次,是否以冪等鍵與業務不變量處理重試。人工核准必須有不可偽造、可過期且只能消費一次的關聯憑證。系統還要說明歷史事件、目前快照、計時器、取消、版本與營運修復的邊界。
回答前需要釐清的問題
- 每個租戶的並發實例、每秒啟動量與最長等待時間是多少?
- 活動是內部函式、容器任務還是外部 HTTP 服務?哪些副作用不可逆?
- 核准人如何驗證、授權與替換?是否需要多人核准或法定人數?
- 重試允許重複執行嗎?業務方能否提供冪等鍵或去重介面?
- 工作流程定義如何發布、凍結與遷移?執行中的實例是否跟隨新版本?
- 稽核需要保留哪些欄位,誰可以讀取或修改?
- 取消、暫停、人工重試與跳過步驟是否屬於正常產品能力?
30 秒回答框架
「我會把工作流程視為持久化狀態機:定義版本、實例、步驟嘗試、事件日誌與目前快照分開儲存。排程器只投遞可重試的活動任務,worker 用活動 ID、嘗試號與冪等鍵回報結果;重複結果由狀態機去重。人工核准建立短期一次性權杖,回呼必須綁定實例、步驟、租戶與核准版本。計時器、逾時與取消都寫入事件並由排程器恢復。先以分片佇列與租戶配額擴展,再透過事件重播、稽核與故障注入驗證恢復語意。」
分步驟深入解答
先定義執行模型。工作流程定義是不可變版本,包含步驟類型、輸入映射、逾時、重試策略與補償規則。實例引用固定版本,不因新版本發布而靜默改變歷史。實例狀態可由事件日誌重建,快照用於加速讀取;事件追加要有單調序號或版本條件,避免並發寫入覆蓋。
活動執行不要直接在資料庫交易中呼叫外部服務。狀態機提交 ActivityScheduled 事件後,排程器把任務放入佇列。worker 取得租約、執行外部呼叫,再提交 ActivitySucceeded、ActivityFailed 或 ActivityTimedOut。狀態機只接受預期實例、步驟與嘗試號的結果;過期或重複結果寫入稽核但不推進狀態。
最小狀態模型可包括:
| 實體 | 關鍵欄位 | 作用 |
|---|---|---|
| DefinitionVersion | tenant、definition、version、digest | 凍結步驟與策略 |
| WorkflowRun | run、definitionVersion、status、sequence | 記錄實例與目前狀態 |
| Event | run、sequence、type、payload、createdAt | 事實日誌與重播依據 |
| ActivityAttempt | step、attempt、lease、idempotencyKey、status | 追蹤投遞、租約與結果 |
| Approval | step、tokenHash、approver、expiresAt、status | 管理人工核准憑證 |
| Timer | run、step、fireAt、generation、status | 管理逾時、延遲與喚醒 |
排程必須是至少一次投遞,不能把佇列確認當成業務完成。任務訊息包含 run ID、step ID、attempt、定義版本與冪等鍵。worker 取得租約防止兩個消費者同時執行,再用 fencing token 或條件更新拒絕舊 worker 寫回。租約過期後可以重新投遞,但外部副作用是否重複取決於活動契約。
冪等要分層處理。狀態機事件使用唯一鍵 (run, sequence);活動結果使用 (run, step, idempotencyKey);外部付款、發信或寫入業務系統則要求下游接受相同冪等鍵,或透過業務唯一約束去重。若呼叫成功但回應遺失,重試只能得到「已完成」或安全的重複結果,不能聲稱整個流程 exactly once。對無法冪等的副作用,提供人工確認、補償或不可重試策略。
人工核准不能把隨機 URL 當作授權。建立核准任務時產生高熵隨機 token,只存雜湊,token 綁定租戶、run、step、核准動作與過期時間。回呼入口驗證 token、簽章或登入身分、CSRF、一次性消費與目前狀態;核准人權限在提交時再次檢查。核准與拒絕都寫入不可竄改稽核事件,過期後回呼變成無效操作。
計時器必須持久化。建立逾時或等待事件時寫入 fireAt 與 generation;排程器以時間索引或分桶掃描到期 timer,再透過條件更新取得處理權。重啟後可從資料庫恢復,重複觸發由 generation 與狀態條件去重。取消或核准完成時標記舊 timer,不依賴記憶體中的 sleep;長時間等待不應佔用 worker。
取消、暫停與人工修復需要顯式狀態機命令。取消請求先記錄意圖,再阻止尚未開始的活動;正在執行的外部副作用不能憑空撤銷,應等待結果或執行補償。管理員跳過步驟、重試或修改輸入必須要求權限、理由、舊狀態與新狀態,並產生稽核事件。不能直接改快照來「修復」流程,否則事件重播會得到不同結果。
版本升級按實例隔離。新定義發布新 digest;執行中的實例預設繼續使用舊版本。若必須遷移,先定義可遷移狀態、相容輸入與人工核准,產生 WorkflowMigrated 事件並記錄前後版本。活動 worker 只接受它執行的定義版本,避免舊任務把新流程推進到錯誤狀態。
擴充性先做分片。按租戶或 run ID 對事件與佇列分區,單一熱租戶設定並發配額;worker 池按活動類型與優先級隔離。事件日誌可追加寫入分區儲存,快照與索引放在線上資料庫,歷史 payload 按敏感度加密並設保留期限。排程器需要背壓、死信、租約監控與限流,避免失控工作流程佔滿全局容量。
可觀測性同時面向執行時與業務。記錄每個 run 的目前步驟、等待原因、嘗試次數、佇列延遲、timer 延遲、核准停留時間、重試放大、補償結果與版本。分散式 trace 的 span 可帶 run、step 與 attempt,但敏感輸入應放在受控稽核儲存。營運介面顯示可解釋狀態與下一步動作,不能把「等待」誤報成失敗。
故障測試至少覆蓋:提交事件後程序崩潰、訊息重複、租約過期、活動成功但回應遺失、核准回呼重播、timer 重複觸發、資料庫故障、佇列積壓、版本發布中斷與租戶配額耗盡。每種故障都要定義可觀察結果,例如舊狀態不會回退、同一副作用不重複、核准 token 不能二次消費、恢復後最終會繼續或進入人工處置。
高品質示範回答
「我會把它設計成持久化狀態機,而不是讓 worker 記住流程。定義版本不可變,實例固定引用一個版本;事件日誌記錄事實,目前快照只做讀取加速。狀態機提交活動排程事件,佇列至少一次投遞,worker 用 run、step、attempt 與冪等鍵執行並回報。重複或過期結果只寫稽核,不推進錯誤狀態。
人工核准建立綁定租戶、實例、步驟與動作的一次性 token,只存雜湊並設定過期時間。回呼時驗證登入身分、權限、CSRF、token 狀態與目前步驟,成功後條件更新為已消費並寫稽核。核准、拒絕、逾時與取消都是事件,不直接改快照。
計時器持久化 fireAt 與 generation,排程器分桶掃描並用條件更新領取;重啟可恢復,重複觸發會被 generation 與狀態去重。租約防止兩個 worker 同時執行,fencing token 拒絕舊 worker 寫回。無法證明下游冪等的付款或發信不能聲稱 exactly once,應使用下游冪等鍵、唯一約束、補償或人工處置。
執行實例預設不自動跟隨新定義,遷移必須生成帶前後版本的事件。按租戶與 run 分片,隔離熱租戶配額,活動類型使用獨立 worker 池;事件追加儲存、線上快照、加密稽核與保留策略分層。故障注入驗證崩潰、重複投遞、回應遺失、回呼重播、timer 重複與版本中斷時的恢復結果。」
常見錯誤
- 把佇列確認當成業務完成 → worker 可能執行後崩潰,訊息再次投遞 → 用持久化狀態與冪等結果去重。
- 聲稱 exactly once → 分散式副作用無法由本地交易保證 → 說明至少一次、下游冪等、補償或人工處置。
- 把核准 URL 當成授權 → 洩露或重播後可能越權 → 綁定身分、租戶、步驟、動作、過期與一次性消費。
- 用記憶體 sleep 等待數天 → 重啟或擴容會遺失等待 → 儲存 timer 並由排程器喚醒。
- 直接修改快照修復 → 事件重播會產生不同結果 → 使用有權限的狀態機命令與稽核事件。
- 所有租戶共用一個佇列 → 熱租戶會拖垮其他客戶 → 分片、配額、優先級與背壓。
- 新定義立即影響舊實例 → 執行中的流程不可解釋、不可重播 → 實例固定版本,遷移顯式化。
追問及應對
追問 1:活動成功了,但 worker 在回寫前崩潰,重試會不會重複扣款?
如果下游支援冪等鍵,重試重用同一鍵並把已完成結果映射回來;否則不能安全重試。可以查詢下游狀態、進入人工處置或執行補償。狀態機只能保證自己的事件一致,不能憑空給外部付款 exactly once。
追問 2:核准人點了兩次批准,如何處理?
token 只允許一次成功消費,使用條件更新或唯一約束把 pending 改為 approved。第二次請求返回已處理或過期,不再次推進流程;兩次請求與身分都寫入稽核。
追問 3:如何支援「任意兩人批准」而不是單人核准?
把核准步驟拆成策略與持久化收件匣,記錄每位核准人的唯一決定、權限快照與策略版本。狀態機依去重後的決定數達到法定人數才推進;拒絕、撤銷與替換核准人都走顯式命令。
追問 4:執行中的流程需要緊急跳過一個步驟,怎麼辦?
先定義誰有權限、哪些步驟可跳過與安全條件。透過帶理由的 StepSkipped 命令記錄舊狀態、新狀態、操作者與核准,不直接改資料庫。若跳過會破壞業務不變量,應拒絕並提供補償或終止路徑。
追問 5:如何防止單一租戶建立無限長的工作流程?
定義步驟數、分支深度、活動並發、歷史大小、timer 數與總執行時間配額;發布定義時靜態驗證,執行時按租戶計量。超過配額進入等待或拒絕,並顯示管理員可理解的原因,避免執行期間無限增長。