🤔

TiDBでの書き込みHotspot回避手段:AUTO_RANDOMとは?

2024/07/02に公開

はじめに

よくMySQLでPKにシーケンシャルな値(AUTO_INCREMENT)をふったりすることありますよね。利用しているフレームワークによって結果的に利用されるケースも多いと思います。

MySQL互換のTiDBでもAUTO_INCREMENTは使えますが、AUTO_RANDOMというものも存在します。
※TiDBのAUTO_INCREMENT(ちょっと独特)についてはこちらの記事を参考にされると良いかと思います。
https://zenn.dev/shigeyuki/articles/7ed78672a92061

この記事ではTiDB特有のAUTO_RANDOMについて紹介していきます。

AUTO_RANDOMの公式ドキュメント:
https://docs.pingcap.com/tidb/stable/auto-random

そもそもなぜPKに連番をふることが推奨されないのか?(AUTO_RANDOMの存在意義)

これ自体は他のKVSであったりNewSQLでも同様です(もちろん製品によります)。

TiDBでは前提として、

  • TiDB(正確にはTiKV)ではRegionと呼ばれる単位でデータが分割管理されている
  • ひとつのRegionは最大96MB(デフォルト)となっており、各Regionの管理範囲はテーブルのPKの範囲によって決まる
     ※データがどのRegionに属するかはRegionが持つデータによって決まる(左閉空間右開空間)
    このようなルールで管理されるため、Auto Incrementを利用しているテーブルで多数のInsertがある場合は、特定のRegion(TiKVノードとここでは読み替えて差し支えないでしょう)に負荷が集中する≒Hotspotになる、ということになります

なので、PKにばらつきのある値にすることで、時系列で見たときにWrite先が特定のノードに集中することを回避し、負荷分散(ここではWriteのHotspot回避の意味)が期待できます。

AUTO_RANDOMとは、そのWriteのHotspot回避のためベストプラクティスとして用意されているTiDB独自(MySQLにはない)機能となります。
https://docs.pingcap.com/tidb/stable/troubleshoot-hot-spot-issues#handle-auto-increment-primary-key-hotspot-tables-using-auto_random

AUTO_RANDOMの実体

AUTO_RANDOMの値 - シャードビット + インクリメンタルビットからなる整数値

本来、AUTO_INCREMENTもそうですがいわゆるサロゲートキーとしての存在であるため、そもそも物理的な意味をもたせることには是非があるかと思いますが、
一方で実際の現場ではAUTO_INCREMENTによって採番された値を〇〇IDにする、といったこともあるかと思います。

そういった場面も想定し、TiDBでAUTO_RANDOMを利用した場合に実際のところどのような値がふられるのかを見ていきます。

まず、AUTO_RANDOMとは主キー列(BIGINT型)に一意な値を自動的に割り当てるための列属性となります。

※その他については公式ドキュメントの制約を確認してください。

実際に採番される値はランダムなわけではなく、前述のWriteホットスポットを回避しつつ一意性を保証した値となります。

AUTO_RANDOMを利用する場合、以下のように定義します(AUTO_INCREMENTとだいたい同じ)

CREATE TABLE t(
    `id` BIGINT UNSIGNED NOT NULL AUTO_RANDOM, 
    PRIMARY KEY (`id`));

AUTO_RANDOM(S,R)のように定義することもできます。
(S,R)の部分を理解するにはAUTO_RANDOMの構造そのものを理解する必要がありますので説明します。

AUTO_RANDOMで割り当てられる値は64bit構造に基づく整数値です。
64bit構造と各bitが指し示す意味は(S,R)の定義の仕方により以下のように変わります。

name 名称 bit長 説明
Sign bit 符号ビット 0 or 1 UNSIGNED属性を持つ場合は0(符号ビットなし)、それ以外の場合は1となります
Reserved bits 予約ビット 64 - R 取り得るbitの値はすべて0となります
Shard bits シャードビット S シャード数を表現します。S値の範囲は1 ~ 15です。デフォルト値は5です。
Auto-increment bits オートインクリメントビット R - Sign bit - S 自動連番割り当て範囲の合計長です。値の範囲は32 ~ 64です。デフォルト値は64です。

