
※このブログは2025年3月31日に公開された英語ブログ「Write Latency, Solved: TiKV’s Journey to Smoother Performance」の拙訳です。
毎秒数千もの同時書き込みを処理している場合でも、急増する需要に対応するためにインフラストラクチャをスケールアウトしている場合でも、レイテンシの急上昇はユーザーエクスペリエンス、信頼性、そして信用を損なう可能性があります。PingCAPでは、これらの細部に徹底的にこだわっています。私たちの使命は、分散SQL上でチームが自信を持ってシステムを構築し、スケールできるように支援することです。このブログ記事では、ストレージエンジンの奥深くにある微妙なパフォーマンス上の課題、つまりデータ取り込み中の書き込み停止が、本番環境のワークロード全体に波及し、TiKVの書き込みレイテンシに影響を与える可能性について解説します。
このブログでは、RocksDB、Raft、そしてTiKVがどのように相互作用するのかを深く掘り下げ、正確性を損なうことなく書き込み停止を解消する2つの主要な最適化を通じて、この書き込み停止の課題をどのように解決したのかを解説します。
一括データ取り込みがTiKVの書き込みレイテンシを遅くする場合
TiDBは分散SQLデータベースであり、現代のアプリケーションに合わせて容易にスケールします。内部的には、パフォーマンスと柔軟性を最適化するために、コンピューティング層とストレージ層を分離しています。TiKVは、オープンソースの分散キーバリューエンジンであり、TiDBのトランザクションワークロードを支えるストレージ層として機能します。
TiDBが大量のデータを移動する必要がある場合、たとえばリージョン移行、クラスタのリバランス、またはスケーリング操作中などには、データを迅速かつ確実にロードするためにTiKVに依存します。
TiKVは、組み込みストレージエンジンとしてRocksDBを使用しています。大量のデータをロードする必要がある場合、Sorted String Table (SST) ファイルをストレージに直接インポートする IngestExternalFile()
というメソッドを使用します。このアプローチは通常の書き込みパスをバイパスするため、メモリへの負荷を軽減し、コンパクションのオーバーヘッドを最小限に抑えます。RocksDBの取り込みプロセスは、特にフォアグラウンドパスを介してデータを書き込む場合と比較して、速度と効率性を重視して設計されています。
しかし、この速度には注意点があります。RocksDBのログ構造マージツリー (LSMツリー) におけるシーケンス番号の一貫性を維持するために、取り込み処理は一時的にすべてのフォアグラウンド書き込みをブロックします。この一時的な停止は、書き込み停止 (ライトストール) と呼ばれます。
単独で見ると、短い書き込み停止は問題にならないかもしれません。しかし、同じTiKVノード上のすべてのリージョン (データの論理パーティション) は、単一のRocksDBインスタンスを共有しているため、あるリージョンが書き込み停止を引き起こすと、そのノード上のすべてのリージョンが影響を受けます。たとえそれらが無関係であってもです。大規模な移行やクラスタのスケーリング中には、これが広範囲にわたる書き込みレイテンシの急上昇につながり、通常であれば無関係なワークロードのパフォーマンスに影響を与える可能性があります。

