photoncloud-monorepo/flaredb/chat.md
centra 8f94aee1fa Fix R8: Convert submodule gitlinks to regular directories
- Remove gitlinks (160000 mode) for chainfire, flaredb, iam
- Add workspace contents as regular tracked files
- Update flake.nix to use simple paths instead of builtins.fetchGit

This resolves the nix build failure where submodule directories
appeared empty in the nix store.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 16:51:20 +09:00

101 KiB
Raw Blame History

You Asked:

Apache Cassandraの運用が大変という話は良く聞きますが、何がそんなに苦痛なのでしょうか

Gemini Responded:

Apache Cassandraはその圧倒的な書き込み性能と可用性の高さで知られていますが、運用エンジニアからは**「猛獣」「じゃじゃ馬」**に例えられるほど、管理が難しいことでも有名です。

その「苦痛」の原因は、主にJava VM (JVM) の挙動分散システム特有のデータ整合性維持、そして独特なデータ削除の仕組みの3点に集約されます。

具体的に何がそれほど大変なのか、現場のエンジニアを悩ませるポイントを整理しました。


1. JVMとガベージコレクションGCの悪夢

CassandraはJavaで動作するため、メモリ管理はJVMに依存します。これが最大の運用課題となることが多いです。

  • Stop-the-World (STW): JVMがメモリ整理ガベージコレクションを行う際、アプリケーションの動作を一瞬停止させることがあります。Cassandraのような高負荷データベースでは、ヒープメモリが巨大になりがちで、この停止時間が長引く数秒〜数十秒ことがあります。
  • 「死んだ」と誤判定される: GCで停止している間、Cassandraは他のードへの応答Heartbeatができなくなります。すると、周囲のードは**「こいつはダウンした」と判定**して切り離してしまいます。GCが終わって復帰しても、クラスタの状態が不安定になり、アクセスエラーが多発します。
  • チューニングの迷宮: これを防ぐためのJVMチューニングNew/Old領域の比率、GCアルゴリズムの選定などは非常に高度なスキルを要し、「秘伝のタレ」化しがちです。

2. 「コンパクション」と「リペア」によるリソース枯渇

Cassandraは「書き込み」を高速化するために、バックグラウンドで重い処理を行います。これが突然サーバーのリソースを食いつぶすことがあります。

  • コンパクション (Compaction): データは不変のファイルSSTableとして次々とディスクに書き込まれます。放っておくとファイル数が膨大になり、読み込み速度が劇的に低下します。 これを防ぐため、バックグラウンドで複数のファイルを結合コンパクションし続けますが、この処理がCPUとディスクI/Oを強烈に消費し、本来のサービスの性能を落とすことがあります。
  • リペア (Repair): Cassandraは「結果整合性」を採用しているため、ード間でデータのズレが生じます。これを直すために定期的に nodetool repair というコマンドを実行する必要がありますが、これがまた重い処理です。失敗したり、時間がかかりすぎたりして、運用者を疲弊させます。

3. 「削除」が実は「書き込み」である (Tombstones問題)

Cassandraでデータを削除しても、すぐにはディスクから消えません。

  • 墓石 (Tombstone) を置く: 削除命令を送ると、「ここは削除済み」というマークTombstone書き込みます。
  • ゾンビデータの復活: もし定期的な「リペア」をサボったり、ノードが長期間ダウンして復帰したりすると、削除したはずのデータが復活することがありますTombstone自体が消えてしまうため
  • 読み込み時の罠: 「10件データを取ってくる」というクエリを投げた際、Cassandraが内部で**「10万個のTombstone削除済みマークをスキャンして、やっと生きている10件を見つける」**という状況に陥ることがあります。これによりクエリがタイムアウトし、「データはあるのに読めない」という事象が発生します。

4. スケールアウト/インの遅さ

「リニアにスケールする」と言われますが、ノードの追加や削除はボタン一つで瞬時に終わるわけではありません。

  • データの再配置 (Streaming): 新しいノードを追加すると、他のノードから担当分のデータをネットワーク越しにコピー(ストリーミング)し始めます。テラバイト級のデータがある場合、これに数時間〜数日かかり、その間ネットワーク帯域が圧迫されます。

まとめ:なぜ苦痛なのか?

一言で言えば、**「データベースが自律的にやってくれるはずの面倒な処理(掃除、整頓、ゴミ捨て)を、人間が手動やスクリプトで絶妙にコントロールしてあげないと、自重で倒壊する」**からです。

注記: 近年のバージョンCassandra 4.0以降では、JavaのZGC/Shenandoah GCへの対応や、ストリーミングの改善により、以前よりはかなり運用が楽になっています。また、C++で書き直された互換DBである ScyllaDB への移行や、AWS Keyspacesなどのマネージドサービスを利用することで、この苦痛を回避するケースも増えています。

次のステップ: もし現在、具体的な技術選定をされているのであれば、「Cassandraのメリットが活きるユースケース書き込み特化」と「運用の楽な代替案ScyllaDB等」の比較について整理しましょうか


You Asked:

Rustで極めて単純で高パフォーマンスなKVSのようなDBを書き、その上のレイヤーにSQLのような高度なクエリのできるものを乗せるということを考えています。これを作るに当たり、考えることはどのようなことがあるでしょうか。

Gemini Responded:

Rustで「KVSストレージエンジン + SQLレイヤークエリエンジン」という2層構造のDBを作るのは、CockroachDB (RocksDB/Pebbleの上に構築)TiDB (TiKVの上に構築)FoundationDB などで採用されている非常にモダンで拡張性の高いアーキテクチャです。

この構成をRustで実装する際に直面する技術的課題と、設計上の重要な決定事項を、下のレイヤーから順に整理します。


1. KVSレイヤーストレージエンジンの設計

「極めて単純で高パフォーマンス」を目指す場合、アーキテクチャの選定が全てです。

A. データ構造の選定B-Tree vs LSM-Tree

  • LSM-Tree (Log-Structured Merge-tree):
    • 特徴: 書き込みWriteが非常に高速。データは追記のみで、後でバックグラウンドでマージコンパクションする。RocksDBやLevelDBのアプローチ。
    • Rustでの課題: コンパクション処理の実装が複雑。また、読み込み時に複数のSSTableを探すため、Bloom Filterの実装が必須。
  • B+Tree:
    • 特徴: 読み込みReadが安定して高速。更新はインプレース場所を指定して書き換え。従来のリレーショナルDBPostgreSQL, MySQL/InnoDBのアプローチ。
    • Rustでの課題: ページキャッシュBuffer Poolの管理が難しい。Rustの借用チェッカーと「相互参照するページ構造」は相性が悪く、unsafeRc<RefCell<T>>、あるいはインデックス(Arenaアロケータ)を使ったグラフ構造の工夫が必要です。

B. 永続化と安全性 (WAL)

KVSが落ちてもデータを失わないために、WAL (Write Ahead Log) の実装が必要です。

  • メモリMemTableに書く前に、必ずディスク上のログファイルに追記する。
  • 課題: ディスクへの fsync のタイミング。毎回の書き込みでやると遅くなるため、バッチ処理や io_uring (Linux) を使った非同期I/Oの活用がパフォーマンスの鍵になります。

2. インターフェース層KVSとSQLの接着剤

ここがこのアーキテクチャの肝です。リレーショナルなデータテーブル、行、列を、どうやってフラットなバイト列Key-Valueに落とし込むか

キーエンコーディング (Key Encoding)

SQLの SELECT * FROM users WHERE id > 100 のような範囲検索を高速に行うには、KVSのキーが辞書順でソートされている必要があります。

  • キーの設計例: 単純な連番ではなく、以下のようなバイナリ形式にする必要があります。 [TableID: 4byte] [IndexID: 4byte] [PrimaryKey: Variable]
  • 課題:
    • 可変長データ文字列などをキーに含める場合、ソート順を壊さないエンコーディングMemcomparable formatの実装が必要です。
    • Rustには byteorder クレートなどがありますが、カスタムシリアライザを書くことになるでしょう。

3. トランザクション制御 (MVCC)

「高パフォーマンス」かつ「SQL」となると、並行処理制御は避けて通れません。単純なロックMutexでは性能が出ないため、MVCC (Multi-Version Concurrency Control) が必須になります。

  • 実装イメージ: KVSのキーにタイムスタンプやバージョン番号を含めます。
    • Key: User:1 -> Value: {"name": "Alice"} (Version 10)
    • 読み込み時は「Version 10時点のスナップショット」を見る。
  • Rustでの課題: 古いバージョンガベージをいつ消すかVacuum/GCの管理。Rustのチャネルtokio::synccrossbeamを使った非同期なGCスレッドの設計が求められます。

4. SQLレイヤークエリエンジン

ここは「車輪の再発明」を避けるか、あえてするかで開発工数が劇的に変わります。

A. パーサーとプランナー

  • sqlparser-rs: SQLのパースAST構築には、デファクトスタンダードである sqlparser クレートを使うのが賢明です。
  • クエリオプティマイザ: ここが一番難しい部分です。「JOINの順序」や「インデックスを使うべきか全件走査すべきか」の判断ロジック。最初は「ルールベースRBO」で作り、余裕があれば「コストベースCBO」へ進化させるのが一般的です。

B. 実行モデル (Executor)

  • Volcano Model (Iterator): next() を呼ぶと1行返ってくる。実装は簡単だが、関数呼び出しのオーバーヘッドが大きい。
  • Vectorized Execution: 一度に数千行(バッチ)を処理する。Apache Arrow (Rust版) を内部データ形式として使うと、SIMD命令が効きやすく、爆速になります。Rustはここが得意です。

Rustのエコシステム活用案

ゼロから書く楽しみを損なわない範囲で、以下のクレートを調査・参考にすると良いでしょう。

コンポーネント 使うべき/参考にすべきクレート 備考
I/O tokio-uring / glommio Linuxのio_uringを活用してI/O待ちを極限まで減らす
SQL Parser sqlparser 自作するとSQL標準への追従だけで数年かかるため
データ形式 arrow-rs / parquet カラムナフォーマットやベクトル処理の参考に
KVエンジン sled / agatedb Rust製KVSのソースコードは宝の山

開発の第一歩としての提案

いきなりSQL層まで作ると挫折しやすいため、まずは**「KVS層だけで、リレーショナルなスキャンができるか」**を検証するプロトタイプ作成をお勧めします。

  1. BTreeMap (メモリオンリー) をバックエンドにする。
  2. その上に「タプルをバイナリキーに変換するエンコーダー」を作る。
  3. scan_prefix(TableID) でテーブル全件取得ができるか試す。

次のステップ: 具体的に、「LSM-TreeとB-Treeのどちらで実装するか」、あるいは**「MVCCトランザクションの設計」**について、詳細な実装戦略を掘り下げてみましょうか?


