HN 日本語サマリー

← 一覧へ戻る
プログラミング

スケールするのか?

Ok, but Does It Scale? (spacetimedb.com)

98 pointsby theanonymousone62 コメント

要約

Spacetimeデータベースの拡張性に関する疑問に答える記事です。拡張性には計算能力、ストレージ、ネットワークの3つの次元があり、Spacetimeは特にストレージの水平拡張に強みを持っています。しかし、すべての計算やネットワーク処理が水平拡張できるわけではなく、Spacetimeは競合するトランザクション下でも高いパフォーマンスを発揮し、並列化可能なOLTPワークロードの拡張を容易にするツールを提供します。

全文翻訳

よし、でもスケールするのか?

[画像: YouTubeのコメントスレッドで「それを水平にどうスケールさせるんだ?」と尋ねられ、Spacetimeにはできないと推測する返信]

おそらく、Spacetimeについて最もよく聞かれる質問です。単純な質問であり、単純な答えがあるはずのように思えます。

スケーリングは複雑なトピックであり、多くの場合は細部に悪魔が潜んでいます。一方で、ブログ記事でスケーラビリティを基本原理から理解できないほど複雑でもありません。

まず、一般的なスケーラビリティを探求し、それから「Spacetimeはどのようにスケールするのか?」という質問に答えていきましょう。

TL;DRが必要な場合:スケーリングには、コンピュート、ストレージ、ネットワークの3つの次元があります。ストレージの水平スケーリングは比較的簡単で、2026年10月31日にリリース予定です。しかし、すべてのネットワークとコンピュートが水平にスケールできるわけではありません。一般的な水平スケーラビリティを謳うOLTPデータベースは、トランザクションごとに莫大なオーバーヘッドを支払い、競合するトランザクションに直面すると極めて低いパフォーマンスを発揮します。Spacetimeは競合下で高いパフォーマンスを提供し、並列化可能なOLTPワークロードを簡単にスケールさせるためのツールを提供します。

注意

注:この記事ではCockroachDBについて多く言及します。CockroachDBは、SpannerやAurora DSQLを含む、実質的にすべての汎用水平スケーリングRDBMSの代用品として扱っています。これらの技術にはいくつかの問題点について言及しますが、それらはすべて信じられないほど素晴らしいエンジニアリングの成果です。

スケール

直感的に、誰もが「スケールする」ことが何を意味するかについてのアイデアを持っています。それは、より多くをこなせることを意味します。それは、需要に追いつけることを意味します。それは、数十億のリクエスト、または「無限」のリクエスト、または無限のデータ量、または無限の顧客数、あるいはソフトウェアを書き直すことなく、毎年10倍または100倍でアプリを成長させる能力を処理できることを意味します。

特に、ほとんどの人がシステムがスケーラブルだと言うとき、それは特に水平スケーラブルかどうかについて話しているのだと思います。一方、垂直スケーリングは単一のコンピュータでより多くをこなすことを意味しますが、水平スケーリングはより多くのコンピュータでより多くをこなす能力です。コンピュータの数を倍にすると、ほぼ2倍の作業ができるなら、コンピュートは水平にスケールします。結局のところ、単一のコンピュータはそれほど大きく速くすることはできませんが、理論的には購入できるコンピュータの数に制限はありません。その考え方には非常に満足感があり、それが誰もが探している特性です。

注意

注:「より多くのコンピュータでより多くをこなす」という定義は、水平スケーラブルなシステムはすべて分散システムでなければならないことを意味します。しかし、それは分散システムの唯一の目的が水平スケーラビリティであるとは意味しません。例えば、分散ステートマシンレプリケーションは、信頼性を目的として、多くのコンピュータで冗長に同じコンピュートを実行するように設計されており、スケールのためではありません。これについては、後述のSpacetimeのセクションで詳しく説明します。

「水平にスケールするか?」という質問は、情報が不足しています。より良い質問は、「どのような点で水平にスケールするか?」です。なぜなら、スケーラビリティには実際には3つのかなり独立した次元があるからです。

  1. コンピュート:処理できるトランザクションの数
  2. ストレージ:保存できるデータの量
  3. ネットワーク:サポートできる接続数と帯域幅

注意

私が何を言っているのかを理解するために、広くPostgreSQL互換インターフェースを公開しているが、根本的に異なるアーキテクチャを持ついくつかのデータベースシステムを見てみましょう。

例えば:

  • Postgresは、ほとんどの場合、単一ノードのデータベースです。Postgresは、コンピュート、ストレージ、ネットワークを水平にスケールしません。もちろん、多くのPostgresインスタンスをデプロイすることでPostgresを水平にスケールすることはできますが、Postgresコード自体はそれらの他のインスタンスをほとんど認識していません。唯一の例外はリードレプリカで、これによりリーダーをレプリカに手動でリダイレクトできます。これはネットワークとコンピュートの両方をスケールするのに役立ちますが、書き込み後の読み取りの一貫性やパフォーマンスに関する注意点があります。Postgres自体はプライマリのクラスタという概念を持たず、それらをまたいだトランザクションやクエリを実行したり、適切なものにルーティングしたりすることはできません。もちろん、これを実行するソフトウェアを書くことはできますし、人々がPostgresを水平にスケールする方法はこれです。しかし、その方法は読者の演習として残されています。

