高同時書き込みのベストプラクティス
このドキュメントでは、TiDB で高同時書き込み負荷の高いワークロードを処理するためのベスト プラクティスについて説明します。これは、アプリケーション開発を容易にするのに役立ちます。
対象読者
このドキュメントは、TiDBの基礎知識があることを前提としています。まず、TiDBの基礎を解説した以下の3つのブログ記事と、 TiDB ベストプラクティス読みいただくことをお勧めします。
同時書き込みの多いシナリオ
高度な同時書き込みシナリオは、クリアリングや決済などのアプリケーションでバッチタスクを実行する際によく発生します。このシナリオには、次のような特徴があります。
- 膨大な量のデータ
- 履歴データを短時間でデータベースにインポートする必要性
- 短時間でデータベースから膨大な量のデータを読み取る必要がある
これらの機能は TiDB に次のような課題をもたらします。
- 書き込み容量または読み取り容量は線形に拡張可能である必要があります。
- 大量のデータが同時に書き込まれてもデータベースのパフォーマンスは安定し、低下しません。
分散データベースでは、すべてのノードの能力を最大限に活用し、単一のノードがボトルネックにならないようにすることが重要です。
TiDBにおけるデータ分散原則
上記の課題に対処するには、TiDBのデータセグメンテーションとスケジューリングの原則から始める必要があります。詳細についてはスケジュールを参照してください。
TiDBはデータをリージョンに分割します。リージョンはそれぞれ、デフォルトで96MBのサイズ制限を持つデータ範囲を表します。各リージョンには複数のレプリカがあり、レプリカの各グループはRaftグループと呼ばれます。Raftグループでは、RaftLeaderがデータ範囲内の読み取りおよび書き込みタスク(TiDBはフォロワー読み取りサポート)を実行します。リージョンLeaderは、 Placement Driver (PD)コンポーネントによって自動的に異なる物理ノードにスケジュールされ、読み取りおよび書き込みの負荷を均等に分散します。

理論上、アプリケーションに書き込みホットスポットがない場合、TiDBはそのアーキテクチャの特性により、読み取りおよび書き込み容量を線形に拡張できるだけでなく、分散リソースを最大限に活用できます。この点から、TiDBは特に、同時実行性が高く書き込み負荷の高いシナリオに適しています。
しかし、実際の状況は理論上の想定とは異なることがよくあります。
注記:
アプリケーションに書き込みホットスポットがないということは、書き込みシナリオに
AUTO_INCREMENT主キーまたは単調に増加するインデックスがないことを意味します。
ホットスポット事件
ホットスポットがどのように生成されるかを、以下のケースで説明します。以下の表を例に挙げます。
CREATE TABLE IF NOT EXISTS TEST_HOTSPOT(
id BIGINT PRIMARY KEY,
age INT,
user_name VARCHAR(32),
email VARCHAR(128)
)
このテーブルは構造が単純です。主キーのid以外に、セカンダリインデックスは存在しません。このテーブルにデータを書き込むには、次のステートメントを実行してください。3 id乱数として離散的に生成されます。
SET SESSION cte_max_recursion_depth = 1000000;
INSERT INTO TEST_HOTSPOT
SELECT
n, -- ID
RAND()*80, -- Number between 0 and 80
CONCAT('user-',n),
CONCAT(
CHAR(65 + (RAND() * 25) USING ascii), -- Number between 65 and 65+25, converted to a character, A-Z
'-user-',
n,
'@example.com'
)
FROM
(WITH RECURSIVE nr(n) AS
(SELECT 1 -- Start CTE at 1
UNION ALL SELECT n + 1 -- increase n with 1 every loop
FROM nr WHERE n < 1000000 -- stop loop at 1_000_000
) SELECT n FROM nr
) a;
負荷は、上記のステートメントを短時間に集中的に実行することによって発生します。
理論上、上記の操作はTiDBのベストプラクティスに準拠しているように見え、アプリケーションにホットスポットは発生しません。十分なマシン数があれば、TiDBの分散キャパシティを最大限に活用できます。これが本当にベストプラクティスに準拠しているかどうかを検証するために、以下の実験的環境でテストを実施しました。
クラスタトポロジーには、TiDBノード2台、PDノード3台、TiKVノード6台が配置されています。このテストはベンチマークではなく原理説明を目的としているため、QPSパフォーマンスは無視してください。

クライアントは短時間で「集中的な」書き込みリクエストを開始し、TiDBは3K QPSの書き込みを受信しました。理論上は、負荷は6つのTiKVノードに均等に分散されるはずです。しかし、各TiKVノードのCPU使用率から判断すると、負荷分散は不均一です。1 tikv-3ノードが書き込みのホットスポットとなっています。