You Asked:

  • KVSを基盤とした分散型超高速DB   - FoundationDBのようなものを目指す     - Rustで書く     - 多分ストレージ部分にはRocksDBを使うこととなるだろう     - 良い感じのモジュール化により、柔軟な構成が取れるように目指す。       - Simple by default, Powerful when neededという感じ     - KVS部分の最終的な処理を変えることにより、強整合性と結果整合性を切り替えることが可能   - 想定用途     - S3的なオブジェクトストレージのための分散DB       - 超高速である必要がある。また、ちゃんとしたクエリができる必要もある(リストアップするようなクエリが必要になるだろう)。     - SNSのバックエンドDB       - まあSNSに限った話ではないかも知れない。超高速のDBとしてイベント等を処理できるようにしたい。   - EC2のようなものから、接続路もボトルネックが出ないように接続すべき     - APIの接続部がボトルネックにならないようにしたい。     - VM基盤は今作っているものがあるので、それと良い感じにメッシュで接続するなどしたい。

このような感じのものを作ろうとしています。全体のアーキテクチャを考えていきたいですが、どのようにするとよいでしょうか。

Gemini Responded:

FoundationDBFDBのアーキテクチャステートレスなトランザクション層とステートフルなストレージ層の分離を参考にしつつ、Rustで「強整合性と結果整合性を切り替え可能」かつ「VM基盤とメッシュ接続」という野心的な要件を満たすアーキテクチャを提案します。

「S3のメタデータ管理」と「SNSのイベント処理」は、実はアクセスパターンが異なります前者はRange Scan重視、後者はWrite Throughput重視。これを両立するためのモジュラーなアーキテクチャが必要です。

以下に、4つのレイヤーに分けた設計案を提示します。


全体アーキテクチャ図(概念)

graph TD
    Client[VM / Client App] -->|Mesh / Shared Mem| Proxy[Network Proxy / Sidecar]
    Proxy -->|Route Request| TLog[Transaction / Query Layer]
    
    subgraph "Control Plane (Placement Driver)"
        PD[Metadata & Shard Manager]
    end

    subgraph "Storage Layer (Distributed)"
        Node1[Storage Node 1]
        Node2[Storage Node 2]
        Node3[Storage Node 3]
    end

    TLog -->|gRPC / QUIC| Node1
    TLog -->|gRPC / QUIC| Node2
    PD -.->|Cluster Info| TLog
    PD -.->|Rebalancing| Node1

1. Storage Nodeデータの永続化と局所性

RocksDBをラップし、ネットワーク越しの読み書きに応答する最下層です。ここで「整合性モデル」の切り替えを吸収します。

モジュール設計: ConsensusTrait の導入

RustのTraitシステムを活用し、コンセンサスアルゴリズムをプラガブルにします。

  • Core Trait:
    trait ConsensusEngine {
        async fn propose(&self, batch: WriteBatch) -> Result<Version>;
        async fn read(&self, key: Key) -> Result<Value>;
    }
    
  • Mode A: Strong Consistency (S3 Metadata向け)
    • 実装: Multi-Raft (TiKV方式)。データを「Region例えば96MBごとの範囲」に分割し、RegionごとにRaftグループを作ります。
    • Rustライブラリ: raft-rs (TiKVで使用) や openraft
    • 特徴: 常に最新のデータを読める。Range Scanが得意。
  • Mode B: Eventual Consistency (SNS/ログ向け)
    • 実装: Consistent Hashing + Vector Clocks。リーダーレスで、任意のードに書き込み可能。
    • 特徴: 書き込みレイテンシが極小。一時的な不整合を許容。

RocksDBの活用

RocksDBは単なるエンジンとして使い、その上の**「キーのプレフィックス設計」**でマルチテナントやテーブル構造を表現します。


2. Transaction / Query Layer頭脳

FoundationDBの最大の特徴は、この層が**ステートレス(状態を持たない)**であることです。これにより、計算リソースCPUだけを個別にスケールアウトできます。

  • 役割:
    • SQLやKV操作のパース。
    • どのードShard/Regionにデータがあるかの解決Placement Driverへの問い合わせ
    • 分散トランザクション制御 (2PC / OOCC):
      • 強整合性モードでは、ここでタイムスタンプを取得し、衝突判定を行います。
  • S3リストアップへの対応:
    • S3の LIST 操作は重いため、単純なスキャンではなく、RocksDBの Iterator を活用し、**「Pagination付きのRange Scan」**として実装します。

3. Network & VM Mesh Integrationボトルネックの排除

ここがあなたの「VM基盤を作っている」という強みが活きる部分です。TCP/IPスタックのオーバーヘッドを極限まで削ります。

A. 接続方式: VM基盤との統合

VMEC2相当からDBへの接続を高速化するために、以下の3段階の接続方式を用意します。

  1. Standard (TCP/QUIC): 外部からの通常の接続。tokio + quinn (QUIC) で実装。
  2. Internal Mesh (eBPF/XDP): 同一データセンター内の通信。Linuxカーネルのネットワークスタックをバイパスし、パケット転送を高速化。
  3. Co-location (vhost-user / Shared Memory):
    • もしVMホストOS上でDBプロキシが動いているなら、VMゲストとホスト間で共有メモリを使って通信します。
    • Rustの vhost クレートなどを使い、virtio経由でゼロコピー通信を行えば、ネットワーク遅延はほぼゼロになります。

B. API Gateway

  • プロトコル: gRPC (Protobuf) を基本としつつ、パフォーマンスが必要な箇所には FlatBuffersrkyv (Rust向けのゼロコピーデシリアライゼーション) を使用。

4. S3 & SNS 向けのデータモデリング戦略

KVSの上に構築する場合、キーの設計がパフォーマンスの9割を決めます。

Case 1: S3 Compatible Object Storage (Metadata)

