TiDBコンピューティング
TiKV が提供する分散storageに基づいて、TiDB は、トランザクション処理の優れた機能とデータ分析の優れた機能を組み合わせたコンピューティング エンジンを構築します。このドキュメントでは、まず、TiDB データベース テーブルから TiKV の (キー、値) キーと値のペアにデータをマッピングするデータ マッピング アルゴリズムを紹介し、次に TiDB がメタデータを管理する方法を紹介し、最後にTiDB SQLレイヤーのアーキテクチャについて説明します。
コンピューティングレイヤーが依存するstorageソリューションについては、このドキュメントでは TiKV の行ベースのstorage構造のみを紹介します。OLAP サービスについては、TiDB は TiKV 拡張機能として列ベースのstorageソリューションTiFlashを紹介します。
テーブルデータをキー値にマッピングする
このセクションでは、TiDB 内の (キー、値) キーと値のペアにデータをマッピングするスキームについて説明します。ここでマッピングされるデータには、次の 2 つのタイプが含まれます。
- テーブル内の各行のデータ(以下、テーブルデータと呼びます)。
- テーブル内のすべてのインデックスのデータ(以下、インデックス データと呼びます)。
テーブルデータからキー値へのマッピング
リレーショナル データベースでは、テーブルに多くの列がある場合があります。行内の各列のデータを (キー、値) キーと値のペアにマップするには、キーの構築方法を考慮する必要があります。まず、OLTP シナリオでは、1 行または複数行のデータの追加、削除、変更、検索などの操作が多く、データベースがデータ行をすばやく読み取る必要があります。したがって、各キーには一意の ID (明示的または暗黙的) があり、すばやく見つけられるようにする必要があります。次に、多くの OLAP クエリでは完全なテーブル スキャンが必要です。テーブル内のすべての行のキーを範囲にエンコードできる場合は、範囲クエリによってテーブル全体を効率的にスキャンできます。
上記の考慮事項に基づいて、TiDB のテーブル データと Key-Value のマッピングは次のように設計されます。
- 同じテーブルのデータが簡単に検索できるようにまとめられるよう、TiDB は各テーブルに
TableID
で表されるテーブル ID を割り当てます。テーブル ID はクラスター全体で一意の整数です。 - TiDB は、テーブル内の各データ行に
RowID
で表される行 ID を割り当てます。行 ID も整数であり、テーブル内で一意です。行 ID については、TiDB は小さな最適化を行っています。テーブルに整数型の主キーがある場合、TiDB はこの主キーの値を行 ID として使用します。
各データ行は、次の規則に従って (キー、値) キーと値のペアとしてエンコードされます。
Key: tablePrefix{TableID}_recordPrefixSep{RowID}
Value: [col1, col2, col3, col4]
tablePrefix
とrecordPrefixSep
どちらも、キー空間内の他のデータを区別するために使用される特別な文字列定数です。文字列定数の正確な値はマッピング関係の概要で紹介されています。
インデックスされたデータのキー値へのマッピング
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
)。
キーと値のマッピング関係の例
このセクションでは、TiDB のキーと値のマッピング関係を理解するための簡単な例を示します。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
各データ行は (キー、値) キーと値のペアにマッピングされ、テーブルには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 のリレーショナル モデルからキー値モデルへのマッピング ルールと、このマッピング スキームの背後にある考慮事項を示しています。
メタデータ管理
TiDB 内の各データベースとテーブルには、その定義とさまざまな属性を示すメタデータがあります。この情報も永続化する必要があり、TiDB はこの情報も TiKV に保存します。
各データベースまたはテーブルには、一意の ID が割り当てられます。一意の識別子として、テーブル データがキー値にエンコードされると、この ID はm_
プレフィックス付きのキーにエンコードされます。これにより、シリアル化されたメタデータが格納されたキーと値のペアが構築されます。
さらに、TiDB は専用の (Key, Value) キーと値のペアを使用して、すべてのテーブルの構造情報の最新のバージョン番号を保存します。このキーと値のペアはグローバルであり、DDL 操作の状態が変化するたびにバージョン番号が1
ずつ増加します。TiDB は、このキーと値のペアを PDサーバーにキー/tidb/ddl/global_schema_version
で永続的に保存し、Value はint64
タイプのバージョン番号値です。一方、TiDB はスキーマ変更をオンラインで適用するため、PDサーバーに保存されているテーブル構造情報のバージョン番号が変更されていないかどうかを常にチェックするバックグラウンド スレッドを維持します。このスレッドにより、バージョンの変更が一定期間内に取得されることも保証されます。
SQLレイヤーの概要
TiDB の SQLレイヤーである TiDB サーバーは、SQL ステートメントをキー値操作に変換し、その操作を分散キー値storageレイヤーである TiKV に転送し、TiKV から返された結果を組み立てて、最終的にクエリ結果をクライアントに返します。
このレイヤーのノードはステートレスです。これらのノード自体はデータを保存せず、完全に同等です。
SQLコンピューティング
SQL コンピューティングの最も単純なソリューションは、前のセクションで説明したテーブルデータからキー値へのマッピングです。これは、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操作
上記の問題を解決するには、計算をstorageノードにできるだけ近づけて、大量の RPC 呼び出しを回避する必要があります。まず、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 サーバーはクエリ結果をユーザーに返す必要があります。