將一組分散式動作協調為單一邏輯操作,使整個操作要麼成功,要麼失敗。 失敗要透明處理,否則就得撤銷已完成的工作。 此方法可藉由從短暫性例外狀況、較持久的故障,以及會中斷在遠端服務與資源之間協調執行的多步驟工作流程之程序故障中復原,來提升分散式系統的韌性。
內容和問題
應用程式常執行跨越多個步驟的任務,有些步驟可能會呼叫遠端服務或在應用程式邏輯的協調下存取遠端資源。 與本地程序以外的任何互動都會使任務暴露於多種失敗模式。 編排邏輯必須在處理這些失效模式時,不失去整個操作的完整性。
你可以用像 Retry 模式這類方法來處理簡單情況,例如會自行解決的瞬態故障。 當故障較為永久性,或編排器本身失效時,系統必須從持久紀錄恢復一致狀態,並確保整體運作的完整性。
解決方案
排程代理監督者模式透過三個邏輯角色協調多步驟分散式任務。 這些演員負責協調整體任務中要執行的步驟,並處理成功與失敗的演出。
排程器
排程器負責安排構成任務的步驟,並協調執行。 排程器負責將步驟合併成流程或工作流程,並以正確的順序執行。 隨著每個步驟的進展,排程器會記錄其狀態,例如 步驟尚未開始、 步驟執行中或 步驟已完成。 該狀態還包含一個 完成期限時間,用來限制該步驟允許花費的時間。
當步驟需要遠端服務或資源時,排程器會呼叫適當的代理並將工作細節傳給該代理。 排程器通常透過非同步請求/回應訊息與代理溝通,通常透過佇列實作。 你可以改用其他分散式訊息技術。
Note
排程器執行類似於 流程管理員模式中程序經理角色的功能。 排程器控制的工作流程引擎通常會定義並實作工作流程,將業務工作流程邏輯與排程器本身解耦。
代理程式
代理封裝了對遠端服務的呼叫或任務步驟所參考的遠端資源存取。 每個代理通常會將呼叫包裹到單一服務或資源,並在排程器設定的完結時間內實作適當的錯誤處理與重試邏輯。 當涉及重試邏輯時,代理會在所有重試嘗試間傳遞穩定的識別碼。 下游服務可利用此進行去重。
如果工作流程中的不同步驟使用不同的服務或資源,每個步驟都可以參考不同的代理程式。 這種映射是模式的實作細節。 關於設計重試策略的指引,請參閱 暫態故障處理的最佳實務。
監督員
主管監控排程器執行的步驟狀態。 監督者會以工作負載指定的頻率定期執行,並檢視排程器記錄的步驟狀態。 若主管發現步驟逾時或失敗,會安排適當的代理恢復該步驟,或觸發其他補救措施,可能包括修改步驟狀態。 主管只要求這些恢復行動。 排程器與代理程式負責實作。
互動
排程器、代理和監督者是邏輯元件,其實體實作取決於你使用的技術。 例如,你可以將多個邏輯代理程式實作為單一 Web 服務的一部分,或實作為同一個託管背景工作程序中的活動。
排程器在一個稱為 狀態儲存庫的持久資料庫中維護任務進度與每步狀態。 主管會利用這些資訊判斷某步驟是否失敗。 下圖顯示了排程器、代理、監督者與狀態儲存之間的關係。
在左上角,排程器負責組織並執行步驟。 標示為「排程器請求代理存取遠端資源」的雙箭頭線會指向右側的代理程式,而標記為「代理存取遠端資源或服務」的雙箭頭線則從代理程式指向遠端資源。 再往下,雙箭頭線從排程器指向另一位代理,再指向遠端服務。 另一條標示為「排程器維護步驟狀態」的雙向箭頭線向下延伸至狀態儲存庫。 一條標示為「監督器監控步驟狀態」的雙向箭頭線,連接狀態儲存區與其右側的監督器。 一個標示為「主管請求步驟重試」的箭頭會從主管往上回撥,再向左轉給排程器。
- 排程器會組織並執行構成任務的步驟,作為工作流程。
- 工作流程中的一個步驟可以向代理發送請求,以存取遠端資源或呼叫遠端服務。 請求與回應通常以非同步方式傳送。
- 代理會存取遠端資源或服務。 代理程式應該包含錯誤處理和重試邏輯。
- 排程器會在狀態存放區中記錄每個步驟在開始和完成時的狀態。
- Supervisor 會監控狀態儲存區中各步驟的狀態,並可能要求更新某個步驟的狀態。
- 主管會要求排程員重試失敗的步驟。
當應用程式準備好執行工作時,它會將要求提交至排程器。 排程器會在狀態儲存中記錄任務及其步驟(例如 尚未開始的步驟)的初始狀態,然後開始執行工作流程定義的操作。 當排程器開始每個步驟時,會在狀態儲存中更新該步驟的狀態,例如 步驟執行。
如果步驟參考遠端服務或資源,排程器會將訊息傳送至適當的 Agent。 訊息包含代理者呼叫服務或存取資源所需的資訊,以及操作的完成時間與重試允許。
代理可以依其工作實作任何適合的重試邏輯,但如果代理未在完成期限內完成工作,排程器會假設操作失敗,停止等待回應。 代理人必須遵守截止日期,且不得在時間結束後開始新的副作用,儘管已進行中的工作仍可能持續進行。
如果 Agent 順利完成其作業,則會傳回排程器的回應。 排程器會更新狀態,例如步驟 已完成 ,然後開始下一步。 此程序會繼續進行,直到整個任務完成為止。
排程器必須忽略任何遲到的回應,且代理程式不得自行嘗試工作流程恢復。 如果代理本身失敗,排程器也不會收到回應。 state store 中的狀態刻意不區分逾時的步驟與失敗的步驟。
當某步逾時或失敗時,狀態儲存記錄仍顯示該步仍在 執行中,但完成時間已過期。 主管會掃描此狀況下的紀錄並安排救援。
主管還需要防止同一步驟無限期重複嘗試。 監督器會為每個步驟維護重試次數,並將其與狀態資訊一併儲存在狀態儲存庫中。 若計數超過預先定義的閾值,監督程式可被設定為先等待較長一段時間,再要求排程器重試該步驟,以防底層故障在此期間內排除。
一種可能的主管策略是延長完成期限值,並向排程器發送訊息,標示逾時步驟,讓排程器能再次嘗試。 此設計要求階梯邏輯為 冪元邏輯,因為相同的工作可能會重複執行。
或者,主管可以要求排程器透過觸發 補償交易來撤銷整個任務。 此方法依賴排程器與代理提供足夠資訊,以對已成功完成的每個步驟實施補償操作。 補償日誌需要排序以及耐久性,因為補償操作通常以已完成步驟的反向順序進行。 補償作業本身若失敗,任務就會處於既未完成、也未完全還原的狀態。 狀態儲存必須能表示此狀態,以便操作員能定位並調查。
Note
監督程式不會監控排程器和代理程式,並在它們失敗時將其重新啟動。 那項職責屬於託管基礎架構。 主管也不需要知道排程員正在執行哪些業務運作,包括如果業務失敗該如何補償。 這些知識屬於排程器執行的工作流程邏輯。 監督者僅負責偵測步驟失敗,並安排該步驟重複或撤銷包含失敗步驟的整個任務。
在實際實作此模式中,多個排程器實例可能同時執行,每個實例處理部分任務。 系統也可能執行每個代理的多個實例,或多個監督者。 當多個 Supervisor 處於啟用狀態時,這些 Supervisor 必須彼此協調,以免同時嘗試復原同一個失敗的步驟或任務。 領袖選舉模式是協調監督者的一種方式。
這種模式的主要好處是系統能對短暫或無法恢復的故障保持韌性,且你可以將其建構為自我修復。 如果代理或排程器失敗,你可以啟動新實例,主管可以安排受影響的任務恢復。 如果監督者本身發生故障,另一個實例可以從前一個實例中斷的地方接手繼續運作。 當主管依照排程執行時,下一個時段會自動啟動一個新實例。 你也可以複製狀態儲存庫以提升韌性。
問題和考慮
決定如何實作此模式時,請考慮下列幾點:
實作複雜度。 此模式難以實作,且需對系統可能遇到的每一種故障模式進行徹底測試。 同時規劃故障注入測試與功能測試。
重試放大。 代理程式、協調流程執行階段、用戶端函式庫、訊息傳遞及監督層各自獨立的重試上限,會使對發生故障的相依服務的呼叫次數成倍增加。 指定排程器擁有一個持久的端對端重試預算,並在其他層設定或停用重試,確保它們的總最大值不會超過預算。
准入控制。 這個模式並不限制同時飛行的工作量。 由於排程器的設計宗旨是不會遺失任何已接受的工作,當下游服務變慢或發生故障時,會導致處理記錄不斷累積,而不是形成提交方可見的明顯背壓。 提交路徑需要有自己的準入控制,例如有界佇列、同時在途任務數量的上限,或在待處理積壓超過門檻時拒絕新的提交。 這種背壓有助於確保下游事故不會悄然導致狀態儲存積壓無限制地增加。
恢復狀態耐久度。 排程器的恢復與重試邏輯複雜,且取決於狀態儲存中所保存的狀態。 你可能還需要記錄在耐用資產倉庫中推動補償交易所需的資訊,而補償交易本身可能會失敗。 將狀態儲存體及任何補償交易日誌視為一級的持久性資產。 這些資產必須比排程員、代理人和主管更久,且必須獨立於寫信給它們的工作人員之外,能夠被追回。
Important
補償日誌必須繼承其需要追蹤的資料分類。若某步驟接受客戶或付款資料,補償交易日誌會繼承該敏感性及相關保留規則。
雙重寫入問題。 提交任務通常涉及不只一次持久性寫入。 例如,任務可能需要同時在商業資料庫中寫入應用程式記錄,並在狀態儲存中寫入初始記錄。 這些寫入不共享交易,因此如果只有一次寫入完成,應用程式記錄可能沒有狀態儲存記錄,或狀態儲存記錄沒有應用程式記錄。 目前的模式本身無法解決這個雙重寫入問題。 常見的解決方案包括在持久性協調流程中進行寫入,並在失敗時執行補償性刪除、採用 交易式外箱模式,或使用穩定的識別碼,例如應用程式本身的任務 ID,讓每次寫入都可安全地重複執行。
工作流程與狀態版本管理。 長時間執行的任務可能會橫跨多次部署,因此,對工作流程邏輯或持久化狀態所做的變更,可能會使進行中的工作無法重播、恢復或補償。 為兩個合約進行版本管理,並保留相容的程式碼與狀態處理機制,直到沒有任何作用中的任務依賴它們為止。
薪酬結果。 補償性交易可能完成、失敗或中途停滯。 排程器維護的狀態機需要代表所有這些狀態。 如果錯誤是唯一的終端狀態,卡住補償看起來與在補償執行前失敗的步驟相同,操作員將失去介入所需的訊號。 在狀態儲存架構中,應區分 補償進行中、補償已完成 與 補償失敗,而不是將它們合併成單一的 錯誤 狀態。
主管週期 Supervisor 應該多久執行一次,是一種刻意的權衡。 它的執行頻率應足夠高,以免失敗或逾時的步驟長時間阻礙工作進行,但也不能高到對狀態儲存體造成負載與成本。 根據步驟的典型持續時間以及對卡住的步驟可接受的偵測延遲時間來調整間隔,並在工作流程量成長時重新檢視。
階梯冪性。 代理程式可能會執行某個步驟不只一次。 例如,主管可能會延長完成時限,並要求排程器重試某個步驟;該步驟在原先執行時其實已產生效果,但排程器從未察覺。 因此,實現每個步驟的邏輯必須是 冪元的。
操作可視性。 該模式的執行時行為是狀態-儲存轉換,而非同步回應。 操作員只能看到州商店和員工的排放。 決定排程器、代理和主管應該發出什麼,以及哪些閾值應該觸發警報。
排程器重啟與恢復。 若排程器在失敗後重新啟動,或排程器執行的工作流程意外終止,排程器必須能夠判斷失敗時所處理任務的狀態,並從該點繼續執行任務。 此復原的實作細節通常依系統而異。 例如,建立在檢查點編排執行時的排程器可以依賴執行時重播歷史並繼續執行,而自訂排程器則必須從狀態儲存庫本身重建執行中狀態。 如果任務無法恢復,該任務已完成的工作可能需要撤銷,這可能需要進行 補償性交易。
使用此模式的時機
當下列情況時,請使用此模式:
程序運行於分散式工作負載中,必須對 其協調的服務與資源間的通訊故障及操作故障保持韌性。
背景工作協調多步驟工作流程,這些步驟涉及遠端服務或資源,整體工作流程必須完成或乾淨地還原。 常見的例子包括訂單處理與資源配置。 欲了解更多資訊,請參閱 背景工作最佳實務。
在下列情況下,此模式可能不適用:
- 該任務不會呼叫遠端服務或存取遠端資源。 排程員、代理和主管角色沒有分散式故障面可以緩解。 該模式的協調開銷增加了成本,卻沒有韌性效益。
工作負載設計
評估如何在工作負載設計中使用排程代理監督者模式,以達成Azure Well-Architected框架支柱所涵蓋的目標與原則。 下表提供此模式如何支援每個要素目標的指引。
| 支柱 | 此模式如何支援支柱目標 |
|---|---|
| 可靠性 設計決策有助於使工作負載具有韌性,並確保在故障發生後能復原到正常運作的狀態。 | 此模式使用持久狀態儲存與定期監督器來偵測逾時或失敗的步驟,並推動復原。 - RE:05 備援 - RE:07 自我保護 |
| 效能效率 可透過調整、數據和程式碼的優化, 有效率地協助您的工作負載符合需求 。 | 此模式將長時間執行的多步驟工作的協調作業,與執行這些步驟的代理程式分離,因此可將步驟分派給具有可用容量的代理程式。 當實作明確提供優先順序訊號時,高優先順序的工作可以排在低優先順序的工作之前執行。 - PE:05 擴展和分區 - PE:09 關鍵流程 |
如果此模式在一個支柱內部引入取捨,請將它們與其他支柱的目標進行考量。
範例
本範例部署了一個在 Microsoft Azure 上實作電子商務系統的網頁應用程式。 使用者透過網頁前端瀏覽產品和下訂單,訂單處理路徑中的背景工作人員則呼叫一個容易出現短暫且持續時間較長故障的遠端服務。 訂單處理透過使用 Durable Functions、Azure 服務匯流排 和 Azure Cosmos DB 實作排程器代理監督者模式。
提交活動會寫入訂單資料庫和 Cosmos DB 狀態存放區,Durable Functions 協調器會認領待處理的狀態記錄,並透過一對 服務匯流排 請求與回應佇列將工作分派給 Agent,而 Supervisor 則會掃描狀態存放區以找出已過期的 CompleteBy 時間。
下圖展示了 Azure 解決方案的高層次視圖。
- 應用程式會向佇列發送請求以處理訂單。
- 提交程序會擷取請求,將訂單詳細資料插入 orders 資料庫,並在狀態存放區中為該訂單建立一筆記錄。
- 排程器會在狀態儲存中尋找設定
LockedBy為 null 的訂單,排程器中的工作流程邏輯負責處理該訂單。 - 工作流程邏輯透過訊息佇列向代理發送請求並接收回應。
- 工作流程任務使用代理程式來呼叫遠端服務。
- 監督程式會尋找其
CompleteBy值已過期的訂單,並更新此值,以便讓排程器重新嘗試該處理程序。
提交
當客戶下訂單時,網頁前端會將訂單訊息貼入 服務匯流排 佇列。 提交函式會以 PeekLock 模式接收訊息,將訂單細節插入訂單資料庫,並為訂單流程建立狀態儲存記錄。
由於訂單資料庫與使用 Azure Cosmos DB 建立的資料庫是不同的資源,且不共用同一筆交易,因此此函式會將每次寫入視為各自獨立且具冪等性。 該函式使用訂單 ID 作為穩定的鍵值,在任一筆紀錄缺失時建立該紀錄,且僅在確認既有紀錄中的不可變訂單資料與訊息相符後,才接受該紀錄。 資料衝突會導致函式以死寫方式傳遞訊息並提醒操作員,而非覆寫紀錄。
記錄包含以下欄位:
| Field | Description |
|---|---|
OrderID |
訂單資料庫中的訂單 ID,作為 Azure Cosmos DB 文件 ID 和分割鍵。 |
LockedBy |
處理該命令的編排的嘗試專屬實例 ID。 多個排程器編排可能同時執行,但條件狀態轉換只允許一次主動嘗試申請訂單。 |
CompleteBy |
訂單必須完成處理的截止時間。 |
ProcessState |
處理訂單之工作的目前狀態。 可能的狀態為: - Pending訂單已建立,但處理尚未開始。 - Processing:該命令目前正在處理中。 - Processed:訂單成功處理。 - Error:訂單處理失敗。 |
FailureCount |
訂單中記錄的失敗或逾時處理次數。 |
當提交活動第一次寫入該記錄時,會從新訂單複製 OrderID,將 CompleteBy 和 LockedBy 設為 null,將 ProcessState 設為 Pending,並將 FailureCount 設為 0。
Note
在此範例中,訂單處理邏輯刻意簡化,僅有一個步驟呼叫遠端服務。 在更複雜的多步驟情境中,提交活動會為每個步驟建立一筆狀態存放區記錄,並將 OrderID 作為分割鍵,同時使用唯一的項目 ID(例如 {OrderID}:{StepID}),讓排程器與監督者能分別判斷每個步驟的狀態。
Scheduling
排程器是 Durable Functions 的編排器。 一個啟動函式會輪詢 Azure Cosmos DB 中 LockedBy 為 null 且 ProcessState 為 Pending 的記錄。 該函式會根據 OrderID 和下一次嘗試編號,建立特定於該次嘗試的 Orchestration 執行個體 ID。 條件式更新會先將 ProcessState 設為 Processing,並將 LockedBy 設為該執行個體 ID,然後啟動程式才會排定協調流程。 此主張防止兩名先發球員排程同一次嘗試。
來自協調流程的每次更新也都會驗證 LockedBy 是否仍與其執行個體 ID 相符。 此檢查可防止已過期的嘗試變更替代嘗試的狀態。 跨序平行運算來自於同時執行多個編排實例,而非單一順序內的平行運算。 編排會設定 CompleteBy 並驅動工作流程。
編排器從訂單資料庫取得訂單細節,並將工作流程作為一系列活動函數執行。 當步驟需要遠端服務時,會呼叫代理。
Execution
當工作生命週期較短,且在相同的函式應用程式中執行時,代理可以是活動函式;或者,當它在不同的執行階段中執行,或跨越信任邊界執行時,代理也可以是透過 服務匯流排 要求/回應佇列組連線的獨立工作者。 在這兩種情況下,協調器都會讓 持久計時器 與 Agent 回應競速,以偵測是否到達 CompleteBy 截止時間。
當代理透過 服務匯流排 被聯繫時,一個佇列觸發函式會接收回應,並使用 Durable Functions 用戶端來引發包含編排實例 ID 與穩定事件 ID 的外部事件。 編排器會透過啟用重複偵測,並在每個要求中設定穩定的MessageId,來排除重複的外部事件,讓 服務匯流排 能夠捨棄因重試而引入的重複項目。
如果代理在持久計時器觸發前收到遠端服務的回應,代理會將結果回傳給編排器。 編排器會完成此步驟,並且只有在 Processed 仍可辨識目前的協調流程時,才會將 LockedBy 更新為 ProcessState 如果計時器先觸發,編排器會將該步驟視為失敗,停止等待,並完成而不更改狀態紀錄。
代理人會在自身期限過後配合停止啟動新的副作用,但已在進行中的工作仍可能繼續。 狀態存放區記錄接著會維持在 Processing 狀態,且其 CompleteBy 值已過期,讓 Supervisor 能安排復原。 使用冪性或目標強制的擊劍標記,防止重疊嘗試重複或產生副作用。
若代理在聯絡遠端服務時偵測到無法復原且非暫時性的故障,會向編排器回傳錯誤回應。 編排器會啟動ProcessStateError並提出事件,提醒操作員,操作員可以調查故障並重新提交失敗的處理步驟。
監督
監督程式會定期查詢 Azure Cosmos DB 狀態存放區中處於 CompleteBy 狀態且 Processing 已過期的訂單。 由於 OrderID 是分割區鍵,即使 ProcessState 和 CompleteBy 被索引,此查詢仍會到達所有實體分割區。 此程序會將每次掃描限制在 CompleteBy 時間視窗、結果數上限及接續權杖分頁的範圍內,並監控其請求單位(RU)費用。
較大的工作負載會維護個別的復原索引或佇列,並依照 Supervisor 的存取模式加以分區。 當主管發現過期紀錄時,會使用條件更新,驗證過期 LockedBy 值後再遞增 FailureCount。 若 FailureCount 低於已設定的閾值,Supervisor 會將 LockedBy 重設為 null,將 CompleteBy 更新為新的到期時間,並將 ProcessState 設回 Pending,讓啟動器得以排程新的嘗試。 若 FailureCount 超過 閾值,監督者將故障視為非瞬態,設 ProcessState 為 Error,並觸發事件提醒操作員。
Note
當掃描時間短且負載輕量時,Supervisor 會以 計時器觸發函式 的形式託管;當掃描需要自訂容器映像、執行時間較長或側車時,則會以 已排程的 Azure 容器應用程式 作業 的形式託管。 這兩種選項都以 cron 風格的排程執行,適合 Supervisor 所執行的週期性、短時間工作。 計時器觸發程序會在同一個函式應用程式中,於擴展後的各執行個體之間使用儲存體鎖定。 獨立部署或容器應用程式工作若可能重疊,請使用前述條件恢復聲明,或透過 領導者選舉模式協調。
狀態報告
協調器可能也需要讓提交訂單的應用程式隨時掌握進度和最終狀態,但圖中未顯示這一點。 應用程式與編排器是刻意分離的。 應用程式不知道是哪個 orchestration 實例在處理訂單,編排也不知道是哪個應用程式實例發佈了這個命令。 由於這種解耦,編排器無法同步回傳進度更新。
為了回報訂單狀態,每個提交的應用程式都會提供自己的私人、存取控制的 服務匯流排 回應佇列。 傳送給提交活動的請求包含該佇列的識別碼,而提交活動會將此識別碼記錄在訂單的狀態存放區記錄中,讓協調器能將訊息投遞到正確的佇列,而不是進行廣播。 編排器接著將狀態訊息(如 請求已收到、 訂單已完成或 訂單失敗 )貼入該佇列,並 OrderID 讓應用程式能將每則訊息與原始請求關聯起來。
支援技術
將模式的邏輯角色對應到符合您工作流程與營運需求的服務:
排程員:使用 Durable Functions 進行優先程式碼的協調,或使用 Azure Logic Apps 進行視覺化工作流程與基於連接器的整合。
客服訊息:使用 服務匯流排 佇列進行非同步請求與回應訊息傳遞。
狀態存放區:使用 Azure Cosmos DB 儲存文件型狀態,或使用 Azure SQL Database 儲存關聯式狀態和跨記錄交易。
Supervisor: 使用 Functions 計時器觸發程序 來執行輕量掃描,或在需要容器執行階段時使用 Container Apps 作業。
薪酬記錄:將補償紀錄與任務狀態一同儲存,或存放在 Azure 表格儲存或 Blob 儲存體 中。
下一步
- 在 Azure Functions 與 Azure Logic Apps 之間進行選擇,以進行工作流程協調。
- 版本 Durable Functions 協調,保護部署期間的飛行中工作流程。
- 結合訊息解決、冪性與死符處理,防止 Azure 服務匯流排 中的訊息遺失與重複處理。
相關資源
當您實作此模式時,下列模式也可能相關:
重試模式。 代理程式可以使用此模式,以透明方式重試先前曾失敗、且會存取遠端服務或資源的操作。 當故障原因預期是暫時性的,且可透過重試修正時使用。
斷路器設計模式。 連接遠端服務或資源的代理程式可利用此模式處理需要不同時間修正的故障,避免重複呼叫堆積在不太可能回應的相依性上。
補償交易模式。 如果排程器無法成功完成工作流程,已完成的工作可能需要撤銷。 補償交易模式說明了如何針對採用最終一致性模型的作業,撤銷先前執行的工作;這種模型常見於由排程器協調的長時間執行業務流程中。
領導者選舉模式。 當多個 Supervisor 實例同時運行時,它們必須彼此協調,以免競相復原同一個失敗的步驟。 領袖選舉模式描述了如何選出單一協調員來執行該工作。