S3の「フォルダのような階層構造」をフラットなKVSで表現し、かつ高速にリストアップする必要があります。

  • Key Design: [BucketID] [0x00] [ParentPathHash] [0x00] [FileName]
  • 解説:
    • ParentPathHash を含めることで、特定のフォルダ直下のファイル一覧(lsコマンドが、KVS上の連続した領域へのシーケンシャルアクセスRange Scanになり、爆速化します。
    • FoundationDBのDirectory Layerの考え方が参考になります。

Case 2: SNS Backend (Timeline/Events)

  • Key Design: [UserID] [ReverseTimestamp] [EventID]
  • 解説:
    • タイムスタンプを逆順(最大値 - 現在時刻にすることで、最新の投稿を取得する際に「先頭から読むだけ」になり、RocksDBのキャッシュ効率が最大化されます。

開発ロードマップの提案

まず「Simple by default」を実現するための最小構成から始めるのが良いでしょう。

  1. Phase 1: "Local" Distributed DB
    • raft-rsrocksdb を組み合わせて、ローカルの複数プロセスで動く「RaftベースのKVS」を作る。
    • この時点で StorageTrait を定義し、APIを確定させる。
  2. Phase 2: Network & Protocol
    • tonic (gRPC) を導入し、クライアントからの Put, Get, Scan を実装。
  3. Phase 3: S3 Metadata Implementation
    • Phase 2のKVSを使って、S3の ListObjects 相当のロジックを実装し、ベンチマークを取る。
  4. Phase 4: Optimization (VM Mesh)
    • 自作VM基盤との接続部分共有メモリやカスタムプロトコルの実装。

次のステップ: 特にどの部分が一番の懸念点、あるいは興味深いポイントですか? 例えば、**「RustでのRaftの実装とステートマシンの設計」について詳しく掘り下げるか、「VMとDB間のゼロコピー通信vhost-user等の設計」**について議論しますか?


You Asked:

もう一つの案として、強整合性は完全に諦めてオブジェクトストレージ側に整合性チェックを作るという案もあります。どちらが良いか比較してみてください。

Gemini Responded:

「DB側で強整合性を捨て、アプリオブジェクトストレージ側で整合性を担保する」というアプローチは、Amazon Dynamoや初期のCassandra、Riakなどが採用した**「AP型可用性・分断耐性重視」**の設計思想です。

結論から言うと、S3のようなオブジェクトストレージを作る場合、**「メタデータ管理はDB側で強整合性を持たせ、実データBlobは結果整合性でも良い」**とするのが現代的な正解に近いと考えられます。すべてをアプリ側で担保するのは、S3互換としての正しさを保証する難易度が跳ね上がるからです。

両者のアプローチを比較・分析します。


比較A案「DBが強整合性を保証」 vs B案「アプリが整合性をチェック」

比較項目 A案DBが強整合性 (CP型) B案アプリがチェック (AP型)
代表例 FoundationDB, TiKV, CockroachDB DynamoDB, Cassandra, Riak
書き込み速度 中〜高 (Raft/Paxosの合意待ちが発生) 極めて高速 (合意不要、Quorumのみ)
読み込み速度 高速 (最新が確定している) (複数ノードから読み、アプリで比較・修復が必要)
アプリの実装 シンプル (DBを信じれば良い) 極めて複雑 (競合解決、リトライ、ベクタークロック管理が必要)
S3互換性 高い (現在のAWS S3は強整合性) 低い (Read-After-Writeの保証が困難)
スケーラビリティ シャード単位ではリニアだが、ホットスポットに弱い 理論上無限にスケールしやすい

[Image of CAP theorem consistency trade-offs]


なぜ B案アプリ側チェックが「S3用途」で茨の道なのか

一見、B案は高速で魅力的に見えますが、オブジェクトストレージ特にメタデータの実装において、以下の**「3つの罠」**が待ち受けています。

1. "Read-After-Write" の保証が極めて難しい

ユーザーが「ファイルをアップロード(Put)し、直後に一覧表示(List)する」という操作をしたとします。

  • B案AP型: 書き込みが全ードに行き渡る前にListが走ると、「ファイルが無い」と返してしまいます。これをアプリ側で防ぐには、「自分が書いた直後のデータは、必ず書き込んだードを見に行く」などの複雑なルーティング制御Sticky Session的なものが必要になります。
  • 現状: 2020年以降、本家AWS S3は強整合性になりました。これに慣れたユーザーやツールは、結果整合性のストレージを使うとエラーを起こす可能性があります。

2. CAS (Compare-And-Swap) が実装できない

「ファイルが存在しない場合のみ作成する」「バージョンがXならYに更新する」といった排他制御が必要です。

  • 罠: DB自体が強整合性アトミックなトランザクションを持っていない場合、アプリ側だけで厳密な排他制御を行うことは論理的に不可能です(分散ロックなどの別の協調システムが必要になり、結局それがボトルネックになる)。
  • 影響: 同じファイル名で同時にアップロードされた際、どちらが勝つか予測不能Last Write Winsになり、データロストのリスクがあります。

3. 「ディレクトリのリネーム/削除」の地獄

S3にはディレクトリはありませんが、擬似的にフォルダとして扱います。 「フォルダごと削除」する場合、内部的には数千〜数万のオブジェクトのメタデータを書き換える必要があります。

  • B案AP型: 途中で通信が切れたり競合したりすると、「一部のファイルだけ消えて、一部は残っているしかもゾンビデータ」という不整合な状態になり、それを直すための「リペアバッチ」を自作する羽目になります。これは最初のCassandraの例で挙げた「運用の苦痛」そのものです。

では、B案アプリ側チェックが輝く場所は

B案が完全にダメなわけではありません。**「実データBlob/Chunk「SNSのタイムライン」**には最適です。

  • Immutableなデータ: 一度書いたら書き換えないデータ画像の実体などは、競合が起きないため、B案の「投げっぱなし書き込み」で超高速に処理できます。
  • SNSの「いいね」やログ: 「いいね」の数が一時的にズレていても誰も困りません。ここではB案の書き込み性能が最大の武器になります。

推奨アーキテクチャ:ハイブリッド戦略

Rustで自作されるのであれば、どちらか一つに絞るのではなく、**「データの中身によって使い分ける」**のが最も賢い戦略です。

ご自身の「モジュール化」の構想を活かし、以下のように階層を分けます。

  1. メタデータ層 (Namespace/File Index) -> 強整合性モード (Raft)

    • ファイル名、サイズ、権限、ディレクトリ構造。
    • データ量は小さいので、Raftのオーバーヘッドは許容範囲。
    • これにより「S3互換の挙動」と「排他制御」をシンプルに実装できます。
  2. ブロックストレージ層 (Object Chunks) -> 結果整合性モード (Consistent Hashing + Replication)

    • ファイルのバイトデータそのもの。
    • Raftは使わず、Dynamoのように「N個のードに並列書き込みし、W個成功したらOK」とする。
    • これで帯域をフルに使った爆速アップロードが可能です。

結論

「メタデータ管理情報に関しては、DB側で強整合性を持つA案」方が、トータルの開発コストと運用コストは圧倒的に安くなります。

アプリ側で分散システムの整合性を担保するコードを書くのは、DBエンジンを自作するよりもデバッグが難しく、バグが出た時の被害甚大だからです。

次のステップ: この「メタデータは強整合性、データは結果整合性」という分離型アーキテクチャの具体的なデータフローPutリクエストが来た時、どうやって2つの層に書き分けるかを図解やシーケンスで整理してみましょうか


You Asked:

まあS3の部分はやろうと思えばできると思うので、とにかくKVSやDBの部分を具体的な仕様に詰めていきたいと思っています。

Gemini Responded:

承知しました。S3やSNSのバックエンドとして耐えうる、**「Rust製・RocksDBベース・強整合性/結果整合性切り替え可能」**な分散DBの具体的な仕様策定に入りましょう。

FoundationDBやTiKVのアーキテクチャをベースにしつつ、Rustのエコシステムを最大限に活かした設計案コードレベルに近い仕様を提案します。

プロジェクト名を仮に "RustDB" と呼びます。


RustDB Architecture Specification

全体を以下の4つのモジュールCrateに分割して開発することをお勧めします。

  1. rdb-storage: ローカルストレージエンジン (RocksDB wrapper)
  2. rdb-consensus: 分散合意・レプリケーション層 (Raft / Async Replication)
  3. rdb-meta: クラスタ管理・スケジューリング (Placement Driver)
  4. rdb-proxy: トランザクション・クエリ層 (Stateless)

1. rdb-storage: ストレージエンジン層

単一ノードでの読み書きを担当します。

  • Backend: rocksdb crate (Rust bindings for RocksDB)
  • Key Design:
    • Column Families (CF) の活用: データを論理的に分離し、物理的なIOを最適化します。
      • default: 実際のデータ (Value)
      • lock: 分散トランザクション用のロック情報
      • write: トランザクションのコミット情報MVCC用
      • raft: Raftのログ自体を保存

インターフェース定義 (Trait)

「強整合性」と「結果整合性」を切り替えるため、ストレージへの書き込みは直接行わず、以下のTraitを通します。

#[async_trait]
pub trait StorageEngine: Send + Sync {
    // スナップショット読み込み
    async fn get(&self, key: &[u8], version: u64) -> Result<Option<Vec<u8>>>;
    
    // スキャン (Iterator)
    async fn scan(&self, start: &[u8], end: &[u8], version: u64) -> Result<Vec<(Vec<u8>, Vec<u8>)>>;
    
    // アトミック書き込み (Batch)
    async fn write(&self, batch: WriteBatch) -> Result<()>;
}

2. rdb-consensus: 分散合意層 (The Core)

ここが最も重要です。データの断片Region/Shard 単位でコンセンサスグループを作ります。

  • 採用ライブラリ: raft-rs (TiKVが作っている、プロダクションレディな実装)
  • データの分割単位: Region
    • デフォルトサイズ: 96MB
    • 各Regionは [StartKey, EndKey) の範囲を担当。

整合性モードの切り替え仕様

設定ファイルやテーブル作成時の指定で、以下の2つのモードをRegion単位で適用します。

Mode A: Strong Consistency (Raft)

S3メタデータ用。

  • Write: Leaderードにリクエスト -> Log Replication -> Quorum達成 -> Apply -> Clientへ応答。
  • Read:
    • ReadIndex: Leaderが「自分がまだLeaderであること」を確認してから返す線形化可能性の保証
    • Lease Read: 時間ベースのリースを使って、Leader確認をスキップし高速化クロック同期依存

Mode B: High Throughput (Async Replication)

SNSタイムライン / Blobデータ用。

  • Write: * クライアントはハッシュリング上の担当ードPrimaryに書き込む。
    • Primaryはローカルに書いて即座にOKを返すAck=1
    • バックグラウンドでReplicaへ非同期転送。
  • Read:
    • 最新の書き込みが見えない可能性があるが、レイテンシはディスクI/Oのみに依存。

3. rdb-proxy: トランザクション層 (Transaction)

クライアントAPI Gatewayからのリクエストを受け、トランザクションを制御します。ここはステートレスです。

  • プロトコル: Google PercolatorモデルTiKVと同じを採用するのが、性能と実装難易度のバランスが良いです。
  • MVCC (Multi-Version Concurrency Control) の実装: Keyには常に8バイトのタイムスタンプが付与されます。

トランザクションフロー (S3 PutObjectの例)

  1. Start: Proxyが rdb-meta からタイムスタンプStartTSを取得。
  2. Prewrite: * 対象のKeyメタデータに対して、Primary Lockを書き込む。
    • データ本体も lock CF に仮書き込み。
    • 他のトランザクションとの衝突をチェック。
  3. Commit:
    • 全てのPrewriteが成功したら、コミットタイムスタンプCommitTSを取得。
    • Primary Lockを解除し、正式なデータとして write CF に記録。
  4. Cleanup:
    • 残りのLockを非同期で解除。

このロジックをRustのライブラリとして実装し、gRPCサーバーに組み込みます。


4. rdb-meta: メタデータ管理 (Placement Driver)

クラスタの脳です。ステートレスなProxyに対して「どのデータがどこにあるか」を教えます。

  • 役割:
    1. Timestamp Oracle (TSO): クラスタ全体で単調増加する一意なタイムスタンプ(論理時計 + 物理時計)を発行。これがないと分散トランザクションはできません。
    2. Region Scheduling: ード間の負荷分散。あるードが過負荷なら、RaftのLeaderを別のードに移動させたり、データを移動させたりする指令を出します。
  • 実装:
    • これ自体も高可用性が必要なため、etcd を組み込むか、自身をRaftグループ3ード構成などとして実装します。

技術スタックとライブラリ選定 (推奨)

ゼロから作る部分と、既存を使う部分のメリハリをつけます。

コンポーネント Rust Crate / 技術 理由
通信 (RPC) tonic (gRPC) 高速、型安全、エコシステム最強。
通信 (Internal) quinn (QUIC) ード間通信のレイテンシ削減。Head-of-Line blocking回避。
非同期ランタイム tokio 事実上の標準。io_uring対応(tokio-uring)も視野に。
ストレージ rocksdb 信頼と実績。自作は泥沼。
Raft raft-rs TiKVの実装。RustでRaftやるならこれ一択。
シリアライズ bincode or rkyv serde_jsonは遅い。内部通信はゼロコピーを目指すならrkyv
メトリクス metrics + Prometheus 運用に入ったらこれが全て。最初から入れておく。
エラー処理 thiserror + anyhow 鉄板構成。

開発のフェーズ分け提案

いきなり分散は難しいので、以下のステップで進めるのが現実的です。

  1. Phase 1: スタンドアロンなトランザクションKVS

    • Raftなし。単一のRocksDBプロセスに対し、PercolatorモデルのMVCCトランザクションPrewrite/Commitが動くことを確認する。
    • ゴール: 複数のクライアントから並列に書き込んでもACIDが守られること。
  2. Phase 2: Raftの組み込み (Multi-Raft)

    • データを複数のRegionに分割し、raft-rs を使って複製する。
    • ゴール: ードを1つkillしても、データが読める・書けること。
  3. Phase 3: PD (Placement Driver) の実装

    • Regionの分割Splitと結合Merge、移動Rebalanceの実装。
    • ゴール: ノードを追加したら自動的にデータが均されること。

まずは 「Phase 1: MVCCトランザクションの実装」 から設計詳細を詰めましょうか? それとも 「Phase 2: Raft周りの設計」 が気になりますか?


You Asked:

MVCCトランザクションについてまずは教えてください。

Gemini Responded:

MVCC (Multi-Version Concurrency Control: 多版型同時実行制御) は、**「データを上書きせず、新しいバージョンとして追記する」**というシンプルな原則に基づいています。これにより、「書き込み中のデータを読み取りたい」というリクエストが来ても、ロック待ちをさせずに「一つ前の確定したバージョン」を返すことができます。

今回は、分散KVSTiKVやCockroachDBでデファクトスタンダードとなっているGoogle Percolatorモデルをベースに、Rust + RocksDBで実装するための具体的な仕組みを解説します。


1. データの物理構造 (RocksDBの中身)

RocksDBは単なるKey-Valueストアですが、MVCCを実現するために3つのColumn Family (CF) を使い分けます。 ここが理解の肝です。

Column Family 役割 Keyの形式 Valueの中身
CF_DEFAULT 実データ置き場 [Key] + [Start_TS] ユーザーデータ (Raw Data)
CF_LOCK 仮押さえ (Lock) [Key] Lock情報 (Primary Keyの場所, Start_TS, TTL)
CF_WRITE コミット証書 [Key] + [Commit_TS] メタデータ (Start_TS, 種類: Put/Delete)

ポイント: RocksDBはKeyでソートされるため、TimestampをKeyの末尾にビッグエンディアン降順で付与することで、常に「最新のバージョン」が先頭に来るようにします。


2. 書き込みトランザクションの流れ (2-Phase Commit)

例として、Key="user:1"Value="Alice" を書き込むケースを考えます。

Step 0: 準備

トランザクション開始時、TSO (Timestamp Oracle) から Start_TS (例: 100) を取得します。

Step 1: Prewrite (仮書き込み)

まず、データの実体とロック情報を書き込みます。この時点ではまだ他の人からは見えません。

  1. CF_DEFAULT にデータを保存
    • Key: user:1 + 100 (Start_TS)
    • Value: "Alice"
  2. CF_LOCK にロックを置く
    • Key: user:1
    • Value: { type: Put, start_ts: 100, primary: "user:1" }
    • もしここで既に他人のロックがあれば、書き込みは失敗Conflictします。

Step 2: Commit (本書き込み)

全てのPrewriteが成功したら、TSOから Commit_TS (例: 110) を取得します。 これが「確定した」合図になります。

  1. CF_WRITE にコミット証書を書く
    • Key: user:1 + 110 (Commit_TS)
    • Value: { start_ts: 100, type: Put }
    • これがあることで、「110の時点でのデータの実体は、100で書かれたものだ」と紐付きます。
  2. CF_LOCK のロックを消す (Cleanup)
    • Key: user:1
    • Value: (削除)

3. 読み込みの流れ (Snapshot Read)

読み込みリクエストには、必ず Read_TS (例: 115) が指定されます。「時刻115時点の世界を見せてくれ」という意味です。

  1. ロックのチェック (CF_LOCK)
    • user:1 にロックが存在し、かつその start_ts が 115 より小さい場合、誰かが書き込み途中です。
    • → 基本はバックオフ(少し待つ)します。
  2. 最新バージョンの特定 (CF_WRITE)
    • CF_WRITE をスキャンし、Commit_TS が 115 以下の最新のレコードを探します。
    • 見つかったレコード: Key=user:1+110, Value={ start_ts: 100 }
  3. 実データの取得 (CF_DEFAULT)
    • CF_WRITE に書いてあった start_ts: 100 を使い、実体を取りに行きます。
    • CF_DEFAULT から Key=user:1+100 を Get します。
    • "Alice" を返却。

4. Rustでの実装イメージ

非常に単純化した実装コードです。

use rocksdb::{DB, WriteBatch, ColumnFamilyDescriptor};
use byteorder::{BigEndian, WriteBytesExt};

// Keyのエンコーディング (Key + Timestamp)
fn encode_key(key: &[u8], ts: u64) -> Vec<u8> {
    let mut encoded = Vec::with_capacity(key.len() + 8);
    encoded.extend_from_slice(key);
    // 降順にするためにビット反転などを入れるテクニックもあるが、ここでは単純に
    encoded.write_u64::<BigEndian>(u64::MAX - ts).unwrap(); 
    encoded
}

struct Transaction<'a> {
    db: &'a DB,
    start_ts: u64,
    mutations: Vec<(Vec<u8>, Vec<u8>)>, // Buffer
}

impl<'a> Transaction<'a> {
    // 1. Prewrite Phase
    fn prewrite(&self) -> Result<(), Error> {
        let mut batch = WriteBatch::default();
        
        for (key, value) in &self.mutations {
            // Check Lock (他人のロックがないか確認)
            let lock_cf = self.db.cf_handle("lock").unwrap();
            if self.db.get_cf(lock_cf, key)?.is_some() {
                return Err(Error::Locked);
            }
            
            // Put Data (CF_DEFAULT)
            let default_cf = self.db.cf_handle("default").unwrap();
            batch.put_cf(default_cf, encode_key(key, self.start_ts), value);
            
            // Put Lock (CF_LOCK)
            batch.put_cf(lock_cf, key, self.start_ts.to_be_bytes()); // 簡易的なLock情報
        }
        
        self.db.write(batch)?;
        Ok(())
    }

    // 2. Commit Phase
    fn commit(&self, commit_ts: u64) -> Result<(), Error> {
        let mut batch = WriteBatch::default();
        
        for (key, _) in &self.mutations {
            // Write Commit Record (CF_WRITE)
            let write_cf = self.db.cf_handle("write").unwrap();
            // Valueとして「実データはstart_tsにあるよ」というポインタを入れる
            batch.put_cf(write_cf, encode_key(key, commit_ts), self.start_ts.to_be_bytes());
            
            // Delete Lock (CF_LOCK)
            let lock_cf = self.db.cf_handle("lock").unwrap();
            batch.delete_cf(lock_cf, key);
        }
        
        self.db.write(batch)?;
        Ok(())
    }
}

この設計のメリット・デメリット

  • メリット:
    • Wait-free Reads: 読み込みはロックを取らないため、書き込み負荷が高くても読み込み速度が落ちません。
    • Snapshot Isolation: トランザクション開始時点の整合性が保証されます。
  • デメリット:
    • 書き込み増幅: 1回の論理的な書き込みに対して、RocksDBへは最低でも2回PrewriteとCommitの書き込みが発生します。
    • GCが必要: 古いバージョン(CF_WRITECF_DEFAULTのゴミが残り続けるため、定期的に古いタイムスタンプのデータを削除するガベージコレクションCompaction Filterの実装が必要です。

次のステップ: このMVCCの仕組みをベースに、**「分散環境で複数のードにまたがるトランザクションPrimary Lockの概念について説明しましょうか? それとも、「GCやコンパクションをRustでどう実装するか」**へ進みますか?


You Asked:

このMVCCトランザクションは、結果整合性が必要な場合も、強整合性が必要な場合も同じく必要なのでしょうか

Gemini Responded:

結論から申し上げますと、いいえ、結果整合性のモードではこの重厚なMVCCトランザクション特にロックと2フェーズコミットは「不要」であり、むしろ「邪魔」になります。

高パフォーマンスを目指す今回のDBにおいて、ここは明確に分岐させるべきポイントです。

なぜなら、MVCCトランザクションは**「安全性ACIDを買うために「性能(レイテンシとスループット)」**を犠牲にする仕組みだからです。結果整合性SNSの投稿やBlobデータの書き込みで求められるのは、安全性よりも圧倒的な書き込み速度です。

具体的な違いを整理します。


モード別の書き込みパスの違い

1. 強整合性モード (S3メタデータ用)

→ MVCC + 分散トランザクション (必須)

先ほど解説した仕組みです。

  • 目的: 「誰かが書き込んでいる最中は読み込ませない」「途中で失敗したらロールバックする」。
  • コスト: 高い。
    • 最低2回のネットワーク往復Prewrite, Commit
    • 最低3回のRocksDB書き込みLock, Data, Write
    • ロック競合時の待ち時間。

2. 結果整合性モード (SNS/Blobデータ用)

→ Raw Write + LWW (Last Write Wins)

こちらが、あなたが求めている「超高速」な世界です。

  • 目的: とにかく早くディスクに永続化し、クライアントに「OK」を返す。
  • 仕組み:
    • ロックCF_LOCKも、コミット証書CF_WRITEも使いません。
    • CF_DEFAULT に直接、一発書き込みます。
  • コスト: 極小。
    • 1回のネットワーク往復。
    • 1回のRocksDB書き込み。

結果整合性モードでの「整合性」の保ち方

MVCCを使わずにどうやって「データの新しさ」を管理するかというと、単純なLWWLast Write Wins後勝ち戦略、あるいはVector Clockを使います。

RocksDBでの実装イメージ (結果整合性版)

MVCCのような複雑なKeyエンコーディングはせず、単純にタイムスタンプを埋め込むだけで十分です。

// 結果整合性モードの書き込み処理
async fn raw_put(&self, key: &[u8], value: &[u8]) -> Result<()> {
    // 1. タイムスタンプを取得 (サーバー時刻でOK、厳密なTSOは不要)
    let ts = current_server_time(); 

    // 2. 既存データより新しいかチェック (Read-modify-writeになってしまうが、Blobなら追記のみ等の最適化が可能)
    // ※SNSのような追記型ならこのチェックすら不要で、単に新しいKey(UUID等)でInsertするだけ
    
    // 3. RocksDBに直接書き込み
    // Key: [User:1][Timeline][TS]  <- TSを含めることでソートされる
    // Value: { body: "Hello World" }
    self.db.put(encode_key(key, ts), value)?;
    
    Ok(())
}

アーキテクチャへの反映:パスの分離

RustのTraitシステムを使って、この2つのパスをきれいに切り替える設計をお勧めします。

// エンジンが持つべき機能の抽象化
#[async_trait]
pub trait StorageEngine {
    // 共通: 読み込み
    async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>>;
}

// 強整合性が必要な場面S3メタデータ
pub struct TransactionalEngine {
    db: Arc<DB>,
}

impl TransactionalEngine {
    // 重厚なトランザクションを開始する
    pub async fn begin_transaction(&self) -> Transaction {
        Transaction::new(&self.db)
    }
}

// 結果整合性でいい場面SNS/Blob
pub struct RawEngine {
    db: Arc<DB>,
}

impl RawEngine {
    // いきなり書き込む (Atomic性は単一Keyのみ保証)
    pub async fn put_fast(&self, key: &[u8], value: &[u8]) -> Result<()> {
        // ロックなし、検証なしの直接書き込み
        self.db.put(key, value)
    }
}

どちらを選ぶかの判断基準

機能 MVCC (強整合性) Raw Write (結果整合性)
Atomic更新 複数の行を同時にアトミック更新できる 単一行(キー)のみアトミック
衝突検知 先に書いた人が勝つ(後からはエラー) 後で書いた人が勝つ(上書き)
読み込み 常に確定した最新データを保証 複製遅延により古いデータが見える可能性あり
用途 ユーザー残高、ファイル一覧、権限管理 画像データ、ログ、SNSタイムライン、「いいね」数

結論

S3的なオブジェクトストレージを作る場合、以下のように完全に使い分けます。

  1. Buckets / Objects Table (メタデータ):
    • MVCC必須。 ディレクトリ構造の一貫性を保つため。
  2. Object Blocks (データ実体):
    • MVCC不要。 ここにトランザクションを使うと遅すぎて使い物になりません。
    • データブロックは Immutable不変として扱い、「同じキーハッシュなら中身は同じ」と割り切るか、Raw Writeでガンガン書き込みます。

次のステップ: トランザクションの仕組みの違いが整理できたところで、次は**「この2つのモードを支える、分散合意Raftと非同期レプリケーションの切り替え」**という、ネットワーク層の話に進みましょうか?


You Asked:

ええ、そこを良く考えたいですね。

Gemini Responded:

「強整合性Raft」と「結果整合性非同期レプリケーション」を、同一のクラスタ、同一のバイナリ内で共存させるための、ネットワークとレプリケーション層の設計を掘り下げます。

これを実現する鍵は、「Multi-Raft」アーキテクチャを拡張し、「Regionデータの断片」ごとにレプリケーション戦略をPluggableにすることです。


1. 基本単位「Region」の抽象化

TiKVやCockroachDBと同様、データを巨大な1枚岩ではなく、96MB〜数GB程度の**「Regionシャード」**に分割します。 このRegionが、レプリケーションの最小単位になります。

Rustのコードレベルでは、Regionは以下のような Enum で表現され、それぞれ異なる「ステートマシン」を持ちます。

enum RegionReplicationStrategy {
    // Mode A: 強整合性 (S3メタデータ用)
    RaftGroup {
        leader_id: NodeId,
        peers: Vec<NodeId>,
        raft_log: RaftLog, // raft-rs state
    },
    // Mode B: 結果整合性 (Blob/SNS用)
    AsyncPrimaryBackup {
        primary_node: NodeId,
        backup_nodes: Vec<NodeId>,
        pending_queue: VecDeque<WriteOp>, // 未送信データ
    },
}

この設計により、**「あるテーブルキー範囲はRaftでガチガチに守る」「あるテーブルは非同期で爆速で流す」**という混在が可能になります。


2. Mode A: 強整合性 (Multi-Raft) の設計

メタデータ管理用です。ここは raft-rs を素直に使いますが、パフォーマンスのために一工夫必要です。

  • 仕組み:
    • 1つのード上で、数百〜数千のRaftグループRegionが同時に動きます。
    • 各RegionのLeaderだけが書き込みを受け付けます。
  • ネットワークの課題:
    • 単純に実装すると、RaftのHeartbeatだけでネットワークがパンクします。
  • 解決策: "Batching & Pipelining"
    • メッセージの集約: 異なるRegion宛てのRaftメッセージVote, AppendEntriesを、宛先ードごとに1つのTCPパケットにまとめて送信します。
    • これにより、システムコールとネットワークオーバーヘッドを劇的に減らします。

3. Mode B: 結果整合性 (Chain / Async Replication) の設計

ここがあなたのDBの「超高速」を実現する部分です。Raftの合意プロセス過半数のAck待ちをスキップします。

戦略: Primary-Backup with Hinted Handoff

S3のBlobデータやSNSログ向けに、**「W=1 (Write 1)」**の設定を可能にします。

  1. 書き込みフロー (Put):

    • クライアントは、Consistent Hashingで計算された Primary Node にデータを送ります。
    • Primary Nodeは、ディスクWAL/MemTableに書いた瞬間、即座に「OK」を返します(これが爆速の理由)。
    • 裏側で: Primary Nodeは、Backup Node次のハッシュ担当ード2つなどへ、非同期でデータを転送します。
  2. 障害時の対応 (Hinted Handoff):

    • もしBackup Nodeがダウンしていたら
    • Primaryは「あいつが戻ってきたら渡すデータ」として、ローカルに一時保存Hintしておきます。
    • 復旧を検知したら、まとめて転送します。
    • CassandraやRiakで使われている手法です

4. ネットワークトポロジーとプロトコル

「VM基盤とメッシュ接続」という要件を活かし、通信レイヤーを最適化します。

Unified Transport Layer (gRPC vs QUIC)

全ての通信Raftの投票も、Blobの転送もを一本のパイプに通します。

  • 採用技術: QUIC (quinn crate) を強く推奨します。
    • 理由1: 多重化 (Multiplexing)
      • Raftの通信と、巨大なBlobの転送が1つのコネクション内で混在しても、BlobのせいでRaftのHeartbeatが遅延するHead-of-Line Blockingのを防げます。これだけでクラスタの安定性が段違いになります。
    • 理由2: 接続維持
      • VMの再起動やネットワークの瞬断に強いです。

ゼロコピー転送の設計 (Rust特有)

Blobデータ数MB〜数GBを転送する際、シリアライズ/デシリアライズのコストがCPUを食いつぶします。

  • Raftメッセージ: Protobuf等でしっかり構造化。
  • Blobデータ: **「ヘッダだけ構造化し、ボディは生バイト列」**として扱います。
    • Rustの Bytes クレートを使い、ネットワークバッファからディスク書き込みまで、メモリコピーなしZero-Copyでスルーパスさせます。
// ネットワークメッセージのイメージ
struct NetworkPacket {
    header: Header, // RegionID, MessageType (Raft or Blob), etc.
    payload: Bytes, // 生データ(ここをコピーしない!)
}

5. 全体のデータフロー図解

sequenceDiagram
    participant Client
    participant Node A (Primary/Leader)
    participant Node B (Follower/Backup)
    participant Node C (Follower/Backup)

    Note over Client, Node C: Mode A: 強整合性 (Raft)
    Client->>Node A: Put(Meta)
    Node A->>Node B: AppendEntries
    Node A->>Node C: AppendEntries
    Node B-->>Node A: Ack
    Node C-->>Node A: Ack
    Note right of Node A: 過半数のAckでCommit
    Node A->>Client: OK

    Note over Client, Node C: Mode B: 結果整合性 (W=1)
    Client->>Node A: Put(Blob)
    Node A->>Disk: Write WAL
    Node A->>Client: OK (即答)
    par Async Replication
        Node A->>Node B: Stream Data
        Node A->>Node C: Stream Data
    end

開発の落とし穴と対策

  1. 「スプリットブレイン」の扱い:

    • Mode A (Raft) では起きませんが、Mode Bでは「ネットワーク分断時に、別々のードで同じキーに書き込まれる」ことが起きます。
    • 対策: User Contextにもある通り、SNSなどの用途なら**「LWW (Last Write Wins: タイムスタンプが新しい方を正とする)」**で割り切るのが、実装コスト対効果で最強です。Vector Clockは実装が複雑になりすぎる傾向があります。
  2. 帯域制御 (Rate Limiting):

    • Mode Bのバックグラウンド転送がネットワーク帯域を使い切ると、Mode AのRaftがタイムアウトしてリーダー交代が頻発します。
    • 対策: トークンバケットアルゴリズム(governor crate等を導入し、**「バックグラウンド転送は帯域の70%まで」**といった制限をかけられるようにします。

次のステップ: ネットワーク層の戦略が見えてきました。次は、これを制御するための頭脳である**「Placement Driver (PD)」、つまり「どのデータを、どのードRegionに配置し、いつ分割するか」を決めるロジック**について設計しますか?


You Asked:

はい。お願いします。

Gemini Responded:

Placement Driver (PD) は、クラスタ全体の「CEO最高経営責任者」です。データードStorage Nodeは現場の作業員であり、PDの指示に従ってデータを移動したり、リーダーを交代したりします。

FoundationDBやTiKVにおいて、このPDの賢さがデータベース全体の性能と安定性を決定づけます。

今回の「ハイブリッド整合性S3メタデータ + Blob」という特殊要件を満たすための、RustによるPDの設計仕様を提案します。


PDの主要な役割

PDには大きく分けて3つの仕事があります。

  1. Timestamp Oracle (TSO):ードに一意で単調増加する時刻を配るMVCC用
  2. Cluster Topology: どのードが生きていて、どのRegionデータ断片を持っているかを把握する。
  3. Scheduling: 「このノードはもうディスクがいっぱいだから、データをあっちへ動かせ」という命令を出す。

1. Timestamp Oracle (TSO) の設計

MVCCトランザクション強整合性モードでは、このTSOの性能が書き込み性能の上限ボトルネックになり得ます。

  • アルゴリズム: ハイブリッド論理時計 (Hybrid Logical Clock)
    • 物理時刻(ミリ秒)の下位ビットに、論理カウンタ(シーケンス番号)を足した u64 を発行します。
    • [ Physical Time (48bit) ] + [ Logical Counter (16bit) ]
  • Rustでの実装:
    • PDのリーダーだけがメモリ上で AtomicU64 をインクリメントして返します。
    • 最適化: クライアントProxyは毎回PDに問い合わせるのではなく、「今後3秒分のタイムスタンプ」をバッチで予約して持っていくようなキャッシュ戦略をとることも可能です(厳密性が少し緩むため、通常はバッチリクエストで対応します)。

2. ハートビートとメタデータ収集

PDは能動的に監視に行くのではなく、**データードからの定期報告Heartbeat**を受け取って状態を更新します。

A. StoreHeartbeat (ノード単位の報告: 10秒毎)

  • 「私は生きています」
  • Payload: ディスク使用率、CPU負荷、ネットワーク帯域使用量。
  • PDの判断: 一定期間来なければ「ダウン」と判定し、そのードにあるデータの再複製Repairを開始します。

B. RegionHeartbeat (データ断片単位の報告: 60秒毎 or 変化時)

  • 「私の担当するRegion Xの状態です」
  • Payload:
    • Leaderは誰か Peer複製先は誰か
    • データサイズMB、書き込みキー数。
    • 現在のモード: Mode::Strong (Raft) か Mode::Async (Blob) か。
  • 重要: ここで「モード」を報告させることで、PDはスケジューリング戦略を切り替えます。

3. スケジューリング・ロジック (The Brain)

ここが腕の見せ所です。Rustの構造体で「スケジューラ」を定義し、プラグイン可能な設計にします。

戦略A: S3メタデータ用 (Raft Regions)

CPU負荷の分散を最優先します。

  • Leader Balance:ードで「Raftのリーダー」の数が均等になるようにします。リーダーには書き込みと読み込みReadIndexが集中するためです。
  • Region Merge: メタデータ削除等で空になったRegionを見つけ、隣接するRegionと結合させてオーバーヘッドを減らします。

戦略B: Blob/SNSデータ用 (Async Regions)

ディスク容量とI/O帯域の分散を最優先します。

  • Disk Usage Balance: 「ディスク使用率が80%を超えたード」から、「20%のード」へ、巨大なBlobデータを強制移動Moveさせます。
  • Cold Data Rebalancing: アクセス頻度の低い古いBlobデータを、安価なHDDード群へ移動させるティアリングロジックもここに組み込めます。

Rustでの実装イメージ

trait Scheduler {
    fn schedule(&self, cluster: &ClusterInfo) -> Option<Operator>;
}

struct DiskBalanceScheduler;
impl Scheduler for DiskBalanceScheduler {
    fn schedule(&self, cluster: &ClusterInfo) -> Option<Operator> {
        // 1. 最もディスクを使っているノードを探す
        let source_node = cluster.get_max_disk_usage_node();
        // 2. 最も空いているノードを探す
        let target_node = cluster.get_min_disk_usage_node();
        
        if source_node.usage - target_node.usage > THRESHOLD {
             // 3. 移動命令(Operator)を生成
             return Some(Operator::MoveRegion { 
                 region_id: select_region(source_node),
                 from: source_node.id, 
                 to: target_node.id 
             });
        }
        None
    }
}

4. PD自体の高可用性 (HA)

PDが死ぬとシステム全体が停止するため、PD自体も3ードでクラスタを組みます。

  • メタデータの保存:
    • ここでも sled (Rust製組み込みDB) や rocksdb を使い、その上に raft-rs を乗せて、PD自身のメタデータRegionの位置情報などを複製します。
    • etcd を外部プロセスとして使うのが楽ですが、**「Rust製の単一バイナリ」**にこだわるなら、PD内部に「メタデータ専用のRaftグループ」を持つのが美しい設計です。

5. VMメッシュ接続との連携 (Topology Awareness)

ユーザーコンテキストにある「VM基盤とのメッシュ接続」を活かすため、PDに物理トポロジーを理解させます。

  • Labels (ラベル機能):
    • 各ノード起動時に zone=us-east-1a, rack=rack-1, host=vm-host-5 といったラベルを付与してPDに報告させます。
  • Isolation Rule (隔離ルール):
    • 「同じ host ラベルを持つードには、絶対に同じRegionのレプリカを置かない」という制約をスケジューラに追加します。
    • これにより、物理ホストが1台死んでも、その上で動いている複数のVMDBードが全滅しても、データロストを防げます。

開発のステップ

PDは複雑になりがちなので、以下の順序で作るのが現実的です。

  1. Mock PD:
    • 固定のタイムスタンプを返し、スケジューリングを一切しない「ダミーPD」を作る。これを使ってデータードStorageの開発を進める。
  2. Basic TSO & ID Allocator:
    • 正しいタイムスタンプと、ユニークなIDRegionID, NodeIDを発行する機能だけ実装する。
  3. Minimal Membership:
    • ードのJoinを受け付け、リストとして保持する機能。
  4. Simple Scheduling:
    • 「ディスクがいっぱいになったら移動」というルールを1つだけ実装する。

次のステップ: これで「脳PD」と「体Storage」と「神経Network」の設計が出揃いました。 次は、これらを動かすための**「クライアントSDKRust製ライブラリ、つまりアプリ開発者が最初に触る部分のインターフェースを設計しませんか? それとも「S3互換APIレイヤー」**の具体的な実装に入りますか?


You Asked:

クライアントSDKをどうすべきかは気になりますね。

Gemini Responded:

クライアントSDKは、開発者あなた自身も含めがこのデータベースを「使いやすい」と感じるか、「使いにくい」と感じるかを決める最重要パーツです。また、パフォーマンスの観点では、SDKが賢く振る舞うことでサーバー側の負荷を劇的に下げることができます。

Rustで実装する**「RustDB Client SDK」の設計案を提案します。 最大のポイントは、「強整合性Transaction」と「結果整合性Raw」を、型システムを使って安全かつ明快に使い分ける**点です。


1. SDKの全体像Smart Clientアーキテクチャ

単にリクエストを投げるだけでなく、クライアント側で「データの場所Region Cache」を保持する**Smart Clientシック・クライアント**として設計します。

これにより、毎回プロキシやPDPlacement Driverに問い合わせるオーバーヘッドをゼロにし、**アプリからデータノードへの「直結」**を実現します。

2. インターフェース設計 (Rustコード案)

開発者が触れるAPIのイメージです。tokio ベースの非同期設計です。

A. 接続とクライアント生成

use rustdb::Client;

// PD管理ードのアドレスを指定して接続
let client = Client::builder()
    .pd_endpoints(vec!["10.0.0.1:2379", "10.0.0.2:2379"])
    .connect()
    .await?;

B. 強整合性モード (S3メタデータ / ユーザー管理)

ACIDトランザクションを提供します。クロージャを使うことで、**「衝突時の自動リトライ」**をSDK内部で隠蔽するのがモダンな設計です。

// キー "bucket:images" のメタデータを更新する例
client.transaction(|txn| async move {
    // 1. 読み込み (Snapshot Read)
    let key = "bucket:images";
    let meta_bytes = txn.get(key).await?;
    
    // 2. ロジック処理 (デシリアライズして変更)
    let mut meta: BucketMeta = bincode::deserialize(&meta_bytes)?;
    meta.object_count += 1;
    
    // 3. 書き込みバッファへ (まだ送信されない)
    txn.put(key, bincode::serialize(&meta)?).await;
    
    // 4. コミット (ここで初めて2PCが走り、衝突なら自動リトライ)
    Ok(())
}).await?;

C. 結果整合性モード (SNSタイムライン / Blobデータ)

トランザクションオブジェクトを作らず、クライアントから直接「投げっぱなし」にします。

// 高速書き込み (Write 1)
// 内部ではハッシュ計算 -> Primaryード特定 -> QUICで送信 -> 即座に完了
client.raw_put("timeline:user:123", event_data).await?;

// 高速読み込み (Read Any)
// 最新である保証はないが、最もレイテンシの低いノードから読む
let data = client.raw_get("timeline:user:123").await?;

3. SDK内部の重要機能

SDKの中身src/lib.rs)で実装すべき、パフォーマンスの肝となる機能です。

① Region Cache地図のキャッシュ

これが「超高速」の鍵です。

  • 仕組み:
    • クライアントは「Key user:auser:zNode-1 にある」という地図Region Mapをメモリに持っておきます。
    • client.get("user:k") が呼ばれると、PDに聞かずに直接 Node-1 へ通信します。
  • 無効化Invalidation:
    • もし Node-1 から「そのRegionはもう持ってないよNotLeader」とエラーが返ってきたら、そのキャッシュを捨ててPDに問い合わせ直します。

② Backoff & Retry賢いリトライ

分散システムでは「一時的なネットワーク断」や「リーダー選出中」のエラーが日常的に起きます。

  • 単純な loop ではなく、Exponential Backoff指数関数的バックオフJitterゆらぎ を入れたリトライロジックを標準装備します。これをアプリ開発者に書かせてはいけません。

③ Connection Pooling (gRPC / QUIC)

  • tonic (gRPC) や quinn (QUIC) のチャンネル(コネクション)を内部でプールし、使い回します。
  • TCPハンドシェイクのコストを削減するため、KeepAlive設定も重要です。

4. SQLレイヤーとの統合イメージ

「SQLのような高度なクエリ」を乗せる場合、このSDKはどう使われるでしょうか SQLエンジンプランナーがこのSDKの利用者になります。

// SQL: SELECT * FROM users WHERE id > 100 LIMIT 10

// SQLエンジンの内部処理
async fn execute_select(&self, plan: Plan) -> Result<Vec<Row>> {
    // 1. スキャンの範囲を決定
    let start_key = encode_key(plan.table_id, 100);
    let end_key = encode_key(plan.table_id, MAX);

    // 2. SDKのScan機能を使ってイテレータを取得
    // SDKは内部で複数のRegionに並列アクセスし、結果をストリームで返す
    let mut stream = self.client.scan(start_key, end_key).await?;

    // 3. フィルタリングとリミット処理
    let mut rows = Vec::new();
    while let Some(kv) = stream.next().await {
        if rows.len() >= 10 { break; }
        rows.push(decode_row(kv));
    }
    Ok(rows)
}

5. 開発の進め方へのアドバイス

SDKをリッチにしすぎると開発が終わらないので、フェーズを分けます。

  1. Phase 1: 「薄い」クライアント
    • 毎回PDに問い合わせる、キャッシュなしの実装。まずは動くことを目指す。
    • APItransaction, raw_put)の形だけ確定させる。
  2. Phase 2: キャッシュの実装
    • HashMap<Range, NodeId> を導入し、PDへのアクセスを減らす。
  3. Phase 3: 非同期ストリーミングスキャン
    • Stream トレイトを実装し、巨大なリストS3のListObjectsをメモリを圧迫せずに返せるようにする。

次のステップ: クライアントのイメージが固まったことで、システム全体の役者が揃いました。 ここまでの議論ストレージ、分散合意、PD、SDKを踏まえて、**「まず最初に書き始めるべき Cargo.toml の構成(ワークスペース構成)」や、「プロトタイプ第1弾として実装すべき最小機能セット」**の定義に進みますか?


You Asked:

このクライアントについてちょっとまだイメージできていないのですが、これが賢い制御を全てやるという認識なのですかSQLのようなクエリに互換させるために、普通にSQLと互換性のあるクエリが飛んでくるとそれを返す部分が別にあると思っていました。

Gemini Responded:

そのご認識の通りです私の説明が少し「SDK」の機能に寄りすぎていました。申し訳ありません。

「賢い制御をやるSDK」は、SQLサーバーの中に「埋め込まれる部品ライブラリ という位置付けになります。

SQLを解釈して返す部分は、「SQLゲートウェイまたはSQLレイヤー という別のサーバープロセス(あるいはモジュール)が存在します。

全体像を整理し直すと、以下の図のような3層構造になります。


正しいアーキテクチャ図

graph TD
    subgraph "External World"
        App1[Web App / CLI]
        App2[BI Tool]
    end

    subgraph "Layer 1: Interface Gateways (あなたが想定している部分)"
        SQL_GW[SQL Gateway Node]
        S3_GW[S3 Compatible Gateway]
    end

    subgraph "Layer 2: RustDB Client SDK (賢い部品)"
        SDK1[SDK (Embedded in SQL GW)]
        SDK2[SDK (Embedded in S3 GW)]
    end

    subgraph "Layer 3: Distributed Storage Cluster"
        Node1[Storage Node 1]
        Node2[Storage Node 2]
        Node3[Storage Node 3]
        PD[Placement Driver]
    end

    App1 -- "SQL (Port 5432)" --> SQL_GW
    App2 -- "S3 API (HTTP)" --> S3_GW
    
    SQL_GW -- "Parse & Plan" --> SDK1
    S3_GW -- "Parse XML/JSON" --> SDK2
    
    SDK1 -- "Get/Put (KV)" --> Node1
    SDK1 -- "Get/Put (KV)" --> Node2
    SDK2 -- "Get/Put (KV)" --> Node3
    
    SDK1 -.-> PD

各レイヤーの役割分担

ここで、それぞれの役割を明確にします。

1. Layer 1: SQL GatewaySQL互換レイヤー

ここが、あなたが仰っていた**「SQLのようなクエリが飛んでくるとそれを返す部分」**です。

  • 役割:
    • プロトコル受信: PostgreSQL (pgwire) や MySQL のプロトコルで通信を受け付けます。
    • SQLパース: SELECT * FROM users WHERE id = 1 という文字列を分解します。
    • クエリプランニング: 「これはインデックスを使ったほうが速いな」と考え、実行計画を立てます。
    • 実行 (Executor): 実際にデータを取るために、Layer 2 の SDK を呼び出します
  • ここで行うこと: 「Key-Valueへの変換」です。「UsersテーブルのID=1」を、「Key: t_users_1 を取ってこい」という命令に変換して、SDKに渡します。

2. Layer 2: Client SDK賢い制御担当

ここは、SQLゲートウェイのプロセス内部で動くライブラリです。

  • 役割:
    • ルーティング: SQL層から「Key: t_users_1 をくれ」と言われた時、「そのキーは今、Storage Node 3にある」と即座に判断しますキャッシュ活用
    • 通信の最適化: 適切なードへ、gRPCやQUICでリクエストを飛ばします。
    • リトライ・リカバリ: 通信エラーが起きても、SQL層にエラーを返す前に、こっそり別のードへリトライして成功させます。
  • なぜここが「賢い」必要があるか: SQL層の開発者が、「分散システムの複雑さどのデータがどこにあるか、誰がリーダーか」を気にせず、**「単なるローカルKVSを使っている感覚」**で実装できるようにするためです。

3. Layer 3: Storage Nodes

  • 役割: 言われたKey-Valueをディスクに読み書きするだけです。

なぜこの構成にするのかS3とSQLの共存

この構成の最大のメリットは、S3互換ゲートウェイも同じSDKを使って作れるという点です。

  • SQLゲートウェイ: SQLをパースして、SDKを叩く。
  • S3ゲートウェイ: HTTP/XMLをパースして、SDKを叩く。
  • SNSバックエンド: アプリから直接 SDK を叩く(最速)。

このように、「分散DBの難しい部分」をSDKに封じ込めることで、その上に色々なインターフェースSQL, S3, Redis互換などを簡単に乗せられるようになります。これが冒頭で仰っていた「モジュール化」の真骨頂です。

結論

  • あなたの認識: 正しいです。「SQLを受け付けて返す別プロセス」が必要です。
  • SDKの正体: そのSQLプロセスが、裏側でデータを取りに行くために使う**「高性能なドライバー」**のことです。

この「SQLゲートウェイ」部分パーサーやプランナーの実装について、もう少し詳しく掘り下げますか それとも、まずは下の足場StorageとSDKを固める方針でしょうか


You Asked:

したの足場が、ゲートウェイ部分を作れるように強固に作られていれば大丈夫だと思っています。そういえば、劔という日本製のRDBMSがあります。書き込み、読み込みともにロックフリーで超高速ということを謳っていますが、これは一体どのような仕組みでなされているのでしょうかもしできれば取り込みたいところですただ、これ自体はJava製なので使いたくはない

https://www.tsurugidb.com/

Gemini Responded:

劔 (Tsurugi) に目をつけられるとは、非常にお目が高いです! 劔は、従来のDBPostgreSQLなどが抱えていた「コア数が増えても性能が上がらないスケーラビリティの限界」という課題を解決するために、日本のNEDOプロジェクトで開発された、まさに**「メニーコア時代のモンスターエンジン」**です。

その「ロックフリーで超高速」の正体は、魔法ではなく、**「Shirakami白神と呼ばれるトランザクションエンジンと、「Limestone」**と呼ばれるログ管理機構にあります。

今回のRustDBに取り入れられる要素は大いにあります。その仕組みを紐解き、RustDBへの移植案を提案します。


1. 劔の「速さ」の正体3つのコア技術

劔が「ロックフリー」と謳っているのは、**「読み込み時にロックを取らないSnapshot Isolationだけでなく、書き込み時にも「楽観的並行性制御 (OCC) の高度な最適化」**を行っているからです。

① "Epoch" ベースのメモリ管理とコミット

劔は時間を「エポック」という数ミリ秒単位の区切りで管理します。

  • 仕組み:
    • トランザクションID発行やガベージコレクションGCを、個別のロックではなく、この「エポック」の切り替わりでまとめて行います。
    • ログWALのディスク書き込みも、1件ずつ fsync するのではなく、**「エポック内のログをまとめてドンGroup Commit」**します。
  • RustDBへの応用:
    • これはRaftのログ書き込みに即応用できます。
    • リクエストが来るたびに fsync するのではなく、**「5ms待って、その間に来たリクエストをまとめて1回のRaftログとして書き込むBatching」**ことで、スループットが劇的に向上します。

② 高度な OCC (Optimistic Concurrency Control)

従来のDB2PL: Two-Phase Lockingは、「書き込む前にロックして、他の人を待たせる」方式でした。 劔ShirakamiOCCを採用しています。

  • 仕組み:
    1. Read/Compute: データを読み込み、メモリ上で計算する(この間、ロックは一切取らない)。
    2. **Validate:**コミット直前に「自分が読んだデータが、計算中に他の誰かに書き換えられていないか?」をチェックする。
    3. Write: 問題なければ一瞬で書き込む。ダメならリトライ。
  • 特徴: ロック待ちが発生しないため、CPUが常に全速力で動き続けられます。

③ "Short" と "Long" トランザクションの分離

これが劔の最大の発明です。OCCの弱点である「長いバッチ処理Long TXが、短い書き込みShort TXのせいで何度も失敗して飢える」という問題を解決しました。

  • 仕組み:
    • Long TX (バッチ): 開始時に「これからこのエリアを触るぞ」と宣言(予約)させます。
    • Short TX (Web): 予約エリアには触れません。
    • これにより、バッチ処理とWebリクエストが互いに邪魔せず、ロックフリーで共存します。

2. RustDB への取り込み案

劔は「インメモリ・シングルード」での最適化がメインですが、分散DBであるRustDBでも、そのトランザクション管理の思想は借用できます。

現在想定している Percolatorモデルロックあり劔モデルOCC を比較し、導入案を示します。

案A: S3メタデータディレクトリ構造への応用

S3のメタデータ操作PutObjectなどは、**「同じキーに対する競合は少ないが、スループットは極限まで欲しい」**という特性があります。 ここで、Percolatorの「ロックを書いて消す」コストは無駄になりがちです。

→ RustDB版 OCCの実装アプローチ

  1. Read Phase:
    • クライアントSDKはデータを読み込み、バージョンTSを覚える。ロックはしない。
  2. Buffer:
    • 変更内容をクライアント側のメモリに貯める。
  3. Commit Phase (Single CAS):
    • 書き込み時、サーバーStorage Nodeに対して**「Compare-And-Swap (CAS)」**命令を送る。
    • 命令: 「もし Key A のバージョンが X なら、Y に更新してくれ。違ったらエラーで返して」
    • RocksDBには MergeOperatorTransactionDB という機能があり、これをアトミックに行えます。

これなら、ロックファイルCF_LOCKを作る Prewrite フェーズを省略でき、**通信回数が半分1 RTT**になります。

案B: "Epoch" ベースの遅延永続化

結果整合性モードSNS等において、劔の Limestone (ログエンジン) の思想を取り入れます。

  • 実装:
    • rdb-storage 層に「書き込みバッファRing Buffer」を用意します。
    • put リクエストは、メモリ上のバッファに書いて即座に Ok を返します。
    • バックグラウンドのスレッドが、数ミリ秒ごとにバッファを切り替えEpoch Rotate、まとめてディスクに write & fsync します。
  • 効果:
    • I/O回数が 1/100 以下になり、SSDの寿命も延び、性能は爆上がりします。
    • (ただし、電源断で数ミリ秒のデータロストは許容する設定になります)

結論:何を取り入れるべきか?

Java実装そのものを組み込むのではなく、**「設計思想」**を以下のようにRustDBのコードに落とし込むのがベストです。

  1. S3メタデータ処理には OCC (Optimistic Concurrency Control) を採用する

    • Percolator悲観的ロックに近いではなく、CASベースのコミット処理にすることで、ロック管理のオーバーヘッドを消す。
    • 「競合したらSDKが自動でやり直す」という責務をクライアントSDKに持たせる。
  2. ログ書き込みに "Group Commit" を徹底する

    • Raftのログや、結果整合性モードの書き込みにおいて、リクエスト毎のI/Oを禁止し、必ずバッチ化する。

これで、RocksDBを使いながらも、劔が目指したような「ロック待ちの少ない、CPUキャッシュ効率の良い」DBに近づけることができます。

次のステップ: トランザクション方式として、より高速な 「OCC (Optimistic) パターンでのAPI設計」 に舵を切りますか? それとも、まずは堅実に 「Percolator (Pessimistic) パターン」 で作り、後からOCCを選べるようにしますかS3互換ならOCCの方が相性が良い可能性が高いです


You Asked:

OCCパターンにしたいところです。

Gemini Responded:

良い判断です。S3のような「オブジェクトストレージのメタデータ管理」において、OCC楽観的並行性制御は最強の選択肢です。

なぜなら、S3の操作PutObjectDeleteObject)は「特定のファイル」に対する操作であり、「同じファイル名のファイルを、1マイクロ秒の差で同時にアップロードし合う」ような激しい競合は、現実にはほぼ発生しないからです。