[画像: アニメーション化されたPostgresアーキテクチャ:ライターはコンピュートとストレージを一緒に保持する単一のプライマリの後ろにキューイングされ、WALは手動設定されたリードレプリカにストリームされ、リーダーはそれらに分散されます。コンピュートとネットワークは読み取り専用で部分的にスケールし、ストレージはスケールしません]

  • NeonはPostgresの変更されたバリアントで、ストレージを水平にスケールします。Neonテーブルはオブジェクトストレージをバックエンドとし、ローカルページキャッシュ構造によりデータアクセスを高速かつ効率的にします。キャッシュミスの場合、ページアクセスレイテンシは高くなる可能性がありますが、このアーキテクチャによりNeonデータベースは実質的に無限のストレージを持つことができます。しかし、Neonはコンピュートとネットワークを自動的に水平にスケールしません。Postgresと同様に、すべての書き込みトランザクションは単一のプライマリを経由する必要があります。ただし、Postgresバリアントであるため、Neonは読み取りワークロードのコンピュートとネットワークを水平にスケールするためのリードレプリカもサポートしていますが、これは同様の一貫性に関する注意点があります。

自動的なコンピュートスケーリングがなくても、ストレージの水平スケーリングは大きな利点です。ユーザー数がそれほど多くない小さなWebアプリでも、原理的には多くのストレージを使用できます。

[画像: アニメーション化されたNeonアーキテクチャ:ライターは単一のPostgresコンピュートの後ろにキューイングされ、WALはページサーバーを経由して底なしのオブジェクトストレージに流れ、ストレージは目に見えて成長し続けます。ページはオンデマンドで引き上げられ、リードレプリカがリーダーにサービスを提供します。ストレージはスケールしますが、コンピュートとネットワークは読み取り専用で部分的にスケールします]

  • CockroachDB(およびSpanner)は、原理的には、各テーブルのデータをクラスタ全体に分散させることでストレージを水平にスケールします。これにより、各テーブルの一部である「レンジ」が各マシンに保存され、通常は他の2つのマシンにレプリケートされます。ライターがすべて同じレンジを同時に変更しようとしない限り、ネットワークも水平にスケールできます。対称的なアーキテクチャを備えており、クラスタ内のどのノードでも読み取りと書き込みの両方のSQLリクエストにサービスを提供できます。最後に、きれいに並列化できるコンピュート(例:分析や無関係なキーへの書き込み)も水平にスケールします。接続するノードがクエリプランをコンパイルし、結果を返しますが、読み取り操作と書き込み操作の両方が、レンジに基づいてクラスタ全体にわたる分散トランザクションの一部として実行されます。

[画像: アニメーション化されたCockroachDBアーキテクチャ:3つの対称ノードがそれぞれレプリケートされたレンジを保存し、書き込みはいずれかのノードに着地し、各書き込みは他のノードのクォーラムにレプリケートされ、コミット前に確認応答を収集します。コンピュート、ストレージ、ネットワークはすべてスケールしますが、トランザクションが競合しない場合に限ります]

聖杯を見つけられるか?

では、CockroachDBが3つの次元すべてでスケールできるなら、すべての点でPostgresやNeonよりも優れているはずですよね?CAP定理[1]や原子時計で何か特別なことをしているのでしょうか?

残念ながら、ここには魔法はありません。CockroachDBは現代の驚異であり、特定のコンピュートを水平にスケールしますが、すべてのコンピュートが水平にスケールできるわけではありません。水平スケーラビリティは、実際には並列コンピューティングの問題です。コンピュートを異なるコンピュータが同時に実行できる断片に分割できますか?答えはしばしば「いいえ」であり、CockroachDBの問題は、データ競合により一度に1つずつ更新を実行せざるを得ない場合でも、水平スケーラビリティの莫大な調整コストを支払うことです。CockroachDBが水平スケーラビリティで得るものは、垂直スケーラビリティで失い、さらにそれ以上です。

残念な真実は、より多くのコンピュータでより多くをこなすのではなく、水平スケーラビリティはしばしばより多くのコンピュータでより少なくこなすことを意味することです。概念的には、1台のコンピュータが1ミリ秒でできることを、10台のコンピュータが100ミリ秒でできるということです。

CockroachDBのアプローチには、水平スケーラビリティに関して主に2つの問題があります。

  1. 各トランザクションに関わるデータは、単一のマシン上に共存することはほとんどありません。
  2. どのトランザクションもレンジへの排他的アクセス権を持たないため、すべてのトランザクションが分散型同時実行制御オーバーヘッドを支払い、競合するトランザクションは待機、中止、または再試行する必要があります。

最初の問題は、データの所有権をクラスタ全体に均等に分散させることによって引き起こされます。データがクラスタ全体に分散している場合、実質的にすべてのトランザクションでネットワークリクエストを行う必要があります。シャーディングは不便に聞こえるかもしれませんが、ほとんどのトランザクションが単一のシャード内で動作する場合、はるかに優れたパフォーマンスを提供できます。また、Neonの設計は、ホットページを同じマシンにキャッシュするため、多くのワークロードでこの同じ問題に悩まされないことに注意してください。

[画像: アニメーション化されたデータ配置の比較:左側では、所有権が均等に分散されているため、単一のトランザクションは3つの異なるノードで行を必要とし、コミット前にネットワーク往復を支払います。右側では、データがシャーディングされ共存しているため、各シャードがその行を所有し、トランザクションはローカルでより高速なレートでコミットされます。Neonはホットページをローカルにキャッシュすることで、これらの問題の多くを回避します]

注意

