重要
このページは英語版のページを機械翻訳しています。原文はこちらからご覧ください。

並行性の高い書き込みのベストプラクティス

このドキュメントでは、アプリケーション開発を容易にするのに役立つ、TiDBでの書き込みの多いワークロードを同時に処理するためのベストプラクティスについて説明します。

ターゲットオーディエンス

このドキュメントは、TiDBの基本を理解していることを前提としています。まず、TiDBの基礎を説明する次の3つのブログ記事とTiDBのベストプラクティスを読むことをお勧めします。

非常に同時の書き込み集約型シナリオ

高度な同時書き込みシナリオは、クリア、決済などのアプリケーションでバッチタスクを実行するときによく発生します。このシナリオには、次の機能があります。

  • 膨大な量のデータ
  • 履歴データをデータベースに短時間でインポートする必要性
  • 短時間でデータベースから大量のデータを読み取る必要性

これらの機能は、TiDBに次の課題をもたらします。

  • 書き込みまたは読み取りの容量は、線形にスケーラブルである必要があります。
  • データベースのパフォーマンスは安定しており、大量のデータが同時に書き込まれるために低下することはありません。

分散データベースの場合、すべてのノードの容量を最大限に活用し、単一のノードがボトルネックにならないようにすることが重要です。

TiDBのデータ配信の原則

上記の課題に対処するには、TiDBのデータセグメンテーションとスケジューリングの原則から始める必要があります。詳細については、 スケジューリングを参照してください。

TiDBは、データをリージョンに分割します。各リージョンは、デフォルトでサイズ制限が96Mのデータの範囲を表します。各リージョンには複数のレプリカがあり、レプリカの各グループはRaftグループと呼ばれます。Raftグループでは、リージョンリーダーがデータ範囲内で読み取りおよび書き込みタスク(TiDBはフォロワー-読むをサポート)を実行します。リージョンリーダーは、配置Driver(PD)コンポーネントによって、読み取りと書き込みの圧力を均等に分散するために、さまざまな物理ノードに自動的にスケジュールされます。

TiDB Data Overview

理論的には、アプリケーションに書き込みホットスポットがない場合、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に加えて、二次インデックスは存在しません。次のステートメントを実行して、このテーブルにデータを書き込みます。 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の分散容量は、適切なマシンで完全に使用できます。それが本当にベストプラクティスに沿っているかどうかを検証するために、次のように説明されている実験的環境でテストが実行されます。

クラスタトポロジの場合、2つのTiDBノード、3つのPDノード、および6つのTiKVノードが展開されます。このテストはベンチマークではなく原理を明確にするためのものであるため、QPSのパフォーマンスは無視してください。

QPS1

クライアントは短時間で「集中的な」書き込み要求を開始します。これは、TiDBが受信する3KQPSです。理論的には、負荷圧力は6つのTiKVノードに均等に分散される必要があります。ただし、各TiKVノードのCPU使用率から、負荷分散は不均一です。 tikv-3ノードは書き込みホットスポットです。

QPS2

QPS3

RaftストアCPUraftstoreスレッドのCPU使用率であり、通常は書き込み負荷を表します。このシナリオでは、 tikv-3がこのRaftグループのリーダーです。 tikv-0tikv-1はフォロワーです。他のノードの負荷はほとんど空です。

PDの監視メトリックは、ホットスポットが発生したことも確認します。

QPS4

ホットスポットの原因

上記のテストでは、操作はベストプラクティスで期待される理想的なパフォーマンスに達していません。これは、新しく作成された各テーブルのデータをTiDBに格納するために、デフォルトで1つのリージョンのみが分割され、データ範囲が次のとおりであるためです。

[CommonPrefix + TableID, CommonPrefix + TableID + 1)

短期間で、大量のデータが同じリージョンに継続的に書き込まれます。

TiKV Region Split