競合が起きないなら、ロック(事前の場所取り)はただの無駄です。 「劔」の思想を取り入れ、RustDBを**「CAS (Compare-And-Swap) ベースの超高速エンジン」**にするための設計仕様を提案します。


RustDB-OCC Architecture

これまでの重厚な Prewrite / Commit の2往復を廃止し、**「1往復 (1 RTT)」**でトランザクションを完了させます。

[Image of optimistic concurrency control transaction flow]

1. Core Concept: "CAS" over Raft

Raftのログとして流すコマンドを、単純な Put ではなく、条件付き書き込みである CompareAndSwap にします。

  • サーバー側のロジック: 「現在DBにある Key のバージョンが expected_ver と一致するなら、new_value に書き換える。違うならエラーを返す」 これだけです。非常にシンプルで、CPU命令のように高速です。

2. データ構造の簡素化 (RocksDB)

Percolatorモデルで必要だった CF_LOCK(ロック情報)は不要です。 データ本体とバージョンを管理するだけで済みます。

  • CF_DEFAULT (Data):
    • Key: [TableID] [Key_Bytes]
    • Value: [Metadata_Bytes] [Version(u64)]
    • ※以前のようにKeyにTimestampを含める必要すらありません最新だけ持つなら。履歴を持つなら含めますが、OCCなら「最新値」との比較がメインになります。