Spannerは、関連テーブルを共存させることをSpannerに指示できる「テーブルインターリービング」により、この問題の一部を解決しています。これにより、単純なケースでパフォーマンスを劇的に向上させることができます。CockroachDBは長年テーブルインターリービングをサポートしていましたが、複雑さを正当化するほどのメリットが小さいと判断し、v21.2で削除しました。

2番目の問題は、任意のコンピュートを並列化する一般的な問題、つまり競合下での調整です。標準的なPostgresでさえ、より小さなタイムスケールで同じ問題に遭遇します。分散システム全体でのトランザクションの調整ではなく、複数のコア全体でのトランザクションの調整を行う必要があります。CPU自体は、L1、L2、L3キャッシュ全体での生のメモリアクセスを調整する必要があります。同じキャッシュラインを変更する別のライターは、ローカルコピーを無効にし、コアの同期を強制する可能性があります。Postgresはトランザクション自体を調整する必要があります。ロックを誰が所有しているか、どのバージョンの行が表示されるか、トランザクションがどの順序でコミットされるか、競合する作業が待機、中止、または再試行する必要があるかです。

[画像: 2つのコアが同じキャッシュラインを書き込むアニメーション化された図。1ナノ秒あたり0.2秒の速度でスケールします。コア1は変更状態のラインを所有し、毎ナノ秒書き込みます。コア2のストアはL1とL2でミスし、共有L3に所有権要求を読み取るリクエストを送信します。L3のディレクトリはコア1をスヌープします。コア1のコピーは無効化され、変更されたデータはL2、L3を経由して転送され、約100ナノ秒後にコア2のキャッシュに入り、変更状態になります。その後、両方のコアが停止し、コア2はフルスピードで書き込みます]

例えば、ユーザー行と関連するメタデータ行を含む小さなOLTPトランザクションを想像してみてください。まず、単一ノードのPostgresでは両方の行が同じマシンにありますが、Nノードとランダムなプライマリキーを持つCockroachDBクラスタでは、それらが同じリーゼホルダー/リーダー上に存在する確率は約1/Nです。単一コアで約3マイクロ秒のクリティカルセクションであったものが、複数のネットワークリクエストを伴う1ミリ秒の分散クリティカルセクションになりました。行が共存する1/Nの場合でも、書き込みがクォーラムにレプリケートされるまでトランザクションはロックを解除できないため、ほぼ同じ往復時間保持されます。次に、さらに重要なこととして、同じ行を読み取りまたは変更したい別のトランザクションは、私たちのトランザクションがコミットまたは中止されるまで1ミリ秒待つ必要があります。これらの2つの問題は乗算的に累積します。約3マイクロ秒のクリティカルセクションを持つホットキーは、毎秒約300,000件の競合トランザクションを処理できます。1ミリ秒では約1,000 TPSしか処理できません。直感に反しますが、このシナリオではシングルスレッドソリューションの方が300倍「スケーラブル」です!

注意

1ミリ秒の分散コミットでは、トランザクションのわずか1%が単一の行で競合するだけで、すべての水平スケーラブルクラスタが単一コアよりも遅くなることに注意してください。これは単にアムダールの法則を水平スケーラビリティに適用したものです。競合するトランザクションはシリアルに実行されなければならず、総スループットはシリアルレートをシリアルトランザクションの割合で割った値を超えることはできません。(1 / 1 ms) / 1% = 100,000 TPS。コアをいくら追加しても無駄です!クラスタがスループットで単一コアを上回るシナリオは基本的に1つしかありません。それは、競合がなく、月額3,600ドル以上を支払っている場合(モデルのオーバーヘッドの仮定に基づく)です。これは一部の企業が直面するシナリオですが、ニッチなシナリオです。

競合下では、複数のライターは、コアが有用な作業を行うよりも共有状態の変更権について合意するのに多くの時間を費やすため、トランザクションを単に一度に実行するよりも大幅に遅くします。

しかし、並列に実行できるコンピュートであっても、調整オーバーヘッドを克服するためだけにどれだけの追加ハードウェアが必要になるかを人々はしばしば過小評価します。大まかな例として、L1キャッシュのレイテンシはL3レイテンシよりも約20倍低くなる可能性があります。ワークロードを並列化することで、安価でローカルなキャッシュアクセスが頻繁な同期やクロスコア通信に変わる場合、1つの慎重にキャッシュ最適化されたコアのパフォーマンスに匹敵するためだけに10コア以上が必要になる可能性は十分にあります。そして、これは、より大きなワーキングセットや、MVCCブックキーピングなどのメタデータがキャッシュから有用なデータを押し出す場合に、メインメモリへのアクセスというはるかに高いコストを考慮に入れていません。そして、分散MVCCブックキーピングとその原因となるキャッシュミスをすべて考慮したネットワークリクエストとシリアライゼーションについては、まだ検討していません。1コアで十分な場合に10コアの料金を払うことを本当に望んでいますか?おそらく、問題の一部については、スケーラビリティは達成できても、その代償は?という結果になるでしょう。

本質的にシリアルなコンピュートの場合、より高速なマシンに移動したり、コードを最適化したり、フォールトトレランスのために結果をレプリケートしたりできますが、いくら望んでも10台のマシンでそれをより速く実行させることはできません。『人月の神話』を読んだことのあるプログラマーなら誰でも直感的にこれを理解しています。プロジェクトにプログラマーを追加しても、ソフトウェアをより速く書けるようになるわけではありません。それは、より遅く書くことになりますが、より多くの会議を伴います。10人の作家が、特に郵便で調整している場合、小説を1人よりも速く書くことはできません。

