すべての Durable Object にゼロレイテンシの SQLite ストレージ
(simonwillison.net)- Cloudflare の Durable Object がキー/バリューストアから SQLite ベースのリレーショナルストアへ変わり、アプリケーションロジックとデータが同じ物理ホスト上で実行される構造になった
- スケーリングは1つのオブジェクトを大きくするのではなく、ドキュメント・ユーザー・データベースシャードのような 論理的な状態単位 ごとに Durable Object を分ける方式で、単一オブジェクトには単一マシン・単一スレッドの制約がある
- 航空便予約のように状態単位が明確なシステムでは、航空便ごとに専用の Durable Object と独自の SQLite データベースを持たせられるため、航空会社ごとに1日数千個の新しいデータベースが生まれる可能性がある
- 耐久性は WAL エントリのストリーミング とオブジェクトストレージへの保存、近隣データセンターへのレプリケーションで構成され、16MB または10秒ごとにバッチ化され、最大30日間のポイントインタイムリカバリをサポートする
- JavaScript API は高速な単一スレッドの永続化処理のために ブロッキング方式 を採用し、Durable Object の位置は作成後に固定されるが、将来的には動的な再配置が計画されている
SQLite に変わった Durable Object の設計
- Cloudflare の Durable Object はキー/バリューストアから SQLite ベースのリレーショナルシステム へアップグレードされた
- 核心は、アプリケーションロジックをそのロジックが扱うデータと 同じ場所 に置くこと
- 1つの Durable Object は、利用する SQLite データベースと同じ物理ホスト上で実行されるコードで構成される
- 読み書きをネットワーク往復なしで処理する方向なので、レイテンシを下げやすい
- 大規模処理は、1つのオブジェクトのスループットを高めるよりも、より多くのオブジェクトを作る方式でアプローチする
- 単一オブジェクトは単一マシンの単一スレッドで実行されるため、スループットには本質的な上限がある
- ドキュメント、ユーザー、データベースシャードのように互いに異なる 論理的な状態単位 を別々のオブジェクトが担当すればスケールしやすい
- 航空便予約システムでは、各航空便を専用の Durable Object と独自の SQLite データベースにマッピングできる
- この構造では、航空会社ごとに1日数千個の新しいデータベースが作られる可能性がある
- 各 Durable Object は固有の名前を持ち、Cloudflare ネットワークは世界中のネットワーク内でそのオブジェクトが存在する場所へリクエストをルーティングする
WAL ストリーミングとレプリケーションで確保する耐久性
- Durable Object ベースのシステムは Litestream に着想を得て、各オブジェクトの WAL エントリ のシーケンスをオブジェクトストレージへ継続的にストリーミングする
- バッチ化は 16MB ごと、または10秒ごとに行われる
- 記録されたトランザクションを再生することで、最大30日間の ポイントインタイムリカバリ が可能
- 10秒のウィンドウ内の耐久性を補完するため、書き込みはコミット直後に近隣の別データセンターにある5つのレプリカへ送られる
- そのうち3つが確認すると、書き込みレスポンスが承認される
- Durable Objects の基盤システムは Storage Relay Service で、Cloudflare の別の D1 SQLite system を1年以上稼働させてきた
API とオブジェクト位置の動作
- JavaScript API は async ではなく ブロッキング方式 を使う
- 高速な単一スレッドの永続化処理を提供することが設計目的
- サンプルコードは、ドキュメント一覧を取得した後、各ドキュメントの著者名を別クエリで取得する N+1 クエリパターンをあえて使っている
- SQLite はこのパターンを処理するのに適した事例として関連づけられている
- Durable Object は現在、作成後に位置を変更しない
- 基本的に最初の
get()リクエストが作られた場所に近いデータセンターでインスタンス化される - 別の場所に手動で作成するには、
get()に任意のlocationHintパラメータを指定できる - 既存の Durable Object の 動的再配置 は今後の計画に含まれている
- 基本的に最初の
- where.durableobjects.live は、Cloudflare ネットワークで新しい Durable Object がどこに作成されるかを追跡するサイト
- 例としての訪問では、Half Moon Bay でページを読み込んだ際、San Jose の worker が San Jose に Durable Object を作成したと表示された
1件のコメント
Hacker News のコメント
ほかにも興味深い点がいくつかある。書き込み API は同期式だが、隠れた非同期の待機があり、次のレスポンス出力時に書き込みが失敗していれば、ランタイムがレスポンスを HTTP エラーに変えてくれる。
そのため、ユーザーがエラーや待機を明示的に処理しなくても、ランタイムが書き込みを自動バッチ化し、成功を楽観的に仮定できる。
読み取りトランザクションはないが、特定時点のスナップショットポインタを得るのに役立ったのではないかと思う。
各ランタイムインスタンスは 128MB RAM に制限される。
WebSocket は休止状態に入ることができ、スリープ中は課金されないため、DO がスリープしている間もクライアントを接続状態に保てる。
ほかの DO や Worker を通常の JS 呼び出しのように扱える自動 RPC のような機能もあり、実際には別のデータセンターを呼び出すことができ、シリアライズとパースはランタイムが処理する。
記事で言及されている Litestream も似た手法を提案している。
一般的な SQLite では、複数プロセスが同じデータベースに同時アクセスできるため、読み取りトランザクションが有用だ。
ここでは単一プロセスだけがデータベースにアクセスできるので、すべての読み取りを1つの同期関数内で実行するか、プロセスレベルのロックを自前で実装すれば、同じ効果を得られる。
WAL エントリがオブジェクトストレージへ 16MB ごと、または 10 秒ごとにバッチストリーミングされるということは、グローバルに書き込みを安定して読み取れるまで最大 10 秒かかる可能性がある、という意味に見える。
これが、大陸規模でミリ秒応答を出すリージョン別データベースクラスタをどう置き換えられるのか、よく分からない。
ストリームを使うのは分かるが、その宛先はフォロワー 5 台だけで、Cloudflare には数百のデータセンターがある。
すべての SQLite インスタンスが常時接続されていないなら、秒単位の読み取りを物理的に保証することはできないし、接続されていてもパケット遅延が問題を起こし得る。
書いてすぐ読めば即座に反映される。書き込みと読み取りが現在のプロセスのメモリ状態も同時に更新するためだ。
ほかのプロセス、たとえば別の DO や Worker がデータにアクセスするには、データを持つ DO を経由する必要があるため、RPC や HTTP リクエストを送ることになり、最新情報を受け取る。
休止は一定時間の非アクティブ後に発生するので、予想と異なり書き込みが unavailable になるケースは、DO や Worker が書き込み直後にクラッシュする状況くらいに見える。
Cloudflare のブログ記事がこの部分をより詳しく扱っている。そのため、書き込みを高速に保ちながら耐久性も確保している。
そのマシンは自分の SQLite データベースについて常に一貫したビューを持つ。
オブジェクトは数十億個作れるが、各オブジェクトは別々のデータベースを持つ。
DO が実行中のマシンとは別のマシンから、そのデータベースを直接読む方法はない。
各 DO はグローバルに一意で、特定 ID の DO はどこかで1つだけ実行され、そのデータセンターのローカルストレージで SQLite を動かす。
Durable Objects でまだ理解できていない点は 物理的な場所です
最初に作成のきっかけになった API 呼び出しをホストしたリージョンに配置されるのかが気になります
もしそうなら、北米で作成されたものの、その後すべての読み書きトラフィックがオーストラリアから来ていると分かった場合、DO を自動的に別の場所へ移す仕組みがあるのかも気になります
locationHintを指定できます。オーストラリアなら"oc"を使えばよいです: https://developers.cloudflare.com/durable-objects/reference/...「既存の Durable Objects の動的な再配置は今後の計画」である点にも注意が必要です
Cloudflare PoP のうち 10〜11% 程度だけが Durable Objects をホストしています
DO の作成のために別の PoP に入ってきたリクエストは、それをホストする近くの PoP のいずれかへ転送されます
記憶では Orleans(https://www.microsoft.com/en-us/research/wp-content/uploads/...)はアクターをマシン間で移動でき、このモデルは DO を場所の間で移す方式と相性がよさそうです
ただし Durable Object を移動させる API 呼び出しはありません
接続がない状態である必要があり、次回または最初の接続時に最も近いデータセンターで再作成されます
このときメモリは失われますが、ストレージは残ります
最近、休止状態関連の細かな機能が追加されたので、この説明は少し古くなっているかもしれません
こうした新しいクラウド技術を理解するのが難しく感じる人がほかにもいるのか気になります
Web 開発歴は 15 年以上で、Laravel / Postgres / Redis スタックを使ってきましたが、こういう記事を読むと、単に 自分には合わない と思ってしまいます
すぐ思い浮かぶリアルタイム共同編集アプリは Google Docs/Sheets、Notion、Miro、Figma などです
いずれもグローバル規模の共同編集アプリで、Laravel スタックがこうしたユースケースを支えられるかはよく分かりません
Google はおそらく大半を自前で実装する必要があり、CRDT の利用を先導したのだと思います
しかしパターンが明らかになり、構成要素が SaaS 化されるにつれて、大規模なリアルタイム共同編集はもはや巨大なエンジニアリング課題ではなくなり、より面白いプロダクトが可能になります
Durable Object の設計は本当に気に入っています。特に内部がどのように動いているかを理解しやすい点が良いです
リアルタイムデータ向けに設計されたほかの多くのソリューションと違って、Durable Objects には Redis やイタリア料理のような シンプルさがあります。材料が全部見えています
十分な時間とリソース、そしてデータセンターがあれば、有能なプログラマーが DO のドキュメントを読んで似たものを再実装できそうです
そのため、関連するトレードオフを判断しやすいです
ただし DO は、高速でオーバーヘッドの低いリアルタイム体験、たとえば 5 人が文書をリアルタイム編集する機能を作るには向いていますが、分析や概要を作るのは非常に難しくしてしまうのではないかと懸念しています
先週どのグループがどの文書を編集したのか、といった問いがそうで、データを SQLite の中に入れるとさらに難しくなるかもしれません
無数の小さな SQLite インスタンスを何らかの形でクエリし、その結果をマージする必要があるでしょうから、DO にそのための何かがあるのか気になります
結局いつも Postgres に戻ってくる理由はここにあります。中核アプリ機能にも、概要や BI などにもすべて使えるからです
本当に興味深い設計だが、こういう賢いシステムは、個人的にはいつも不気味の谷にある
必要になるケースは正確に2つだけだ。非常に高負荷なシステムを賢くスケールさせる必要がある場合か、趣味でおもちゃプロジェクトを作る場合だ
おもちゃプロジェクトなら何を使っても構わない
だが本番環境や業務用途なら、実証済みのものが必要だ
これが必要だと分からないなら、それは不要なもので、退屈なPostgresデータベースやVMのようなものを使えばいい
逆に必要だと分かっているなら厄介になる。まだ新しく十分に成熟していないので、妙なエッジケースに多く遭遇する可能性が高く、それをデバッグしたり受け入れたりしたくはないはずだ
結局、こういうシステムが誰のためのものなのか分からない
ニッチすぎて、多くの真剣なプレイヤーが使い、簡単に成熟していくのは難しく、99.9%の企業が使うには複雑すぎてトレードオフも多い
はっきりした対象層は、きらびやかなものに惹かれて会社を作る、あるいはもっと悪い場合、他人の会社をその上に作ってしまい、すぐに後悔してもっと退屈なものへ移行する開発者だけのように思える
もう1つのFigmaやGoogle Docsのようなものを作りたいなら、Durable Objectsのプログラミングモデルは非常に便利だ
この記事がより詳しく扱っている: https://digest.browsertech.com/archive/browsertech-digest-cl...
この古い記事もかなり関連がある: http://ithare.com/scaling-stateful-objects/
Figmaのマルチプレイヤー記事を読んで「自分に必要なのはだいたいこれだ」と思った人には、Durable Objectsはよく合うと思う: https://www.figma.com/blog/rust-in-production-at-figma/
別のアプローチもある。以前WebRTC上でCRDTを使ったことがあるが、本当に未来の技術のように感じられた
しかし、クラウドのどこかにあるWebSocketと単一のクラスインスタンスという基盤よりはるかに複雑だ
データと振る舞いを同じ場所に置くことは、実際に複雑さを定量的に減らしてくれる
レイテンシと帯域幅の心配をなくすので、運用上の懸念と開発上の懸念の両方を減らす。有名なN+1問題の影響も大きく減る
ネットワーク越しのPostgresの方が別の理由で優れていると主張することもできるし、実際にそうかもしれない
しかしSQLiteは、退屈で予測可能なものとしては屈指で、既知の強みも明確だ
そのためサーバーでも人気が高まりつつある
ただしDurable Objectsが提案するように、小さなデータベースを多数作る方式はあまり好きではない
NoSQLの悪夢を思い出させるし、リレーショナルデータベースの重要な不変条件を壊す可能性がある
SQLiteはCloudflareのD1製品のように、単一のデータベースとして使う方がずっと良いと思う
Cloudflareは良い顧客成功事例を必要としており、優秀なエンジニアたちが成功事例を得る代わりに、これがどう動き、どう役立つのか、バグにはどう対処するのかを一緒に見てくれる可能性が高い
検証されればサービス関係に変わるだろうが、初期にはパートナーシップに近い
社内ツールのように、性能チューニングや深い内部観察を必要としないサービスは多い
こういう場所では、サーバーレスフレームワークがデプロイに使う時間を大きく減らしてくれるので、よく合うと思う
速いに越したことはないが、それが中核的な要件であることはまれだ
たいていの中核要件は、素早く作れて保守が少ないことだ
Cloudflareがここで良い開発者体験を提供する可能性はあるが、それが彼らの主な売り文句というわけではなく、こうした開発を速くしてくれると謳う競合サービスも多い
一方、高いデバッグ可能性と内部の可観測性が必要な場合には、こういうサービスはあまり合わないと思う
どんなメトリクスを得られるのか、一部のDurable Objectsが遅い場合になぜそうなのか理解できる情報があるのか、修正できるのか、ロギングはどうでコストはいくらなのかが重要だ
こういうサービスは、スタートアップが初日から賢い分散システムを作って、スケールの心配を先送りするには良いかもしれない
しかしスケールアップ段階の企業なら、もっと深く覗ける何かへ移りたくなると思うし、その移行は難しいだろう
他の人たちが言っているように、用途はマルチプレイヤーであり、アプリが良く感じられるには全員が変更をできるだけ早く見る必要があるからだ
より広く見ると、ストレージ業界は長い間、一貫性があり、レイテンシが低く、マルチユーザーに適したものを作ろうとしてきた
物理的にも、通常は一貫性とレイテンシの間にトレードオフがあるため非常に難しい
そのため複数のモデルを試している最中で、その実験のかなりの部分はSQLiteを中心に起きている。すべてではなく、YugabyteやCockroachのような例外もある
DOの設計にはずっと感心させられる
こういうやり方は何か間違っているという反射的な反応が出やすいが、実際には多くの実在するプロダクトが暗黙のうちにこのように構成されていると思う
トランザクション一貫性が必要な原子的な単位ごとに、非常に小さな規模で複雑な処理が多く行われる構造だ
振り返ると、Framerでマルチプレイヤー対応プロジェクトのために作ったものも、今DOがやっていることの、より応用的なバージョンだった
編集内容は60 FPSで複製され、すべてのクライアントに正しい順序で適用され、JSONオブジェクト編集のWALのようなものも結局作った
そのためプロジェクトインスタンスがクラッシュしても、バックアップが何事もなかったかのように引き継げた
巨大なプロジェクトデータオブジェクトにJSONパッチをコミットする時間がなかったとしても、ここで説明されているようにN回の更新ごと、またはM秒ごとに処理する方式だった
愚かな質問かもしれないが、このような構成でスキーママイグレーションをどう扱うのか気になる
理解している限りでは、テナントごとのデータベース、あるいはそれよりさらに細かく分割された構造を目指しているように見える
スキーママイグレーションをまともに処理する方法があるのか、それともこうしたデータベースはもっと短命で、削除されるまで複数のDB/DOバージョンをサポートすることが期待されているのか気になる
頭の中では、ユーザーごとにDOを持つブックマークサービスを作ると面白そうだと思う
しかし既存のテーブルに新しいフィールドを追加しようとした瞬間、その変更を個々のDOそれぞれに適用しなければならない、かなり厄介な問題にぶつかる
もしかするとその例はデータが長く残りすぎるので、この設計はもっと一時的な用途向けなのかもしれない
実際にやってみた人がいるなら、どうしているのか知りたい
SQLite向けにPythonで書いたバージョンがあるが、Durable Objects上で実行できるかは分からない。WASMとPyOdide経由なら可能かもしれない
あるいはJavaScriptに移植する必要がある
https://github.com/simonw/sqlite-migrate
最近Cloudflareは、開発者にWorkerよりもどこでもDOを使えと推しているように見える
WorkerのWebSocket接続も約30秒後にタイムアウトし、推奨される方法はDOを使うことだ
元のチャットデモも2020年からDOとWebSocketを使っていた: https://github.com/cloudflare/workers-chat-demo
これは、DO用SQLiteではDOが失敗する状況で最大10秒分のデータを失う可能性があるという意味なのか?
ここでSimonは「beyond」ではなくwithinと言いたかったのだと思う