具体的な処理フロー (S3 PutObject の例)

ユーザーが bucket-a/image.jpg をアップロードする場合のSDKとサーバーの動きです。

Step 1: Read (SDK)

まず、現在の状態を確認します。

  • SDK -> Server: Get("bucket-a/image.jpg")
  • Server -> SDK:
    • データなし (None) の場合: Version = 0
    • データありの場合: Version = 105

Step 2: Local Compute (SDK)

SDK内部で書き込みデータを準備します。サーバーへの通信は発生しません。

  • 「新しいメタデータを作成。サイズは5MB...」

Step 3: CAS Commit (SDK -> Server)

SDKは、「私が読んだ時はバージョン0存在しないだったので、新規作成します」というリクエストを投げます。

// ネットワークリクエストのイメージ
struct CasRequest {
    key: Vec<u8>,
    expected_version: u64, // 0 なら「新規作成」を意味する
    new_value: Vec<u8>,
}

Step 4: Validation & Apply (Server / Raft)

Raftリーダーはこのリクエストを受け取ると、ログに複製し、各ードで適用Applyしようとします。

  • 成功ケース:

    • DB上の現在のバージョンが 0 である。
    • -> 書き込み実行。バージョンを 1 (またはTSOの時刻) に更新。
    • -> SDKに Ok を返す。
  • 失敗ケース (競合発生):

    • Step 1 と Step 3 の間に、別の誰かがアップロードを完了させており、バージョンが 108 になっていた。
    • -> 0 != 108 なので書き込み拒否。
    • -> SDKに Error::Conflict { current: 108 } を返す。

