blog.dopana

Back

Durable Objects (DO) là giải pháp stateful cho Cloudflare Workers. Mỗi DO có một instance duy nhất — state tồn tại, consistency mạnh.

flowchart LR
    subgraph EdgeWorkers["Stateless Workers (Toàn cầu)"]
        direction TB
        W_Tokyo["Worker Tokyo"]
        W_London["Worker London"]
        W_US["Worker US"]
    end

    subgraph DO_Instance["Durable Object (Instance Duy Nhất Toàn Cầu)"]
        direction TB
        ID["ID: 'user-room-42'"]
        
        subgraph Memory["RAM (In-Memory State)"]
            State["Active Connections & State"]
        end
        
        subgraph Storage["Persistent Storage"]
            SQL[("SQLite / KV Storage")]
        end

        ID --> Memory
        Memory <--> Storage
    end

    W_Tokyo -->|&quot;Tất cả request cùng ID&lt;br/&gt;được điều hướng về 1 điểm&quot;| DO_Instance
    W_London -->|&quot;Strong Consistency&lt;br/&gt;&lpar;Không bị race condition&rpar;&quot;| DO_Instance
    W_US -->|&quot;Tồn tại state liên tục&quot;| DO_Instance

Workers vs Durable Objects#

flowchart TB
    subgraph Clients["Clients & Browsers"]
        C1["User A (WebSocket)"]
        C2["User B (WebSocket)"]
        C3["User C (HTTP REST)"]
    end

    subgraph Edge["Cloudflare Global Edge (Stateless Workers)"]
        W1["Worker (Edge Location 1)"]
        W2["Worker (Edge Location 2)"]
    end

    subgraph DO_Cluster["Durable Objects (Single Coordinated Instance per ID)"]
        subgraph DO1["Chat Room DO (ID: room-123)"]
            Mem["In-Memory State & Active WebSockets"]
            SQL[(&quot;Built-in SQLite / KV Storage&quot;)]
            Alarm["Alarms / Timers"]
            Mem <--> SQL
            Mem <--> Alarm
        end
        subgraph DO2["Chat Room DO (ID: room-456)"]
            Mem2["In-Memory State"]
            SQL2[(&quot;SQLite Storage&quot;)]
        end
    end

    C1 <-->|&quot;Edge Connection&quot;| W1
    C2 <-->|&quot;Edge Connection&quot;| W2
    C3 -->|&quot;Request&quot;| W1

    W1 <-->|&quot;Route by ID: room-123&quot;| DO1
    W2 <-->|&quot;Route by ID: room-123&quot;| DO1
    W1 -.->|&quot;Route by ID: room-456&quot;| DO2
WorkersDurable Objects
StatelessStateful
Nhiều request - nhiều instanceMỗi ID - một instance duy nhất
Không giữ stateGiữ state trong memory + storage
Scale vô hạnScale by sharding (nhiều DO)
330+ locationsMột location (có migration)

Chat Room — Ví Dụ Cơ Bản#

sequenceDiagram
    autonumber
    actor ClientA as User Alice
    actor ClientB as User Bob
    participant Worker as Stateless Worker Router
    participant DO as ChatRoom [Durable Object]

    Note over ClientA, DO: Khởi tạo kết nối WebSocket
    ClientA->>Worker: GET /?room=general&name=Alice (Upgrade: WebSocket)
    Worker->>DO: stub.fetch(request) [idFromName("general")]
    DO->>DO: server.accept(), lưu session "Alice"
    DO-->>ClientA: 101 Switching Protocols (WebSocket Connected)
    DO--)ClientB: Broadcast "Alice joined the chat"

    Note over ClientA, DO: Gửi tin nhắn Real-time
    ClientA->>DO: WS Message: "Hello room!"
    DO->>DO: broadcast("Alice: Hello room!", sender=Alice)
    DO--)ClientB: WS Message: "Alice: Hello room!"

SQLite Storage — D1 Built-in#

Mỗi Durable Object có SQLite storage riêng:

flowchart TD
    Req["Request: GET /increment /get /reset"] --> Match{url.pathname}
    Match -->|&quot;/increment&quot;| Inc["storage.get('count')<br/>storage.put('count', count + 1)"]
    Match -->|&quot;/get&quot;| Get["storage.get('count')"]
    Match -->|&quot;/reset&quot;| Res["storage.put('count', 0)"]
    Match -->|Khác| Err["404 Not Found"]
    
    Inc --> Resp["Return JSON count"]
    Get --> Resp
    Res --> Resp

