S3 上にインメモリ級のアーキテクチャを構築し、実用レベルまで仕上げた話

ここ数年、「S3 をプライマリストレージに」という話題が盛り上がっています。コンピュートとストレージを分離し、すべてをクラウドに保存し、無限にスケールするという図を見たことがあるでしょう。たしかに聞こえはいい。でも、実際に SLA（サービス品質保証）が厳しいエンタープライズグレードのシステムを構築しようとすると、すぐに気づくことがあります。それは「話はそんなに単純ではない」ということです。

[RisingWave](https://risingwave.com/) では、あえて困難な道を選びました。最初から、[S3](https://aws.amazon.com/s3/) を単なるバックアップ用途にとどめず、**唯一のストレージレイヤー**として使う分散ストリーミングデータベースを構築してきました。内部状態、[マテリアライズドビュー](https://risingwave.com/materialized-views-for-streamline-data-analysis/)、オペレーターの出力、リカバリーログ —— すべてが S3 に保存されます。

このたった一つの設計決定によって、想像し得るあらゆる制約に対処する必要が生じました。高レイテンシな読み込み、API リクエストのコスト、一貫性の欠如、従来のディスクのセマンティクスが完全に通用しないことなどです。

それでも私たちはやり遂げました。S3 をバックエンドにしながら、インメモリシステムに匹敵する性能と一貫性を実現するアーキテクチャを構築したのです。

このブログでは、私たちがその目標をどうやって実現したかを詳しく紹介します —— 一歩ずつ、失敗の連続、そしてその過程で得られた教訓まで。ごまかしや曖昧な説明、飾り立てた表現は一切ありません。

### S3 はオブジェクトストレージ。ファイルシステムではない。

今日の多くのシステムは、もともとローカルディスクや [EBS](https://aws.amazon.com/ebs/) ボリューム上に構築されています。そこへ誰かがやってきて「S3 に移せばいいじゃん」と言い出します。一見、簡単そうに思えますが、現実は？――全面的な書き直しになるでしょう。

S3 はディスクの代替品としてそのまま使えるものではありません。シーク（seek）、追記（append）、リネーム（rename）といった操作をサポートしていません。S3 は不変（immutable）で、最終的な一貫性（eventually consistent）しか保証せず、読み書きのたびに高レイテンシが発生します。もしあなたのシステムがランダムアクセス、同期書き込み、ファイルレベルのアトミック性に依存しているなら、S3 はその前提をすべて崩壊させてしまうのです。

RisingWave は、最初から S3 をダンプ先としてではなく「プライマリストア」として扱う前提で設計されています。だからこそ、状態モデルから圧縮戦略（compaction strategy）、リカバリーメカニズムに至るまで、永続状態はすべてオブジェクトストレージに存在することを前提としています。

私たちはすべてを不変な BLOB として書き込みます —— マテリアライズドビュー、中間状態、オペレーター出力など。それらはテーブル ID とエポックで論理的に整理され、バージョン管理され、インプレース更新は一切行いません。メタデータは [Postgres](https://www.postgresql.org/) に格納されており、各オブジェクトがどこに属していて、どのようにリカバリすべきかを追跡します。

このアーキテクチャによって、コンピュートノードは完全にステートレスになります。ノードがクラッシュしたり再起動されたりしても、Postgres に問い合わせて必要な情報を S3 から引き出すだけで済みます。ローカルの [RocksDB](https://rocksdb.org/) は不要で、協調プロトコルも必要ありません。これこそが、RisingWave がフォールトトレランスとスケーラビリティを実現する方法です —— 最初から S3 を前提に設計しているからこそ、後から継ぎ足すような必要がないのです。

### 最初の苦痛：レイテンシ

ストリーミングシステムにおいて、内部状態はすべての中核です。すべてのオペレーター —— ジョイン、ウィンドウ、集計 —— は、何百万ものイベントを跨いで状態を維持する必要があります。しかし、その状態を直接 S3 に置いたら、システムは完全に機能停止します。

S3 は遅いのです。最良のケースでも TTFB（最初のバイトまでの時間）は約 30ms 程度。しかし、実際の運用環境では 100〜300ms になることが多く、特に負荷が高い場合はさらに増加します。1 秒あたり 1,000 イベントを処理し、それぞれで状態の更新または読み取りが必要な状況を想像してみてください。もし状態読み取りに毎回数百ミリ秒かかると、遅延はどんどん蓄積し、最終的にはシステム全体が詰まって崩壊してしまいます。

もちろん、キャッシュを使います。メモリも活用します。でも、メモリには限界があります —— クラウドマシンの多くは 32～64GB 程度が上限で、プレミアムインスタンスでなければそれ以上は期待できません。複雑なクエリ、巨大なウィンドウ、マッシブなストリーム間のジョインなどにはとても足りません。

そこで私たちは中間層を導入しました：ローカルディスク。具体的には NVMe SSD や EBS です。これは RAM と S3 の間にある「ウォームキャッシュ」として機能します。

「EBS はローカル SSD より遅いじゃないか」とすぐに否定する人もいるでしょう。でも、それは常に正しいとは限らず、話はそう単純ではありません。

以下の理由から、EBS を選ぶことには合理性があります：

1. すべてのインスタンスタイプにローカルディスクがあるわけではない。AWS では特定のインスタンスファミリーのみが NVMe に対応しており、それらは CPU 使用率の低いワークロードに対しては過剰なスペックです。
2. ディスクサイズの柔軟性。ローカル SSD はインスタンスが提供するサイズに固定されていますが、EBS ならサイズを独立してスケーリングできます。つまり、小さな集計ジョブしか動かさないテナントは、使いもしないローカルディスクに余計な支払いをしなくて済みます。
3. デプロイのポータビリティ。RisingWave は AWS だけでなく Azure、GCP、オンプレミスにも対応しています。EBS スタイルのブロックストレージはどの環境にもありますが、ローカルディスクの構成は環境によって大きく異なるため、標準化が難しくなります。
4. 最新の EBS は十分に高速。適切なプロビジョニング（たとえば io2）を行えば、EBS は一貫性のある高スループットの IOPS を提供でき、中間状態キャッシュには十分です。

私たちは [Foyer](https://github.com/foyer-rs/foyer) というスマートキャッシュをこのディスク層に構築しました。Foyer はエビクション（キャッシュの追い出し）やテレメトリの処理を担い、RisingWave のクエリエンジンと密に統合されています。メモリ、SSD、S3 —— それぞれの層が適切な役割を果たします。そして S3 は真の意味で「長期的かつ不変で、コスト効率の高いストア」になり、ホットクエリのパスでアクセスされることはほとんどありません。

### コンパクションはリモートであるべき

内部状態がメモリ、ディスク、S3 にまたがって存在する場合、特にディスクと S3 上のデータを再構成し、読み取りを高速化し、ストレージの肥大化を防ぐメカニズムが必要になります。それがコンパクション（圧縮）の役割です。

RisingWave は LSM-tree スタイルのアーキテクチャを採用して、この階層型ストレージを管理しています。しかし、RocksDB のようにユーザークエリを処理するマシン上でローカルにコンパクションを実行するのではなく、それを完全に分離しました。RocksDB スタイルのローカルコンパクションでは、重いクエリとコンパクション処理が CPU や I/O を奪い合い、競合が発生します。その結果、システムは一時的にスタッター（ガクつき）を起こし、ストリーミングワークロードでは致命的です。

RisingWave はこの問題をリモートコンパクションで解決しました。専用のコンパクションワーカーが S3 から状態データを取得し、ファイルを再構成・マージして、再び S3 に書き戻します —— コンピュートノードには一切触れません。これによりクエリのレイテンシは常に安定し、コンピュートノードはクエリ処理に専念できます。

とはいえ、コンパクション専用のマシンを用意するのは、特にワークロードに波がある場合は非効率です。そこで RisingWave Cloud では、さらに一歩進んでコンパクションをサーバーレス化しました。異なるテナントやジョブが共有するコンパクションプールが自動でスケールし、オペレーションの手間なく弾力性と効率性を実現しています。

プライベート環境向けには、軽量なコンパクターノードを立てる選択肢も残しています —— 小型で効率的なマシンが、静かにバックグラウンドで処理を行う仕組みです。

この設計 —— 高速性のためのメモリ、バッファとしての EBS や SSD、永続性を担う S3、そして再構成を行うリモートコンパクション —— によって、RisingWave はどんなワークロードでも高速かつ安定して動作します。

### すべての S3 アクセスが等しいわけではない

S3 の料金は、保存しているデータ量だけではなく、アクセス頻度やアクセス方法にも大きく依存します。API コールのたびに料金が発生するのです。GET、PUT、LIST、HEAD —— どの操作でも例外はありません。もしあなたのシステムが、1 日あたり何百万回も小さなリードで S3 にアクセスしていたら、ストレージ容量よりも高額な AWS 請求書が届くことになります。

実際に私たちはこのような事例を現場で目にしてきました。1 日あたり 100 万回の GET リクエスト × $0.0004 = 月 $400。これを 30 日間、複数のテナントで繰り返すと、データ読み込みだけで数千ドルに膨れ上がります。

この問題を回避するために、私たちはシステムを以下のように再設計しました：

- **ブロックレベルの読み取り：** 論理行を 4MB のブロックにまとめ、S3 の GET コストを最小化しつつ、過剰なプリフェッチも防止。1 回のリクエストで取得可能です。
- **スパースインデックス：** すべてのテーブル、ビュー、マテリアライズドリザルトに、論理キー範囲と S3 オブジェクトキー＋バイトオフセットを対応づける軽量なインデックスを付与。インデックスはメモリまたは SSD に保存され、無駄な S3 アクセスを回避できます。
- **プリフェッチ：** クエリ計画時にスキャンパターンを解析し、必要なブロックを事前取得。たとえばクエリがブロック 1〜10 を必要とするなら、1 を返す間にバックグラウンドで 2〜4 を取得します。

この仕組みにより、本番環境では S3 とのラウンドトリップを 80〜90％ 削減し、1 クエリあたりのコストも抑制できます。リアルタイムシステムで S3 を持続的に使うには、これが唯一の現実解です。

### リアルタイム性と一貫性

ストリーミングシステムで「鮮度」はごまかせません。誰も、30 分前のデータを返すクエリなど求めていません。ユーザーは「今」のデータを期待しており、それは即時性と整合性を意味します。定期的なバッチフラッシュに頼ることはできません —— 即時の一貫性が求められるのです。

そこで私たちが構築したのは以下の機構です：

1. 書き込み後の読み取り一貫性（Read-after-write consistency）
2. 書き込みパスからクエリノードへの低レイテンシな伝搬
3. データをクリーンに保つためのアグレッシブなキャッシュ無効化

書き込みが発生すると、影響を受けるメモリやディスクのブロックが即座に無効化されます。マテリアライズドビューはインクリメンタルに更新されるため、再計算を待つ必要はありません。コールドデータはすばやく追い出され、古いスナップショットは残りません。

このアーキテクチャにより、イベントがシステムに到達すれば、その影響は数秒以内に下流のクエリに反映されます。Eventually ではなく、**Immediately（即時）** です。これが、RisingWave に「ライブ感」をもたらす所以です。

### マルチテナンシーとアイソレーション

S3 を前提とすると、マルチテナンシーの考え方が根本から変わります。従来のアーキテクチャでは、アイソレーションとは「テナントごとにクラスターを分ける」ことを意味していました —— ディスク、キャッシュ、ストレージ層を個別に用意するということです。しかし、すべての永続状態を S3 に保存することで、前提は覆されます。

RisingWave ではすべての永続状態を S3 に格納するため、テナント間でローカルディスクを取り合うことがありません。各クエリ、各マテリアライズドビュー、各テーブルスキャンは、同じオブジェクトストアを、それぞれ独立した論理名前空間として利用します。ディスクを分割したり、一方のテナントが他方のストレージバッファを汚染したりする心配は不要です。

これにより、コンピュート層における真のアイソレーション設計が可能になりました：

- クエリごとの CPU 使用制限（実行エンジンによって強制）
- テナントごとのメモリクォータ（SSD/S3 階層と連携したエビクション戦略付き）
- 独立した I/O バジェット（ノイジーなワークロードが他の SSD キャッシュや S3 プリフェッチに影響しない）

一方で、すべてのテナントは共有メタデータカタログにアクセスできます。これにより、同じストリームやマテリアライズドビューをチームで共同利用でき、データの重複やパフォーマンスの干渉を避けられます。

S3 を一貫したソース・オブ・トゥルースとし、RisingWave のステートレスな実行レイヤーと組み合わせることで、マルチテナンシーは「可能」になるだけでなく、**明快で、安価で、安定したもの**になるのです。

### キャッシュのハイドレーションとピア転送

S3 は耐久性に優れていますが、ライブクエリ処理中のホットデータ取得には向きません。そこで RisingWave では、アップデート発生時に積極的にキャッシュをハイドレート（補充）する仕組みを備えました。

たとえば、新しい [CDC](https://en.wikipedia.org/wiki/Change_data_capture) イベントバッチや更新されたマテリアライズドビューのフラグメントが書き込まれると、読み取りパスに即座に通知が送られます。これによりプリフェッチロジックが作動し、ディスクおよびメモリキャッシュがウォームアップされ、次回のクエリが S3 GET を待たずに済むのです。

ただし、まだ改善の余地があります。将来的には、ピアツーピアのキャッシュハイドレーションにも対応する予定です —— 一つのノードが、ホットブロックを他のノードに直接共有できるようにするのです。すべてのノードがそれぞれ独立して S3 にアクセスするのではなく、最新のブロックを直前に書き込んだ隣接ノードから取得できるようになります。言い換えれば、**低レイテンシイベント伝播に最適化された内部 CDN** のような仕組みです。

コンピュートが弾力的に変動し、S3 アクセスがコスト的に厳しいこの世界において、このような仕組みは非常に重要です。ホットリードでオブジェクトストアへの依存度を下げれば下げるほど、システムのスケーラビリティと性能が向上します。

### 最後に：やる価値はある

S3 をプライマリストレージとするストリーミングデータベースを構築するのは、ただ「難しい」だけではありません —— **苛烈** です。キャッシュ、状態管理、コンパクション、クエリの鮮度、調整メカニズムなど、すべてのレイヤーを再設計しなければなりません。そして、これまで当たり前だったディスク、メモリ、ファイル I/O に関する前提は一切通用しません。

しかし、それをやり切ったときの報酬は計り知れません：

- どのノードでも即時リスタート可能。データロスなし。
- リージョンやクラウドをまたいで自然にスケーリング。
- ディスクの空き容量や SSD 障害を気にしなくてよい。
- オブジェクトストレージの価格帯で完全な弾力性と耐久性を実現。
- そして何より：スマートなキャッシングとリモートコンパクションにより、**インメモリ並みの性能** を維持。

私たちは、S3 がコールドな永続層、EBS やローカルディスクがウォーム状態、メモリがホットパスを処理し、裏側でコンパクションエンジンがデータを整理するというシステムを構築しました。その結果は、**インメモリのような体験とクラウド経済性の両立**です。

本番環境でその成果を見たい方は、ぜひ試してみてください。[risingwave.com](https://risingwave.com/)

私たちは、このアーキテクチャを実現するために 4 年を費やしました。あなたはもう、ゼロから作る必要はありません。