Multi-Region
plasma のデフォルトのデプロイは単一リージョンです: 1 つの Worker、1 つの D1、DB が割り当てる単調増加の row_version を持つ 1 つの change log。これは単一の D1 が push できるスループットまで機能します。それを超えると multi-region です。
なぜ単一リージョンが破綻するか
Section titled “なぜ単一リージョンが破綻するか”D1(ほとんどの SQLite ベースのストアと同様)は書き込みを直列化します。すべての mutation は同じ DB にヒットします。持続的に毎秒数百書き込みを超えると、競合下で SQLITE_BUSY が見え始め、change log の INSERT がボトルネックになります。
Hyperdrive 経由の Postgres はさらにスケールしますが、すべての書き込みでリージョンをまたぐホップのコストがかかります。グローバルなアプリは最終的に、mutation をユーザーの近くで適用したくなります。
2 つのプリミティブ
Section titled “2 つのプリミティブ”plasma v1.0 は、multi-region デプロイに組み合わせるプリミティブを提供します:
SequencerDO—storage.transactionを介して単調なバージョン ID を発行する、リージョンごとの Durable Object。各リージョンはリージョン ID でキーが付いた自身の DO インスタンスを所有します。- 因果 cookie — pull cookie はすでにリージョンごとのカーソル(
{ "0": "123", "1": "456" })を運ぶため、クライアントは書き込みを取りこぼすことなく任意のリージョンから pull できます。
SequencerDO — リージョンローカルの id ソース
Section titled “SequencerDO — リージョンローカルの id ソース”// worker.ts(DO 自身のスクリプト)import { SequencerDO, MemorySequencerStorage } from "@sh1n4ps/plasma-server"
export class Sequencer extends SequencerDO { constructor(state: DurableObjectState, env: Env) { super({ storage: state.storage }) }}
// 呼び出し側はバージョン ID を予約:const stub = env.SEQUENCER.get(env.SEQUENCER.idFromName(`region-${regionID}`))const res = await stub.reserve(1)// { start: "12345", end: "12345" } — bigint 精度を保つため文字列reserve(count) は連続した [start, end] 範囲を割り当てるため、書き込みのバッチは書き込みごとのラウンドトリップなしに連続 ID の連なりを得ます。
ID は文字列化された bigint です — DO は JS の number 精度を失うことなく 2^53 を超えて発行でき、DO とその呼び出し側の間の JSON 境界が完全な値を保ちます。
リージョンごとの割り当て
Section titled “リージョンごとの割り当て”リージョン ID はリクエストの Cloudflare メタデータから割り当てます:
async function fetchWithRegion(req: Request, env: Env) { const cf = req.cf as { region?: string } | undefined const regionID = mapCfRegionToPlasmaRegion(cf?.region ?? "default") // ... その regionID に対して env.SEQUENCER 経由でバージョン ID を予約。}origin_region を持つ change log
Section titled “origin_region を持つ change log”change log の origin_region column は、各書き込みをどのリージョンが生成したかを識別します:
_plasma_changes: row_version origin_region table row_id op value 123 0 todos t1 put {...} 456 1 todos t2 put {...}リージョン 0(例えば US)とリージョン 1(例えば EU)は両方とも自身の SequencerDO から row_version を割り当てます。それぞれリージョンの DB に書き込み、変更をリージョンをまたいでレプリケートすることで協調します — pull ループは最も近いリージョンからクライアントに提供します。
因果 cookie
Section titled “因果 cookie”pull cookie は、リージョンでキーが付いた base64 エンコードの JSON オブジェクトです:
c1:eyIwIjoiMTIzIiwiMSI6IjQ1NiJ9= c1: + base64({ "0": "123", "1": "456" })各エントリは「このリージョンから見た最も高い row_version」です。pull 時、(クライアントがヒットしたどのリージョンにいても)サーバーは、すべてのリージョンについて row_version > cookie[origin_region] のすべての変更を返します。
advanceCookieFromChanges(cookie, rows) は、受信した変更を cookie 形状に折り込み直します。
pullPredicate(cookie) は、「この cookie がまだ見ていないすべての row_version」を選択する SQL の WHERE 句を生成します:
const { text, params } = pullPredicate(cookie)// text: "(_plasma_changes.origin_region = ? AND _plasma_changes.row_version > ?)// OR (_plasma_changes.origin_region = ? AND _plasma_changes.row_version > ?)"// params: [0, 123, 1, 456]Hyperdrive + Postgres
Section titled “Hyperdrive + Postgres”Postgres ベースの multi-region デプロイには、Hyperdrive を使って各リージョンに共有 Postgres インスタンスへの低レイテンシアクセスを与えます:
import { fromPostgres } from "@sh1n4ps/plasma-server/postgres-adapter"
const executor = fromPostgres(env.HYPERDRIVE.connectionString)change log は Postgres に置かれます。書き込みは依然として DB レイヤーで直列化しますが、Hyperdrive が読み取りと pull のリージョンをまたぐレイテンシを償却します。
簡略化のパス
Section titled “簡略化のパス”ほとんどのアプリは multi-region を必要としません:
- 単一リージョン + Cloudflare エッジキャッシュ:
GET /sync/pullレスポンスは Cache API レイヤーでエッジにキャッシュされるため、新しいクライアントの初期ハイドレーションが D1 に 2 回ヒットしません。 - 単一リージョン +
compactChangeLog: これをスケジュールされた Worker から実行して、同じ(table, row_id)上の中間的なputを最新のものに折り畳みます。change log を小さく保ち、pull を高速に保ちます。
単一の D1 が扱える以上の持続的な書き込み帯域があるとき、または規制 / レイテンシ要件がデータのローカリティを強制するときに multi-region を使ってください。
次に読むべきもの
Section titled “次に読むべきもの”- Concepts / Change Log and Cookies — multi-region がその上に組み合わさるプリミティブ
- Deployment —
wrangler.jsoncにSequencerDOをどこにインストールするか