Durable Execution: Xây dựng workflow không sợ crash trong hệ thống phân tán

Posted on: 4/22/2026 11:10:40 PM

Vấn đề với workflow truyền thống

Hãy tưởng tượng bạn đang xây dựng một hệ thống xử lý đơn hàng e-commerce. Flow xử lý bao gồm: xác minh thanh toán → trừ kho → gửi email xác nhận → gọi API vận chuyển → cập nhật trạng thái. Nếu server crash giữa bước 3, chuyện gì xảy ra?

Với cách tiếp cận truyền thống, bạn phải tự quản lý state: lưu trạng thái vào database sau mỗi bước, viết logic retry thủ công, xử lý idempotency, và build cron job để "quét" các đơn hàng bị kẹt. Code business logic 50 dòng bỗng phình thành 500 dòng infrastructure code.

Durable Execution là gì?

Durable Execution (thực thi bền vững) là mô hình cho phép bạn viết code tuần tự bình thường, nhưng platform đảm bảo code đó sẽ chạy đến hoàn tất — dù server crash, network timeout, hay deployment giữa chừng. State được tự động persist và restore mà developer không cần viết một dòng code lưu trữ nào.

0 Dòng code quản lý state thủ công
100% Đảm bảo chạy đến hoàn tất
~2000+ Công ty dùng Temporal production
Giây → Năm Thời gian workflow có thể chạy

Cơ chế hoạt động: Event History và Replay

Trái tim của Durable Execution là Event History — một log bất biến (append-only) ghi lại mọi sự kiện xảy ra trong workflow. Khi một worker crash, platform sẽ replay lại event history trên một worker mới, tái tạo lại toàn bộ state mà không cần chạy lại side effects.

sequenceDiagram
    participant W as Worker
    participant S as Server/Scheduler
    participant DB as Event Store

    W->>S: Bắt đầu Workflow
    S->>DB: Ghi WorkflowStarted
    W->>S: Activity: Xác minh thanh toán ✓
    S->>DB: Ghi ActivityCompleted(payment)
    W->>S: Activity: Trừ kho ✓
    S->>DB: Ghi ActivityCompleted(inventory)
    Note over W: 💥 Worker CRASH!
    S-->>W: Worker mới được assign
    S->>DB: Đọc Event History
    DB-->>S: [Started, Payment✓, Inventory✓]
    S-->>W: Replay → skip payment, skip inventory
    W->>S: Activity: Gửi email (tiếp tục từ bước 3)
    S->>DB: Ghi ActivityCompleted(email)

Cơ chế replay: Worker mới đọc event history, skip các activity đã hoàn tất, tiếp tục từ điểm dừng

Ràng buộc Deterministic

Điểm quan trọng nhất cần hiểu: workflow code phải deterministic. Khi replay, platform chạy lại workflow code từ đầu, nhưng thay vì thực thi thật các activity, nó so khớp với event history. Nếu code không deterministic (ví dụ dùng DateTime.Now hay Random trực tiếp), replay sẽ cho kết quả khác và workflow fail.

⚠️ Những thứ KHÔNG được dùng trong workflow code

Cấm: DateTime.Now, Random, Thread.Sleep, gọi API/DB trực tiếp, đọc file, đọc biến môi trường thay đổi được.
Thay thế: Dùng API của platform — Workflow.CurrentTime, Workflow.Random, Workflow.Sleep. Mọi side effect phải đặt trong Activity.

Temporal — Kiến trúc và code thực tế

Temporal là durable execution engine phổ biến nhất hiện tại, được Netflix, DoorDash, Stripe, Snap sử dụng trong production. Temporal là open-source (MIT license) với managed cloud option.

Kiến trúc Temporal