Step 5: Retry (SDK)

エラーを受け取ったSDKは、**「賢いクライアント」**の本領を発揮します。 ユーザーにエラーを返さず、内部でリトライします。

  • 「おっと、誰かが先に書いたのか。じゃあバージョン 108 をベースにもう一度試そう(あるいは上書き設定ならそのまま上書きしよう)」

Rustでの実装イメージ

1. RPCメッセージ定義 (rdb-proto)

// gRPCの定義イメージ
message CasRequest {
    bytes key = 1;
    uint64 expected_version = 2;
    bytes value = 3;
}

message CasResponse {
    bool success = 1;
    uint64 current_version = 2; // 失敗時に最新バージョンを教える
}

2. ストレージエンジンの実装 (rdb-storage)

RocksDBの Merge 機能を使うか、単純に Get -> Put をアトミックに行います。RocksDBの TransactionDB を使うと楽です。

impl StorageEngine {
    pub fn compare_and_swap(&self, key: &[u8], expected: u64, val: &[u8]) -> Result<u64> {
        // RocksDBの楽観的トランザクションを開始
        let txn = self.db.transaction(); 
        
        // 1. 最新値を読む (Snapshot Read)
        let current_val = txn.get_for_update(key, true)?; 
        let current_ver = extract_version(&current_val);

        // 2. 検証
        if current_ver != expected {
            return Err(Error::Conflict(current_ver));
        }

        // 3. 書き込み (メモリバッファへ)
        let new_data = append_version(val, generate_new_version());
        txn.put(key, new_data)?;

        // 4. コミット (ここでRocksDB内部で最終チェックが行われる)
        txn.commit()?;
        
        Ok(new_version)
    }
}