SRを指定しない場合、実質的にはAUTO_RANDOM(5, 64)となります。
つまり、AUTO_RANDOMの64bits構造における各bitの長さ(および意味)は定義の仕方によって以下のようになります。

AUTO_RANDOMの値を見てみる

環境

TiDB(ローカルのtiup playground)を起動します。
値のパターンを捉えるため、TiDBは2台用意します。※--db 2とすることでTiDBノード(正確にはプロセス)がport番号40004001で2つ起動しています

% tiup playground v7.5.1 --db 2
tiup is checking updates for component playground ...
・・・ 
🎉 TiDB Playground Cluster is started, enjoy!

Connect TiDB:    mysql --comments --host 127.0.0.1 --port 4000 -u root
Connect TiDB:    mysql --comments --host 127.0.0.1 --port 4001 -u root
TiDB Dashboard:  http://127.0.0.1:2379/dashboard
Grafana:         http://127.0.0.1:3000

立ち上げたクラスターでは以下のようにTiDBは2台起動しています。

% tiup playground display
・・・
Pid    Role     Uptime
---    ----     ------
59325  pd       10h39m43.820428375s
59326  tikv     10h39m43.790082625s
59327  tidb     10h39m43.756866583s
59328  tidb     10h39m43.720724083s
59366  tiflash  10h39m27.458181583s

mysql clientからmysql --comments --host 127.0.0.1 --port 4000 -u rootで片方のTiDBに接続し、確認用のテーブルを作成します。

