ローカル環境でTiDBを実行し色々触ってみた

tiupコマンドを使用することで簡単にクラスターを起動することができます。TiDBを選択する理由はシャーディング不要というだけではない!
2024.01.05

ゲームソリューション部の えがわ です。

tiupコマンドを使用してTiDBをローカルで起動してみました。
PingCAPの公式ガイドに手順が詳細に記載されています。

環境

  • Windows 11
  • Ubuntu22.04(WSL2)

インストール

tiupコマンドのインストール

curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh

PATHにtiupを追加

インストール時に表示されたパスを指定します。

source ${your_shell_profile}

(例)

cm-egawa@HL01375:~/tidb-eval$ source /home/cm-egawa/.bashrc

触ってみる

クラスターを起動

TiDBのクラスターを起動します。

tiup playground

※参考
各コンポーネントのインスタンス数やバージョンを以下で指定できます。
指定なしの場合、各コンポーネントは1つずつ起動します。

tiup playground v7.5.0 --db 2 --pd 3 --kv 3

起動できたので別ウィンドウでアクセスしてみましょう。

mysql --comments --host 127.0.0.1 --port 4000 -u root

接続できました

まず文字コードを確認してみます。

mysql> SHOW VARIABLES LIKE '%character_set%';
+--------------------------+---------+
| Variable_name            | Value   |
+--------------------------+---------+
| character_set_client     | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database   | utf8mb4 |
| character_set_filesystem | binary  |
| character_set_results    | utf8mb4 |
| character_set_server     | utf8mb4 |
| character_set_system     | utf8    |

変更しなくても文字コードがutf8mb4になっているのは嬉しいですね!

検証用のデータベースを作成し選択します。

CREATE DATABASE tidb_eval;
USE tidb_eval;

ユーザーテーブルを作成しレコードも追加してみます。

CREATE TABLE users (
  id CHAR(36) PRIMARY KEY,
  name VARCHAR(255) NOT NULL, 
  birthday DATE,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO users (id, name, birthday)
VALUES 
  (UUID(), 'えがわ', '2024-01-01'),
  (UUID(), 'ほげ', '2024-01-01');

問題なくレコードが追加されています。

mysql> SELECT * FROM users;
+--------------------------------------+-----------+------------+---------------------+---------------------+
| id                                   | name      | birthday   | created_at          | updated_at          |
+--------------------------------------+-----------+------------+---------------------+---------------------+
| 87f6fd66-aad8-11ee-b66a-00155dab40bb | えがわ    | 2024-01-01 | 2024-01-04 17:09:06 | 2024-01-04 17:09:06 |
| 87f6fea6-aad8-11ee-b66a-00155dab40bb | ほげ      | 2024-01-01 | 2024-01-04 17:09:06 | 2024-01-04 17:09:06 |
+--------------------------------------+-----------+------------+---------------------+---------------------+
2 rows in set (0.01 sec)

トランザクションの確認

サクッと試してみます。

ROLLBACK

COMMIT

ROLLBACKもCOMMITも想定通り動いてくれました。

外部キーの確認

注文テーブルを作成し、ユーザーが削除された場合にこちらのレコードも削除されるように設定します。

CREATE TABLE orders (
  id CHAR(36) PRIMARY KEY,
  user_id CHAR(36) NOT NULL,
  total_price INT NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) 
    REFERENCES users(id)
    ON DELETE CASCADE
);

INSERT INTO orders (id, user_id, total_price)
VALUES(UUID(), '87f6fea6-aad8-11ee-b66a-00155dab40bb', 5000);
mysql> SELECT * FROM orders;
+--------------------------------------+--------------------------------------+-------------+---------------------+
| id                                   | user_id                              | total_price | created_at          |
+--------------------------------------+--------------------------------------+-------------+---------------------+
| 93999a55-aad9-11ee-b66a-00155dab40bb | 87f6fea6-aad8-11ee-b66a-00155dab40bb |        5000 | 2024-01-04 17:16:35 |
+--------------------------------------+--------------------------------------+-------------+---------------------+
1 row in set (0.00 sec)

ユーザーを削除してみます。

注文テーブルのレコードも削除されたので想定通り動いています。

インデックスの確認

まずusersテーブルに100万レコードを追加して動きを確認してみます。ダミーデータを追加するプログラムはおまけに記載しています。
EXPLAINで実行計画を見てみましょう。

mysql> EXPLAIN SELECT * FROM users;
+-----------------------+------------+-----------+---------------+----------------------+
| id                    | estRows    | task      | access object | operator info        |
+-----------------------+------------+-----------+---------------+----------------------+
| TableReader_5         | 1000002.00 | root      |               | data:TableFullScan_4 |
| └─TableFullScan_4     | 1000002.00 | cop[tikv] | table:users   | keep order:false     |
+-----------------------+------------+-----------+---------------+----------------------+
2 rows in set (0.00 sec)

お!MySQLと表示がだいぶ違っていますね!
各項目の説明は以下に記載されています。

ではこの状態で2020年以降に生まれた人数を抽出してみます。

