blog.dopana

Back

Durable Objects(DO)は、Cloudflare Workersにステートフルな協調レイヤーを提供するソリューションです。各DOはグローバルに唯一のインスタンスとして動作し、強整合性(Strong Consistency)を保ちながら状態を保持します。

flowchart LR
    subgraph EdgeWorkers["ステートレス Workers(グローバルエッジ)"]
        direction TB
        W_Tokyo["Worker 東京"]
        W_London["Worker ロンドン"]
        W_US["Worker 米国"]
    end

    subgraph DO_Instance["Durable Object(世界で唯一のインスタンス)"]
        direction TB
        ID["ID: 'user-room-42'"]
        
        subgraph Memory["RAM(メモリ内ステート)"]
            State["アクティブ接続 & 状態"]
        end
        
        subgraph Storage["永続ストレージ"]
            SQL[("SQLite / KV Storage")]
        end

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

    W_Tokyo -->|&quot;同一IDの全リクエストを<br/>1つのインスタンスへルーティング&quot;| DO_Instance
    W_London -->|&quot;強い整合性<br/>(競合状態なし)&quot;| DO_Instance
    W_US -->|&quot;状態を継続的に保持&quot;| DO_Instance

Workers vs Durable Objects#

flowchart TB
    subgraph Clients["クライアント & ブラウザ"]
        C1["ユーザー A (WebSocket)"]
        C2["ユーザー B (WebSocket)"]
        C3["ユーザー C (HTTP REST)"]
    end

    subgraph Edge["Cloudflare グローバルエッジ(ステートレス Workers)"]
        W1["Worker(エッジ拠点 1)"]
        W2["Worker(エッジ拠点 2)"]
    end

    subgraph DO_Cluster["Durable Objects(IDごとに1つの調停インスタンス)"]
        subgraph DO1["Chat Room DO (ID: room-123)"]
            Mem["メモリ内ステート & 接続中WebSocket"]
            SQL[(&quot;組み込み SQLite / KV ストレージ&quot;)]
            Alarm["アラーム / タイマー"]
            Mem <--> SQL
            Mem <--> Alarm
        end
        subgraph DO2["Chat Room DO (ID: room-456)"]
            Mem2["メモリ内ステート"]
            SQL2[(&quot;SQLite ストレージ&quot;)]
        end
    end

    C1 <-->|&quot;エッジ接続&quot;| W1
    C2 <-->|&quot;エッジ接続&quot;| W2
    C3 -->|&quot;HTTP リクエスト&quot;| W1

    W1 <-->|&quot;ID: room-123 へルーティング&quot;| DO1
    W2 <-->|&quot;ID: room-123 へルーティング&quot;| DO1
    W1 -.->|&quot;ID: room-456 へルーティング&quot;| DO2
WorkersDurable Objects
ステートレスステートフル
リクエストごとに複数インスタンス1つのIDに対して世界で1つのインスタンス
メモリ状態を保持しないメモリ(RAM)+ストレージに状態を保持
無限に自動スケールシャーディング(複数のDO)でスケール
330以上の拠点に分散単一のロケーションで実行(マイグレーション可能)

チャットルーム — 基本的な例#

sequenceDiagram
    autonumber
    actor ClientA as ユーザー Alice
    actor ClientB as ユーザー Bob
    participant Worker as ステートレス Worker ルーター
    participant DO as ChatRoom [Durable Object]

    Note over ClientA, DO: WebSocket 接続の初期化
    ClientA->>Worker: GET /?room=general&name=Alice (Upgrade: WebSocket)
    Worker->>DO: stub.fetch(request) [idFromName("general")]
    DO->>DO: server.accept(), セッション "Alice" を保存
    DO-->>ClientA: 101 Switching Protocols (WebSocket 接続確立)
    DO--)ClientB: ブロードキャスト "Alice joined the chat"

    Note over ClientA, DO: リアルタイムメッセージ配信
    ClientA->>DO: WS メッセージ: "Hello room!"
    DO->>DO: broadcast("Alice: Hello room!", sender=Alice)
    DO--)ClientB: WS メッセージ: "Alice: Hello room!"

SQLite Storage — 組み込みストレージエンジン#

各Durable Objectには専用のSQLiteストレージが組み込まれています:

flowchart TD
    Req["リクエスト: 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 -->|その他| Err["404 Not Found"]
    
    Inc --> Resp["JSON count を返却"]
    Get --> Resp
    Res --> Resp

storage.sql による SQL クエリ#

Durable Object 内で直接 SQL クエリを実行できます:

flowchart TD
    Req["HTTP リクエスト"] --> Init["テーブルが存在しなければ作成: CREATE TABLE IF NOT EXISTS todos (...)"]
    Init --> Router{HTTP メソッド}
    
    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 -->|その他| Q5["405 Method Not Allowed"]

    Q1 --> Out1["JSON: todos の配列"]
    Q2 --> Out2["201 Created: 作成された todo"]
    Q3 --> Out3["JSON: &#123; success: true &#125;"]
    Q4 --> Out4["JSON: &#123; success: true &#125;"]

アラーム — DO のスケジュールタスク#

sequenceDiagram
    autonumber
    actor User as ユーザー / クライアント
    participant DO as Reminder DO
    participant Storage as ctx.storage [アラームキュー]
    participant Webhook as 外部 Webhook

    Note over DO, Storage: 1. タイマー / アラームの登録
    User->>DO: GET /?delay=5000
    DO->>Storage: setAlarm(futureTimestamp)
    DO-->>User: {"message": "Alarm set for 5000ms"}

    Note over Storage, Webhook: 2. 時間経過による実行(DOの起動)
    Storage-->>DO: alarm() ハンドラーの起動
    DO->>Webhook: POST https://hooks.example.com/notify { event: 'alarm_fired' }
    DO->>Storage: setAlarm(nextHourTimestamp) [次回の定期実行をスケジュール]

マルチプレイヤー ゲームサーバー#

flowchart TD
    WS["WebSocket メッセージイベント"] --> EventType{msg.type}

    EventType -->|&quot;join&quot;| Join["1. プレイヤーセッションを Map に追加<br/>2. 初期状態を作成: x=0, y=0<br/>3. ブロードキャスト: type: 'players'"]
    EventType -->|&quot;move&quot;| Move["1. 座標を更新: player.x += dx, player.y += dy<br/>2. ブロードキャスト: type: 'move'"]
    EventType -->|&quot;shoot&quot;| Shoot["1. 弾のベクトル・角度を計算<br/>2. ブロードキャスト: type: 'shoot'"]

    Close["WebSocket 切断イベント"] --> Leave["1. Map & State からプレイヤーを削除<br/>2. ブロードキャスト: type: 'leave'"]

    Join --> BroadcastAll["ルーム内の全接続プレイヤーに更新ステートを送信"]
    Move --> BroadcastAll
    Shoot --> BroadcastAll
    Leave --> BroadcastAll

マイグレーション — DO の移行管理#

DO はクラスやスキーマの移行をダウンタイムなしでサポートします:

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

マイグレーションタイプ:

  • new_tag — 新規クラスタグ
  • new_classes — 複数クラスの追加
  • rename — クラス名変更
  • transfer — ネームスペース間のデータ移行

パターン — シャーディング(Sharding)#

単一の Durable Object インスタンスは物理的に1箇所で動作します。水平スケールを行うにはキーでシャーディングします:

flowchart LR
    Req["リクエスト: userId"] --> Formula["shardId = floor(userId / 1000)"]
    Formula --> ShardMap{シャードルーター}
    
    ShardMap -->|&quot;userId: 0 - 999&quot;| S0["DO インスタンス: shard-0"]
    ShardMap -->|&quot;userId: 1000 - 1999&quot;| S1["DO インスタンス: shard-1"]
    ShardMap -->|&quot;userId: 2000 - 2999&quot;| S2["DO インスタンス: shard-2"]
    ShardMap -->|&quot;userId: N - N+999&quot;| Sn["DO インスタンス: shard-N"]

まとめ#

Durable Objects はサーバーレスにおける「状態管理」の課題を解決します:

  • リアルタイム: WebSocket接続と中央集権的な強い整合性メモリ
  • ゲームサーバー: 低遅延なマルチプレイヤー協調
  • 分散協調: 分散ロック、リーダー選出、レートリミット
  • 組み込み SQLite: 外部データベース不要でリレーショナルクエリを実行

アーキテクチャの基本:通常のリクエストはステートレスな Workers で処理し、状態が必要な処理を Durable Objects に委譲します。キーによるシャーディングで無限のスケールアウトが可能です。

参考文献#