この物語の教訓は、水平スケーラビリティは単にシステムが持つか持たないかの特性ではないということです。それは主にワークロードの特性です。

Spacetime

では、Spacetimeについて話しましょう。

まず、使用するAPIとそれを実装するアーキテクチャを区別する価値があります。Spacetimeプログラミングモデルのどの部分も、単一ノードまたは分散実装を本質的に必要としません。

実装方法はたくさんあります。

ctx.db.myTable.insert({ name: 'Tyler' });

または:

SELECT * FROM my_table

APIは、そのデータがどこに存在するか、どのマシンがトランザクションを実行するか、または舞台裏で何台のマシンが関与しているかについては何も語りません。例えば、ConvexはMySQLやPostgresなどの既存のストレージエンジン上にトランザクションレイヤーを構築しました。原理的には、私たちはSpacetimeモジュールサーバーのフリートと巨大なCockroachDBクラスタをストレージエンジンとしてSpacetimeを実装することもできたでしょう。驚くかもしれませんが、これは私たちが始めた方法にかなり近いです。Spacetimeの最初のプロトタイプはPostgres + Kafka上に構築されました!

しかし、完全な並列性がない場合や、数十台、数百台未満のマシンでは、コストが大幅に高くなるにもかかわらず、パフォーマンスが悪化します。このブログ記事でその理由を詳しく説明しています。

Spacetimeは、私たちのリアルタイムMMORPGのバックエンドとして始まりました。そのため、最初から極端なトランザクションレイテンシとスループット要件がありました。まれなケースが任意の数のマシンにまたがることを可能にするために、一般的なケースを法外に高価にすることはできませんでした。これらの要件により、カスタムストレージおよび実行エンジンをゼロから構築する必要がありました。

驚くべきことに、今日のSpacetimeデータベースの実行モデルは、設計によりシングルスレッドです。直感的には、特にキャッシュパフォーマンスの最適化経験が限られているエンジニアにとっては、これは「悪いニュース」のように聞こえるかもしれません。当初、私たちもその仮定で始めました。カスタムデータベースエンジンの初期バージョンは、MVCCトランザクションによって有効化された並列実行を使用していました。問題は、実際に測定したところ、シングルスレッド実行が並列実行を完全に上回ったことです。ある意味では、単に大きなロックを使用すべきだと理解するのに100万ドル以上かかったかもしれません。将来的に、限定的な状況でより良いパフォーマンスを達成する単一データベース内の並列実行を実装することは不可能ではありませんが、それに見合う以上の複雑さとオーバーヘッドを導入しないことを保証するための、極めて慎重なエンジニアリングとパフォーマンス測定なしには実現できません。

[画像: ベルカーブミーム:「シングルコアを使えばいいのに」]

では、これはSpacetimeが水平にスケールできないことを意味するのでしょうか?全く違います。

Spacetimeの水平スケーリング

Spacetimeの水平スケーリングソリューションの基盤は、アクターモデルに触発されています。アクターモデルは並列コンピューティングの一般的なモデルであり、「多数の独立したモノプロセッサからなる高度に並列化されたコンピューティングマシン、それぞれが独自のローカルメモリと通信プロセッサを持ち、高性能通信ネットワークを介して通信するという見通しに動機づけられています」。

[画像: HewittとBakerが、多数の独立したモノプロセッサを持つ高度に並列化されたマシンでアクターモデルを動機づける抜粋]

今日のSpacetimeでは、各データベースはシングルスレッドのアクターであり、水平スケーラビリティを開発者体験にとってますますフレンドリーにするための6段階の戦略があります。

次の段階はすでに日付が決まっています。非同期データベース間通信と階層型ストレージは、スケーラビリティローンチの一部として2026年10月31日にリリースされます:Spacetime Continuum。

ステップ機能ステータス
1世界クラスのパフォーマンスを持つレプリケートされたデータベースを提供今日
2世界クラスのシャーディング体験(非同期IDC)を提供2026年10月31日
3各データベースのストレージを水平にスケール(階層型ストレージ)2026年10月31日
4各データベースのネットワークを水平にスケール(リードレプリカ)計画中
5データベース間トランザクション(同期IDC)を実装計画中
6データベース内パーティショニングを実装計画中

詳細に入る前に、区別する価値のあるSpacetimeの2つのバージョンがあります。

  • SpacetimeDB Standalone:GitHubで利用可能なシングルノードバージョン。
  • SpacetimeDB Cloud:プロプライエタリなクラスタ化バージョン。
世界クラスのパフォーマンスを持つレプリケートされたデータベース

各Spacetimeデータベースが世界クラスのシングルスレッドパフォーマンスをどのように達成するかについてはすでに詳しく説明しましたが、SpacetimeDB Cloudが分散ステートマシンレプリケーションもサポートしていることに注意することが重要です。

これは、各データベースがシングルスレッドであっても、必ずしも1台のマシンで実行されているわけではないことを意味します。これは直感に反するように聞こえるかもしれませんが、一般的な考え方は、単一スレッドが行っていることを複数のマシンにレプリケートできるため、マシンが故障してもデータベースは利用可能であるということです。これは、SpacetimeDB Cloudの各データベースが分散システムとして機能することを意味します。

[画像: Viewstamped Replicationのアニメーション化された図:クライアントはノード1のシングルスレッドプライマリにトランザクションを送信し、ノード2と3のバックアップにPREPAREメッセージをパイプライン化し、PREPARE_OK確認応答を収集します。ノード1が故障すると、バックアップはビュー変更を実行し、STARTVIEWCHANGEおよびDOVIEWCHANGEメッセージを交換します。ノード2がビュー2のプライマリになり、STARTVIEWを送信し、クライアントはそれにリダイレクトします]