mysql> EXPLAIN SELECT COUNT(id) FROM users WHERE birthday >= '2020-01-01';
+------------------------------+------------+-----------+---------------+----------------------------------------------------------+
| id                           | estRows    | task      | access object | operator info                                            |
+------------------------------+------------+-----------+---------------+----------------------------------------------------------+
| HashAgg_13                   | 1.00       | root      |               | funcs:count(Column#7)->Column#6                          |
| └─TableReader_14             | 1.00       | root      |               | data:HashAgg_6                                           |
|   └─HashAgg_6                | 1.00       | cop[tikv] |               | funcs:count(tidb_eval.users.id)->Column#7                |
|     └─Selection_12           | 90549.25   | cop[tikv] |               | ge(tidb_eval.users.birthday, 2020-01-01 00:00:00.000000) |
|       └─TableFullScan_11     | 1000002.00 | cop[tikv] | table:users   | keep order:false                                         |
+------------------------------+------------+-----------+---------------+----------------------------------------------------------+
5 rows in set (0.01 sec)

処理の推定値として9万レコードほどあるようです。
では実際にクエリを実行してみます。

mysql> SELECT COUNT(id) FROM users WHERE birthday >= '2020-01-01';
+-----------+
| COUNT(id) |
+-----------+
|     91228 |
+-----------+
1 row in set (0.62 sec)

推定値と大体あっていました。
次にusersのbirthdayカラムにインデックスを追加してみます。

ALTER TABLE users 
ADD INDEX idx_birthday (birthday);

実行計画を見てみましょう。

mysql> EXPLAIN SELECT COUNT(id) FROM users WHERE birthday >= '2020-01-01';
+-----------------------------+----------+-----------+-------------------------------------------+-------------------------------------------+
| id                          | estRows  | task      | access object                             | operator info                             |
+-----------------------------+----------+-----------+-------------------------------------------+-------------------------------------------+
| HashAgg_12                  | 1.00     | root      |                                           | funcs:count(Column#7)->Column#6           |
| └─IndexReader_13            | 1.00     | root      |                                           | index:HashAgg_6                           |
|   └─HashAgg_6               | 1.00     | cop[tikv] |                                           | funcs:count(tidb_eval.users.id)->Column#7 |
|     └─IndexRangeScan_11     | 89709.79 | cop[tikv] | table:users, index:idx_birthday(birthday) | range:[2020-01-01,+inf], keep order:false |
+-----------------------------+----------+-----------+-------------------------------------------+-------------------------------------------+
4 rows in set (0.01 sec)

お!大丈夫そう

mysql> SELECT COUNT(id) FROM users WHERE birthday >= '2020-01-01';
+-----------+
| COUNT(id) |
+-----------+
|     91228 |
+-----------+
1 row in set (0.06 sec)

想定通り動いてくれました。
クエリの実行速度も大きく改善されています。

TiDBダッシュボード

ダッシュボードではメトリクスやクエリの実行回数や応答時間など詳細な情報を表示できます。
触ってみましたがとても強力なツールで、必要な情報は網羅されているように感じました。

項目
URL http://127.0.0.1:2379/dashboard
Username root
Password 空欄

負荷原因になっているクエリも一目瞭然

先ほどのインデックスの有無でPlanID違うため、同じクエリでも別々に確認できます。

Prometheusダッシュボード

Prometheusのダッシュボードを確認することができます。

項目
URL http://127.0.0.1:9090

Grafanaダッシュボード

Grafanaのダッシュボードを確認することができます。

項目
URL http://127.0.0.1:3000
Username admin
Password admin

さいごに

構築の簡単さ、MySQL互換性、モニタリングツールの強力さを体験できました。
特にモニタリングツールの網羅性、視認性が素晴らしく、リアルタイムにシステム状況を把握できるほか、トラブルシューティングにも有用な機能が揃っていると感じました。
MySQLユーザーにとって移行コストがとても低く、大規模化にも柔軟に対応できるデータベースだと思います。
TiDBを選択する理由はシャーディング不要というだけではない!

おまけ

usersテーブルに100万レコードをインサートするプログラムを置いておきます。

import mysql.connector
import uuid 
import pandas as pd
import numpy as np
from mysql.connector import pooling
from random import randint
from datetime import datetime, timedelta

config = {
  "host": "localhost",
  "port": 4000,
  "user": "root",
  "database": "tidb_eval",
  "pool_name": "mypool",
  "pool_size": 10
}

db_pool = mysql.connector.pooling.MySQLConnectionPool(**config)

NUM_RECORDS = 1_000_000
CHUNK_SIZE = 100_000
START_DATE = datetime(1980, 1, 1)
END_DATE = datetime(2024, 1, 1)

def get_random_birthday():
    random_days = randint(0, (END_DATE - START_DATE).days)
    birthday = START_DATE + timedelta(days=random_days)
    return birthday

values = []
for i in range(NUM_RECORDS):
    user_id = str(uuid.uuid4()) 
    name = 'test'
    birthday = get_random_birthday()
    values.append((user_id, name, birthday))

df = pd.DataFrame(values, columns=['id', 'name', 'birthday'])
num_chunks = int(np.ceil(len(df) / CHUNK_SIZE))

for i, chunk in enumerate(np.array_split(df, num_chunks)):
    print(f"Inserting chunk {i+1} / {num_chunks}")
    
    conn = db_pool.get_connection() 
    cursor = conn.cursor()
    cursor.execute("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")

    data = [tuple(x) for x in chunk.values]
    sql = "INSERT INTO users (id, name, birthday) VALUES (%s, %s, %s)"
    cursor.executemany(sql, data)

    conn.commit()
    cursor.close()
    conn.close()
    
print(f"{NUM_RECORDS} records inserted.")