iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Software Development

Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程系列 第 19

Day19 - Temporal Child Workflow:大型流程的治理模式

  • 分享至 

  • xImage
  •  

1. 為什麼需要 Child Workflow?

在 Temporal 裡,我們會把流程封裝成一個 Workflow。

但當流程越來越複雜時,我們會發現:

  • 有些任務要跑很久,可能幾小時、甚至幾天。
  • 有些任務本質獨立,需要分開觀測、追蹤。
  • 有些任務有自己的 Timeout、Retry 或 Continue-As-New 的條件,否則會把主流程拖垮。

如果全都只靠 Activity,Workflow 會很快膨脹,並且難以維護;

這時候,Child Workflow 就可以派上用場,讓複雜流程可以結構化的拆分,保持簡單。

2. 使用時機分析

2.1 具體使用時機與解決方案

使用時機 具體情境 為什麼用 Child Workflow 不用會怎樣 設計思維
分離服務 / Worker 池 支付與訂單分流到不同 Task Queue/團隊 子流程可掛在不同 Task Queue / Worker,不跟父流程搶 CPU、IO 資源 所有任務擠在同一個 Worker,造成延遲或卡死 父流程只協調,子流程各自跑,分散資源消耗
拆大量工作 1 萬筆報表/批次同步需要 fan-out/fan-in 父流程 Event History 有上限,長期任務會導致歷史膨脹 父流程歷史過大 → 重播慢、記憶體暴增 把子任務拆成多個子流程,降低單一流程的歷史負擔
資源唯一對應 以商品 ID/host 為單位序列化更新 一個子流程守護一個資源(如商品 ID、host),確保操作序列化 多個操作同時打到父流程,容易出現亂序或 race condition 以 resourceId 當子流程 ID,天然序列化
週期性邏輯 每日對帳/外部狀態輪詢/定時同步 子流程可用 Continue-As-New 做週期性任務,不讓父流程變肥 父流程歷史不斷累積,最後變成「巨無霸」 把週期性工作獨立成子流程,靠續跑保持輕量

2.2 設計思維

  • 隔離資源:把子任務丟到不同 Task Queue / Worker,父流程只負責協調。
  • 控制歷史:避免父流程 Event History 膨脹,讓長任務各自跑在子流程。
  • 單一責任:一個資源一條子流程,天然序列化,避免亂序與 race condition。
  • 持續運行:子流程可用 Continue-As-New 做週期性任務,父流程保持輕量。

2.3 與 Activity 的比較

  • Activity 適合:短至中長時間、需呼叫外部服務、允許使用不具決定性(non-deterministic)的程式庫;但 Activity 本身沒有 Workflow 的子樹事件歷史,也不支援再啟動後保有細緻狀態機制。
  • Child Workflow 適合:需要完整 Workflow 能力(訊號、查詢、重試、版本、可觀測屬性)、可長時間執行、需要人機互動或多步驟協調;也常用於 fan-out 的分治與隔離。
  • 成本與重量:Child 比 Activity 更「重」,因為它是完整 Workflow 實例,有獨立事件歷史與任務分派。若只是單一 RPC 或批次寫入,Activity 通常更划算;若是多步驟、跨時段、需互動,選 Child。

3. 執行方式與生命週期

3.1 生命週期與事件流程

  1. 父流程透過 SDK 發出「啟動子流程」,運用 workflowId 可避免重複啟動相同子流程。。
  2. Temporal 產生子流程的事件歷史,排入子流程所屬 Task Queue
  3. 子流程 Worker 接手執行,能力與一般 Workflow 基本無異,如:重試、超時、訊號、查詢等。
  4. 父流程可選擇:
    • 立即等待 child.result()(同步等待完成)
    • 僅等待 child.started(確保已啟動),之後並行處理其他工作
    • 透過訊號與子流程互動,或在需要時取消
  5. 收斂(fan-in):父流程等待多個子流程結果,彙整後繼續。
  6. 關閉方式:依 ParentClosePolicy 決定當父流程關閉時對子流程的處置。

3.2 Parent Close Policy 比較

操作 情境 最佳實踐 注意事項
REQUEST_CANCEL 父流程關閉時對子流程送出取消請求,給子流程時間進行補償或清理 預設優先;實作取消/補償路徑;設定監控避免長尾取消中流程 子流程需實作取消處理;未妥善處理可能佔用資源或留下不一致
ABANDON 父流程關閉不影響子流程,子流程持續執行;適合可獨立完成的分析/延遲任務 僅用於可獨立完成任務;加上 Search Attributes;定期掃描並收斂孤兒 容易產生孤兒工作;務必搭配觀測(Search Attributes、警報)與收斂機制
TERMINATE 立即終止子流程;用於必須立刻停止或確定不需收尾的情境 僅在確定無需收尾或外部自動回滾時使用;文件化副作用;定期演練 不給清理時間,可能留下外部副作用或半完成狀態

4. 程式碼範例