RaftストアCPUはスレッドraftstoreのCPU使用率で、通常は書き込み負荷を表します。このシナリオでは、 tikv-3がこのRaftグループのLeader、 tikv-0とtikv-1がフォロワーです。他のノードの負荷はほぼ空です。
PD の監視メトリックでも、ホットスポットが発生したことが確認されます。

ホットスポットの原因
上記のテストでは、ベストプラクティスで期待される理想的なパフォーマンスを達成できていません。これは、TiDBに新しく作成された各テーブルのデータを、以下のデータ範囲で保存するために、デフォルトで1つのリージョンのみが分割されているためです。
[CommonPrefix + TableID, CommonPrefix + TableID + 1)
短期間のうちに、同じリージョンに大量のデータが継続的に書き込まれます。

上の図は、リージョン分割プロセスを示しています。データがTiKVに継続的に書き込まれると、TiKVはリージョンを複数のリージョンに分割します。リーダー選出は、分割対象のリージョンLeaderが配置されている元のストアで開始されるため、新しく分割された2つのリージョンのリーダーは同じストアに残っている可能性があります。この分割プロセスは、新しく分割されたリージョン2とリージョン3でも発生する可能性があります。このように、書き込み負荷はTiKVノード1に集中します。
継続的な書き込みプロセス中、ノード1でホットスポットが発生していることを検出すると、PDは集中しているリーダーを他のノードに均等に分散させます。TiKVノードの数がリージョンレプリカの数より多い場合、TiKVはこれらのリージョンをアイドルノードに移行しようとします。書き込みプロセス中のこれらの2つの操作は、PDの監視メトリクスにも反映されます。

一定期間の連続書き込みの後、PDはTiKVクラスタ全体の負荷が均等に分散される状態を自動的にスケジュールします。その時点で、クラスタ全体の容量を最大限に活用できるようになります。
ほとんどの場合、ホットスポットが発生する上記のプロセスは正常であり、これはデータベースのリージョンウォームアップフェーズです。ただし、同時書き込みが集中するシナリオでは、このフェーズを回避する必要があります。
ホットスポットソリューション
理論上期待される理想的なパフォーマンスを実現するには、リージョンを必要な数のリージョンに直接分割し、これらのリージョンをクラスター内の他のノードに事前にスケジュールすることで、ウォームアップ フェーズをスキップできます。
v3.0.x、v2.1.13以降のバージョンでは、TiDBは分割リージョンと呼ばれる新機能をサポートしています。この新機能は、以下の新しい構文を提供します。
SPLIT TABLE table_name [INDEX index_name] BETWEEN (lower_value) AND (upper_value) REGIONS region_num
SPLIT TABLE table_name [INDEX index_name] BY (value_list) [, (value_list)]
しかし、TiDBはこの事前分割操作を自動的には実行しません。その理由は、TiDB内のデータ分散に関係しています。

上図から、行のキーのエンコーディング規則によれば、 rowIDのみが可変部分となります。TiDB では、 rowID Int64整数です。ただし、リージョン分割も実際の状況に基づいて行う必要があるため、 Int64整数範囲を必要な数の範囲に均等に分割し、リージョンを異なるノードに分散させる必要はないかもしれません。
rowIDへの書き込みが完全に離散的であれば、上記の方法ではホットスポットは発生しません。行IDまたはインデックスが固定範囲またはプレフィックスを持つ場合(例えば、 [2000w, 5000w)の範囲にデータを離散的に挿入する場合)、ホットスポットも発生しません。ただし、上記の方法でリージョンを分割した場合、開始時に同じリージョンにデータが書き込まれる可能性があります。
TiDBは汎用的な用途向けのデータベースであり、データ分布について想定していません。そのため、最初はテーブルのデータを格納するために1つのリージョンのみを使用し、実際のデータが挿入された後は、データ分布に応じてリージョンを自動的に分割します。
このような状況とホットスポット問題を回避する必要性を踏まえ、TiDBはSplit Regionの構文を使用して、同時書き込みが集中するシナリオでパフォーマンスを最適化します。上記のケースに基づいて、 Split Region構文を使用してリージョンを分散させ、負荷分散を観察してみましょう。
テストで書き込まれるデータは正の範囲内で完全に離散的であるため、次のステートメントを使用して、テーブルをminInt64からmaxInt64範囲内の 128 個の領域に事前に分割できます。
SPLIT TABLE TEST_HOTSPOT BETWEEN (0) AND (9223372036854775807) REGIONS 128;
事前分割操作の後、 SHOW TABLE test_hotspot REGIONS;のステートメントを実行して、 リージョン scattering の状態を確認します。3 SCATTERINGの列の値がすべて0であれば、スケジューリングは成功です。
以下のSQL文を使って、リージョンリーダーの分布を確認することもできます。1 table_name実際のテーブル名に置き換えてください。
SELECT
p.STORE_ID,
COUNT(s.REGION_ID) PEER_COUNT
FROM
INFORMATION_SCHEMA.TIKV_REGION_STATUS s
JOIN INFORMATION_SCHEMA.TIKV_REGION_PEERS p ON s.REGION_ID = p.REGION_ID
WHERE
TABLE_NAME = 'table_name'
AND p.is_leader = 1
GROUP BY
p.STORE_ID
ORDER BY
PEER_COUNT DESC;
次に、書き込みロードを再度操作します。



