TiDB コンピューティング
TiKV が提供する分散storageに基づいて、TiDB はトランザクション処理の優れた機能とデータ分析の機能を組み合わせたコンピューティング エンジンを構築します。このドキュメントは、TiDB データベース テーブルのデータを TiKV の (Key, Value) キーと値のペアにマッピングするデータ マッピング アルゴリズムを紹介することから始まり、次に TiDB がメタデータを管理する方法を紹介し、最後にTiDB SQLレイヤーのアーキテクチャを説明します。
コンピューティングレイヤーが依存するstorageソリューションについては、このドキュメントでは TiKV の行ベースのstorage構造のみを紹介します。 OLAP サービスの場合、TiDB は TiKV 拡張機能として列ベースのstorageソリューションTiFlashを導入します。
テーブルデータの Key-Value へのマッピング
このセクションでは、TiDB でデータを (Key, Value) キーと値のペアにマッピングするスキームについて説明します。ここでマッピングするデータには以下の2種類が含まれます。
- テーブル内の各行のデータ。以下、テーブルデータと呼びます。
- テーブル内のすべてのインデックスのデータ。以下、インデックスデータと呼びます。
テーブルデータの Key-Value へのマッピング
リレーショナル データベースでは、テーブルに多くの列が含まれる場合があります。行内の各列のデータを (Key, Value) キーと値のペアにマップするには、キーの構築方法を検討する必要があります。まず、OLTP シナリオでは、単一行または複数行のデータの追加、削除、変更、検索などの多くの操作があり、データベースがデータ行を迅速に読み取る必要があります。したがって、各キーには、すばやく見つけられるように一意の ID (明示的または暗黙的) が必要です。さらに、多くの OLAP クエリではテーブル全体のスキャンが必要になります。テーブル内のすべての行のキーを範囲にエンコードできれば、テーブル全体を範囲クエリで効率的にスキャンできます。
上記の考慮事項に基づいて、TiDB でのテーブル データの Key-Value へのマッピングは次のように設計されています。
- 同じテーブルのデータが簡単に検索できるようにまとめて保持されるようにするために、TiDB は
TableID
で表される各テーブルにテーブル ID を割り当てます。テーブル ID は、クラスター全体で一意の整数です。 - TiDB は、テーブル内のデータの各行に
RowID
で表される行 ID を割り当てます。行 ID も整数であり、テーブル内で一意です。行 ID に関して、TiDB は小さな最適化を行いました。テーブルに整数型の主キーがある場合、TiDB はこの主キーの値を行 ID として使用します。
データの各行は、次のルールに従って (Key, Value) キーと値のペアとしてエンコードされます。
Key: tablePrefix{TableID}_recordPrefixSep{RowID}
Value: [col1, col2, col3, col4]
tablePrefix
とrecordPrefixSep
両方とも、キー空間内の他のデータを区別するために使用される特別な文字列定数です。文字列定数の正確な値はマッピング関係の概要で紹介されています。
インデックス付きデータの Key-Value へのマッピング
TiDB は、主キーと副次インデックス (一意のインデックスと非一意のインデックスの両方) の両方をサポートします。テーブル データ マッピング スキームと同様に、TiDB はIndexID
で表されるテーブルの各インデックスにインデックス ID を割り当てます。
主キーと一意のインデックスの場合、キーと値のペアに基づいて対応するRowID
すばやく見つける必要があるため、そのようなキーと値のペアは次のようにエンコードされます。
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: RowID
一意性制約を満たす必要のない通常のセカンダリ インデックスの場合、1 つのキーが複数の行に対応する可能性があります。キーの範囲に応じて、対応するRowID
クエリする必要があります。したがって、キーと値のペアは次の規則に従ってエンコードする必要があります。
Key: tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID}
Value: null
マッピング関係の概要
上記のすべてのエンコード規則のtablePrefix
、 recordPrefixSep
、およびindexPrefixSep
は、KV をキー空間内の他のデータから区別するために使用される文字列定数であり、次のように定義されます。
tablePrefix = []byte{'t'}
recordPrefixSep = []byte{'r'}
indexPrefixSep = []byte{'i'}
また、上記のエンコード スキームでは、テーブル データまたはインデックス データ キーのエンコード スキームに関係なく、テーブル内のすべての行が同じキー プレフィックスを持ち、インデックスのすべてのデータも同じプレフィックスを持つことに注意してください。このようにして、同じプレフィックスを持つデータが TiKV のキー空間にまとめて配置されます。したがって、エンコード前とエンコード後の比較が同じになるようにサフィックス部分のエンコード方式を慎重に設計することで、テーブル データまたはインデックス データを順序どおりに TiKV に保存できます。このエンコーディング スキームを使用すると、テーブル内のすべての行データが TiKV のキー空間にRowID
順番に配置され、特定のインデックスのデータもインデックス データの特定の値に従ってキー空間に順番に配置されます ( indexedColumnsValue
)。
Key-Value マッピング関係の例
このセクションでは、TiDB の Key-Value マッピング関係を理解するための簡単な例を示します。 TiDB に次のテーブルが存在するとします。
CREATE TABLE User (
ID int,
Name varchar(20),
Role varchar(20),
Age int,
PRIMARY KEY (ID),
KEY idxAge (Age)
);
テーブルに 3 行のデータがあるとします。
1, "TiDB", "SQL Layer", 10
2, "TiKV", "KV Engine", 20
3, "PD", "Manager", 30
データの各行は (Key, Value) キーと値のペアにマップされ、テーブルにはint
種類の主キーがあるため、値RowID
がこの主キーの値になります。テーブルのTableID
10
で、TiKV に保存されるテーブル データは次のようになるとします。
t10_r1 --> ["TiDB", "SQL Layer", 10]
t10_r2 --> ["TiKV", "KV Engine", 20]
t10_r3 --> ["PD", " Manager", 30]
主キーに加えて、テーブルには非一意の通常の副次インデックスidxAge
があります。 IndexID
が1
であると仮定すると、TiKV に保存されるインデックス データは次のようになります。
t10_i1_10_1 --> null
t10_i1_20_2 --> null
t10_i1_30_3 --> null
上の例は、TiDB におけるリレーショナル モデルから Key-Value モデルへのマッピング ルールと、このマッピング スキームの背後にある考慮事項を示しています。
メタデータ管理
TiDB の各データベースとテーブルには、その定義とさまざまな属性を示すメタデータがあります。この情報も永続化する必要があり、TiDB はこの情報を TiKV にも保存します。
各データベースまたはテーブルには一意の ID が割り当てられます。一意の識別子として、テーブル データが Key-Value にエンコードされる場合、この ID はm_
というプレフィックスを付けて Key にエンコードされます。これにより、格納されているシリアル化されたメタデータを使用してキーと値のペアが構築されます。
さらに、TiDB は、専用の (Key, Value) キーと値のペアを使用して、すべてのテーブルの構造情報の最新バージョン番号を保存します。このキーと値のペアはグローバルであり、DDL 操作の状態が変化するたびにバージョン番号が1
ずつ増加します。 TiDB は、このキーと値のペアを/tidb/ddl/global_schema_version
というキーを使用して PDサーバーに永続的に保存します。 Value はint64
タイプのバージョン番号の値です。一方、TiDB はスキーマ変更をオンラインで適用するため、PDサーバーに格納されているテーブル構造情報のバージョン番号が変更されているかどうかを常にチェックするバックグラウンド スレッドを保持します。このスレッドは、バージョンの変更を一定期間内に取得できることも保証します。
SQLレイヤーの概要
TiDB の SQLレイヤーである TiDB サーバーは、SQL ステートメントを Key-Value オペレーションに変換し、そのオペレーションを分散 Key-Valuestorageレイヤーである TiKV に転送し、TiKV から返された結果を組み立てて、最後にクエリ結果をクライアントに返します。
このレイヤーのノードはステートレスです。これらのノード自体はデータを保存せず、完全に同等です。
SQLコンピューティング
SQL コンピューティングの最も単純なソリューションは、前のセクションで説明したテーブルデータの Key-Value へのマッピングです。これは、SQL クエリを KV クエリにマップし、KV インターフェイスを通じて対応するデータを取得し、さまざまな計算を実行します。
たとえば、 select count(*) from user where name = "TiDB"
SQL ステートメントを実行するには、TiDB はテーブル内のすべてのデータを読み取り、 name
フィールドがTiDB
であるかどうかを確認し、そうであればこの行を返します。プロセスは次のとおりです。
- キー範囲を作成します。テーブル内のすべての
RowID
は[0, MaxInt64)
範囲にあります。行データKey
エンコード規則に従って、0
とMaxInt64
を使用すると、左が閉じて右が開く[StartKey, EndKey)
範囲を構築できます。 - スキャン キー範囲: 上記で構築されたキー範囲に従って TiKV 内のデータを読み取ります。
- データのフィルター: 読み取られたデータの各行に対して、
name = "TiDB"
の式を計算します。結果がtrue
場合は、この行に戻ります。そうでない場合は、この行をスキップしてください。 Count(*)
を計算します。要件を満たす各行について、結果をCount(*)
に加算します。
プロセス全体を次のように図示します。
このソリューションは直感的で実現可能ですが、分散データベースのシナリオでは明らかな問題がいくつかあります。
- データのスキャン中に、少なくとも 1 つの RPC オーバーヘッドを伴う KV 操作を介して TiKV から各行が読み取られます。スキャンするデータが大量にある場合、このオーバーヘッドは非常に高くなる可能性があります。
- すべての行に適用できるわけではありません。条件を満たさないデータは読み出す必要はありません。
- このクエリの返された結果から、必要なのは、それらの行の値ではなく、要件に一致する行の数のみです。
分散SQL操作
上記の問題を解決するには、多数の RPC 呼び出しを避けるために、計算をできるだけstorageノードの近くで行う必要があります。まず、SQL 述語条件name = "TiDB"
を計算のためにstorageノードにプッシュダウンして、有効な行のみが返されるようにする必要があります。これにより、無意味なネットワーク転送が回避されます。次に、集約関数Count(*)
事前集約のためにstorageノードにプッシュダウンすることもでき、各ノードは結果Count(*)
を返すだけで済みます。 SQLレイヤーは、各ノードから返されたCount(*)
結果を合計します。
次の図は、データがレイヤーレイヤーにどのように返されるかを示しています。
SQLレイヤーのアーキテクチャ
前のセクションでは、SQLレイヤーのいくつかの関数を紹介しました。SQL ステートメントがどのように処理されるかについての基本を理解していただければ幸いです。実際、TiDB の SQLレイヤーははるかに複雑で、多くのモジュールとレイヤーがあります。次の図は、重要なモジュールと呼び出し関係を示しています。
ユーザーの SQL リクエストは、直接またはLoad Balancer
を介して TiDB サーバーに送信されます。 TiDB サーバーはMySQL Protocol Packet
解析し、リクエストの内容を取得し、SQL リクエストを構文的および意味的に解析し、クエリ プランを開発および最適化し、クエリ プランを実行し、データを取得して処理します。すべてのデータは TiKV クラスターに保存されるため、このプロセスでは、TiDB サーバーが TiKV と対話してデータを取得する必要があります。最後に、TiDB サーバーはクエリ結果をユーザーに返す必要があります。