パイプライン化された実装により、十分なノード間ネットワーク帯域幅とパイプライン深度用のメモリがあれば、レプリケートされたデータベースがレプリケートされていないデータベースと同じスループット(ベンチマークトランザクションで約30万TPS)を達成できることを、ベンチマークで実証したことに注意してください。

[画像: パイプライン化されたレプリケーションのアニメーション化された図:リーダーのトランザクションログは、現在の、適用済み、決定済み、永続化済みの4つのウォーターマークを流れます。リーダーは新しいリクエストをヘッドで即座に適用します。リーダーはPREPAREメッセージを2つのバックアップにストリームします。最初のPREPARE_OK確認応答は決定済みウォーターマークを進めます。最初のDURABLE_OKは永続化済みウォーターマークを進めます。応答は、適用済み、決定済み、永続化済みの各ティアでリッスンしているクライアントに解放されます。データベースロックは、ヘッドで実行中のトランザクションに対してのみ保持されます]

confirmedReads(true)、Spacetimeのデフォルト設定は、クライアントがクラスタ上でトランザクションが永続化されるまで読み取らないように構成します。

世界クラスのシャーディング体験

データベースをシャーディングすると、単一のシャード内で動作するトランザクションのパフォーマンスが向上します。分散トランザクションは、シャードをまたぐトランザクションを実行する機能を提供します。なぜ両方の世界の最高を持てないのでしょうか?ほとんどのトランザクションが単一のマシン内で動作し、ほとんどのトランザクションがそのマシン内で発生するようにデータを整理するためのツールをプログラマーに提供したらどうでしょうか?まれなインスタンスでのみ分散トランザクションが必要になるはずです。

鍵は、シャード境界を明示的かつ人間工学的にすることです。アクターモデルでは、各シャードは独自の独立してスケジュールされたアクターのように動作できます。シャード内のトランザクションは、高速でローカルでシングルスレッドのままであり、真に複数のシャードに触れる必要があるトランザクションのみが調整コストを支払います。これにより、アーキテクチャはSpacetimeを今日の高速化しているパフォーマンス特性を維持しながら、分散調整が無料であると偽ることを防ぎます。

各データベースは、独自のステートとトランザクションログを持つ独立したアクターであるため、異なるデータベースに対するトランザクションは調整なしで同時に実行できます。

Spacetimeはすでにこのアーキテクチャを管理するためのツールを提供しています。データベースは他のデータベースの子として作成できます。プロシージャはデータベースを公開し、それらの関数を呼び出すことができます。ルートデータベースは、その下のデータベースのIDとステートを追跡できます。

顧客の多くは、この方法で数百または数千のデータベースを運用しており、BitCraftもこの方法でスケールしています。単一のルートデータベースがグローバルデータを維持し、リージョンデータベースが世界の異なる部分や異なるプレイヤーグループを処理します。1つのリージョン内のトランザクションは高速でローカルなままであり、リージョン自体は並列に実行されます。

最初のクラスのデータベース間通信(IDC)を通じて、このモデルを大幅に容易にする予定です。非同期IDCにより、1つのデータベースが型安全な方法で別のデータベースの関数を呼び出すことができるようになります。

[画像: 6つのノードを持つSpacetimeクラスタのアニメーション化された図。各ノードは複数のデータベースをホストします。非同期データベース間メッセージはノード間でデータベースをホップし、すべてのデータベースは独立して実行を続けます]

高レベルでは、TypeScript APIは次のようになります。

// 非同期IDC:型安全なメッセージを別のデータベースに送信します。
// これはターゲットDBで正確に1つのレデューサー呼び出しにつながります。
cxt.db.receivePlayer.insert({
 msgId: 0n,
 target: regionDb,
 player,
});
各データベースのストレージを水平にスケール

現在、Spacetimeはすべてのテーブルデータをリーダーノードのメモリ内に保存しています。これは、単一のデータベースでは、テーブルに格納できるデータの量がマシンの物理メモリによって制限されることを意味します。しかし、この制限はSpacetimeやその驚異的なパフォーマンスの鍵とは根本的に関係ありません。メモリ、ディスク、オブジェクトストレージをCPUキャッシュモデルの自然な拡張と見なしています。精神的にはメモリをL4キャッシュ、ディスクをL5キャッシュ、オブジェクトストレージをL6キャッシュとして扱います。このキャッシュモデルは、単一ライターから最大限のパフォーマンスを得るための、古くからある方法です。これはしばしば「階層型ストレージ」と呼ばれます。

[画像: 階層型ストレージピラミッド:L1からL3までのCPUキャッシュ階層が、メモリをL4、NVMeディスクをL5、オブジェクトストレージをL6としてSpacetimeによって拡張されています。各階層を下るごとに、サイズが大きく、安価で、遅くなります。メモリは現在利用可能で、ディスクとオブジェクト階層は2026年10月にリリースされます]

キャッシュラインとプリフェッチにより、安価で遅いストレージから高価で高速なストレージにデータのバッチを事前に読み込むことができます。データベースに階層型ストレージを提供することで、キャッシュミスのコストを償却し、より大きく安価なストレージのスケーラビリティを維持しながら、全体としてより高速で高価なストレージに近いパフォーマンスを達成できます。

