TiDBにおけるデータ移行のベストプラクティス - AUTO_INCREMENT編
この記事について
ずっと以前にTiDBのAUTO_INCREMENTについての記事を書きました。
この記事の中で以下の脚注を書いていました。
既存DBからデータをインポートする際には特に注意が必要です。このあたりのことはどこかでまた説明する予定です。
このことについて書くのをずっと放置してきたのですが、先日、以下の記事が出たこともあり、ちゃんと書いてみようと思います。
このような記事が書いていただいことにとても感謝しています。実際のユーザーによる知見ほど信頼に足るものはありません。今後も多くの皆様にこのような記事を執筆いただけると大変ありがたいです。
以降の記事で説明する内容はTiDBのデフォルトのAUTO_INCREMENTを前提としています。また、v8.1.0で確認しています。
何が問題だったか
上記の記事ではTiDBのAUTO INCREMENTのキャッシュが更新されないということで、TiDBのAUTO_INCREMENTに関連した問題が書かれていました。
この問題に触れる前にAUTO_INCREMENTに関連するTiDBの性質を改めて確認しておきましょう。
- TiDBはAUTO_INCREMENTするIDを一定の範囲でキャッシュする
- TiDBノードは自身が追加するIDの値が他のノードでキャッシュされているかどうかを気にしない
- TiDBノードは自身がキャッシュしているIDが他のノードで利用されたかどうかを知ることはない
他にも色々ありますが、今回の問題に関連するのは主に上記の3つです。
それでは改めて今回の問題を見ていきましょう。先に今回の問題の原因を簡単に言うと、あるTiDBノードがキャッシュしていたIDのいくつかは別のTiDBノードで使われていた、ということです。
この状況自体は簡単に再現できます。例えばTiDBノードが2つある状況を想定します。2台のTiDBノードはAとBとします。AにID: 1を、BにID: 2を、INSERT INTO t (id) VALUES (1)
のようにそれぞれ明示的に挿入します。つぎに、Aに対して INSERT INTO t VALUES ()
とIDが自動採番されるようにINSERT文を実行します。すると、Aは2を挿入しようとしますが、2はすでにBを経由して挿入されているため、AではDuplicate Errorが発生します。
# | Node | SQL | |
---|---|---|---|
1 | A | INSERT INTO t (id) VALUES (1) | [2, 30002) がキャッシュされる |
2 | B | INSERT INTO t (id) VALUES (2) | [30002, 60002) がキャッシュされる |
3 | A | INSERT INTO t VALUES () | キャッシュから2を取り出して挿入しようとするが、 すでに追加済なのでエラーとなる[1]。 |
このような状況が発生するのは、先ほど触れたTiDBノードの性質である以下の2つによるものです。
- TiDBノードは自身が追加するIDの値が他のノードでキャッシュされているかどうかを気にしない
- TiDBノードは自身がキャッシュしているIDが他のノードで利用されたかどうかを知ることはない
この例ではINSERT
を使っていますが、LOAD DATA
でのデータ追加でも同様です。要は、明示的にIDを指定されることで他のノードでキャッシュしているIDが使われしまうことがあるということが問題なのです。
既存データベースからTiDBへデータ移行する際にはこういったことが起きやすいため、以下のベストプラクティスを参考にしてください。
TiDBにおけるデータ移行のベストプラクティス
ここでタイトル回収となるのですが、AUTO_INCREMENTを意識したベストプラクティスを挙げていきたいと思います。
1. データ移行時はTiDBノードを1つにしておく
TiDBノードが1つであれば、他のノードにキャッシュしているIDが使われるということはありません。よって、今回のような問題は起きません。
2. データ移行完了後にTiDBクラスタを再起動する
データ移行の速度を上げるために、データ移行に複数のTiDBノードが必要な状況となることもあるでしょう。その場合、データ移行完了後にTiDBクラスタを再起動することで、各TiDBノードのIDキャッシュをリフレッシュさせることができます。仮にキャッシュしていたIDが別のノードで使われていたとしても、IDキャッシュがリフレッシュされる(既存のIDキャッシュを破棄して新たにIDキャッシュを取得する)ことで、つぎのキャッシュ帯を利用することになり、今回のような問題はおきません。
もう少し補足すると、TiDBノードはTiKVからIDキャッシュを取得するのですが、そのIDキャッシュは当然ながらすでに払い出されているものよりも新しい、つまり大きな値の範囲となります。例えば、再起動前のTiDBノードで最大90,000までのIDがキャッシュされていたとします。これは移行済みデータのIDは最大で90,000以下であることを示します(TiDBではキャッシュの範囲を超える値が挿入されるとIDキャッシュを更新するため)。そして、TiDBクラスタが再起動すると、TiDBノードが新たに取得するキャッシュは少なくとも90,000よりも大きい数字となります。よって、以降の自動採番では追加済のIDと重複することは無いのです。
TiDBノードのスケールインとスケールアウトを繰り返すことで同様の効果を狙うこともできます。しかし、TiDB Cloudの場合では任意のノードをスケールインさせることができないため、あまり良いやり方ではないでしょう。
ALTER TABLE ... AUTO_INCREMENT=1
でIDのキャッシュを強制的に更新する
3. ここまでに紹介したやり方である#1と#2は初回のデータ移行では有効ですが、すでにサービスインしている場合には採用しづらい方法です。すでにサービスインしている場合、オンラインでIDのキャッシュを更新する必要があります。
ALTER TABLE ${table_name} AUTO_INCREMENT=1
[2]を実行することで、オンラインのままIDのキャッシュを更新することができます。このALTER文を実行するとwarningが出ますが、今回の目的であるIDキャッシュを更新するためならば無視して良いです。
このALTER文を実行することで全TiDBノードのIDキャッシュがリフレッシュされて、今回の問題を防ぐことができます。
この方法で留意する点としては、対象テーブルが多い場合に時間がかかる可能性があります。また、対象に漏れがないように注意する必要があります。
4. 移行先(つまりTiDBクラスタ)にテーブル作成をするときに、AUTO_INCREMENTの値をあらかじめ十分大きくしておく
例えば移行元テーブルのIDが高々10万程度である場合、移行先のTiDBクラスタの対象テーブルの自動採番を100万番台から始まるようにAUTO_INCREMENT=1000000
としてCREATE TABLE
するかALTER TABLE
で調整しておくという方法も考えられます。これにより、データ移行によって追加されるIDとアプリケーションなどによる自動採番によるIDの重複を回避することが可能となります。
こちらの方法も#3と同様にオンラインで行えるため、すでに接続しているアプリケーションに影響を与えることもなく、TiDBクラスタの再起動と同じ効果を得ることができます。また、これまでの方法に比べて自動採番されるIDの開始番号を調整しやすく、他の方法に比べて無駄になるIDのキャッシュを可能な限り減らすことも可能です[3]。
こちらもテーブル数が多い場合に対応に時間がかかる可能性があります。また、手動でコマンドを実行するため、予期しないミスが発生する可能性があります。
まとめ
いかがでしたか?この記事では以下の点について説明しました。
- なぜTiDBにデータ移行すると、AUTO_INCREMENTが思ったような動作にならないことがあるのか
- それに対してどのようなベストプラクティスがあるのか
TiDBの特性故にこれまでのDB移行では考慮することがなかった点が見えてきたのではないでしょうか。しかし、そういった特性もベストプラクティスに則ることで適切に対処することが可能です。TiDBへのデータ移行を行う際にはご参考ください。
冒頭にも申し上げましたが、実際のユーザーからの知見はとても貴重です。ぜひ今後もTiDBの様々な知見を広めていただければと思います。
-
この場合に限って言えば、INSERTをリトライすれば次のIDである
3
を利用するので、INSERTは最終的には成功します。これはIDの利用はtransaction awareでないため、利用されたIDは成功かどうかにかかわらず破棄されるためです。 ↩︎ -
1でなくても良いです。仕様としては、現在キャッシュしている範囲の下限よりも小さい値であれば同様の効果となります。ここではわかりやすさのために
1
としています ↩︎ -
他の方法だと少なくとも数万はIDが無駄になります。ただし、現代においてこの程度の無駄は許容されるでしょうし、IDの枯渇が心配な場合はUNSIGNED BIGINTでIDを運用すると良いでしょう。また、この方法を採用する場合、安全マージンを大きく取るほうが安心できるでしょう ↩︎
Discussion