「劔」要素の取り込み: Group Commit & Epoch

さらにパフォーマンスを上げるために、劔のアイデアをここで注入します。

Group Commit (Raft層の最適化)

OCCのリクエストは非常に軽量なので、1件ずつRaftのログに書いて fsync しているとディスクI/Oがボトルネックになります。

  • 実装:
    • Raftリーダーのスレッドは、リクエストが来てもすぐにはディスクに書きません。
    • **「数ミリ秒のバス」**を待機させます。
    • バスが満員になるか、時間が来たら、数十〜数百個のCASリクエストをまとめて1つのRaftログエントリとして書き込みます。
  • 効果:
    • IOPSI/O回数の限界を超えてスループットが出せます。

Epoch-based GC (読み込みのノンブロッキング化)

「読み込みロックフリー」を徹底するため、古いバージョンのデータをいつ消すかGCをEpochで管理します。

  • 現在処理中のEpochより古いデータは、「どのトランザクションからも参照されていない」ことが保証された瞬間に、別スレッドでバッサリ削除します。
  • これにより、メインの書き込みスレッドはGCのことを忘れて走り続けられます。

この設計のメリット・デメリット

項目 OCCパターン (CAS) 従来のPercolatorパターン
通信回数 1 RTT (爆速) 2 RTT (Prewrite -> Commit)
ロック なし (サーバー負荷が低い) あり (ロック情報の管理コスト大)
競合時の挙動 リトライ地獄になる可能性あり ロック待ちで解決できる
S3適性 最適 (同じKeyへの同時書き込みは稀) オーバースペック
カウンター適性 不向き (100人が同時にカウントアップすると99人が失敗・リトライする) 向いている (ロックで順序制御)