ページングはシングルスレッドを停止させませんか?

すべてのトランザクションが1つのスレッドで実行される場合、オブジェクトストレージへの単一のキャッシュミスがデータベース全体を50ミリ秒間停止させるように思えるかもしれません。それは図書館を運営するようなもので、顧客が本を注文するたびに、本が入荷するまで全顧客の列を待たせてから次の顧客を処理するようなものです。明らかに愚かです。階層型ストレージをシングルスレッド実行と互換性のあるものにするルールは単純です。ロックを保持している間にキャッシュミスを待たないでください。本は非同期に注文し、待っている間に次の顧客を処理してください!

幸いなことに、これはよく研究された問題です。H-Storeは、この技術をアンチキャッシングと呼んでいました。トランザクションを通常どおり実行し、メモリにないデータに触れた瞬間に中止し、非同期にデータをフェッチし、待っている間に他のトランザクションを実行します。データが到着したら、トランザクションを再実行します。中止は、何もコミットされていないため、ほぼ無料であり、各再実行は、シリアライズを回避するI/Oミリ秒に対してマイクロ秒のコストがかかります。Calvinは偵察クエリと呼ばれる同様のトリックを使用していました。トランザクションのドライランを最近のスナップショットに対して実行して、読み取るものを発見し、それをプリフェッチしてから、実際に実行します。TigerBeetleは、各トランザクションバッチを同期的に適用する前に明示的なプリフェッチフェーズを実行します。

[画像: 中止と再実行のアニメーション化された図:トランザクションはリーダーのシングルスレッド実行プログラムを流れます。ホットページをメモリ内で高速な往復で読み取ります。1つのトランザクションがメモリにないページに触れ、ページミスで中止され、オブジェクトストレージからディスクを経由してメモリにページがフェッチされるのを待つ間、リーダー内の待機スロットに駐車されます。実行プログラムは、フェッチ全体で他のトランザクションを実行し続け、ページが到着すると、駐車されたトランザクションは再実行され、コミットされます]

Spacetimeは、レデューサーが決定論的であるため、これらの技術に異常によく適しています。レデューサーはI/Oを実行したり、クロックを読み取ったり、ランダム性を生成したりすることはできません。すべてのデータアクセスはレデューサーコンテキストを経由するため、エンジンはトランザクションが行うすべての読み取りを観測します。これは、中止に目に見える影響がないこと、同じステートに対する再実行がまったく同じ行に触れること、およびドライランが実際の実行が必要とするデータを発見することを意味します。

これは、非常に有能なTigerBeetleチームが「対角線スケーリング」と呼ぶ戦略です。

ディスクおよびオブジェクトストレージテーブルは2026年10月31日にリリースされます。これをSpacetimeのスケーラビリティに対する重要な改善と見なしています。ディスクテーブルを使用すると、ストレージ制限を大幅に増やし、データストレージの価格を下げることができます。オブジェクトストレージテーブルを使用すると、ストレージ制限を事実上削除し、単一のデータベースに理論的に無制限のデータを保存できるようになります。

各データベースのネットワークを水平にスケール

前述のように、コンピュートと同様に、ネットワークの水平スケーリングは常に可能とは限りません。なぜなら、一部のケースではライターが同じステートを変更したいと考えており、そのため同じコンピュータに接続してデータを送信する必要があるからです。これは、CockroachDBがクラスタのホットスポットを回避するためにランダムキーの使用を推奨する理由の1つです。しかし、そのようなインスタンスでも、可能な限り最大のリーダーおよびライタースループットをサポートするように努めるべきです。

CockroachDBと同様に、SpacetimeDB Cloudも対称的なアーキテクチャを備えています。つまり、どのノードにも接続でき、SpacetimeDB Cloudは書き込みリクエストが処理されるか、適切な場所にプロキシされることを保証します。外部からは、あたかも1つの巨大なコンピュータのように見えます。ライターにとって、これは接続を「ファンイン」できることを意味します。クライアントはいずれかのノードに接続し、そのノードはそれらの接続をデータベースをホストするノードへの単一接続に多重化します。

原理的には、一貫性のあるリードレプリカを導入することで、リーダーからSQLサブスクリプションと読み取りクエリの処理をオフロードすることもできます。一貫性のあるリードレプリカにより、リーダーから「ファンアウト」できます。リーダーに直接接続する代わりに、サブスクリプション評価を実際の一貫性のあるリードレプリカにルーティングできます。そうすることで、リーダー接続と帯域幅を水平にスケールできます。

ここでの「一貫性のある」とは、レプリカがリーダーのトランザクションログを同じ総順序で適用することを意味します。そのため、サブスクリプションはまったく同じ更新シーケンスを見ますが、遅延します。ポイントインタイムSQLクエリの場合、リーダーは各クエリにトランザクションログ内の位置を割り当てますが、それを実行しません。レプリカは、そのオフセットまで適用されるまでクエリに応答しません。したがって、読み取りはすべての書き込みに対して線形化可能であり、リーダーの唯一のコストはシーケンス番号を配布することです。

[画像: ファンイン・ファンアウト図のアニメーション化された図:ライタークライアントはいずれかのノードに接続し、そのノードはトランザクションをシングルスレッドリーダーに多重化します。リーダーのトランザクションログは計画された一貫性のあるリードレプリカにストリームされ、それらはサブスクリプション更新をリーダークライアントにファンアウトします]

データベース間トランザクション

