🔥

Cloudflare Workers の Durable Objects について

2020/10/17に公開

とくに断りがない限り、引用文は Workers Durable Objects Beta: A New Approach to Stateful Serverless を Deepl で翻訳したものです。

前提知識として、 Cloudflare Workers 自体の解説はしません。こちらを参照してください。 Cloudflare Workers それは Frontend / Node.js が CDN Edge Side まで拡張されるもの

要約すると Cloudflare Workers は CDN Edge で低遅延、低スペックCPUの Node.js の Worker が高速に動くサーバーレス環境です。

Durable Objects とは

注意: まだクローズドベータです。

本来は、手元で動かしてから記事を書くつもりでしたが、クローズドベータに申し込みしてから待てども待てども解禁されないので、現時点で判明している情報を元に記事を書きます。

Durable Objects は、 Cloudflare Workers 上のメモリ状態を永続化する機能です。

JavaScript の通常のオブジェクトとは異なり、耐久性のあるオブジェクトはディスク上に永続的に保存された状態を持つことができます。各オブジェクトの耐久性のある状態はそれ自身にプライベートであるため、ストレージへのアクセスが速いだけでなく、オブジェクトはメモリ内の状態の一貫したコピーを安全に維持し、レイテンシをゼロにしてその上で操作することさえできます。メモリ内のオブジェクトは、アイドル時にはシャットダウンされ、後でオンデマンドで再作成されます。

Durable Objects で何ができるか

まずは、サンプルコードとして挙げられているアトミックカウンターの例を見てみましょう。

export class Counter {
  constructor(controller, env) {
    // `controller.storage` is an interface to access the object's
    // on-disk durable storage.
    this.storage = controller.storage
  }

  // Private helper method called from fetch(), below.
  async initialize() {
    let stored = await this.storage.get("value");
    this.value = stored || 0;
  }

  // Handle HTTP requests from clients.
  //
  // The system calls this method when an HTTP request is sent to
  // the object. Note that these requests strictly come from other
  // parts of your Worker, not from the public internet.
  async fetch(request) {
    // Make sure we're fully initialized from storage.
    if (!this.initializePromise) {
      this.initializePromise = this.initialize();
    }
    await this.initializePromise;

    // Apply requested action.
    let url = new URL(request.url);
    switch (url.pathname) {
      case "/increment":
        ++this.value;
        await this.storage.put("value", this.value);
        break;
      case "/decrement":
        --this.value;
        await this.storage.put("value", this.value);
        break;
      case "/":
        // Just serve the current value. No storage calls needed!
        break;
      default:
        return new Response("Not found", {status: 404});
    }

    // Return current value.
    return new Response(this.value);
  }
}

storage が永続化層のように見えますね。 fetch は fetch handler で、 /increment/decrement が実装されています。

この例では出てきませんが、 websocket プロトコルにも対応しています。 chat のデモのコードの一部です。

export class ChatRoom {
  // ... 中略
  async fetch(request) {
    return await handleErrors(request, async () => {
      let url = new URL(request.url);
      switch (url.pathname) {
        case "/websocket": {
          if (request.headers.get("Upgrade") != "websocket") {
            return new Response("expected websocket", {status: 400});
          }
          let ip = request.headers.get("CF-Connecting-IP");
          let [a, b] = new WebSocketPair();
          // We're going to take pair[1] as our end, and return pair[0] to the client.
          await this.handleSession(b, ip);
          // Now we return the other end of the pair to the client.
          return new Response(null, { status: 101, webSocket: a });
        }
        default:
          return new Response("Not found", {status: 404});
      }
    });
  }
}

それを呼ぶクライアントのコード

let ws = new WebSocket("wss://" + hostname + "/api/room/" + roomname + "/websocket");