graph TB
    subgraph Client["Client Application"]
        A["Temporal Client
SDK"] end subgraph TS["Temporal Server Cluster"] F["Frontend Service
API Gateway"] H["History Service
Event Storage + Replay"] M["Matching Service
Task Queue Dispatch"] W2["Internal Worker"] end subgraph Workers["Worker Fleet"] W1["Worker 1
Workflow + Activity"] W3["Worker 2
Workflow + Activity"] W4["Worker N
Workflow + Activity"] end subgraph Storage["Persistence"] DB2["Database
PostgreSQL / MySQL / Cassandra"] ES["Elasticsearch
Visibility"] end A -->|"StartWorkflow
Signal/Query"| F F --> H F --> M H --> DB2 M -->|"Dispatch Task"| W1 M -->|"Dispatch Task"| W3 M -->|"Dispatch Task"| W4 H --> ES style A fill:#e94560,stroke:#fff,color:#fff style F fill:#2c3e50,stroke:#fff,color:#fff style H fill:#2c3e50,stroke:#fff,color:#fff style M fill:#2c3e50,stroke:#fff,color:#fff style W1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style W3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style W4 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style DB2 fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50 style ES fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50

Kiến trúc Temporal Server: Frontend nhận request, History quản lý event, Matching phân phối task đến Worker

Code ví dụ: Order Processing Workflow

Dưới đây là ví dụ workflow xử lý đơn hàng với Temporal SDK cho .NET:

// Định nghĩa Workflow Interface
[Workflow]
public class OrderWorkflow
{
    [WorkflowRun]
    public async Task<OrderResult> RunAsync(Order order)
    {
        // Bước 1: Xác minh thanh toán
        var paymentResult = await Workflow.ExecuteActivityAsync(
            (OrderActivities act) => act.VerifyPaymentAsync(order.PaymentInfo),
            new ActivityOptions { StartToCloseTimeout = TimeSpan.FromSeconds(30) });

        if (!paymentResult.Success)
            return OrderResult.Failed("Payment verification failed");

        // Bước 2: Trừ kho — có compensate nếu fail sau này
        await Workflow.ExecuteActivityAsync(
            (OrderActivities act) => act.ReserveInventoryAsync(order.Items),
            new ActivityOptions
            {
                StartToCloseTimeout = TimeSpan.FromMinutes(1),
                RetryPolicy = new RetryPolicy { MaximumAttempts = 3 }
            });

        // Bước 3: Gửi email xác nhận
        await Workflow.ExecuteActivityAsync(
            (OrderActivities act) => act.SendConfirmationEmailAsync(order),
            new ActivityOptions { StartToCloseTimeout = TimeSpan.FromSeconds(15) });

        // Bước 4: Tạo shipment — có thể chờ hàng giờ/ngày
        var trackingId = await Workflow.ExecuteActivityAsync(
            (OrderActivities act) => act.CreateShipmentAsync(order),
            new ActivityOptions
            {
                StartToCloseTimeout = TimeSpan.FromMinutes(5),
                RetryPolicy = new RetryPolicy
                {
                    MaximumAttempts = 5,
                    InitialInterval = TimeSpan.FromSeconds(10),
                    BackoffCoefficient = 2.0
                }
            });

        // Bước 5: Chờ xác nhận giao hàng (có thể nhiều ngày)
        var delivered = await Workflow.WaitConditionAsync(
            () => _deliveryConfirmed,
            timeout: TimeSpan.FromDays(14));

        return delivered
            ? OrderResult.Completed(trackingId)
            : OrderResult.DeliveryTimeout(trackingId);
    }

    private bool _deliveryConfirmed;

    [WorkflowSignal]
    public async Task ConfirmDeliveryAsync()
    {
        _deliveryConfirmed = true;
    }

    [WorkflowQuery]
    public string GetStatus() => _currentStatus;
}
// Activity Implementation — nơi chứa side effects
[Activity]
public class OrderActivities
{
    private readonly IPaymentGateway _payment;
    private readonly IInventoryService _inventory;
    private readonly IEmailService _email;

    public OrderActivities(
        IPaymentGateway payment,
        IInventoryService inventory,
        IEmailService email)
    {
        _payment = payment;
        _inventory = inventory;
        _email = email;
    }

    [Activity]
    public async Task<PaymentResult> VerifyPaymentAsync(PaymentInfo info)
        => await _payment.ChargeAsync(info);

    [Activity]
    public async Task ReserveInventoryAsync(List<OrderItem> items)
        => await _inventory.ReserveAsync(items);

    [Activity]
    public async Task SendConfirmationEmailAsync(Order order)
        => await _email.SendOrderConfirmationAsync(order.CustomerEmail, order);

    [Activity]
    public async Task<string> CreateShipmentAsync(Order order)
        => await _inventory.CreateShipmentAsync(order.ShippingAddress, order.Items);
}

💡 Signal và Query

Signal cho phép gửi event vào workflow đang chạy từ bên ngoài (ví dụ: webhook xác nhận giao hàng). Query cho phép đọc state hiện tại của workflow mà không ảnh hưởng đến execution. Cả hai đều là cơ chế mạnh mẽ để tương tác với long-running workflow.

Azure Durable Functions — Serverless Durable Execution

Azure Durable Functions là extension của Azure Functions, cung cấp durable execution trong môi trường serverless. Phù hợp nhất nếu bạn đã ở trong hệ sinh thái Azure và cần workflow đơn giản đến trung bình.

// Orchestrator Function — tương đương Temporal Workflow
[Function("OrderOrchestrator")]
public static async Task<OrderResult> RunOrchestrator(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var order = context.GetInput<Order>();

    // Mỗi CallActivityAsync tương đương một Activity trong Temporal
    var payment = await context.CallActivityAsync<PaymentResult>(
        "VerifyPayment", order.PaymentInfo);

    if (!payment.Success)
        return OrderResult.Failed("Payment failed");

    await context.CallActivityAsync("ReserveInventory", order.Items);

    await context.CallActivityAsync("SendConfirmationEmail", order);

    var trackingId = await context.CallActivityAsync<string>(
        "CreateShipment", order);

    // Durable Timer — chờ tối đa 14 ngày
    using var cts = new CancellationTokenSource();
    var deadline = context.CurrentUtcDateTime.AddDays(14);
    var timerTask = context.CreateTimer(deadline, cts.Token);

    // Chờ external event (tương tự Signal trong Temporal)
    var deliveryEvent = context.WaitForExternalEvent<bool>("DeliveryConfirmed");

    var winner = await Task.WhenAny(deliveryEvent, timerTask);
    if (winner == deliveryEvent)
    {
        cts.Cancel();
        return OrderResult.Completed(trackingId);
    }

    return OrderResult.DeliveryTimeout(trackingId);
}

// Activity Function
[Function("VerifyPayment")]
public static async Task<PaymentResult> VerifyPayment(
    [ActivityTrigger] PaymentInfo info,
    [FromServices] IPaymentGateway gateway)
{
    return await gateway.ChargeAsync(info);
}

Các pattern phổ biến trong Durable Functions

graph LR
    subgraph FC["Function Chaining"]
        A1["Activity A"] --> A2["Activity B"] --> A3["Activity C"]
    end

    subgraph FO["Fan-out / Fan-in"]
        B1["Start"] --> B2["Task 1"]
        B1 --> B3["Task 2"]
        B1 --> B4["Task 3"]
        B2 --> B5["Aggregate"]
        B3 --> B5
        B4 --> B5
    end

    subgraph MN["Monitor"]
        C1["Check"] --> C2{"Done?"}
        C2 -->|No| C3["Timer"] --> C1
        C2 -->|Yes| C4["Complete"]
    end

    style A1 fill:#e94560,stroke:#fff,color:#fff
    style A2 fill:#e94560,stroke:#fff,color:#fff
    style A3 fill:#e94560,stroke:#fff,color:#fff
    style B1 fill:#2c3e50,stroke:#fff,color:#fff
    style B5 fill:#2c3e50,stroke:#fff,color:#fff
    style C1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style C4 fill:#4CAF50,stroke:#fff,color:#fff

Ba pattern chính: Function Chaining (tuần tự), Fan-out/Fan-in (song song), Monitor (polling có timer)

Restate — Lightweight Durable Execution

Restate là engine mới nổi, tập trung vào sự đơn giản. Thay vì yêu cầu bạn tổ chức code theo workflow/activity riêng biệt, Restate cho phép "đánh dấu" bất kỳ hàm nào thành durable chỉ bằng decorator.

// Restate SDK — TypeScript
import * as restate from "@restatedev/restate-sdk";

const orderService = restate.service({
  name: "OrderService",
  handlers: {
    processOrder: restate.handlers.handler(
      async (ctx: restate.Context, order: Order) => {
        // ctx.run() tạo "journal entry" — tương tự activity
        const payment = await ctx.run("verify-payment", async () => {
          return await paymentGateway.charge(order.paymentInfo);
        });

        if (!payment.success) {
          return { status: "failed", reason: "payment" };
        }

        await ctx.run("reserve-inventory", async () => {
          await inventoryService.reserve(order.items);
        });

        await ctx.run("send-email", async () => {
          await emailService.sendConfirmation(order);
        });

        // Sleep durable — survive restarts
        await ctx.sleep(60_000); // chờ 1 phút

        const tracking = await ctx.run("create-shipment", async () => {
          return await shippingService.create(order);
        });

        return { status: "completed", trackingId: tracking };
      }
    ),
  },
});

restate.endpoint().bind(orderService).listen(9080);

💡 Điểm khác biệt của Restate

Restate không yêu cầu chạy cluster riêng biệt phức tạp như Temporal. Restate server là một single binary nhẹ, handler là HTTP endpoint bình thường. Đặc biệt phù hợp với team nhỏ muốn durable execution mà không muốn vận hành infrastructure phức tạp.

So sánh chi tiết 3 engine

Tiêu chí Temporal Azure Durable Functions Restate
Hosting Self-hosted hoặc Temporal Cloud Azure serverless (consumption plan) Self-hosted (single binary)
Ngôn ngữ SDK Go, Java, TypeScript, Python, .NET, PHP C#, JavaScript, Python, Java, PowerShell TypeScript, Java, Kotlin, Go, Python
Persistence PostgreSQL, MySQL, Cassandra, SQLite Azure Storage / MS SQL / Netherite RocksDB (embedded) hoặc external
Scalability Hàng triệu workflow đồng thời Auto-scale, giới hạn bởi storage backend Tốt cho medium scale
Versioning Worker Versioning (2026), Build ID Task hub versioning, code conditions Service versioning qua deployment
Observability Web UI, Metrics, Tracing tích hợp Azure Monitor, Application Insights OpenTelemetry, Admin API
Độ phức tạp vận hành Trung bình — cần cluster + DB Thấp — fully managed Thấp — single binary
Pricing OSS miễn phí, Cloud tính theo action Pay-per-execution (serverless) OSS miễn phí, Cloud option
Phù hợp cho Enterprise, high-throughput, multi-cloud Azure-native, serverless workloads Team nhỏ, startup, lightweight needs

Khi nào cần Durable Execution?

Durable Execution không phải silver bullet. Dưới đây là checklist giúp bạn quyết định:

✅ Nên dùng khi

  • Long-running processes: Workflow kéo dài hàng giờ, ngày, hoặc tuần (đơn hàng, onboarding, subscription billing)
  • Saga pattern phức tạp: Cần compensating transactions qua nhiều service với rollback logic rõ ràng
  • Human-in-the-loop: Workflow cần chờ approval từ người dùng (leave request, expense report)
  • Scheduled jobs phức tạp: Cron jobs có state, cần retry, cần monitoring (data pipeline, report generation)
  • Reliable async operations: Gọi external API không đáng tin cậy, cần retry + timeout + fallback

❌ Không nên dùng khi

  • Request-response đơn giản: API trả kết quả trong vài millisecond — overhead không đáng
  • Stateless processing: Xử lý event không cần track state (log aggregation, simple ETL)
  • Real-time strict: Yêu cầu latency < 10ms — replay mechanism thêm overhead
  • Team chưa sẵn sàng: Đường cong học tập khá dốc, đặc biệt deterministic constraint

Best Practices cho Production

1. Thiết kế Activity đúng cách

// ❌ SAI: Activity quá lớn, làm quá nhiều việc
[Activity]
public async Task ProcessEntireOrder(Order order)
{
    await VerifyPayment(order);
    await ReserveInventory(order);
    await SendEmail(order);       // Nếu fail ở đây, phải retry TẤT CẢ
    await CreateShipment(order);
}

// ✅ ĐÚNG: Mỗi Activity là một đơn vị retry độc lập
[Activity]
public async Task<PaymentResult> VerifyPayment(PaymentInfo info) { ... }

[Activity]
public async Task ReserveInventory(List<OrderItem> items) { ... }

[Activity]
public async Task SendEmail(Order order) { ... }

2. Đảm bảo Activity idempotent

Activity có thể bị retry bất kỳ lúc nào (network timeout sau khi đã execute thành công). Mỗi activity phải idempotent — chạy nhiều lần cho cùng kết quả.

[Activity]
public async Task<PaymentResult> ChargePayment(string orderId, decimal amount)
{
    // Dùng idempotency key để đảm bảo không charge 2 lần
    var idempotencyKey = $"order-payment-{orderId}";
    return await _paymentGateway.ChargeAsync(amount, idempotencyKey);
}

3. Versioning workflow an toàn

graph TB
    subgraph V1["Version 1 (đang chạy)"]
        A["Step A"] --> B["Step B"] --> C["Step C"]
    end

    subgraph V2["Version 2 (mới deploy)"]
        D["Step A"] --> E["Step B"] --> F["Step B2 (mới)"] --> G["Step C"]
    end

    subgraph Strategy["Chiến lược"]
        S1["Workflow mới → V2"]
        S2["Workflow đang chạy → V1 cho đến khi hoàn tất"]
    end

    style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style D fill:#e94560,stroke:#fff,color:#fff
    style E fill:#e94560,stroke:#fff,color:#fff
    style F fill:#4CAF50,stroke:#fff,color:#fff
    style G fill:#e94560,stroke:#fff,color:#fff
    style S1 fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
    style S2 fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50

Worker Versioning: workflow đang chạy tiếp tục trên code cũ, workflow mới dùng code mới

4. Testing workflow

// Temporal cung cấp test framework để test workflow mà không cần server
[Fact]
public async Task OrderWorkflow_SuccessfulPayment_CompletesOrder()
{
    var env = await WorkflowEnvironment.StartTimeSkippingAsync();
    var worker = new TemporalWorker(env.Client, new TemporalWorkerOptions("test-queue")
        .AddWorkflow<OrderWorkflow>()
        .AddAllActivities(new OrderActivities(
            mockPayment.Object,
            mockInventory.Object,
            mockEmail.Object)));

    await worker.ExecuteAsync(async () =>
    {
        var result = await env.Client.ExecuteWorkflowAsync(
            (OrderWorkflow wf) => wf.RunAsync(testOrder),
            new WorkflowOptions { Id = "test-order-1", TaskQueue = "test-queue" });

        Assert.Equal("completed", result.Status);
    });
}

// TimeSkipping environment: Workflow.Sleep(14 ngày)
// thực tế chạy trong milliseconds khi test

Kết luận

Durable Execution đang thay đổi cách chúng ta xây dựng hệ thống phân tán. Thay vì dành 80% effort cho infrastructure code (retry, state management, recovery), bạn tập trung 100% vào business logic và để platform xử lý phần còn lại.

Chọn Temporal nếu bạn cần scale lớn, multi-cloud, và team có đủ resource vận hành. Chọn Azure Durable Functions nếu đã ở trong Azure ecosystem và ưu tiên serverless. Chọn Restate nếu team nhỏ, cần nhanh gọn, và chưa cần scale khổng lồ.

Dù chọn engine nào, hãy nhớ: Activity phải idempotent, Workflow phải deterministic, và luôn có chiến lược versioning trước khi lên production.

Tham khảo