FABRKNT
Reth Expert — 本番エンジニアリング
パフォーマンス & システム
レッスン 2 / 25·CONTENT18 分40 XP
コース
Reth Expert — 本番エンジニアリング
レッスンの役割
CONTENT
順序
2 / 25

レッスン1 — MDBX & ストレージ内部

問い

各アカウントの残高、各ストレージスロット、各レシート — Reth はそれらをすべて 1 つの KV ストアに収める。Postgres でも RocksDB でも独自フォーマットでもなく、MDBX。LMDB から派生したメモリマップ B+tree。500GB の DB 全体が、まるで巨大な in-memory slice であるかのように Rust コードから見える。なぜ RocksDB ではなく MDBX なのか?

原理(最小モデル)

  • MDBX = mmap された B+tree. OS のページキャッシュが無料の読み取りキャッシュ、ホット読み出しは「ポインタ参照、システムコールなし」。
  • LSM ツリー(RocksDB)は書き込み速いがコンパクションでストールする. Ethereum は読み取り重く・レイテンシ敏感 → コンパクションのストールが致命傷。
  • Database trait は 2 つの関連型 TX / TXMut で読み書きを分離. 読み取り tx に put を呼ぶことをコンパイル時に防ぐ。
  • テーブルは型(<T: Table>)であって文字列ではない. タイポは即コンパイルエラー。
  • append vs put. append は単調増加キー専用、B+tree 探索を省略して速い。ブロック順次処理は append、reorg は put フォールバック。
  • Cursor が範囲スキャンの正解. 一度 B+tree に位置決めし隣接エントリを歩く。隣接キーは同じページを共有するのでキャッシュ局所性が効く。

具体例

なぜ MDBX か(vs RocksDB):

特徴RocksDBMDBX
アーキテクチャLSM ツリーB+tree(mmap)
読み取りレイテンシ可変(コンパクション)予測可能
書き込み増幅高い約 1 倍
クラッシュ安全性手動 flushMVCC で ACID
読み取り並列性ロックロックフリー読み取り

Reth の本物の Database trait(crates/storage/db-api/src/database.rs):

pub trait Database: Send + Sync + Debug {
    type TX: DbTx + Send + Sync + Debug + 'static;
    type TXMut: DbTxMut + DbTx + TableImporter + Send + Sync + Debug + 'static;

    #[track_caller]
    fn tx(&self) -> Result<Self::TX, DatabaseError>;

    #[track_caller]
    fn tx_mut(&self) -> Result<Self::TXMut, DatabaseError>;

    fn path(&self) -> PathBuf;

    fn oldest_reader_txnid(&self) -> Option<u64>;

    fn last_txnid(&self) -> Option<u64>;
}

DbTx / DbTxMut の操作(crates/storage/db-api/src/transaction.rs):

// DbTx (読み取り専用)
fn get<T: Table>(&self, key: T::Key) -> Result<Option<T::Value>, DatabaseError>;
fn commit(self) -> Result<(), DatabaseError>;
fn cursor_read<T: Table>(&self) -> Result<Self::Cursor<T>, DatabaseError>;
fn cursor_dup_read<T: DupSort>(&self) -> Result<Self::DupCursor<T>, DatabaseError>;
fn entries<T: Table>(&self) -> Result<usize, DatabaseError>;

// DbTxMut (読み書き)
fn put<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError>;
fn append<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError>;
fn delete<T: Table>(&self, key: T::Key, value: Option<T::Value>) -> Result<bool, DatabaseError>;
fn clear<T: Table>(&self) -> Result<(), DatabaseError>;
fn cursor_write<T: Table>(&self) -> Result<Self::CursorMut<T>, DatabaseError>;

失敗例(誤解)

append を任意の順番で呼んでも遅いだけ」— 間違いappend は現在の最大より小さいキーで呼ぶと 不変条件違反 で破損する。MDBX ではなく あなたの責任 で順序を守る(B+tree 探索を省略するため)。逆順なら put を使う。

「state root 検証は pre-execution で十分」— 間違い。post-execution が知る = 実 EVM 実行の結果。state root は post-execution のみ検証可能。

🛑 予測。 RocksDB は多くのブロックチェーンクライアントで支配的な KV ストア(geth、erigon 歴史的に)。Reth はなぜ代わりに MDBX を選ぶのか?(答え: 読み取りレイテンシ予測可能性 + クラッシュ安全性 + 書き込み増幅 ~1 倍 + ロックフリー読み取り。Ethereum は読み取り重く・レイテンシ敏感 → LSM のコンパクションストールは sync 速度・バリデータレイテンシに致命的。MDBX の B+tree mmap モデルが噛み合う。)

ステップで組み立てる

Step 1: trait の 2 関連型を即答

TX(読み取り専用)/ TXMut(読み書き)— 分離はコンパイル時の型安全のため。

Step 2: append vs put を 1 文で

append は単調増加キー前提、B+tree 探索省略で速い。put は任意順、安全。reorg は put フォールバック。

Step 3: Cursor を使う

範囲スキャンには cursor_read を一度位置決めして隣接歩き。N 個の独立 get よりキャッシュ局所性で数桁速い。

Step 4: ホットパス読みの構造

mmap → ウォーム読みはポインタ参照(システムコールなし)→ OS ページキャッシュが無料の読み取りキャッシュ。Execution ステージ(アカウント → ストレージ → コード)が温まったページを叩く順序になっている。

Step 5: テーブルを見る

crates/storage/db-api/src/tables を開く:

  • Headers テーブル: key = BlockNumber、value = Header
  • DupSort テーブル: 1 キーに複数値を許す(ストレージスロット、ログなど)

Step 6: 設計対比 — SALT

MegaETH の SALT は B+tree + MPT を、4 段 256-ary trie + SHI バケットで置き換え、認証層を完全メモリ上に持つ。state root 更新中のランダムディスク I/O ゼロ。MDBX は成熟・ACID・クラッシュセーフだが、高 TPS では state root 更新のランダム I/O がボトルネック。設計判断は対比で初めて見える

答え合わせ

  • LSM コンパクションとは: log-structured-merge tree が定期的に階層を再書き出しする。書き込みは速いがコンパクション中は読み書きストール。B+tree はコンパクションせず、ページ分割 / マージで空間を回収。
  • oldest_reader_txnid の用途: 最古実行中読み取り tx を公開、長時間 reader が GC をブロックしている検知に使う。長い読み tx は DB を膨らませる。
  • mmap で 500GB DB が Rust slice に見える理由: OS が mmap でアドレス空間にマップ、ページフォルト時に該当ページをディスクからロード。ヒット時はメモリアクセスと同コスト、ミスでもシステムコールなしの透過的フォールト。

合格基準

  • LSM vs B+tree のトレードオフを 1 文で言える。
  • append / put の使い分けと不変条件を即答できる。
  • Cursor が独立 get より速い理由(B+tree 構造 + ページキャッシュ局所性)を言える。
  • 「mmap → ポインタ参照、システムコールなし」を OS ページフォルト機構で説明できる。
  • MDBX と SALT の設計対比を 1 つ言える。

まとめ(3行)

  • MDBX = mmap された B+tree、Ethereum の読み取り重・レイテンシ敏感ワークロードに合う(LSM のコンパクションストールが致命的)。
  • Database trait は TX/TXMut で読み書き分離、テーブルは型、append は単調増加専用、Cursor が範囲スキャンの正解。
  • mmap + OS ページキャッシュで「500GB DB が Rust の slice」になる — SALT との対比でこの設計判断の意味が見える。