// ...
  ws.addEventListener("message", event => {
    let data = JSON.parse(event.data);
    if (data.error) {
      addChatMessage(null, "* Error: " + data.error);
    } else if (data.joined) {
      let p = document.createElement("p");
      p.innerText = data.joined;
      roster.appendChild(p);
    } else if (data.quit) {
      for (let child of roster.childNodes) {
        if (child.innerText == data.quit) {
          roster.removeChild(child);
          break;
        }
      }
    } else if (data.ready) {
// ...

ライブラリに頼らず、生の WebSocket プロトコルで実装されています。

この応用例として、 chat, 多人数参加のゲームのセッション管理、 Social Feeds, ショッピングカードなどが挙げられています。

Workers KV との違い

2年前、Workers KVというグローバルキーバリューデータストアを紹介しました。KV はかなりミニマリストなグローバルデータストアで、特定の目的には適していますが、万人向けではありません。KVは最終的には一貫性があり、ある場所で行われた書き込みがすぐに他の場所では表示されない可能性があります。さらに、KVは「最後の書き込みが勝つ」というセマンティクスを実装しています。つまり、あるキーが世界の複数の場所から同時に変更された場合、それらの書き込みがお互いに上書きされやすいということです。KVはこのように設計されており、頻繁に変更されないデータの低レイテンシの読み取りをサポートしています。しかし、このような設計上の決定により、頻繁に変化するステートや、世界中で変更を即座に確認する必要がある場合には、KVは不適切なものとなります。

対照的に、耐久性のあるオブジェクトは、主にストレージ製品ではありません。耐久性のあるオブジェクトは、ストレージを提供するという点では、KVとは正反対の立場にあります。耐久性のあるオブジェクトは、トランザクションの保証と即時の一貫性を必要とするワークロードに非常に適しています。しかし、トランザクションは本質的に単一の場所で調整されなければならず、その場所から世界の反対側にいるクライアントは、光速の固有の制限により、適度な遅延を経験することになります。耐久性のあるオブジェクトは、使用される場所の近くに住むために自動移動することで、この問題に対処します。

要するに、Workers KVは静的なコンテンツや設定、その他のめったに変化しないデータを世界中に提供するのに最適な方法であることに変わりはありませんが、Durable Objectsは動的な状態や調整を管理するのに適しています。

つまり Workers KV は結果整合、 Durable Objects は強整合とのこと。地理的に離れてる場合はどうなるんだろう?と思ったんですが、次のような記述があります。

耐久性のあるオブジェクトを使用する場合、Cloudflare は各オブジェクトが居住する Cloudflare データセンターを自動的に決定し、必要に応じてロケーション間でのオブジェクトの移行を透過的に行うことができます。

ということなので、アクセスが同じ地域だったら高速で、地理的に離れてる人同士でオブジェクトを共有すると読み書きが遅くなりそうですね。

将来性: エッジ分散データベースの未来

私たちは、 Durable Objects を分散システムを構築するための低レベルのプリミティブと考えています。上記のようなアプリケーションの中には、オブジェクトを直接使用して調整層を実装したり、唯一のストレージ層として使用することができるものもあります。
しかし、現在の Durable Objects は完全なデータベースソリューションではありません。各オブジェクトは独自のデータしか見ることができません。複数のオブジェクトにまたがってクエリやトランザクションを実行するためには、アプリケーションはいくつかの余分な作業を行う必要があります。
つまり、すべての大規模な分散型データベース(リレーショナル、ドキュメント、グラフなど)は、低レベルで構成されています。大規模な分散データベースでは、低レベルでは、全体のデータの一部を格納する「チャンク」や「シャード」で構成されています。分散データベースの仕事は、チャンク間の調整です。
私たちは、各「チャンク」を耐久性のあるオブジェクトとして保存するエッジデータベースの未来を見ています。そうすることで、リージョンやホームロケーションを持たず、完全に分散したエッジで動作するデータベースを構築することが可能になるでしょう。このようなデータベースは、私たちが構築する必要はありません。誰でもDurable Objectsの上に構築することができます。耐久性のあるオブジェクトは、エッジ ストレージの旅の第一歩に過ぎません。

これをみて自分が思い出したのは、 Akka でした。

並行処理初心者のためのAkka入門

もちろん Akka のような耐障害ではなく、そして Durable Objects 決め打ちですが、もし将来的に Durable Objects 同士が相互に通信できるようになれば、それを Actor と捉えることが可能になるのではないでしょうか。

今現在の Web Application のパラダイムでは、あらゆる状態を一度データベースに永続化するのが主流ですが、Durablo Objects があれば、 短時間〜中時間の処理を、これらのアクター内部の永続化で高速に処理させるといった選択肢も増えてきます。

過去の似たような事例

実は Azure にも似た機能があるんですが、あえて Cloudflare Workers でやることの意味は、 CDN レベルの再配置での効率性が追求可能で、高速なワークロードが(おそらく)実現可能ということでしょう。

Durable Functions の概要 - Azure | Microsoft Docs

もっといえば、今までも websocket の sticky セッションで似たようなことができなくはなかったのですが、 sticky セッションの取り扱いが、k8s や各クラウドのロードバランサによっては面倒 or 実現不可能なことがあって、だるいことが多かったです。

で、使えるの?

まだ未知数ですが、チャットを pusher などで実現してる場合、 durable functions に置き換えることは可能でしょう。websocket は色々と不遇なプロトコルでしたが、やっと現実的なユースケースが出てきた気がしていて、嬉しいです。

プロダクションレベルで投入することを考えた場合、現状API自体はプリミティブなものしかないので、これ専用のフレームワークを構築する必要はあると思います。

Discussion