レッスン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 は読み取り重く・レイテンシ敏感 → コンパクションのストールが致命傷。
Databasetrait は 2 つの関連型TX/TXMutで読み書きを分離. 読み取り tx にputを呼ぶことをコンパイル時に防ぐ。- テーブルは型(
<T: Table>)であって文字列ではない. タイポは即コンパイルエラー。 appendvsput.appendは単調増加キー専用、B+tree 探索を省略して速い。ブロック順次処理はappend、reorg はputフォールバック。- Cursor が範囲スキャンの正解. 一度 B+tree に位置決めし隣接エントリを歩く。隣接キーは同じページを共有するのでキャッシュ局所性が効く。
具体例
なぜ MDBX か(vs RocksDB):
| 特徴 | RocksDB | MDBX |
|---|---|---|
| アーキテクチャ | LSM ツリー | B+tree(mmap) |
| 読み取りレイテンシ | 可変(コンパクション) | 予測可能 |
| 書き込み増幅 | 高い | 約 1 倍 |
| クラッシュ安全性 | 手動 flush | MVCC で 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 =HeaderDupSortテーブル: 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 のコンパクションストールが致命的)。
Databasetrait はTX/TXMutで読み書き分離、テーブルは型、appendは単調増加専用、Cursor が範囲スキャンの正解。- mmap + OS ページキャッシュで「500GB DB が Rust の slice」になる — SALT との対比でこの設計判断の意味が見える。