明らかなホットスポットの問題が解決されたことがわかります。
この場合、テーブルは単純です。他のケースでは、インデックスのホットスポット問題も考慮する必要があるかもしれません。インデックスリージョンを事前に分割する方法の詳細については、 分割リージョンを参照してください。
複雑なホットスポットの問題
問題1:
テーブルに主キーがない場合、または主キーがInt型ではなく、ランダムに分布する主キーIDを生成したくない場合は、TiDBは暗黙的に_tidb_rowid列目を行IDとして提供します。一般的に、 SHARD_ROW_ID_BITS列目のパラメータを使用しない場合、 _tidb_rowid列目の値も単調に増加するため、ホットスポットが発生する可能性があります。詳細はSHARD_ROW_ID_BITSを参照してください。
このような状況でホットスポット問題を回避するには、テーブル作成時にSHARD_ROW_ID_BITSとPRE_SPLIT_REGIONS使用できます。 PRE_SPLIT_REGIONS詳細については、 分割前のリージョンを参照してください。
SHARD_ROW_ID_BITS 、 _tidb_rowid列目に生成された行 ID をランダムに分散させるために使用されます。4 PRE_SPLIT_REGIONSテーブルの作成後にリージョンを事前に分割するために使用されます。
注記:
PRE_SPLIT_REGIONSの値はSHARD_ROW_ID_BITSの値以下でなければなりません。
例:
create table t (a int, b int) SHARD_ROW_ID_BITS = 4 PRE_SPLIT_REGIONS=3;
SHARD_ROW_ID_BITS = 4、tidb_rowidの値が 16 (16=2^4) の範囲にランダムに分散されることを意味します。PRE_SPLIT_REGIONS=3、テーブルが作成後に 8 (2^3) 個のリージョンに事前に分割されることを意味します。
テーブルtにデータの書き込みが開始されると、データは事前に分割された 8 つのリージョンに書き込まれます。これにより、テーブルの作成後に 1 つのリージョンしか存在しない場合に発生する可能性のあるホットスポット問題が回避されます。
注記:
tidb_scatter_regionグローバル変数はPRE_SPLIT_REGIONSの動作に影響します。この変数は、テーブル作成後に結果を返す前に、リージョンが事前に分割され、分散されるまで待機するかどうかを制御します。テーブル作成後に書き込みが集中する場合は、この変数の値を
globalに設定する必要があります。そうしないと、TiDBはすべてのリージョンが分割され、分散されるまでクライアントに結果を返しません。そうでない場合、TiDBは分散が完了する前にデータを書き込むため、書き込みパフォーマンスに大きな影響が出ます。
問題2:
テーブルの主キーが整数型で、テーブルが主キーの一意性を確保するためにAUTO_INCREMENT使用している場合 (必ずしも連続または増分ではない)、TiDB は主キーの行値を_tidb_rowidとして直接使用するため、このテーブルでホットスポットを分散するためにSHARD_ROW_ID_BITS使用することはできません。
このシナリオの問題に対処するには、データを挿入する際にAUTO_INCREMENT AUTO_RANDOM (列属性)に置き換えます。そうすることで、TiDBは整数の主キー列に自動的に値を割り当て、行IDの連続性が失われ、ホットスポットが分散されます。
パラメータ設定
バージョン2.1では、書き込み競合が頻繁に発生するシナリオにおいて、トランザクションの競合を事前に特定するためにTiDBにラッチ機構導入されました。これは、書き込み競合によるTiDBおよびTiKVにおけるトランザクションコミットの再試行を削減することを目的としています。通常、バッチタスクはTiDBに既に保存されているデータを使用するため、トランザクションの書き込み競合は発生しません。このような状況では、TiDBのラッチを無効化することで、小さなオブジェクトへのメモリ割り当てを削減できます。
[txn-local-latches]
enabled = false