なぜシーケンス番号がTiKVの書き込みレイテンシーに影響するのか
RocksDBのログ構造マージツリー (LSMツリー) では、データは複数のレベルにまたがって存在します。一貫性を維持し、高速かつ正確な読み取りを保証するために、RocksDBはすべての書き込みにグローバルなシーケンス番号を割り当てます。
これらのシーケンス番号は、SST (Sorted String Table) ファイルの取り込み中に特に重要になります。SSTは、RocksDBがディスク上にデータを効率的に格納するために使用する、変更不可能でソートされたファイルです。 IngestExternalFile()
を介して新しいSSTファイルが取り込まれる際には、RocksDBが強制するシーケンス番号の順序付けルールに従う必要があります。
具体的には以下の通りです。
- より低いレベル (L0など) に格納されたキーは、より高いレベル (L6など) にあるそのキーのどのバージョンよりも大きいシーケンス番号を持つ必要があります。
- この順序付けにより、RocksDBは不必要に深いレベルをスキャンすることなく、最新の値を迅速に返すことができます。
- また、SST全体をスキャンする際に、古いまたは部分的な読み取りを防ぎます。
これを強制するために、RocksDBは取り込み時に厳格な手順を踏みます。
- 取り込み前のキーの重複:新しいSST内のキーがMemTable内のキーと重複する場合、RocksDBはまずMemTableをL0にフラッシュします。フラッシュされたデータはシーケンス番号を受け取り、SSTは同じレベルに配置されますが、より大きいシーケンス番号が割り当てられます。
- 取り込み中のキーの重複:RocksDBは書き込み停止 (ライトストール) を引き起こし、すべてのフォアグラウンド書き込みを一時停止します。これはロックのように機能し、SST内のすべてのキーが一貫したグローバルシーケンス番号を受け取ることを保証します。
- 取り込み後のキーの重複:SSTファイルが取り込まれ、書き込みが再開されると、同じキーへの新しい書き込みはMemTableに送られ、より新しいシーケンス番号を受け取ります。
このプロセスは、読み取りが常に最新の状態を反映し、スキャン結果で同じキーの古いバージョンと新しいバージョンが混在するのを防ぐことを保証します。
読み取りの正確性を維持するために、RocksDBは、取り込まれたSSTが、より低いレベルにある重複するどのデータよりも大きいシーケンス番号を持つ必要があることを強制します。これは一貫性を保つための書き込み停止 (ライトストール) によって強制されます。

最初のTiKV書き込み遅延最適化:フラッシュをスキップしてストールを削減
SSTファイル取り込み中の書き込み停止時間の主な要因の1つは、MemTableのフラッシュです。フラッシュは時間のかかる高コストなI/O操作であり、すべての書き込みを一時的にブロックします。 IngestExternalFile()
は、MemTable内で重複するキー範囲を検出した場合にフラッシュをトリガーする可能性があるため、多くの場合、不必要に停止時間を延長してしまいます。
この問題に対処するため、TiKVはTiKV#3775で、RocksDBの allow_blocking_flush
と呼ばれる機能を使用した最適化を実装しました。
どのように機能するのか
RocksDBに取り込み中のフラッシュのタイミングを任せる代わりに、現在のTiKVは次の処理を行います。
- まず、
allow_blocking_flush = false
で取り込みを試みます。フラッシュが不要な場合、取り込みはすぐに進行します。 - 必要なフラッシュが原因で取り込みが失敗した場合、TiKVは書き込み停止の外部でMemTableを手動でフラッシュします。
- その後、
allow_blocking_flush = true
で取り込みを再試行します。これにより、追加の停止を引き起こすことなく成功します。
影響
MemTableのフラッシュを取り込みのパスから取り除いた後、書き込み停止時間は大幅に改善され、最悪のシナリオでは最大100倍高速になりました。

