この記事では、2017年8月28日に開催されたRocksDBミートアップでSiddon Tangが行った講演の内容を紹介します。
講演者の紹介
RocksDBチームのみなさんこんにちは。本日はお招きいただきありがとうございます。
これから、TiKVでRocksDBをどのように利用しているのか説明していきたいと思います。本題に入る前に、簡単な自己紹介をします。PingCAPのチーフエンジニアを務めているSiddon Tangと申します。現在、次世代SQLデータベースであるTiDBと、キーと値の分散型トランザクションストアであるTiKVの開発に携わっています。私はオープンソースを強く支持しており、LedisDB(バックエンドエンジンもRocksDB)、go-mysql、go-mysql-elasticsearchなどのオープンソースプロジェクトを推進してきました。
アジェンダ
本日は、以下の事項を説明します。
- なぜRocksDBを選択したのか
- TiKVではどのようにRocksDBを利用しているのか
- RocksDBに対し、どのような貢献をしているのか
- 今後のプラン
なぜRocksDBを選択したのか
それでは始めましょう。当社は、LevelDBやWiredTiger等のエンジンではなく、RocksDBを利用することに決めました。それはなぜでしょうか。多くの理由があります。
- 第一に、RocksDBは高速です。単一のインスタンスに大量のデータがあっても、書き込み/読み取りのスピードを維持できます。
- そして言うまでもなく、RocksDBは安定しています。RocksDBチームは、安定性を保証するために多くのストレステストを行ってきました。
- また、組み込みが容易です。TiKVはRustで記述されているので、FFIを介し、RocksDBのC APIをRustで直接呼び出すことができます。
- 多くの役立つ機能を備えているほか、実稼働環境でそれらの機能を直接利用し、パフォーマンスを改善することができます。
- さらに、RocksDBは今でも速いペースで開発が進んでいます。多くの素晴らしい機能が追加され、パフォーマンスが改善し続けています。
- その上、RocksDBのコミュニティは非常に活発です。何かわからないことがあったら、気軽に相談することができます。多くのRocksDBチームメンバーと私たちは、WeChat(中国で広く普及しているIMツール)フレンドでもありますので、お互いに直接会話することができます。
RocksDBをどのように利用しているのか
TiKVのアーキテクチャ
RocksDBを利用することを決断した後、次の問題となるのは、TiKVでRocksDBをどのように利用するかということです。TiKVアーキテクチャの簡単な説明から始めましょう。
第一に、TiKVノードのデータはすべて、2つのRocksDBインスタンスを共有しています。1つはデータ用、もう1つはRaftログ用です。
リージョン
リージョンとは、ある範囲のデータを対象とした、論理的概念です。各リージョンは複数のマシンに常駐しており、多数の複製を持っています。これらすべての複製が1つのRaftグループを形成します。
Raft
TiKVはRaftコンセンサスアルゴリズムを使用してデータを複製します。したがって、書き込みリクエストがある度に、私たちはまずRaftログにリクエストを書き込み、ログがコミットされた後、Raftログを適用してデータを書き込みます。
RocksDBに保存されるRaftログのキーフォーマットは、リージョンIDにログIDを付けたものになります。ログIDは単調に増加していきます。
InsertWithHint
新しいRaftログはすべて、リージョンに追加します。例えば、まずリージョン1にログ1を追加し、その後同一のリージョンにログ2を追加します。ヒント機能を持ったMemtable挿入を利用します。この機能によって、挿入パフォーマンスは15パーセント以上向上します。
該当するバージョンはサフィックスとしてキー内に組み込まれ、ACIDトランザクションに対応するために利用されます。しかし、トランザクション管理については、今回のトピックから外れるので、説明は省略します。
プレフィックスイテレーター
ご存知のとおり、私たちはタイムスタンプサフィックスを付けてキーを保存します。ところが、タイムスタンプがないキーしかシークできないので、プレフィックス抽出機能を設定し、Memtableブルームフィルタを有効化します。これにより、読み取りパフォーマンスを10パーセント以上改善することができます。
リージョン分割チェックのためのテーブルプロパティ
多くのデータをリージョンに挿入していると、データサイズは所定のしきい値をすぐに超えてしまいます。すると、リージョンの分割が必要になります。
以前の実装では、まずリージョンの範囲内のデータをスキャンし、次いでデータの合計サイズを計算し、合計サイズがしきい値を超えていれば、そのリージョンを分割していました。
リージョンのスキャンにはI/Oコストが多くかかっていました。しかし、現在は代わりにテーブルプロパティを利用します。圧縮を行う際にデータ合計サイズをSSTテーブルプロパティに記録します。範囲内のすべてのテーブルプロパティを取得し、その後、合計サイズを合算します。
最終的に合算されたデータサイズは概算となりますが、この方法は効率がよく、無意味なスキャンを回避できるので、I/Oコストが削減されます。
GCチェックのためのテーブルプロパティ
キーについては、私たちは複数のバージョンを利用しており、古くなったキーは定期的に削除します。しかし、ある範囲においてGCを実行する必要があるかどうかわかりません。この場合、以前はすべてのデータをスキャンするしかありませんでした。
ところが、GCを実行する必要があるのは所定のセーフポイントの前だけであり、ほとんどのキーには1つのバージョンしかないため、これらのキーを毎回スキャンするのは非効率です。
そこで、MVCCプロパティコレクターを作成し、バージョン情報を収集します。このバージョン情報には、最大・最小タイムスタンプ、行番号、バージョン番号などが含まれています。すると、範囲をスキャンする前に毎回、これらのプロパティをチェックし、GC手順をスキップできるかどうかを確認することができます。
例えば、テーブルプロパティの最小タイムスタンプがセーフポイントよりも大きいことがわかれば、その範囲のスキャンを即座にスキップすることができます。
SSTファイルの取り込み
以前の実装の場合、一括読み込みを行うときは、該当する範囲内のすべてのキーと値をスキャンし、ファイルに保存する必要がありました。その後、別のRocksDBで、ファイルからすべてのキーと値を読み取り、それらをバッチで挿入していました。
ご存知のとおり、このフローは非常に遅く、RocksDBには高い負荷がかかる可能性があります。そこで現在は、代わりにIngestFile
機能を利用します。最初にキーと値をスキャンし、それらをSSTファイルに保存します。その後、SSTファイルを直接取り込みます。
その他
上記以外にも、サブ圧縮やパイプライン方式の書き込みを実現しているほか、ダイレクトI/Oを利用して圧縮やフラッシュを行っています。これらの素晴らしい機能のおかげで、パフォーマンスを改善することができます。
どのような貢献をしているのか
私たちは、RocksDBを利用しているだけではなく、コミュニティに貢献できるよう最善の努力をしています。多くのストレステストを行い、重大なデータ破損バグをいくつか発見しました。一部の例を挙げます。
- #1339: 同期書き込み + WALは、依然として最新データを喪失させる場合がある。
- #2722: 削除済みのキーの一部が、圧縮後に表示される場合がある。
- #2743: 範囲削除とMemtableプレフィックスブルームフィルタのバグ。
幸いにも、ユーザーの実稼働環境でこれらの問題が発生したとの報告はありません。
また、機能を追加したり、いくつかのバグを修正したりしました。TiKVで呼び出せるのはRocksDB C APIのみなので、私たちは欠落しているC APIをRocksDB向けに多く追加しています。
- #2170: WriteBatchのPopSavePointをサポート。
- #2463: nullptrをリリースする際のcoredumpを修正。
- #2552: CMake、対応できる圧縮タイプを追加。
- さまざまなC API
今後のプラン
今後、DeleteRange APIを開発する予定ですが、これは非常に便利なAPIです。ただ現在のところ、2752や2833のバグが見つかっており、これを修正するために最大限努力しています。もちろん、RocksDBチームとも協力しています。
また、BLOB DBの安定性が確認されたら、これを試用する予定です。一方、挿入パフォーマンスを向上させるため、さまざまなMemtableタイプを試してみる予定です。また、SATAディスク向けにパーティションインデックスやパーティションフィルタを利用していきます。
TiDB Cloud Dedicated
TiDB Cloudのエンタープライズ版。
専用VPC上に構築された専有DBaaSでAWSとGoogle Cloudで利用可能。
TiDB Cloud Serverless
TiDB Cloudのライト版。
TiDBの機能をフルマネージド環境で使用でき無料かつお客様の裁量で利用開始。