同期IDCは、CockroachDBが提供するものと同様の真の分散トランザクションを実行するために必要な組み込みの2フェーズコミットメカニズムを提供します。これにより、真の原子性が必要な場合に、クラスタ内の複数のデータベースにまたがるトランザクションが可能になります。

これのTypeScript APIは、通常のレデューサーを呼び出すのと同じように見えますが、リモートデータベースに対して行われます。

// 同期IDC:現在のトランザクション内で別のデータベースを呼び出します。
cxt.at(regionDb).reducers.receivePlayer(player);

ここには明らかな緊張があります。各データベースが1つの大きなロックの後ろにあるシングルスレッドである場合、データベースAからデータベースBへの同期呼び出しは、Aのトランザクションがコミットまたは中止されるまでBのロックを保持します。これにはネットワーク往復が必要です。私たちの計画は、実行をシングルスレッドに保ちつつ、これらの分散トランザクションのためにMVCCを再導入して、往復が進行中であっても同時トランザクションが続行できるようにすることです。

[画像: パイプライン化された2フェーズコミットのアニメーション化された図:ラウンド1では、データベースAのクライアントトランザクションがデータベースBを呼び出し、PREPAREDを受け取り、両方のデータベースがメモリ内でコミットしてロックを解放します。ラウンド2では、データベースはPREPARED TO PERSISTおよびCOMMIT PERSISTを交換し、ロックを保持せずにバックアップにログエントリをレプリケートします。その間、同時トランザクションは実行を続けます]

トランザクションを最初にメモリ内でコミットし、ディスクI/O中にデータベースロックを保持しないパイプライン化された2フェーズコミットプロトコルを設計し、TLA+でその安全プロパティをモデルチェックしました。詳細は別の投稿に値します。

これらの機能は together、すべてのトランザクションのパスに分散調整を置くことなく、複数のデータベースを統一された分散アプリケーションに変えます。ほとんどの作業はローカルで非同期のままになります。真にデータベース間の原子性を必要とする操作のみが、分散トランザクションのコストを支払います。

データベース内パーティショニング

複数のデータベースは自然なスケーリング境界ですが、複数のモジュールの管理、スキーマの独立した更新、および負荷の変化に応じて再分散する方法の決定も必要になります。これらのUXの問題に対処するための次のステップは、単一論理データベース内にパーティションを導入することです。

パーティションにより、1つのデータベースモジュールが複数の独立して実行されるシャードを含むことができるようになります。これにより、単一のデプロイメントアーティファクトが維持され、スキーマ変更をデータベース全体にアトミックに適用できるようになります。一方、異なるパーティションに対するトランザクションは同時に実行されます。

Spacetimeのパーティションは、CockroachDBのレンジとは異なるものです。レンジは配置決定です。つまり、どのマシンがテーブルの一部を保存するかを示しますが、どのトランザクションも任意のレンジに触れることができ、書き込みはどこに着地しても同じようにレプリケートおよび調整されます。Spacetimeパーティションは実行ユニットです。それは独自のデータを所有し、独自のシリアルトランザクションログを持ち、そのデータに対して動作するレデューサーコードがその中で実行されます。開発者は、一般的なトランザクションが決してパーティションを離れないように、ワークロードの構造に合わせてパーティション境界を選択します。Spacetimeは、その保証を弱めることなく、負荷を再分散するためにパーティション全体をマシン間で移動できます。

[画像: データベース内パーティショニングのアニメーション化された図:1つのモジュールから構築された単一論理データベースは2台のマシンにまたがり、4つのパーティションを含み、それぞれが独自のトランザクションログを持っています。クライアントトランザクションはキーでパーティションにルーティングされ、同時に実行されます。まれなクロスパーティショントランザクションは2つのパーティション間で2フェーズコミットを実行し、再分散はすべて実行中にパーティション全体をマシン1からマシン2に移動します]

パーティション内に留まるトランザクションは、他のパーティションの存在に対して何も支払いません。これはゼロオーバーヘッドの原則です。ワークロードが実際に必要とするまで、水平スケーラビリティのコストを支払う必要はありません。[2]

[画像: データベースパーティションをまたぐクエリの2つの探索的形状の静的なスケッチ:オプションA、スキャッターギャザーSQLは、1つのクライアントクエリをゲートウェイ経由で各ライブパーティションにファンアウトし、結果をマージします。そのため、読み取りは書き込みパスに触れます。オプションBは、パーティション変更を、クライアントがクエリする読み取り専用分析データベースにストリームし、読み取りをホットコアから外します]

この点については、顧客の声を聞き、彼らが実際に何を必要としているかを見るのが最善だと思います。

では、スケールするのか?

はい、します。今日では、各シャードを高速かつローカルに保ちながら、データベース全体にワークロードをシャーディングすることで、並列ワークロードをスケールできます。見てきたように、このアーキテクチャは、ワークロードにほとんど競合がない場合、または単一コアのパフォーマンスを得るために大規模なクラスタのコストを払いたくない場合に最適です。

時間の経過とともに、階層型ストレージ、データベース間通信、分散トランザクション、パーティションにより、そのアーキテクチャはますます透過的になります。目標は、調整を無料にすることではありません。それは、ワークロードが実際にそれを必要とするときにのみ、そのコストを支払うことを保証することです。

Tyler Cloutier
Cofounder, Clockwork Labs


[1] 分散システムオタク向けのCAP定理に関する短い余談。CAP定理、別名Brewerの定理は、一般的に3つのすべて(一貫性、可用性、パーティショントレランス)を同時に満たすシステムは存在しないことを単純に述べています。システムはそれらのうち0、1、または2つを持つことができますが、決して3つではありません。