CREATE TABLE t (
    `id` BIGINT NOT NULL AUTO_RANDOM, 
    `seq` BIGINT NOT NULL AUTO_INCREMENT,
    `port` VARCHAR(16),
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`));

AUTO_RANDOMの挙動を見るための対比として、AUTO_INCREMENTのカラムもseqとして定義しています。
また、この後実施するINSERT元のTiDBノード判別のため、各TiDBノードのport番号を挿入するためのカラムportとしています。

ちなみにテーブルに設定されているAUTO_RANDOMのShard bitsは以下で確認できます。

SELECT tidb_row_id_sharding_info FROM information_schema.tables WHERE table_name = 't';
+---------------------------+
| tidb_row_id_sharding_info |
+---------------------------+
| PK_AUTO_RANDOM_BITS=5     |
+---------------------------+
1 row in set (0.01 sec)

データの仕込み

では、データの仕込みをしていきます。
もう一方のTiDBにも接続し、
mysql --comments --host 127.0.0.1 --port 4001 -u root
各セッション(つまり2つのTiDBそれぞれ)から以下のINSERTをそれぞれ複数回実行してみます。

INSERT INTO t(port) SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.SESSION_VARIABLES WHERE VARIABLE_NAME = 'port';

値の確認

時間軸(created_at)でソートするとテーブルには以下の値が入っています。

SELECT * FROM t ORDER BY created_at;
+---------------------+-------+------+---------------------+
| id                  | seq   | port | created_at          |
+---------------------+-------+------+---------------------+
| 1729382256910270465 |     1 | 4000 | 2024-05-16 18:05:22 |
|  288230376151711746 |     2 | 4000 | 2024-05-16 18:05:26 |
| 8070450532247928835 |     3 | 4000 | 2024-05-16 18:05:28 |
| 5764607523034264881 | 30001 | 4001 | 2024-05-16 18:05:32 |
|  576460752303453490 | 30002 | 4001 | 2024-05-16 18:05:35 |
| 5188146770730841395 | 30003 | 4001 | 2024-05-16 18:05:36 |
| 5476377146882523140 |     4 | 4000 | 2024-05-16 18:05:40 |
| 2882303761517117445 |     5 | 4000 | 2024-05-16 18:05:41 |
| 1729382256910270470 |     6 | 4000 | 2024-05-16 18:05:43 |
| 2882303761517147444 | 30004 | 4001 | 2024-05-16 18:05:46 |
| 8358680908399670581 | 30005 | 4001 | 2024-05-16 18:05:47 |
| 2594073385365405703 |     7 | 4000 | 2024-05-16 18:05:50 |
| 3746994889972252680 |     8 | 4000 | 2024-05-16 18:05:51 |
| 8070450532247958838 | 30006 | 4001 | 2024-05-16 18:05:54 |
| 5476377146882553143 | 30007 | 4001 | 2024-05-16 18:05:55 |
| 8935141660703064073 |     9 | 4000 | 2024-05-16 18:05:58 |
| 8935141660703094072 | 30008 | 4001 | 2024-05-16 18:06:20 |
| 1441151880758588729 | 30009 | 4001 | 2024-05-16 18:06:21 |
| 4323455642275676170 |    10 | 4000 | 2024-05-16 18:06:29 |
|  864691128455135243 |    11 | 4000 | 2024-05-16 18:06:30 |
| 3458764513820540940 |    12 | 4000 | 2024-05-16 18:06:34 |
| 5764607523034264890 | 30010 | 4001 | 2024-05-16 18:06:38 |
| 4899916394579129659 | 30011 | 4001 | 2024-05-16 18:14:25 |
| 5188146770730841404 | 30012 | 4001 | 2024-05-16 18:14:28 |
| 2305843009213693965 |    13 | 4000 | 2024-05-16 18:14:51 |
|  864691128455135246 |    14 | 4000 | 2024-05-16 18:14:56 |
| 6052837899185976637 | 30013 | 4001 | 2024-05-16 18:15:01 |
|                  15 |    15 | 4000 | 2024-05-16 18:15:04 |
| 4611686018427417918 | 30014 | 4001 | 2024-05-16 18:15:08 |
| 4035225266123994431 | 30015 | 4001 | 2024-05-16 18:15:09 |
+---------------------+-------+------+---------------------+
30 rows in set (0.00 sec)

なんとなく規則性はあるかも・・・?くらいは分かるかもしれませんが、これを見ただけではまずわけがわかりませんね。

規則性の確認

ではAUTO_RANDOMの64bit構造に合わせるため、id列をBIN()関数で2進数にしLPAD()で0埋めして表現してみます(BIN-id)。
※自動で採番される値は正の整数なので符号ビットは考慮せず単純に0埋めで問題なし
※対比としてみやすいのでseqも2進数表現しています(BIN-seq)。

また、各TiDBサーバー(port)ごとにcreated_atでソートします。

SELECT LPAD(BIN(id),64,0) as "BIN-id", id, BIN(seq) as "BIN-seq", seq, port, created_at
 FROM t
  ORDER BY port,created_at;
+------------------------------------------------------------------+---------------------+-----------------+-------+------+---------------------+
| BIN-id                                                           | id                  | BIN-seq         | seq   | port | created_at          |
+------------------------------------------------------------------+---------------------+-----------------+-------+------+---------------------+
| 0001100000000000000000000000000000000000000000000000000000000001 | 1729382256910270465 | 1               |     1 | 4000 | 2024-05-16 18:05:22 |
| 0000010000000000000000000000000000000000000000000000000000000010 |  288230376151711746 | 10              |     2 | 4000 | 2024-05-16 18:05:26 |
| 0111000000000000000000000000000000000000000000000000000000000011 | 8070450532247928835 | 11              |     3 | 4000 | 2024-05-16 18:05:28 |
| 0100110000000000000000000000000000000000000000000000000000000100 | 5476377146882523140 | 100             |     4 | 4000 | 2024-05-16 18:05:40 |
| 0010100000000000000000000000000000000000000000000000000000000101 | 2882303761517117445 | 101             |     5 | 4000 | 2024-05-16 18:05:41 |
| 0001100000000000000000000000000000000000000000000000000000000110 | 1729382256910270470 | 110             |     6 | 4000 | 2024-05-16 18:05:43 |
| 0010010000000000000000000000000000000000000000000000000000000111 | 2594073385365405703 | 111             |     7 | 4000 | 2024-05-16 18:05:50 |
| 0011010000000000000000000000000000000000000000000000000000001000 | 3746994889972252680 | 1000            |     8 | 4000 | 2024-05-16 18:05:51 |
| 0111110000000000000000000000000000000000000000000000000000001001 | 8935141660703064073 | 1001            |     9 | 4000 | 2024-05-16 18:05:58 |
| 0011110000000000000000000000000000000000000000000000000000001010 | 4323455642275676170 | 1010            |    10 | 4000 | 2024-05-16 18:06:29 |
| 0000110000000000000000000000000000000000000000000000000000001011 |  864691128455135243 | 1011            |    11 | 4000 | 2024-05-16 18:06:30 |
| 0011000000000000000000000000000000000000000000000000000000001100 | 3458764513820540940 | 1100            |    12 | 4000 | 2024-05-16 18:06:34 |
| 0010000000000000000000000000000000000000000000000000000000001101 | 2305843009213693965 | 1101            |    13 | 4000 | 2024-05-16 18:14:51 |
| 0000110000000000000000000000000000000000000000000000000000001110 |  864691128455135246 | 1110            |    14 | 4000 | 2024-05-16 18:14:56 |
| 0000000000000000000000000000000000000000000000000000000000001111 |                  15 | 1111            |    15 | 4000 | 2024-05-16 18:15:04 |
| 0101000000000000000000000000000000000000000000000111010100110001 | 5764607523034264881 | 111010100110001 | 30001 | 4001 | 2024-05-16 18:05:32 |
| 0000100000000000000000000000000000000000000000000111010100110010 |  576460752303453490 | 111010100110010 | 30002 | 4001 | 2024-05-16 18:05:35 |
| 0100100000000000000000000000000000000000000000000111010100110011 | 5188146770730841395 | 111010100110011 | 30003 | 4001 | 2024-05-16 18:05:36 |
| 0010100000000000000000000000000000000000000000000111010100110100 | 2882303761517147444 | 111010100110100 | 30004 | 4001 | 2024-05-16 18:05:46 |
| 0111010000000000000000000000000000000000000000000111010100110101 | 8358680908399670581 | 111010100110101 | 30005 | 4001 | 2024-05-16 18:05:47 |
| 0111000000000000000000000000000000000000000000000111010100110110 | 8070450532247958838 | 111010100110110 | 30006 | 4001 | 2024-05-16 18:05:54 |
| 0100110000000000000000000000000000000000000000000111010100110111 | 5476377146882553143 | 111010100110111 | 30007 | 4001 | 2024-05-16 18:05:55 |
| 0111110000000000000000000000000000000000000000000111010100111000 | 8935141660703094072 | 111010100111000 | 30008 | 4001 | 2024-05-16 18:06:20 |
| 0001010000000000000000000000000000000000000000000111010100111001 | 1441151880758588729 | 111010100111001 | 30009 | 4001 | 2024-05-16 18:06:21 |
| 0101000000000000000000000000000000000000000000000111010100111010 | 5764607523034264890 | 111010100111010 | 30010 | 4001 | 2024-05-16 18:06:38 |
| 0100010000000000000000000000000000000000000000000111010100111011 | 4899916394579129659 | 111010100111011 | 30011 | 4001 | 2024-05-16 18:14:25 |
| 0100100000000000000000000000000000000000000000000111010100111100 | 5188146770730841404 | 111010100111100 | 30012 | 4001 | 2024-05-16 18:14:28 |
| 0101010000000000000000000000000000000000000000000111010100111101 | 6052837899185976637 | 111010100111101 | 30013 | 4001 | 2024-05-16 18:15:01 |
| 0100000000000000000000000000000000000000000000000111010100111110 | 4611686018427417918 | 111010100111110 | 30014 | 4001 | 2024-05-16 18:15:08 |
| 0011100000000000000000000000000000000000000000000111010100111111 | 4035225266123994431 | 111010100111111 | 30015 | 4001 | 2024-05-16 18:15:09 |
+------------------------------------------------------------------+---------------------+-----------------+-------+------+---------------------+
30 rows in set (0.00 sec)

なんとなく規則性がみえてきたでしょうか?

AUTO_RANDOMの64bit構造に立ち返って理解していきます。
まず、確認したテーブルのAUTO_RANDOMは以下の定義でした。
id BIGINT NOT NULL AUTO_RANDOM,
すなわち、以下のパターンです。

上記前提でBIN-id列の値を最上位ビットから順に見ていきます。

Sign bit(符号ビット)

idの値はすべて正の整数のため、最上位ビットはすべて0です。

Shared bits(シャードビット)

つづく5ビットはシャードを表現しています。(つまり、2^5=32シャードになります)
例えば、seq=9のレコードに着目すると、
id8935141660703064073となっておりこの中では比較的大きな値です。
BIN-idの上位2~6ビット:11111となっており、正の整数(id)としては大きな値をとりそうなことが分かります。

逆にseq=15のレコードに着目すると、
上位2~6ビット:00000となっているため(以降もすべて0)、id=seqつまりAUTO_INCREMENTと同じ値になっています。

Auto-Increment bits(オートインクリメントビット)

残り58ビットは連番を表現しています。
上のレコードから順に見ていくと、1bit加算されていっていることが分かります。
そしてそもそもなのですが、seqの値を見てみるとport=4000のレコードは1~15port=4001のレコードは30001~30015という値が採番されています。

これは、冒頭に紹介した記事で語られていますが、
TiDBのAUTO_INCREMENT(デフォルトの)では、各TiDBノードが連番の値をキャッシュしており、その中から連番を付与する仕組みに起因したものとなっています。

このように、下位ビットは連番であっても上位2~6ビットにバラツキがある値が採用されるため、
仮に大量のWriteがある場合でも、負荷を散らしてくれる仕組みになっています。

これがTiDBにおけるWrite Hotspotの対処策であるAUTO_RANDOMの考え方です。
この考え方が理解できると、TiDBにおけるテーブル設計(何を主キーにするか?)という観点でも似たように、
例えば、複合主キーにし左側はランダムな値にしておき右側は日時にしておく、みたいな設計にすることが良さそうだな、ということが分かるかと思います。

まとめ

以上から、AUTO_RANDOMにより採番される値は「シャードビット + インクリメンタルビット(TiDBのAUTO_INCREMENTの仕組みを利用した)からなる整数値」ということが理解できたかと思います。
この値を主キーにとることにより、WriteのHotspot回避を狙っているのが、TiDBのAUTO_RANDOMの意義となります。

また、AUTO_RANDOMの一意性についても、前述のAUTO_INCREMENTの仕組みに依存する形で担保しているのでそちらも併せて確認して頂ければと思います。

このようにTiDBもアーキテクチャーに即したテーブル設計することがMySQL同様にパフォーマンス観点で重要となるので、
本記事で言及した観点も踏まえ、よりコスパよくTiDBを利用してみましょう。

Tips

テーブル作成時にAUTO_RANDOM_BASEを指定することで、Rで定義するオートインクリメントビットの初期値を定義することが出来ます。

-- テーブル作成
CREATE TABLE t1 (
    `id` BIGINT NOT NULL PRIMARY KEY AUTO_RANDOM(1),
    `seq` BIGINT NOT NULL AUTO_INCREMENT,
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
    ) AUTO_RANDOM_BASE=1024;

-- 3レコードほど挿入
INSERT INTO t1 VALUES();
Query OK, 1 row affected (0.01 sec)

-- 値の確認:オートインクリメント部はAUTO_RANDOM_BASEで指定した1024から始まっている
SELECT LPAD(BIN(id),64,0) as "BIN-id", id, seq, created_at FROM t1 ORDER BY created_at;
+------------------------------------------------------------------+---------------------+-----+---------------------+
| BIN-id                                                           | id                  | seq | created_at          |
+------------------------------------------------------------------+---------------------+-----+---------------------+
| 0000000000000000000000000000000000000000000000000000010000000000 |                1024 |   1 | 2024-05-27 11:00:36 |
| 0000000000000000000000000000000000000000000000000000010000000001 |                1025 |   2 | 2024-05-27 11:00:38 |
| 0100000000000000000000000000000000000000000000000000010000000010 | 4611686018427388930 |   3 | 2024-05-27 11:00:41 |
+------------------------------------------------------------------+---------------------+-----+---------------------+
3 rows in set (0.01 sec)

Discussion