上の図は、リージョン分割プロセスを示しています。データが継続的にTiKVに書き込まれると、TiKVはリージョンを複数のリージョンに分割します。リーダー選挙は、分割されるリージョンリーダーが配置されている元のストアで開始されるため、新しく分割された2つのリージョンのリーダーが同じストアに残っている可能性があります。この分割プロセスは、新しく分割されたリージョン2とリージョン3でも発生する可能性があります。このように、書き込み圧力はTiKVノード1に集中します。

連続書き込みプロセス中に、ホットスポットがノード1で発生していることを検出した後、PDは集中したリーダーを他のノードに均等に分散します。 TiKVノードの数がリージョンレプリカの数よりも多い場合、TiKVはこれらのリージョンをアイドル状態のノードに移行しようとします。書き込みプロセス中のこれら2つの操作は、PDの監視メトリックにも反映されます。

QPS5

継続的な書き込み期間の後、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でのデータ分散に関連しています。

Table Region Range

上の図から、行のキーのエンコード規則によれば、 rowIDが唯一の可変部分です。 TiDBでは、 rowIDInt64の整数です。ただし、リージョンの分割も実際の状況に基づいている必要があるため、 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列の値がすべて0の場合、スケジューリングは成功しています。

次のSQLステートメントを使用して、リージョンリーダーの分布を確認することもできます。 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;

次に、書き込みロードを再度操作します。

QPS6

QPS7

QPS8

明らかなホットスポットの問題が解決されたことがわかります。

この場合、テーブルは単純です。また、インデックスのホットスポットの問題を考慮する必要がある場合もあります。インデックスリージョンを事前に分割する方法の詳細については、 スプリットリージョンを参照してください。

複雑なホットスポットの問題

問題1:

テーブルに主キーがない場合、または主キーがIntタイプではなく、ランダムに分散された主キーIDを生成したくない場合、TiDBは行IDとして暗黙の_tidb_rowid列を提供します。一般に、 SHARD_ROW_ID_BITSパラメーターを使用しない場合、 _tidb_rowid列の値も単調に増加し、ホットスポットも発生する可能性があります。詳細については、 SHARD_ROW_ID_BITSを参照してください。

この状況でのホットスポットの問題を回避するために、テーブルを作成するときにSHARD_ROW_ID_BITSPRE_SPLIT_REGIONSを使用できます。 PRE_SPLIT_REGIONSの詳細については、 分割前の領域を参照してください。

SHARD_ROW_ID_BITSは、 _tidb_rowid列で生成された行IDをランダムに分散するために使用されます。 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の動作に影響を与えます。

この変数は、テーブルの作成後に結果を返す前に、リージョンが事前に分割および分散されるのを待つかどうかを制御します。テーブルの作成後に集中的な書き込みがある場合は、この変数の値を1に設定する必要があります。そうすると、すべてのリージョンが分割されて分散されるまで、TiDBは結果をクライアントに返しません。そうしないと、TiDBはスキャッタリングが完了する前にデータを書き込み、書き込みパフォーマンスに大きな影響を与えます。

問題2:

テーブルの主キーが整数型であり、テーブルが主キーの一意性を確保するためにAUTO_INCREMENTを使用する場合(必ずしも連続または増分である必要はありません)、TiDBは行の値を直接使用するため、 SHARD_ROW_ID_BITSを使用してこのテーブルのホットスポットを分散させることはできません。主キーの_tidb_rowidとして。

このシナリオの問題に対処するには、データを挿入するときにAUTO_INCREMENTAUTO_RANDOM (列属性)に置き換えることができます。次に、TiDBは整数の主キー列に値を自動的に割り当てます。これにより、行IDの連続性が排除され、ホットスポットが分散されます。

パラメータ設定

v2.1では、書き込みの競合が頻繁に発生するシナリオでトランザクションの競合を事前に識別するために、 ラッチ機構がTiDBに導入されています。目的は、書き込みの競合によって引き起こされるTiDBおよびTiKVでのトランザクションコミットの再試行を減らすことです。通常、バッチタスクはTiDBにすでに保存されているデータを使用するため、トランザクションの書き込みの競合は存在しません。この状況では、TiDBのラッチを無効にして、小さなオブジェクトへのメモリ割り当てを減らすことができます。

[txn-local-latches]
enabled = false