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

レッスン0 — Reth のためのパフォーマンスエンジニアリング

問い

Reth フォークを本番投入。ベンチではブロック取り込み 12ms だったのに、本番では 80ms。残り 68ms はどこに消えた? — 答えられない。誰もプロファイルを取っていなかったから。これこそが「見えない遅さ」— 気づかぬ間に積み重なり、バリデータが 200 ブロック遅れるまで誰も気づかない。測定なしの最適化はどう間違うか?

原理(最小モデル)

  • 測ってから直す. flamegraph(「全体としてどこで時間を使っているか?」)と Criterion(「特定の変更で関数 X が速くなったか?」)の 2 ツールで両側面をカバー。
  • マイクロベンチはシステム効果を測れない. Criterion で 20% 改善 → ノード全体が 20% 改善する保証はない。ノード全体は schelk(ブロックデバイススナップショット)でディスク状態をロールバックして実測する。
  • キャッシュラインで考える. RAM 読み出しはレジスタ演算の ~100 倍遅い。Struct of Arrays > Array of Structs、ホット / コールド分離、64 バイトキャッシュライン単位。
  • jemalloc は p99 を救う. スループットではなくテイルレイテンシ。1 行で 10-30% 改善するワークロードが珍しくない。
  • 3 つのリリースプロファイル. release(日常)/ maxperf(バリデータ・ベンチ)/ maxperf-symbols(本番プロファイリング)。

具体例

flamegraph を 30 秒で:

cargo install flamegraph
cargo flamegraph --bin reth -- node --chain mainnet
# flamegraph.svg をブラウザで開く

Criterion でマイクロベンチ:

// Cargo.toml
// [dev-dependencies]
// criterion = "0.5"

// benches/my_bench.rs
use criterion::{criterion_group, criterion_main, Criterion};

fn bench_my_thing(c: &mut Criterion) {
    c.bench_function("hash 1KB", |b| {
        let data = vec![0u8; 1024];
        b.iter(|| keccak256(&data))
    });
}

criterion_group!(benches, bench_my_thing);
criterion_main!(benches);

ホット / コールド分離:

// 悪い例:1イテレーションごとに200バイト触る
struct Row {
    id: u64,
    big_blob: [u8; 192],
}

// 改善:ホットとコールドを分離
struct Hot { id: u64, version: u32 }
struct Cold { big_blob: [u8; 192] }

jemalloc 1 行で:

# Cargo.toml
[dependencies]
tikv-jemallocator = "0.5"
// main.rs
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;

Reth の実本番プロファイル (Cargo.toml):

[profile.release]
opt-level = 3
lto = "thin"
debug = "none"
strip = "symbols"
panic = "unwind"
codegen-units = 16

[profile.maxperf]
inherits = "release"
lto = "fat"
codegen-units = 1

[profile.maxperf-symbols]
inherits = "maxperf"
debug = "full"
strip = "none"

起動:

cargo build --profile maxperf --bin reth
# またはネイティブCPU命令で(AVX2など):
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf --bin reth

失敗例(誤解)

「コードを短くすると速くなる」— 間違い。現代 CPU では行数ではなく メモリレイアウト が支配的。短い行でも200バイト触れば 1 キャッシュラインでは収まらない。

「マイクロベンチで関数 X が 20% 速くなった → ノード全体が 20% 速くなる」— 間違い。マイクロベンチはシステム効果(cache miss、I/O 直列化、aliasing)を測らない。1 関数が速くても他の遅さに飲まれる。

「maxperf を日常開発でも使う」— 間違いcodegen-units = 1 + lto = "fat" は全モジュール跨ぎインライン化でコンパイル時間が桁違いに長くなる。日常開発は release、出荷時だけ maxperf。

🛑 予測。 ジュニアエンジニアが「ノードが遅い気がする、HashMap を BTreeMap に置き換えてみよう」と言う。アプローチの問題点を 3 つ挙げよ。(答え: ① 測定なしに原因を仮定している(実際の遅さの原因は HashMap ではないかもしれない)、② BTreeMap も遅い可能性がある(cache 局所性は劣る場合あり)、③ 変更後の再測定計画がない(最適化が打ち消し合う場合あり)。測定なしの最適化は逆効果になりうる。)

ステップで組み立てる

Step 1: flamegraph を 1 回取れる

cargo flamegraph --bin reth → svg をブラウザで → 上部の横に広いバーがホットパス。

Step 2: Criterion ベンチを 1 つ書ける

benches/ 配下に 1 つ。性能改善の主張にはベンチ結果をコミットに添える規律。

Step 3: schelk でディスク状態をロールバック

500GB Reth DB を毎ラン再構築するのは数時間 → 不可。tempoxyz/schelkdm-era でブロックデバイススナップショット、ロールバックは数秒。ロールバック規律のないベンチは再現不可能

Step 4: jemalloc + maxperf プロファイル

features = ["jemalloc", "asm-keccak"] + cargo build --profile maxperf。バリデータ向けの「1 サイクルでも惜しい」ビルド。

Step 5: 3 ルール

  1. 何かを変える前に必ず測る(「速くなった気がする」はデータではない)
  2. プロファイラが示すパスだけを最適化
  3. 変更後にもう一度測る(手動最適化がコンパイラ最適化と打ち消す場合あり)

答え合わせ

  • Criterion 20% → ノード全体 20% は保証されない理由: マイクロベンチはシステム効果(cache、I/O 直列化、aliasing)を測らない。同じ関数が単独で 20% 速くても、ホットパス全体の中では他の遅さに飲まれて改善が薄まる。
  • 100 万要素 Vec<Row>row.id だけ読むとき CPU がロードする量: 200 MB(200バイト × 100万)。big_blob を読まなくても 1 ロウあたり 200 バイト全部キャッシュラインに乗る = キャッシュラインを 200 バイト触ったのと同じ。SoA に分離すれば 12 バイト × 100 万 = 12 MB に縮む(~16 倍速)。
  • jemalloc の 10-30% はテイル(p99): 平均レイテンシではなく、断片化が進んだ状態での p99 が改善する。glibc malloc は arena ロック競合 + 断片化でテイルが暴れる。

合格基準

  • 2 つのツール(flamegraph + Criterion)の役割を即答できる。
  • 3 つのリリースプロファイル(release / maxperf / maxperf-symbols)の使い分けを言える。
  • 「マイクロベンチがノード全体について嘘をつく理由」を 2 つ言える。
  • jemalloc が救うのは p50 ではなく p99 と説明できる。

まとめ(3行)

  • 測ってから直す = flamegraph(全体)+ Criterion(関数別)。マイクロベンチはシステム効果を測れないので、ノード全体は schelk でロールバックして実測する。
  • メモリレイアウト > 行数。SoA + ホット / コールド分離 + jemalloc がテイルレイテンシを救う。
  • 3 プロファイル(release / maxperf / maxperf-symbols)を使い分け、性能改善の主張にはベンチ結果を必ず添える。