2026-09-07 AWS Benchmark Report
ShardedT1Index(T1のrange-shardingによるO(コーパスサイズ)問題の解消)を本番結線後、初のフルCRUDマトリクス+background jobs probe計測。today: split protocolの3件のバグ修正(重複キュー投入によるsplit中断、abortパスのリセット順序、split境界を跨ぐstraggler書き込みのデータ損失)、KVStoreコンセプトへのreorganize/checkpoint/get_statistics追加、T1統計をreorgカウントから実際のsplit回数(T1_Splits)へ刷新、YCSB-Eの強制reorganize()トリガー撤去(background-jobs-probeと重複測定だったため)。background jobsのscan劣化率は前回検証済みのbounded rangeを維持(in_memory: 14.6/17.8%, ltm: 2.0/4.8% — シャーディング前は65-83%だった)。追記: organicなper-shard splitの書き込み停止区間を直接計測する仕組みを追加し、単調増加キー挿入下では約0.20〜0.22秒でコーパスサイズ非依存に安定していることを確認(詳細は下部のセクション参照)。ベンチ実行自体はコード変更ではなく、monorepo再編以降放置されていたAWSオーケストレーションスクリプトの複数のパスバグ(rsync同期先、NVMeデバイス検出、結果ダウンロード先)を発見・修正して初めて完走した。
Stacking Variants
各最適化の詳細は Design Document を参照。
- RocksDB: LSM-Tree のベースライン比較
- LMDB: B+Tree / mmap ベースの比較対象
- RocksDB-BlobDB: RocksDB の Blob 分離ストレージ変種(大きな Value 向け)
- Baseline: vmemkv 最適化なし
- +BF: + T1 Bloom Filter
- +Inline: +BF + T1 Inline Value(<8B のValueをT1のみで処理)
- +Prefault: +Inline + T2 Async Prefaulting(全部盛り、= VMemKVStore)。旧
ScanBaseSequentialアブレーションは実測の結果撤去され、その最適化(base領域専用mmap経路)はGet/Scan双方の標準パスへ無条件で統合済み。
Environment
AWS i4i.8xlarge (32 vCPU, local NVMe), 5-parallel spot instances (in_memory:8B/1KB, ltm:1KB/64KB, background-jobs-probe専用インスタンス), Ubuntu 24.04, Release -O3 -march=native.
Workloads
- Insert: 順次挿入(1/4/16/32 スレッド)
- Get Hit/Miss (Zipf/Uniform): ポイントルックアップ
- Update / Delete: Zipf 分布での更新・削除
- Scan (Zipf/Uniform): レンジスキャン(旧レポートにあった reorganize モード分割は撤去済み)
- YCSB-E: 95% Scan + 5% Insert 混合(32スレッド / 30秒タイムライン)
Overall Notes & Annotations
Workload Winners & Relative Speedup
Best VMemKV variant vs. whichever rival (RocksDB, LMDB, RocksDB-BlobDB) is FASTEST at that cell, at 32 threads (max concurrency tested) -- not the best-of-any-thread-count-or-rival figure, since both axes are known to shrink (sometimes invert) the ratio if picked favorably. The rival actually being compared against is named in small text under each badge.
| Workload | 8B In-Memory | 1KB In-Memory | 1KB LTM | 64KB LTM |
|---|---|---|---|---|
| Insert |
✅ WIN (1.48x)
vmemkv (+BF)
vs RocksDB-BlobDB
|
✅ WIN (1.40x)
vmemkv (+Inline)
vs RocksDB-BlobDB
|
✅ WIN (1.36x)
vmemkv (+BF)
vs RocksDB
|
✅ WIN (1.98x)
vmemkv (+Inline)
vs RocksDB
|
| Update (Zipf) |
✅ WIN (1.41x)
vmemkv (+Inline)
vs RocksDB-BlobDB
|
✅ WIN (1.56x)
vmemkv (+BF)
vs RocksDB-BlobDB
|
✅ WIN (1.92x)
vmemkv (+Inline)
vs RocksDB
|
✅ WIN (2.44x)
vmemkv (Baseline)
vs LMDB
|
| Delete |
✅ WIN (1.71x)
vmemkv (+BF)
vs RocksDB-BlobDB
|
✅ WIN (1.79x)
vmemkv (Baseline)
vs RocksDB
|
✅ WIN (1.79x)
vmemkv (Baseline)
vs RocksDB-BlobDB
|
✅ WIN (3.89x)
vmemkv (+Inline)
vs RocksDB-BlobDB
|
| Get (Miss) |
✅ WIN (2.28x)
vmemkv (+BF)
vs RocksDB-BlobDB
|
✅ WIN (2.52x)
vmemkv (+BF)
vs LMDB
|
✅ WIN (2.42x)
vmemkv (+BF)
vs LMDB
|
✅ WIN (2.25x)
vmemkv (+Inline)
vs LMDB
|
| Get (Hit, Zipf) |
✅ WIN (1.39x)
vmemkv (+Inline)
vs LMDB
|
✅ WIN (1.24x)
vmemkv (Baseline)
vs LMDB
|
❌ LOSE (0.65x)
vmemkv (+Inline)
vs RocksDB-BlobDB
|
✅ WIN (1.21x)
vmemkv (+Inline)
vs RocksDB
|
| Get (Hit, Uniform) |
WIN (1.08x)
vmemkv (+Inline)
vs LMDB
|
✅ WIN (1.20x)
vmemkv (Baseline)
vs LMDB
|
❌ LOSE (0.83x)
vmemkv (+Inline)
vs RocksDB-BlobDB
|
≈ EVEN (0.99x)
vmemkv (+BF)
vs RocksDB-BlobDB
|
| Scan (Zipf) |
LOSE (0.89x)
vmemkv (+Inline)
vs LMDB
|
WIN (1.05x)
vmemkv (Baseline)
vs LMDB
|
✅ WIN (1.26x)
vmemkv (Baseline)
vs RocksDB
|
WIN (1.08x)
vmemkv (+Inline)
vs LMDB
|
| Scan (Uniform) |
LOSE (0.92x)
vmemkv (+Inline)
vs LMDB
|
WIN (1.13x)
vmemkv (Baseline)
vs LMDB
|
✅ WIN (1.42x)
vmemkv (+BF)
vs RocksDB
|
≈ EVEN (0.98x)
vmemkv (+BF)
vs RocksDB-BlobDB
|
Background Jobs: reorganize() / checkpoint()
Fixed reference point (1KB values, 10,000,000 records; in-memory unconstrained, LTM cgroup-constrained to the same memory budget as the rest of the suite) -- not swept across the CRUD matrix's 4 scenario/value-size combos. Duration is a single call in isolation; the three degradation columns are the percentage drop in concurrent Insert/Update/Scan QPS while that one call runs, measured over a matched-duration window on both sides (see run_background_jobs_probe.sh). reorganize() forces every shard through a synchronous merge in one call -- a manual escape hatch (pre-backup flush, capacity planning), not what happens during ordinary operation. The last two rows summarize the organic per-shard split alternative that actually runs day to day (averaged across every split observed in that run; see the detail section further down for the full breakdown and charts).
| Job / Scenario | Duration (1 call) | Insert QPS degradation | Update QPS degradation | Scan QPS degradation |
|---|---|---|---|---|
| reorganize() (forced, all shards) / in-memory | 1.482s | 51% | -4% | 15% |
| reorganize() (forced, all shards) / LTM | 14.082s | 51% | 3% | 2% |
| checkpoint() / in-memory | 3.727s | 49% | 9% | 18% |
| checkpoint() / LTM | 4.094s | 54% | 9% | 5% |
| per-shard split (organic) / in-memory | 209ms avg of 6 splits | 76% | n/a | n/a |
| per-shard split (organic) / LTM | 217ms avg of 6 splits | 70% | n/a | n/a |
Organic Per-Shard Splits
Full per-split detail behind the "per-shard split (organic)" summary rows in the Background Jobs table above: what actually happens during ordinary operation, as opposed to the forced whole-store reorganize() there. Starting from an empty store with real background workers active, insert continuously for 90s (fixed 1KB values, monotonically increasing keys) and detect each automatic per-shard split as ShardedT1Index's own background worker pool completes it. Pause duration is the exact, directly-instrumented span writers targeting that shard were blocked (get_statistics().t1_last_split_pause_us) -- not an inferred window; the QPS chart plots this event's own local Insert ops/sec just before the pause against ops/sec across the pause itself, both in absolute terms (see run_organic_split_probe.sh). With monotonically increasing keys, exactly one shard is ever "hot" at a time regardless of how many other shards exist, so the drop stays large across every split here rather than shrinking with shard count -- that property (if it holds at all) would need a workload whose writes spread across multiple hot shards concurrently, e.g. random keys.
in-memory: 6 split(s) observed over 90s · 9,403,453 total inserted · ended at 8 shards
LTM: 6 split(s) observed over 90s · 7,694,305 total inserted · ended at 7 shards
Charts below truncated to shard count ≤7 (the highest both scenarios reached in this run) for a like-for-like comparison.
Writer-visible pause duration
Insert QPS: just before the pause vs. during it
Insert Workload
Update (Zipf) Workload
Delete (Zipf) Workload
Get Miss (Zipf) Workload
Get Hit (Zipf) Workload
Get Hit (Uniform) Workload
Scan (Zipf) Workload
Scan (Uniform) Workload
YCSB-E — Scan + Insert 混合スループット(30秒タイムライン)
YCSB-E ワークロード: 95% Scan(100件レンジスキャン)+ 5% Insert を 32スレッドで 30秒間実行。
藍色の点線: 自然発生のT1 Reorganize(Adaptive Soft Limitによる自動トリガー、全バリアント共通)。
強制トリガーはバリアントごとに独立集計(全バリアント共通の1本の線には集約しない): t=5秒予定のreorganize()(細かい点線)、t=10秒・t=25秒予定のcheckpoint()(粗い点線)を、そのバリアント自身の色+マーカー形状(スキャンQPS線をホバーした際の点と同じ形。凡例のポイント形状も対応)で描画。
実際に発火する秒(線の位置)は予定秒とは限らない(将棋倒しに対するガード無し、書き込み負荷次第で数十秒遅延することがある)。8B In-Memoryのように挿入スループットが極端に高いワークロードでは、t=5秒予定のreorganize()自体が20秒以上かかり、後続のcheckpoint()トリガー(t=10s/25s)がこの30秒間に一度も発火しないまま終わることもある(この場合、線は1本しか出ない)。カーソルを線に合わせると、その秒に発火したバリアント・種類・予定秒・実発火秒・所要時間を表示。
Scan QPS タイムライン
32 threads / 30 secondsInsert Workload
Update (Zipf) Workload
Delete (Zipf) Workload
Get Miss (Zipf) Workload
Get Hit (Zipf) Workload
Get Hit (Uniform) Workload
Scan (Zipf) Workload
Scan (Uniform) Workload
YCSB-E — Scan + Insert 混合スループット(30秒タイムライン)
YCSB-E ワークロード: 95% Scan(100件レンジスキャン)+ 5% Insert を 32スレッドで 30秒間実行。
藍色の点線: 自然発生のT1 Reorganize(Adaptive Soft Limitによる自動トリガー、全バリアント共通)。
強制トリガーはバリアントごとに独立集計(全バリアント共通の1本の線には集約しない): t=5秒予定のreorganize()(細かい点線)、t=10秒・t=25秒予定のcheckpoint()(粗い点線)を、そのバリアント自身の色+マーカー形状(スキャンQPS線をホバーした際の点と同じ形。凡例のポイント形状も対応)で描画。
実際に発火する秒(線の位置)は予定秒とは限らない(将棋倒しに対するガード無し、書き込み負荷次第で数十秒遅延することがある)。8B In-Memoryのように挿入スループットが極端に高いワークロードでは、t=5秒予定のreorganize()自体が20秒以上かかり、後続のcheckpoint()トリガー(t=10s/25s)がこの30秒間に一度も発火しないまま終わることもある(この場合、線は1本しか出ない)。カーソルを線に合わせると、その秒に発火したバリアント・種類・予定秒・実発火秒・所要時間を表示。
Scan QPS タイムライン
32 threads / 30 secondsInsert Workload
Update (Zipf) Workload
Delete (Zipf) Workload
Get Miss (Zipf) Workload
Get Hit (Zipf) Workload
Get Hit (Uniform) Workload
Scan (Zipf) Workload
Scan (Uniform) Workload
YCSB-E — Scan + Insert 混合スループット(30秒タイムライン)
YCSB-E ワークロード: 95% Scan(100件レンジスキャン)+ 5% Insert を 32スレッドで 30秒間実行。
藍色の点線: 自然発生のT1 Reorganize(Adaptive Soft Limitによる自動トリガー、全バリアント共通)。
強制トリガーはバリアントごとに独立集計(全バリアント共通の1本の線には集約しない): t=5秒予定のreorganize()(細かい点線)、t=10秒・t=25秒予定のcheckpoint()(粗い点線)を、そのバリアント自身の色+マーカー形状(スキャンQPS線をホバーした際の点と同じ形。凡例のポイント形状も対応)で描画。
実際に発火する秒(線の位置)は予定秒とは限らない(将棋倒しに対するガード無し、書き込み負荷次第で数十秒遅延することがある)。8B In-Memoryのように挿入スループットが極端に高いワークロードでは、t=5秒予定のreorganize()自体が20秒以上かかり、後続のcheckpoint()トリガー(t=10s/25s)がこの30秒間に一度も発火しないまま終わることもある(この場合、線は1本しか出ない)。カーソルを線に合わせると、その秒に発火したバリアント・種類・予定秒・実発火秒・所要時間を表示。
Scan QPS タイムライン
32 threads / 30 secondsInsert Workload
Update (Zipf) Workload
Delete (Zipf) Workload
Get Miss (Zipf) Workload
Get Hit (Zipf) Workload
Get Hit (Uniform) Workload
Scan (Zipf) Workload
Scan (Uniform) Workload
YCSB-E — Scan + Insert 混合スループット(30秒タイムライン)
YCSB-E ワークロード: 95% Scan(100件レンジスキャン)+ 5% Insert を 32スレッドで 30秒間実行。
藍色の点線: 自然発生のT1 Reorganize(Adaptive Soft Limitによる自動トリガー、全バリアント共通)。
強制トリガーはバリアントごとに独立集計(全バリアント共通の1本の線には集約しない): t=5秒予定のreorganize()(細かい点線)、t=10秒・t=25秒予定のcheckpoint()(粗い点線)を、そのバリアント自身の色+マーカー形状(スキャンQPS線をホバーした際の点と同じ形。凡例のポイント形状も対応)で描画。
実際に発火する秒(線の位置)は予定秒とは限らない(将棋倒しに対するガード無し、書き込み負荷次第で数十秒遅延することがある)。8B In-Memoryのように挿入スループットが極端に高いワークロードでは、t=5秒予定のreorganize()自体が20秒以上かかり、後続のcheckpoint()トリガー(t=10s/25s)がこの30秒間に一度も発火しないまま終わることもある(この場合、線は1本しか出ない)。カーソルを線に合わせると、その秒に発火したバリアント・種類・予定秒・実発火秒・所要時間を表示。