ここでの「一貫性」とは、すべての読み取りが最新の書き込みまたはエラーを受け取ることを意味します。実際には、このプロパティは分散ステートマシンレプリケーション(別名、Paxos/Viewstamped Replicationのバリアント)を正しく実装することによって達成されます。

「可用性」とは、障害のないノードへのすべてのリクエストがエラー応答を受け取らないことを意味します。

「パーティショントレランス」とは、ノード間のメッセージが遅延したり完全にドロップされたりする可能性がある世界にシステムが存在することを意味します。

もし誰かがCAP定理を、データベースが水平にスケーラブルであるか、またはそうでないかの「証明」として曖昧に言及するのをもう一度聞いたら、私は精神的に崩壊するかもしれません。なぜか、CAP定理がOLTPデータベースシステムのスケールを制限するという一般的な誤解があります。例えば、投資家やエンジニアから、Spacetimeベンチマークの数値を達成するために、どのように「CAP定理を回避したか」と半ダース回ほど尋ねられました。実際には、CAP定理はコメントしていません。

この誤解は、この出版物と、その仮説の提唱者(現在は定理:後にSeth GilbertとNancy Lynchによるこの論文で証明された)であるGoogleのEric Brewerによるこの論文から生じたと私は推測しています。なぜなら、Spannerは有名な水平スケーラブルSQLデータベースとして知られているからです。おそらく、この論文がCAP定理とSpannerの両方を参照しているため、CAP定理が何らかの形でスケーラビリティにとって重要であるという空気が広まったのでしょう。しかし、オリジナルのSpanner論文はCAP定理に全く言及していないことに注意してください。

いずれにせよ、CAP定理は、最善でもCPシステムまたはAPシステムを選択するという選択肢しか提供しないという点で、データベースに関連しています。CPシステムでは常に一貫性がありますが、ネットワーク障害のために利用できない場合があり、APシステムでは常に利用可能ですが、ネットワーク障害のために一貫性がなくなる場合があります。

したがって、ネットワークを障害にしないことはできないため、ある意味でCAP定理は、正しいか利用可能か、どちらを望むかという質問をしているだけです。CAP定理は、スループット、レイテンシ、またはスケーラビリティについては何も述べていません。

いずれにせよ、実際にはOLTPデータベースにとって、実際には選択肢はありません。Spannerを含む強力な一貫性保証を持つOLTPデータベースは、CPシステムでなければなりません。なぜなら、不整合はユーザーが間違った結果(古い読み取り、非単調な読み取り、競合する書き込みなど、まれにeコマースビジネス全体を沈める可能性があるもの)を見ることを意味するからです。

このレンズを通して、CAP定理はスケーラビリティにとって興味深くも関連性もないことが明確にわかります。実際に興味深い質問は、水平スケーラブルで一貫性のある(つまり正しい)OLTPデータベースシステムをどのように構築できるかということです。

CAPの一貫性は、ACIDの分離と密接に関連しています(紛らわしいことに、ACIDの一貫性はまったく別のことを記述しています)。CAPの一貫性は線形化可能性です。操作は実時間を尊重する単一の順序で発生したように見えます。ACIDの分離は、その最も強力な形態ではシリアル化可能性です。トランザクションは、あるシリアル順序で発生したように見えます。両方を提供するデータベースは「厳密にシリアル化可能」であり、これはSpannerが提供するものです。

線形化可能性は、すべてのトランザクションが実時間を尊重する1つのグローバル順序での位置を割り当てられることを必要とします。同じデータに触れるトランザクションの場合、その順序は本質的にシリアルであり、これが上記の競合問題です。互いに依存しないトランザクションの場合でも、順序は合意される必要があり、合意は通常、メッセージパッシングによる調整を意味します。

ここで、Spannerが原子時計を使用する理由も理解できます。原子時計は、Spacetimeが互いのデータ依存関係を持たないトランザクション(つまり、それらは自明に並列化可能)で、地球の異なる側で実行されている可能性のあるトランザクションに線形化可能な順序を割り当てることができます。時計は、そうでなければ調整を必要としないトランザクションの相対的な順序を決定するために世界中にメッセージを送信することなく、どのトランザクションが先に発生したかをSpannerが言うことを可能にします。

[2] パーティションをまたぐクエリやトランザクションについてはどうでしょうか?確かに、最終的にはサポートできるでしょう。パーティションをまたぐ分散SQLクエリは、CockroachDBのようなシステムが提供する一般的な水平スケーラビリティを回復しつつ、単一パーティション内に留まるトランザクションのSpacetimeの高速ローカルパスを維持します。

しかし、レデューサーがパーティションをまたいで読み書きできることは許可しないでしょう。なぜなら、それは私たちが長々と議論してきたのと同じパフォーマンスのフットガンだからです。代わりに、パーティションは引き続き関連データを共存させ、モジュールコードはそれらの境界を明示的に尊重しますが、クライアントはすべてのパーティション全体でグローバルにSQLクエリとサブスクリプションを実行できるようになります。

これは運用上の便宜や特殊なワークロードに役立つ可能性がありますが、この最終段階が複雑さに見合うかどうか、またはこれらのクエリのほとんどは実際には読み取り専用の分析データベースクラスタをターゲットにすべきかどうかは、現時点では不明です。いずれにせよ、パフォーマンスのために、クロスパーティションクエリはアプリケーションの主要なものであってはならないでしょう。