2つ目のTiKV書き込み遅延最適化:書き込み停止を完全に排除
最初の最適化で書き込み停止の時間は劇的に短縮されましたが、それでも発生する可能性はありました。次の疑問は、「書き込み停止を完全に排除できるか?」ということでした。
前提:Raftによるシーケンシャル書き込み
TiKVはRaftコンセンサスプロトコルを使用しており、リージョンごとにシーケンシャルなコミットを強制します。通常の状態では、以下のいずれの場合でも一度に1つのスレッドのみがリージョンに書き込みます。
- リージョン作成時 (SSTの取り込み)
- 通常動作時 (クライアント書き込みの処理)
- リージョン破棄時 (クリーンアップ)
このシングルスレッドの書き込み動作は、ある一つの例外的なケースを除いて、取り込み中に書き込みが重複することはないと安全に仮定できることを意味します。
例外:コンパクション・フィルター・ガベージコレクション
TiKVは、RocksDBのコンパクションフィルタメカニズムを使用して、MVCC (多版型同時実行制御) ガベージコレクションを実装しています。これはRaftの外側、RocksDBレイヤーで直接動作します。
ここで複雑な問題が生じます。
TiKVは、単一のRocksDBインスタンス内の別々の論理パーティションである複数のカラムファミリー(CF) を使用します。各CFは異なる種類のデータを格納します。
- Default CFは、ユーザーデータ (実際の値) を保持します。
- Write CFは、キーの有効性を決定するコミットタイムスタンプなどのバージョンメタデータを格納します。
- Lock CFは、トランザクションの状態を処理します。
GC中、コンパクションフィルタは Write CF上で実行され、キーの古いバージョンを特定して破棄します。しかし、クリーンアップを完了するには、TiKVは Default CF内の対応する値も削除する必要があり、これにはRocksDBの Write()
メソッドの呼び出しが必要です。これらの削除は、Raftレイヤーの外側のフォアグラウンド書き込みとして発生し、SST取り込みと重複する可能性があります。
これにより、書き込みの重複がないという仮定が破られ、潜在的な競合状態が発生します。
解決策:書き込みをブロックせずに取り込む
正確性を維持しながら書き込み停止を解消するために、TiKVは2つの主要な変更を導入しました。
allow_write = trueで取り込みを有効にする
RocksDB#400で新しいオプションが追加され、書き込み停止をトリガーせずに取り込みを進めることができるようになりました。allow_write = true
の場合、取り込み中に書き込みが重複しないようにするのは、RocksDBのユーザー (TiKV) の責任となります。
安全のためのレンジラッチの追加
その保証を確実にするため、TiKVはTiKV#18096でレンジラッチを追加しました。SST取り込みまたはGCが開始される前に、プロセスは関連するキー範囲に対するロックを取得する必要があります。これにより、以下のことが保証されます。
- GCによってトリガーされるフォアグラウンド書き込みが、取り込みを妨害しません。
IngestExternalFile()
はallow_write = true
で安全に実行でき、書き込み停止を完全に回避できます。
影響
これらの変更により、TiKVは書き込みをブロックすることなく安全にSSTを取り込むことができ、負荷の高い状況下でのテールレイテンシと書き込みパフォーマンスを劇的に向上させます。
TPCCベンチマークの結果
- P9999の書き込みスレッド待機時間が90%以上減少 (25ms → 2ms)

- P99の書き込み待ち時間が50%以上短縮 (2〜4ms → 1ms)

まとめ
SST取り込み中に発生する書き込み停止という、ストレージ層におけるわずかな課題として始まったものが、TiKVにとって意義深いパフォーマンスの飛躍的向上へと繋がりました。
RocksDBがシーケンス番号の一貫性をどのように処理するかを深く理解し、的を絞った最適化を設計することで、TiKVチームは2つの重要な改善を実現しました。
- MemTableフラッシュを取り込みパスの外に移動することで書き込み停止時間を短縮 (
allow_blocking_flush = false
を使用) - コンパクションフィルタGCとインジェストを
allow_write = true
とレンジラッチを通じて連携させることで、書き込み停止を完全に解消
これらの変更は、測定可能な影響をもたらしました。
- 最大書き込み時間がほぼ100倍向上
- P9999の書き込み待機時間が90%以上減少
- P99の書き込みレイテンシが半分以下に減少
これらの最適化が連携することで、TiKVは、リージョン移行、ガベージコレクション、またはクラスタのスケールアウトによる負荷の下でも、チームが分散データベースに期待するような、予測可能でスケーラブルな書き込みパフォーマンスを提供できるようになります。
TiKVまたはTiDB上に構築しており、データ移動中またはスケーリング中のパフォーマンスの安定性がワークロードにとって不可欠な場合、これらの最適化はすでに活用されています。また、ご自身のシステムで同様の問題に取り組んでいる場合は、このブログが、私たちにとって有効だったトレードオフと解決策について、役立つ視点を提供できたことを願っています。
私たちは常に学びを深め、共有しています。ご質問がありましたら、X (旧Twitter) 、LinkedIn、またはSlackチャンネルを通じてお気軽にお問い合わせください。
TiDB Cloud Dedicated
TiDB Cloudのエンタープライズ版。
専用VPC上に構築された専有DBaaSでAWSとGoogle Cloudで利用可能。
TiDB Cloud Serverless
TiDB Cloudのライト版。
TiDBの機能をフルマネージド環境で使用でき無料かつお客様の裁量で利用開始。