DBを運用していて、「データが増えてきて特定の集計クエリが遅い」という悩み、よくありますよね。 インデックスを足しても限界がある、かといってDWHを新設するほどの予算も工数もない。そんなときの選択肢の一つとして、TiDBの列指向ストレージエンジン TiFlash を「既存MySQLの分析クエリオフローダー」として使う構成を検証してみました。
TiDBは通常、行ベースKVSの TiKV をストレージエンジンとして使いますが、TiKVのデータをレプリケーションして列指向で保持する TiFlash を追加することで、OLAP(集計)クエリを高速化できます。
今回試すのは、既存のMySQLには一切手を入れず、その横にTiDB(TiKV + TiFlash)を立てて、変更データだけを継続的に流し込み、分析クエリだけをTiDB側で受ける、という構成です。将来的にTiDB本体への移行を検討する際のPoC環境としても、最小構成のまま流用できるのがメリットです。
前提とする環境
- OS: Oracle Linux 9.8
- 構築ツール: TiUP
- TiDBバージョン: v8.5.7
- 構成: TiDB / TiKV / TiFlash / DMを同一ホストに同居させた最小構成からスタートするのがおすすめです。 導入コストと検証スピードを優先した構成であり、リソース競合やSPOFといった制約は残るため、本格運用に進む段階では後述の「将来のスケールアウト」でノード分離を行う前提です。
- ソースDB: 既存運用中のMySQL 8.0(binlog_format=ROW、GTID有効を前提)
- 想定ソース台数: 2台のMySQLインスタンスからのFan-in(テーブル統合)を想定
環境構築
まずはTiUPで最小構成クラスタを立てます。将来のスケールアウト(ノード分離)をそのまま延長線上で検証できるよう、最初から tiup cluster でトポロジーを定義して構築します。
|
1 2 3 4 |
# TiUPインストール curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh source ~/.bashrc |
topology.yaml で、PD/TiKV/TiDB/TiFlashを同一ホスト(ポートを分けて同居)に定義します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# topology.yaml pd_servers: - host: 10.0.0.1 tidb_servers: - host: 10.0.0.1 tikv_servers: - host: 10.0.0.1 tiflash_servers: - host: 10.0.0.1 |
|
1 2 3 |
tiup cluster deploy tidb-offloader v8.5.7 topology.yaml --user root -p tiup cluster start tidb-offloader |
(tiup cluster deploy コマンドリファレンス)
DM(Data Migration)とは:MySQL互換DBからTiDBへのデータ同期を専門に担うPingCAP製のツールです。フルダンプ/ロードによる初期移行と、binlogベースの増分同期をひとつのタスクで連続実行できるのが特徴で、今回のように複数MySQLのテーブルを1つのTiDBテーブルへ集約する「Fan-in」もルール定義だけで実現できます(複数MySQLインスタンスの統合について)。設定はYAMLで「ソース(接続先MySQL)」と「タスク(同期対象・ルーティングルール)」を分けて定義 する方式で、sourceをDMクラスタに登録した上で、source-idを指定したtaskを起動する2段構成になっています(DMのアーキテクチャ)。DM自体もTiUPでmaster/workerからなる別クラスタとしてデプロイします。
|
1 2 3 4 5 6 7 |
tiup install dmctl dm-master dm-worker # dm-topology.yaml # master_servers: / worker_servers: を同様に定義 tiup dm deploy dm-offloader v8.5.7 dm-topology.yaml --user root -p tiup dm start dm-offloader |
検証: クラスタが正しく起動しているか
|
1 2 3 4 5 6 7 8 9 |
# TiDB Dashboardで各コンポーネントの状態を確認 # http://<host>:2379/dashboard # CLIから接続確認 mysql -h 127.0.0.1 -P 4000 -u root -e "SELECT tidb_version();" # TiFlashノードが認識されているか mysql -h 127.0.0.1 -P 4000 -u root -e "SELECT * FROM information_schema.tiflash_replica;" |
tidb_version() が返り、TiFlashノードがPDに登録されていれば起動は成功です(information_schema.tiflash_replicaの参照方法は公式ドキュメント参照)。
DMでのFan-inレプリケーション
今回は、既存のMySQL(複数台)を想定し、特定テーブルをTiDBに集約するレプリケーション(Fan-inレプリケーション)を設定します(設定ファイルの詳細はMigrate Data Using Data Migrationを参照)。 以下のYAMLのように、ソースDBへの接続定義を行います。 データソースごとに、個別の設定ファイルを作成する必要がある点に注意してください。 今回は、source1.yamlとsource2.yamlを作成しました。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# source1.yaml source-id: "mysql-01" from: host: "mysql-host-1" port: 3306 user: "repl_user" password: "xxxx" # source2.yaml source-id: "mysql-02" from: host: "mysql-host-2" port: 3306 user: "repl_user" password: "xxxx" |
tiup dmctl operate-source コマンドを使って、ソースDBをDMに認識させます。
|
1 2 3 |
tiup dmctl --master-addr 127.0.0.1:8261 operate-source create source1.yaml tiup dmctl --master-addr 127.0.0.1:8261 operate-source create source2.yaml |
tiup dmctl operate-source show コマンドを使ってソースが登録された事を確認できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$ tiup dmctl --master-addr 127.0.0.1:8261 operate-source show Starting component dmctl: /home/vagrant/.tiup/components/dmctl/v8.5.7/dmctl/dmctl --master-addr 127.0.0.1:8261 operate-source show { "result": true, "msg": "", "sources": [ { "result": true, "msg": "", "source": "mysql-01", "worker": "dm-192.168.56.10-8262" }, { "result": true, "msg": "", "source": "mysql-02", "worker": "dm-192.168.56.10-8263" } ] } |
次に、各ソースからのレプリケーションタスクを定義します。 今回は、複数ソースからのレプリケーションのため、レプリカとなるTiDBのテーブルをanalytics.ordersにまとめています。 route-rules を複数定義して別々のテーブルとしてマッピングすることももちろん可能です。 詳細はKey Features – Table Routingを参照ください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# task.yaml name: fanin-task task-mode: all shard-mode: pessimistic target-database: host: 127.0.0.1 port: 4000 user: root password: "" mysql-instances: - source-id: "mysql-01" route-rules: ["rule-1"] - source-id: "mysql-02" route-rules: ["rule-1"] routes: rule-1: schema-pattern: "app_db" table-pattern: "orders" target-schema: "analytics" target-table: "orders" |
このように複数の上流テーブルを1つの下流テーブルへ集約する場合は、 タスク設定のshard-modeでpessimisticまたはoptimisticを明示します (Merge and Migrate Data from Sharded Tables)。 両者の違いが表れるのは、主に上流の各shardでDDLを実行するときです。
| モード | DDL実行時の動作 | 上流DDLの条件 | 注意点 |
|---|---|---|---|
pessimistic |
DDLを実行したshardのDML同期を一時停止し、shard group全体で同じDDLが揃ってから下流へ適用 | 各shardでDDLの順序と内容を揃える | DDL待ちで同期が止まる可能性はあるが、誤ったDDLを下流へ適用する前に検出しやすい |
optimistic |
各shardのDDLを互換性のある形へ調整して直ちに下流へ適用し、DML同期を止めない | 実行順やDDL文が異なってもよいが、shard間でスキーマ互換性を維持する | 不適切なDDLが先に下流へ適用され、同期停止やデータ不整合につながる可能性がある |
DMの既定はpessimisticですが、設定の意図を明確にするため、このPoCでも shard-mode: pessimisticを明示しています。2つのMySQLでordersのスキーマを 同一に保ち、DDLも同じ順序・内容で実行する前提です。DDL中もDML同期を止めたく ないという理由だけでoptimisticを選ぶのではなく、互換性の制約と障害時の リカバリー方法を理解したうえで選択する必要があります。
タスクを開始します。
|
1 2 |
tiup dmctl --master-addr 127.0.0.1:8261 start-task task.yaml |
検証: 同期状態の確認
tiup dmctl query-status コマンドでレプリケーションタスクの状態を確認できます( SHOW REPLCIA STATUS 相当)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
tiup dmctl --master-addr 127.0.0.1:8261 query-status fanin-task : "subTaskStatus": [ { "name": "fanin-task", "stage": "Running", "unit": "Sync", "result": null, "unresolvedDDLLockID": "", "sync": { "totalEvents": "141", "totalTps": "0", "recentTps": "0", "masterBinlog": "(mysql-bin.000003, 1895)", "masterBinlogGtid": "bf155581-8aff-11f1-a48b-525400ef86a5:1-12", "syncerBinlog": "(mysql-bin.000003, 1895)", "syncerBinlogGtid": "bf155581-8aff-11f1-a48b-525400ef86a5:1-12", "blockingDDLs": [ ], "unresolvedGroups": [ ], "synced": true, "binlogType": "remote", "secondsBehindMaster": "0", "blockDDLOwner": "", "conflictMsg": "", "totalRows": "141", "totalRps": "0", "recentRps": "0" }, "validation": null } ] : |
データ量は?
TiFlashはTiKVのデータをレプリケーションして保持するため(TiFlashのレプリケーション方式:Raft Learnerとして低コストにリアルタイム複製)、論理的には同じデータをTiKV/TiFlash双方で二重に持つことになります。ただし、TiFlashはRocksDBベースの列指向フォーマットで圧縮して保持するため、単純に元データの2倍にはなりません。
検証: ストレージ使用量の比較
各MySQLに100万行ずつ、合計200万行の比較用データを用意しました。1行あたりの payloadは平均655 bytesで、主キーに加えて (customer_id, event_type, created_at) の複合インデックスを持たせています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# TiKVでのテーブルサイズ(MiB) mysql -h 127.0.0.1 -P 4000 -u root -e " SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_SIZE, TABLE_KEYS, REGION_COUNT FROM information_schema.TABLE_STORAGE_STATS WHERE TABLE_SCHEMA='analytics' AND TABLE_NAME='benchmark_events';" # TiFlashでの圧縮前サイズと実ディスクサイズ(bytes) mysql -h 127.0.0.1 -P 4000 -u root -e " SELECT TIDB_DATABASE, TIDB_TABLE, TOTAL_ROWS, TOTAL_SIZE, TOTAL_STABLE_SIZE_ON_DISK FROM information_schema.TIFLASH_TABLES WHERE TIDB_DATABASE='analytics' AND TIDB_TABLE='benchmark_events';" |
MySQLは2台の.ibd実割当サイズを合算しました。TiKVは TABLE_STORAGE_STATS.TABLE_SIZE、TiFlashは TIFLASH_TABLES.TOTAL_STABLE_SIZE_ON_DISKを使用しています。TiKVについては、 ロード直後に生成されるWALやRaft logを除外したデータ(リージョンのみ)のサイズを対象としています。
実測結果:
| 対象 | 計測方法 | サイズ | MySQL比 |
|---|---|---|---|
| MySQL 2台合計 | .ibd実割当サイズ |
1,656.0 MiB | 1.000 |
| MySQL 2台合計(参考) | DATA_LENGTH + INDEX_LENGTH |
1,623.3 MiB | 0.980 |
| TiKV | TABLE_STORAGE_STATS.TABLE_SIZE |
1,775.0 MiB | 1.072 |
| TiFlash | TOTAL_STABLE_SIZE_ON_DISK |
301.8 MiB | 0.182 |
| TiKV + TiFlash | 上記2値の合計 | 2,076.8 MiB | 1.254 |
今回のデータでは、TiKVとTiFlashを合わせた保持量はMySQL 2台分の約1.25倍に収まりました。 TiFlash単体はMySQL比約18.2%で、TiFlash内部の非圧縮サイズ1,456,500,000 bytesに対し、実ディスクサイズは316,442,178 bytes、圧縮率は約4.60:1でした。
この結果は、VARCHAR(1024) の列が主なデータサイズの要因である表を使った場合の値です。 列指向圧縮が効きやすいデータであり、型・値の偏り・カーディナリティによって結果は変わります。
クエリは高速化された?
まず、集計対象のテーブルにTiFlash replicaを作成します。今回のTiFlashは1台 なので、レプリカ数には1を指定しました。
|
1 2 |
ALTER TABLE analytics.benchmark_events SET TIFLASH REPLICA 1; |
このDDLはTiFlashへのデータ複製完了を待たずに戻ります。ベンチマークを始める前に、 AVAILABLE=1かつPROGRESS=1になったことを確認します。
|
1 2 3 4 5 6 7 8 9 |
SELECT TABLE_SCHEMA, TABLE_NAME, REPLICA_COUNT, AVAILABLE, PROGRESS FROM information_schema.TIFLASH_REPLICA WHERE TABLE_SCHEMA = 'analytics' AND TABLE_NAME = 'benchmark_events'; |
TiFlashにクエリを向けるには、オプティマイザヒントかセッション変数で読み出しエンジンを指定します(system variable: tidb_isolation_read_engines、EXPLAIN ANALYZE)。
|
1 2 3 4 5 |
-- TiFlashに固定し、MPPを有効化 SET SESSION tidb_isolation_read_engines = 'tiflash'; SET SESSION tidb_allow_mpp = 1; SET SESSION tidb_enforce_mpp = 1; |
検証: MySQLとTiFlashの集計性能比較
ストレージ比較後の同期ラグ検証でデータを追加したため、今回は MySQL 1の1,500,000行とMySQL 2の1,500,000行、合計3,000,000行を 使用しました。TiDB側のanalytics.benchmark_eventsにも、DMでFan-inした 同じ3,000,000行が存在します。
表はcustomer_id、event_type、amount、created_atなどの複数列と、 JSONに似たテキストを格納したpayload VARCHAR(1024)列で構成されています。 各MySQLとTiDBで先にANALYZE TABLEを実行し、次の3種類を比較しました。
| クエリ | 読み取る主な列 | 処理内容 |
|---|---|---|
| 顧客別売上Top 20 | customer_id, amount |
10万顧客でCOUNT/SUMし、売上順にTop 20 |
| イベント・payload集計 | event_type, amount, payload |
8種別で集計し、payload文字数の平均も算出 |
| 日別イベント集計 | created_at, event_type, amount |
日付とイベント種別でCOUNT/SUM |
今回のMySQLは2台にデータが分かれているため、1台だけを測るとTiFlash側の 半分のデータしか処理せず、結果も一致しません。そこでMySQL側は2台へ同時に 部分集計を実行し、取得した結果をクライアントでマージして最終結果を作りました。 TiFlash側は、Fan-in済みのテーブルへ1回だけクエリを実行します。
顧客別売上Top 20の場合、MySQL 2台にはそれぞれ次の部分集計を実行します。
|
1 2 3 4 5 6 |
SELECT customer_id, COUNT(*) AS event_count, SUM(amount) AS total_amount FROM app_db.benchmark_events GROUP BY customer_id |
各MySQLから返る顧客別の部分集計を合算し、売上順に並べてTop 20を選びます。 TiFlash側では同じ処理をSQL内で完結させます。
|
1 2 3 4 5 6 7 8 |
SELECT customer_id, COUNT(*) AS event_count, SUM(amount) AS total_amount FROM analytics.benchmark_events GROUP BY customer_id ORDER BY total_amount DESC, customer_id LIMIT 20; |
MySQL側の時間は、2台への並列クエリ開始から、結果受信、部分集計のマージ、 並び替えが完了するまでのエンドツーエンド時間です。ウォームアップ後に 両方式を交互に5回ずつ実行し、中央値を採用しました。\n\nマージを行ったpythonコードはシンプルにsubprocessとしてmysqlクライアントを呼び出し、結果TSVを以下のループで合算しています。
|
1 2 3 4 5 6 7 |
def merge_rows(query_name, result_sets): merged = {} for rows in result_sets: for row in rows: ...集計処理... |
実測結果:
| クエリ | MySQL 2台+マージ(5回の範囲/中央値) | TiFlash(5回の範囲/中央値) | 高速化 | 時間短縮 |
|---|---|---|---|---|
| 顧客別売上Top 20 | 9,604.8~10,005.3 ms/9,717.9 ms | 150.2~207.2 ms/163.3 ms | 59.51倍 | 98.3% |
| イベント・payload集計 | 8,111.2~8,676.1 ms/8,450.5 ms | 574.7~598.4 ms/587.9 ms | 14.37倍 | 93.0% |
| 日別イベント集計 | 10,876.8~11,333.4 ms/11,087.6 ms | 135.0~186.2 ms/173.2 ms | 64.02倍 | 98.4% |
このPoC構成では、3種類すべてでMySQL 2台から集計するよりもTiFlashへ オフロードした方が高速でした。特に、ソースをまたいだ大量の部分集計結果を アプリケーション側で統合せず、TiFlashのMPP内で処理を完結できる効果が 大きく出ています。
スケールアウト検証
検証: PD、TiKV、TiDB Serverのノード追加
最後に、このOLAP用環境を本格的に利用したくなった場合の事を想定し、スケールアウト検証をしました。
ここでは、単一VM構成からSPOFの排除のために PDを2台、TiKVを2台、TiDB Serverを1台 追加しました。\n今回はPD/TiKVを同居させていますが、これは推奨ではなくできれば分離させたほうが望ましい点はご注意ください。\nまた、DMはそのままTiDBに残しています。
| VM | IP | vCPU / メモリ | 追加コンポーネント |
|---|---|---|---|
scale-pd-tikv2 |
192.168.56.13 |
2 / 4 GiB | PD、TiKV |
scale-pd-tikv3 |
192.168.56.14 |
2 / 4 GiB | PD、TiKV |
scale-tidb2 |
192.168.56.15 |
4 / 8 GiB | TiDB Server |
tiup cluster scale-outへ渡したトポロジーは次のとおりです (tiup cluster scale-out)。
|
1 2 3 4 5 6 7 8 9 10 11 |
pd_servers: - host: 192.168.56.13 - host: 192.168.56.14 tikv_servers: - host: 192.168.56.13 - host: 192.168.56.14 tidb_servers: - host: 192.168.56.15 |
以下のコマンドでスケールアウトを実行します。
|
1 2 3 4 |
tiup cluster scale-out tidb-offloader config/scale-out.yaml \ --user tidb \ --identity_file ~/.ssh/tidb_poc |
PDのmax-replicas=3に対し、確認時の通常TiKV Region 22個が3台すべてへ 配置されました。今回はLeader数が .13のTiKVに偏りましたが、概ね按分されたと見てよいでしょう。
|
1 2 3 4 5 |
TiKV store Region数 Leader数 192.168.56.10:20160 22 4 192.168.56.13:20160 22 11 192.168.56.14:20160 22 7 |
DMLの継続性も確認するため、連続INSERTを流しながらスケールアウトを実行しました。
書き込みは約100ms間隔で次の3系列へ流しました。
- MySQL source 1の
app_db.orders(DMでTiDBへ同期) - MySQL source 2の
app_db.orders(DMでTiDBへ同期) 192.168.56.10:4000の検証用TiDBテーブルへの直接INSERT
ワークロードは173.2秒継続し、その開始3秒後から160.272秒かけて スケールアウトしました。完了後も10秒間書き込みを継続しています。
| 書き込み先 | 成功 | SQLエラー | 平均 | p99 | 最大 |
|---|---|---|---|---|---|
| MySQL source 1 | 1,150 | 0 | 46.0ms | 58ms | 125ms |
| MySQL source 2 | 1,151 | 0 | 45.9ms | 59ms | 74ms |
TiDB .10:4000へ直接 |
1,255 | 0 | 33.3ms | 38ms | 12,874ms |
SQLエラーやデータ欠落は観測されず、クラスタ全体を停止せずに 拡張できました。一方、TiDB直接INSERTのうち1件は完了まで12.874秒かかっています。 今回の環境の想定ではまだ初期導入段階と考えられるため、これが大きな問題になるケースは少ないと考えますが、できるだけワークロードが少ない時間帯を選んで実施することを推奨します。
以上で想定通りSPOFが排除できました。追加したTiDBを活用する場合は上位のLBで改めてルーティングする必要がありますのでご注意ください。
TiDBには、TiProxyという製品に特化したロードバランサがありますので、これをデプロイすると良いでしょう。
まとめ
- 既存MySQLに手を入れず、DM経由でTiDB(TiKV + TiFlash)に変更データを集約し、分析クエリだけをオフロードする構成はPoCとして始めやすいかと思います。
- 300万行の集計3種類では、TiFlashへのオフロードはMySQL 2台からのFan-in相当集計より約14.37~64.02倍高速でした。これはあくまで検証用サーバでの結果になりますので本番相当のスペックで確認をおすすめします。
- PD 3台、TiKV 3台、TiDB Server 2台、へオンラインで拡張できました。
- TiDB Server間の透過的な切り替えにはロードバランサーまたはVIPが必要です
- この提案を始める際は、まず単一ホストの最小構成でスタートするのがおすすめです。本格運用に進む段階でノード分離・Placement Rulesでの配置設計を行うとよいでしょう。
Let’s Enjoy TiDB!