結論

RustDBは「OCC (CAS) ベース」で行きましょう。

想定されている用途S3、SNS、高速イベント処理において、Percolatorのような悲観的ロックが必要なシーンは限定的です。 もし将来的に「カウンターのような激しい競合」が必要になれば、その時だけ使える「悲観ロックAPI」を追加すれば良いのですRedisの INCR のような専用コマンドを作るなど)。

次のステップ: これでDBのコアエンジンの仕様が決まりました。 いよいよ具体的な開発に入るために、**「Rustのワークスペース構成ディレクトリ構成「最初に実装する Hello World 的なミニマムゴール」**を提示しましょうか?


You Asked:

そうですね、ロックの仕様も選べるようにしたいです。全体像(アーキテクチャ)と計画、各部で開発すべきものとそれぞれのゴールをまとめて、コーディングエージェントでもわかる程度の粒度にまとめてほしいです。

Gemini Responded:

承知しました。コーディングエージェントAIや他の開発者に渡しても実装が進められるレベルで、RustDB の全体アーキテクチャ定義、ディレクトリ構成、そして開発フェーズごとの詳細仕様を定義します。

方針として、「Core (Storage/Net) は強固に、Logic (Consensus/Lock) はプラガブルに」 を徹底します。


RustDB Project Definition

1. ハイレベル・アーキテクチャ

システムは以下の4つの独立したコンポーネントマイクロサービスで構成されます。

graph TD
    UserApp[User App / Gateway] -->|Uses| SDK[rdb-client (Smart SDK)]
    
    subgraph Control_Plane
        PD[rdb-pd (Placement Driver)]
    end
    
    subgraph Data_Plane_Cluster
        Node1[rdb-server (Node 1)]
        Node2[rdb-server (Node 2)]
        Node3[rdb-server (Node 3)]
    end

    SDK -.->|Metadata/TSO| PD
    SDK ==|Direct Access (gRPC/QUIC)|==> Node1
    PD -.->|Heartbeat/Schedule| Node1
    Node1 <-->|Raft/Replication| Node2
  1. rdb-client (Smart SDK): アプリに埋め込まれるライブラリ。シャーディング情報のキャッシュ、リトライ、トランザクション制御OCC/Pessimisticの使い分けを担当。
  2. rdb-pd (Placement Driver): クラスタの脳。TSO時刻配信、トポロジー管理、データ再配置の指令を行う。
  3. rdb-server (Storage Node): データの保存と複製。raft-rs による合意形成、または非同期レプリケーションを実行。
  4. rdb-storage (Engine): RocksDBのラッパー。MVCC, CAS, Raw Writeの物理的な実行を担当。

2. Workspace構成 (Cargo.toml)

Monorepo構成を採用します。

[workspace]
members = [
    "rdb-proto",   # Protocol Buffers / gRPC definitions
    "rdb-common",  # Shared utilities (Error types, Configs)
    "rdb-storage", # RocksDB wrapper & Local Storage Engine
    "rdb-server",  # The main Data Node binary
    "rdb-pd",      # Placement Driver binary
    "rdb-client",  # Client SDK library
    "rdb-cli",     # Admin CLI tool
]

[profile.release]
lto = true
opt-level = 3
codegen-units = 1

3. モジュール別 詳細仕様と開発ゴール

コーディングエージェントへの指示書として使える粒度です。

Module 1: rdb-proto (インターフェース定義)

全ての通信規約をここで定義します。

  • Tech Stack: tonic (gRPC), prost
  • Definitions (.proto):
    • meta.proto: Region, Peer, NodeInfo 等の型定義。
    • pdpb.proto: GetTsoRequest, HeartbeatRequest, AskSplitRequest 等。
    • kvrpc.proto: データ操作用。
      • RawPutRequest: 結果整合性用Key, Value
      • CasRequest: OCC用Key, Value, ExpectedVersion
      • LockRequest / UnlockRequest: 悲観的ロック用(将来用)。
  • Goal: cargo build でRustのコードが自動生成されること。

Module 2: rdb-storage (ストレージエンジン)

分散を意識せず、**「単体ードでの最強のKVS」**を作ります。

  • Tech Stack: rocksdb binding
  • Architecture:
    • Trait StorageEngine: テスト容易性のため、RocksEngineMemoryEngine (test用) を実装。
  • Key Features:
    1. Column Families: default (Data), lock (Pessimistic Lock用), raft (Raft Log用).
    2. Method compare_and_swap(key, expected_ver, new_val):
      • RocksDBの Transaction または MergeOperator を使用。
      • Atomicに「読み出し -> 比較 -> 書き込み」を行う。
    3. Method put_raw(key, val):
      • WAL最適化Syncなしでの高速書き込み。
  • Goal: ユニットテストで compare_and_swap が正しく競合検知(バージョン不一致エラー)できること。

Module 3: rdb-server (分散ノード・合意形成)

最難関パートです。rdb-storage をラップし、ネットワーク越しの複製を行います。

  • Tech Stack: raft-rs, tokio, quinn (Data stream)
  • Internal Structure:
    • RegionRouter: RegionID を受け取り、対応する Peer (Raft Group) へメッセージを配送。
    • RaftStore: raft-rs の駆動ループ。Tick(時計), Step(メッセージ処理), Apply(ストレージ適用) を回す。
    • Strategy Pattern for Consistency:
      • ConsistencyMode::Strong: Raftの propose を呼ぶ。
      • ConsistencyMode::Eventual: ローカルの rdb-storage に書き込み、非同期キューに積む。
  • Optimization:
    • Batch System: 複数のClientリクエストを1つのRaft Log Entryにまとめる劔/TiKVの思想
  • Goal:
    • 3つのプロセスを立ち上げ、1つをKillしてもデータが読み書きできることRaftモード
    • Rawモードで書き込み、非同期で他ードに伝播すること。

Module 4: rdb-pd (司令塔)

ステートレスに見えて、自身のメタデータは堅牢に守る必要があります。

  • Tech Stack: axum (HTTP API), sled (Embedded DB for metadata)
  • Features:
    1. TSO (Timestamp Oracle): AtomicU64 で単調増加IDを発行。
    2. ID Allocator: ードID、RegionIDの一意な発行。
    3. Memberlist: 現在生きているノードのリスト保持。
  • Goal: HTTP/gRPC経由で「現在時刻」と「ード一覧」が取得できること。

Module 5: rdb-client (SDK)

「賢い」ロジックの集約場所です。

  • Features:
    1. Region Cache: BTreeMap<Key, NodeAddr> で「どのキーがどこにあるか」を管理。
    2. Smart Routing: キャッシュを見て直接ードへgRPC。エラーならPDへ問い合わせてキャッシュ更新。
    3. Transactions (Optimistic):
      • txn.commit() 時に自動でバージョンの不整合をチェックし、指定回数リトライするロジック。
    4. Transactions (Pessimistic - Option):
      • txn.lock_for_update(key) を呼ぶと、サーバーへ LockRequest を投げる実装。
  • Goal:
    • client.cas_put("key", "val") のようなシンプルなコードで、裏側の分散処理が隠蔽されていること。

4. 開発ロードマップStep-by-Step Plan

エージェントには以下の順序でタスクを割り振ります。

Phase 1: 足場とストレージ (Week 1-2)

  • Workspaceの作成と rdb-proto の定義完了。
  • rdb-storage で RocksDB をラップし、CASRaw Put の単体テストをパスさせる。
  • rdb-server のスケルトン作成gRPCサーバーが立ち上がり、rdb-storage に書き込めるだけの単一ノード版)。

Phase 2: PDと基本通信 (Week 3)

  • rdb-pd の実装。TSOの実装。
  • rdb-server が起動時に rdb-pd へRegister登録し、Heartbeatを送る機能の実装。
  • rdb-client の作成。PDからード情報を取得して接続する機能。

Phase 3: Raftの実装 (Week 4-6)

  • rdb-serverraft-rs を組み込む。
  • 固定の1 Regionだけでいいので、3ード間でのログ複製Leader Election, Log Replicationを成功させる。
  • Clientからの CAS リクエストを Raft 経由で処理するパイプラインの構築。

Phase 4: マルチモードとSDKの高度化 (Week 7-8)

  • rdb-server に「結果整合性モードAsync Replication」の分岐を追加。
  • rdb-client に「Region Cache」と「リトライロジック」を実装。
  • ロック仕様の追加: ここで初めて LockRequest (悲観的ロック) の処理を rdb-server とSDKに追加するオプション扱い

Phase 5: VMメッシュ統合と最適化 (Week 9+)

  • quinn (QUIC) の導入による通信高速化。
  • PDへのスケジューリングロジックディスク容量バランス等の実装。

5. ロック仕様の選定について(補足)

ご要望の「ロック仕様を選べるようにする」点は、SDKのメソッドで表現します。

// A. 楽観的ロック (デフォルト・推奨)
// サーバー側でロック保持せず、書き込み瞬間のCAS判定のみ。高速。
let res = client.optimistic_txn(|txn| async move {
    let val = txn.get("key").await?;
    txn.put("key", update(val));
    Ok(()) // commit時に検証
}).await;

// B. 悲観的ロック (オプション)
// サーバーのLock CFにレコードを書き、他をブロックする。
// カウンターや厳密な在庫管理などに使用。
let res = client.pessimistic_txn(|txn| async move {
    txn.lock("key").await?; // ここでサーバーへLockRequestが飛ぶ
    let val = txn.get("key").await?;
    txn.put("key", update(val));
    Ok(())
}).await;

この設計書をベースに、まずは Phase 1 (Proto & Storage) の実装を開始するのが良いでしょう。