このブログは、siddontangの"How we optimize RocksDB in TiKV — The Battle Against the DB Mutex"の抄訳です。翻訳はPlamo翻訳をベースに、@bohnenが担当しました。
私たちが9年前にTiKVの開発を始めた時、ストレージエンジンとしてRocksDBを選択しました。これには十分な理由があります――これは非常に効率的で、実際の運用環境で実績があり、柔軟なLSMツリー構造を持つキーバリューストアだからです。大規模環境でも確実に動作することが証明されていました。これにより、分散型のトランザクション対応キーバリューデータベースを構築するための優れた基盤を得ることができました。
しかし、ユーザーのデータ量がテラバイト単位、数百万のSSTファイルへと増加していくにつれ、RocksDBの暗い側面に直面することになりました。特に高い同時接続時や大規模展開時には、パフォーマンスのボトルネックが発生するようになったのです。時間の経過とともに、TiKVの規模に対応できるようRocksDBに数多くの最適化を施してきました。
そして今、約10年の時を経て、これらの取り組みを振り返り記録する適切な時期が来たと感じています。これは単に私たちが学んだことを共有するためだけでなく、このエンジニアリングの旅路を祝うためでもあります。これらの最適化の中には、現在では公式のRocksDBリポジトリに統合されたものもあります。一方で、TiKV固有の最適化もあり、これらは現在も大規模な本番環境クラスタで日々活用されています。
本記事では、これらの最適化手法の中からいくつかをピックアップして紹介していきます。
最も嫌だったもの:グローバルDBミューテックス
私たちが長年悩まされ続けたものがあるとするなら、それはRocksDBのグローバルDBミューテックスです。
多くのパフォーマンス問題は、このミューテックスに起因しています。
実際の運用環境での事例を1つ紹介しましょう:非同期スナップショットの終了遅延の改善(#18743)。
ある顧客環境では、典型的な大量データの継続的な取り込み作業が行われていました。時間の経過とともに、RocksDB内のデータセットは約4TB、29万以上のSSTファイルにまで成長しました。P99レイテンシは100msを超えるという、到底許容できない状態になっていました。
私たちはローカル環境でシミュレーションを実施したところ、主要なボトルネックはグローバルミューテックスへのアクセス競合、特にRocksDBのバージョン管理におけるLogAndApplyフェーズで発生していることが判明しました。
RocksDBはなぜ全体のロックが必要なのか?
RocksDBは内部で多くの処理を安全かつ一貫性を持って行うために、全体のロック(DBミューテックス)を使用しています。このロックは以下の操作を保護するために使われます:
- バージョン管理 - データベースのバージョンを作成したり切り替えたりする操作
- MemTable操作 - テーブルの切り替えや書き込みの完了処理
- カラムファミリー - 作成、変更、削除などの操作
- 圧縮スケジュール - どのファイルを圧縮するかの決定
- メタデータ更新 - ファイルや階層情報の一貫性を保つための処理
- スナップショットリストの管理 - 同時に変更が行われないようにするため
- 書き込み調整 - 書き込みバッチ処理やWAL操作の同期
これらの操作はすべて1つのミューテックス内で行われます。つまり、このロックを必要とする操作は他の操作を待たせる可能性があるということです。
LogAndApplyの仕組みについて
このミューテックスによる競合は主に、RocksDBのLogAndApply操作が原因で発生します。この操作は毎回の書き込み完了処理、圧縮処理、データ取り込み処理などで実行されます。その処理コストはSSTファイルの数に比例して増加します - これが大規模環境において主要なパフォーマンスボトルネックとなる理由です。
LogAndApply処理には主に3つの主要なロック関連部分を含む4つの段階があります:
PrepareApply - ミューテックスの外で実行されるため、ここでは無視しても問題ありません。
SaveTo(第1段階) - バージョンの変更内容を基本のLSMツリーファイルセットと統合し、新しいLSMツリーファイルセットを生成します。
- リリースビルドではこのCheckConsistencyチェックはスキップされます
- SaveSSTFilesToではマージソートを実行し、ソート結果に基づいてファイルの配置を生成します。SSTファイルは作成後変更できないため、この操作はミューテックスで保護する必要はありません
Unref ~Version(パート2) — LSM-treeファイルセットの現在のバージョンを削除し、関連する領域を解放します。
-
~Versionはバージョンリストからの削除が必要で、これはミューテックスによって保護されています
-
ただし、VersionのサブフィールドであるVersionStorageInfoはバックグラウンドスレッドで解放可能なため、この処理の全ての部分がミューテックスを必要とするわけではありません
AppendVersion(パート3) — 新しいバージョンを追加し、現在のバージョンの参照を解除します。 -
この処理は共有されているバージョンリストを直接操作するため、バージョンの整合性を保証する必要があり、ミューテックスの外に移動させるのは困難です。
私たちの最適化:ミューテックス外での処理移動
この問題を解決するため、VersionStorageInfoをスタックオブジェクトではなくヒープ上に割り当てるように変更し、SaveTo処理の重い部分をミューテックスの外に移動させました。
具体的には:
- VersionStorageInfoをメンバオブジェクトからポインタに変換し、非同期のバックグラウンド削除を可能にしました
- 競合を減らすため、SaveTo処理(マージロジック)をミューテックスの外に移動させました
- RocksDBの環境スケジューラを利用して、VersionStorageInfoのバックグラウンドクリーンアップを追加しました
この変更により、CPU負荷の高い処理のほとんどがグローバルミューテックスから切り離され、並行処理能力が大幅に向上しました。この変更の詳細はこちらをご覧ください:LogAndApply処理のミューテックス内実行時間の最適化
結果:テイルレイテンシが100倍改善
この最適化を適用した結果、TiKVにおけるローカル読み取り非同期スナップショットのp999レイテンシが約100msから1ms未満に減少しました。
(左:ベースライン、右:最適化後)
振り返り
これはTiKVのために私たちがRocksDBに対して行った最適化の一つです。
この経験から私たちは重要な教訓を得ました。時には問題はコード自体ではなく、疑問を持たずに使っていたロックにあるということです。
このシリーズの次回記事では、他の事例も紹介していきます。書き込み用ロックとデータベースロックの分離、書き込み増幅率に基づくレート制限、PerfFlagを使ったパフォーマンス計測など――それぞれに独自の苦労話や技術的なトレードオフがあります。ぜひご期待ください。
次の展開
次世代のTiDB Xでは、さらに一歩進んだ進化を遂げています。私たちはストレージエンジン全体を再構築し、もはやRocksDBに依存しない設計にしました。
現在は直接オブジェクトストレージ上に構築されており、クラウド時代に合わせてゼロから設計されたこのシステムは、RocksDBでは実現できなかったレベルの拡張性、コスト効率、柔軟性を備えています。
今後の記事では、TiDB Xについてさらに詳しく紹介し、クラウドネイティブなストレージアーキテクチャをどのように再定義しているかを解説していきます。現在のTiDBがどのような機能を持っているか知りたい方は、無料のTiDB Cloudクラスターを数分で簡単に試してみることができます。