以下展示使用 Java SDK 啟動、等待子流程的常見寫法。

4.1 父、子流程介面

import io.temporal.workflow.SignalMethod;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;

@WorkflowInterface
public interface ChildWorkflow {
  @WorkflowMethod
  ChildResult execute(ChildArgs args);

  @SignalMethod
  void expedite(String reason);
}

@WorkflowInterface
public interface ParentWorkflow {
  @WorkflowMethod
  void execute(String orderId);
}

4.2 父流程實作(啟動子流程、等待啟動、發訊號、等待結果)

import io.temporal.api.common.v1.WorkflowExecution;
import io.temporal.api.enums.v1.ParentClosePolicy;
import io.temporal.common.RetryOptions;
import io.temporal.workflow.Async;
import io.temporal.workflow.ChildWorkflowOptions;
import io.temporal.workflow.Promise;
import io.temporal.workflow.Workflow;
import java.time.Duration;

public class ParentWorkflowImpl implements ParentWorkflow {
  @Override
  public void execute(String orderId) {
    ChildWorkflowOptions options = ChildWorkflowOptions.newBuilder()
        .setWorkflowId("order:" + orderId + ":payment") // 指定子流程 WorkflowId,利於冪等與追蹤
        .setTaskQueue("payments") // 子流程由哪個 Task Queue 的 Workers 執行
        .setParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_REQUEST_CANCEL) // 父流程關閉時對子流程送出取消請求
        .setRetryOptions(RetryOptions.newBuilder()
            .setInitialInterval(Duration.ofSeconds(10)) // 重試初始間隔
            .setBackoffCoefficient(2.0) // 指數退避係數
            .setMaximumAttempts(10) // 最大嘗試次數
            .build()) // 建立子流程重試設定
        .build(); // 建立子流程選項

    ChildWorkflow child = Workflow.newChildWorkflowStub(ChildWorkflow.class, options); // 建立子流程型別化 stub(尚未啟動)

    // 啟動子流程並取得「已啟動」的保證
    Promise<WorkflowExecution> started = Workflow.getWorkflowExecution(child);
    Promise<ChildResult> resultPromise = Async.function(child::execute, new ChildArgs(orderId));

    // 僅等待子流程已啟動,之後可並行做其他事
    started.get();

    // 對子流程發送訊號
    child.expedite("VIP customer");

    // 視需求可取消:取消 scope 或透過 stub 取消
    // WorkflowStub.fromTyped(child).cancel();

    // 等待子流程結果(fan-in 可等待多個 Promise)
    ChildResult result = resultPromise.get();
    // 使用 result 繼續後續步驟
  }
}

4.3 子流程實作(接收訊號並調整路徑)

import io.temporal.workflow.Workflow;
import java.time.Duration;

public class ChildWorkflowImpl implements ChildWorkflow {
  private boolean expedited;

  @Override
  public void expedite(String reason) {
    this.expedited = true;
  }

  @Override
  public ChildResult execute(ChildArgs args) {
    // 模擬長流程
    Workflow.sleep(Duration.ofHours(1));
    // 根據 expedited 調整流程邏輯或參數
    return new ChildResult(true, expedited);
  }
}

4.4 簡單資料型別

public record ChildArgs(String orderId) {}
public record ChildResult(boolean ok, boolean expedited) {}

4.5 程式碼範例重點

  • Workflow.newChildWorkflowStub + Async.function 啟動子流程;用 Workflow.getWorkflowExecution(child) 等待「已啟動」。
  • 透過型別化的 @SignalMethod 直接呼叫 child.expedite(...) 發送訊號。
  • 取消可用取消範疇(CancellationScope)或 WorkflowStub.fromTyped(child).cancel()(依實作需求選擇)。

5. 常見陷阱

  • 將長流程/需互動任務誤用 Activity:失去訊號、查詢、版本等流程語義。
  • ParentClosePolicy 設定不當(含濫用 ABANDON):造成孤兒或不一致/副作用殘留。
  • 取消處理薄弱:子流程未感知或未快速收尾,導致資源佔用與延遲。
  • Search Attributes 設計不佳(未串聯或命名不一致):關聯與查詢困難,觀測性差。
  • 與父流程共用 Task Queue:尖峰資源爭奪,父流程決策飢餓。
  • Timeout/Retry 過寬:長尾重試與成本放大,難以控管。
  • 收斂(fan-in)缺少錯誤與補償策略:只等成功,忽略部分失敗。
  • workflowId 冪等與重用未管理:重複啟動同一子流程。
  • 子流程過於龐大:失去拆分與治理的價值。

結語

透過 Child Workflow,把大型流程切割成可治理、可伸縮、可觀測的子單元,讓整體系統更穩健、可維運且易於演進。


上一篇
Day18 - Temporal Entity Pattern 的設計思維:讓每個商品庫存都有專屬 Workflow
下一篇
Day20 - Asynchronous Activity Completion - 非同步完成,讓資源不再卡住
系列文
Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言