SQL Queries Với storage.sql#

Dùng SQL trực tiếp với Durable Objects:

flowchart TD
    Req["HTTP Request"] --> Init["Tạo bảng nếu chưa có: CREATE TABLE IF NOT EXISTS todos (...)"]
    Init --> Router{HTTP Method}
    
    Router -->|&quot;GET&quot;| Q1["SELECT * FROM todos ORDER BY id DESC"]
    Router -->|&quot;POST&quot;| Q2["INSERT INTO todos (title) VALUES (?)"]
    Router -->|&quot;PUT&quot;| Q3["UPDATE todos SET completed = ? WHERE id = ?"]
    Router -->|&quot;DELETE&quot;| Q4["DELETE FROM todos WHERE id = ?"]
    Router -->|Other| Q5["405 Method Not Allowed"]

    Q1 --> Out1["JSON: List of todos"]
    Q2 --> Out2["201 Created: new todo"]
    Q3 --> Out3["JSON: &#123; success: true &#125;"]
    Q4 --> Out4["JSON: &#123; success: true &#125;"]

Alarm — Cron Cho DO#

sequenceDiagram
    autonumber
    actor User as User / Client
    participant DO as Reminder DO
    participant Storage as ctx.storage [Alarm Queue]
    participant Webhook as External Webhook

    Note over DO, Storage: 1. Đăng ký Timer / Cron
    User->>DO: GET /?delay=5000
    DO->>Storage: setAlarm(futureTimestamp)
    DO-->>User: {"message": "Alarm set for 5000ms"}

    Note over Storage, Webhook: 2. Kích hoạt khi hết giờ (Wake up DO)
    Storage-->>DO: Kích hoạt handler alarm()
    DO->>Webhook: POST https://hooks.example.com/notify { event: 'alarm_fired' }
    DO->>Storage: setAlarm(nextHourTimestamp) [Đặt lịch lặp lại]

Multiplayer Game Server#

flowchart TD
    WS["WebSocket Message Event"] --> EventType{msg.type}

    EventType -->|&quot;join&quot;| Join["1. Lưu session player vào Map<br/>2. Thêm Player state: x=0, y=0<br/>3. Broadcast: type: 'players'"]
    EventType -->|&quot;move&quot;| Move["1. Cập nhật player.x += dx, player.y += dy<br/>2. Broadcast: type: 'move'"]
    EventType -->|&quot;shoot&quot;| Shoot["1. Tính góc & tọa độ bắn<br/>2. Broadcast: type: 'shoot'"]

    Close["WebSocket Close Event"] --> Leave["1. Xóa player khỏi Player Map & State<br/>2. Broadcast: type: 'leave'"]

    Join --> BroadcastAll["Gửi State cập nhật tới toàn bộ người chơi trong phòng"]
    Move --> BroadcastAll
    Shoot --> BroadcastAll
    Leave --> BroadcastAll

Migration — Di Chuyển DO#

DO có thể migrate giữa các region:

// wrangler.jsonc
{
  "durable_objects": {
    "bindings": [
      {
        "name": "COUNTER",
        "class_name": "Counter",
        "migration": "new_tag"
      }
    ]
  }
}
typescript

Migration types:

  • new_tag — Class mới
  • new_classes — Thêm nhiều class
  • rename — Đổi tên class
  • transfer — Chuyển dữ liệu

Pattern — Sharding#

Một DO chỉ chạy ở một location. Để scale, shard theo key:

flowchart LR
    Req["Request: userId"] --> Formula["shardId = floor(userId / 1000)"]
    Formula --> ShardMap{Shard Router}
    
    ShardMap -->|&quot;userId: 0 - 999&quot;| S0["DO Instance: shard-0"]
    ShardMap -->|&quot;userId: 1000 - 1999&quot;| S1["DO Instance: shard-1"]
    ShardMap -->|&quot;userId: 2000 - 2999&quot;| S2["DO Instance: shard-2"]
    ShardMap -->|&quot;userId: N - N+999&quot;| Sn["DO Instance: shard-N"]

Kết Luận#

Durable Objects giải quyết vấn đề state trong serverless:

  • Real-time — WebSocket với state central consistency
  • Game server — multiplayer coordination
  • Coordination — distributed lock, rate limiting
  • SQLite storage — không cần DB riêng

Kiến trúc: Workers stateless cho request thông thường, DO cho những gì cần state. Shard DO theo key để scale ngang.

Tài liệu tham khảo#