GA

2026/09/04

InnoDBのCHECK TABLEが何をやっているのかざっと見てみたメモ

MySQL 8.0.46 ベースに見ていく。

まずは俺の好きな sql/sql_yacc.yy からダイブ。 CHECK TABLE の定義はこのへん。

 9627 check_table_stmt:
 9628           CHECK_SYM table_or_tables table_list opt_mi_check_types
 9629           {
 9630             $$= NEW_PTN PT_check_table_stmt(YYMEM_ROOT, $3,
 9631                                             $4.flags, $4.sql_flags);
 9632           }
 9633         ;
 9634
 9635 opt_mi_check_types:
 9636           %empty { $$.flags = T_MEDIUM; $$.sql_flags= 0; }
 9637         | mi_check_types
 9638         ;
 9639
 9640 mi_check_types:
 9641           mi_check_type
 9642         | mi_check_type mi_check_types
 9643           {
 9644             $$.flags= $1.flags | $2.flags;
 9645             $$.sql_flags= $1.sql_flags | $2.sql_flags;
 9646           }
 9647         ;
 9648
 9649 mi_check_type:
 9650           QUICK
 9651           { $$.flags= T_QUICK;              $$.sql_flags= 0; }
 9652         | FAST_SYM
 9653           { $$.flags= T_FAST;               $$.sql_flags= 0; }
 9654         | MEDIUM_SYM
 9655           { $$.flags= T_MEDIUM;             $$.sql_flags= 0; }
 9656         | EXTENDED_SYM
 9657           { $$.flags= T_EXTEND;             $$.sql_flags= 0; }
 9658         | CHANGED
 9659           { $$.flags= T_CHECK_ONLY_CHANGED; $$.sql_flags= 0; }
 9660         | FOR_SYM UPGRADE_SYM
 9661           { $$.flags= 0;                    $$.sql_flags= TT_FOR_UPGRADE; }
 9662         ;

フラグの定数名が分かるくらいなのでさっさと PT_check_table_stmt に飛ぶ。

3211 Sql_cmd *PT_check_table_stmt::make_cmd(THD *thd) {
3212   thd->lex->sql_command = SQLCOM_CHECK;
3213
3214   LEX *const lex = thd->lex;
3215   Query_block *const select = lex->current_query_block();
3216
3217   if (lex->sphead) {
3218     my_error(ER_SP_BADSTATEMENT, MYF(0), "CHECK");
3219     return nullptr;
3220   }
3221
3222   lex->check_opt.flags |= m_flags;
3223   lex->check_opt.sql_flags |= m_sql_flags;
3224   if (select->add_tables(thd, m_table_list, TL_OPTION_UPDATING, TL_UNLOCK,
3225                          MDL_SHARED_READ))
3226     return nullptr;
3227
3228   thd->lex->alter_info = &m_alter_info;
3229   return new (thd->mem_root) Sql_cmd_check_table(&m_alter_info);
3230 }

Sql_cmd_check_table に飛ぶ。

1755 bool Sql_cmd_check_table::execute(THD *thd) {
1756   Table_ref *first_table = thd->lex->query_block->get_table_list();
1757   thr_lock_type lock_type = TL_READ_NO_INSERT;
1758   bool res = true;
1759   DBUG_TRACE;
1760
1761   if (check_table_access(thd, SELECT_ACL, first_table, true, UINT_MAX, false))
1762     goto error; /* purecov: inspected */
1763   thd->enable_slow_log = opt_log_slow_admin_statements;
1764
1765   res = mysql_admin_table(thd, first_table, &thd->lex->check_opt, "check",
1766                           lock_type, false, false, HA_OPEN_FOR_REPAIR, nullptr,
1767                           &handler::ha_check, 1, m_alter_info, true);
1768
1769   thd->lex->query_block->m_table_list.first = first_table;
1770   thd->lex->query_tables = first_table;
1771
1772 error:
1773   return res;
1774 }

mysql_admin_table に飛ぶ。この時、10番目の引数が handler::ha_check になっているので

 693 /*
 694   RETURN VALUES
 695     false Message sent to net (admin operation went ok)
 696     true  Message should be sent by caller
 697           (admin operation or network communication failed)
 698 */
 699 static bool mysql_admin_table(
 700     THD *thd, Table_ref *tables, HA_CHECK_OPT *check_opt,
 701     const char *operator_name, thr_lock_type lock_type, bool open_for_modify,
 702     bool repair_table_use_frm, uint extra_open_options,
 703     int (*prepare_func)(THD *, Table_ref *, HA_CHECK_OPT *),
 704     int (handler::*operator_func)(THD *, HA_CHECK_OPT *), int check_view,
 705     Alter_info *alter_info, bool need_to_acquire_shared_backup_lock) {

案の定 mysql_admin_table 側では int (handler::*operator_func)(THD *, HA_CHECK_OPT *) として受け取られていて

1107     if (check_opt && (check_opt->sql_flags & TT_FOR_UPGRADE) != 0) {
1108       if (table->table->s->tmp_table) {
1109         result_code = HA_ADMIN_OK;
1110       } else {
1111         dd::String_type snam = dd::make_string_type(table->table->s->db);
1112         dd::String_type tnam =
1113             dd::make_string_type(table->table->s->table_name);
1114
1115         Check_result cr = check_for_upgrade(thd, snam, tnam, [&]() {
1116           DBUG_PRINT("admin", ("calling operator_func '%s'", operator_name));
1117           return (table->table->file->*operator_func)(thd, check_opt);
1118         });
1119
1120         result_code = cr.second;
1121         if (cr.first) {
1122           goto err;
1123         }
1124       }
1125     }
1126     // Some other admin COMMAND
1127     else {
1128       DBUG_PRINT("admin", ("calling operator_func '%s'", operator_name));
1129       result_code = (table->table->file->*operator_func)(thd, check_opt);
1130     }
1131     DBUG_PRINT("admin", ("operator_func returned: %d", result_code));

まあ結局 handler::ha_check を叩くことになる。

4680 int handler::ha_check(THD *thd, HA_CHECK_OPT *check_opt) {
4681   int error;
4682   assert(table_share->tmp_table != NO_TMP_TABLE || m_lock_type != F_UNLCK);
4683
4684   if ((table->s->mysql_version >= MYSQL_VERSION_ID) &&
4685       (check_opt->sql_flags & TT_FOR_UPGRADE))
4686     return 0;
4687
4688   if (table->s->mysql_version < MYSQL_VERSION_ID) {
4689     // Check for old temporal format if avoid_temporal_upgrade is disabled.
4690     mysql_mutex_lock(&LOCK_global_system_variables);
4691     const bool check_temporal_upgrade = !avoid_temporal_upgrade;
4692     mysql_mutex_unlock(&LOCK_global_system_variables);
4693
4694     if ((error = check_table_for_old_types(table, check_temporal_upgrade)))
4695       return error;
4696     error = ha_check_for_upgrade(check_opt);
4697     if (error && (error != HA_ADMIN_NEEDS_CHECK)) return error;
4698     if (!error && (check_opt->sql_flags & TT_FOR_UPGRADE)) return 0;
4699   }
4700   return check(thd, check_opt);
4701 }

これは結局ストレージエンジン層の check メソッドを呼ぶので

$ grep 'int check(' storage/ -r
storage/archive/ha_archive.h:  int check(THD *thd, HA_CHECK_OPT *check_opt) override;
storage/csv/ha_tina.h:  int check(THD *thd, HA_CHECK_OPT *check_opt) override;
storage/innobase/handler/ha_innodb.h:  int check(THD *thd, HA_CHECK_OPT *check_opt) override;
storage/innobase/handler/ha_innopart.h:  int check(THD *thd, HA_CHECK_OPT *check_opt) override;
storage/myisam/ha_myisam.h:  int check(THD *thd, HA_CHECK_OPT *check_opt) override;
storage/myisammrg/ha_myisammrg.h:  int check(THD *thd, HA_CHECK_OPT *check_opt) override;
storage/temptable/include/temptable/handler.h:  int check(THD *, HA_CHECK_OPT *) override;

こう分かれていて、まあ興味があるのはInnoDBなので storage/innobase/handler/ha_innodb.h から辿っていく。

18152 /** Tries to check that an InnoDB table is not corrupted. If corruption is
18153  noticed, prints to stderr information about it. In case of corruption
18154  may also assert a failure and crash the server.
18155  @return HA_ADMIN_CORRUPT or HA_ADMIN_OK */
18156
18157 int ha_innobase::check(THD *thd,                /*!< in: user thread handle */
18158                        HA_CHECK_OPT *check_opt) /*!< in: check options */
18159 {

一応ここまで check_opt は引き回され続けてはいる。

18218   for (index = m_prebuilt->table->first_index(); index != nullptr;
18219        index = index->next()) {
18220     /* If this is an index being created or dropped, skip */
18221     if (!index->is_committed()) {
18222       continue;
18223     }

このループから、フラグと言うか修飾子というかによらずセカンダリインデックスも範囲なんだってことはわかった。

18225     if (!(check_opt->flags & T_QUICK) && !index->is_corrupted()) {
18226       /* Enlarge the fatal lock wait timeout during
18227       CHECK TABLE. */
18228       srv_fatal_semaphore_wait_extend.fetch_add(1);
18229
18230       bool valid = btr_validate_index(index, m_prebuilt->trx, false);
18231
18232       /* Restore the fatal lock wait timeout after
18233       CHECK TABLE. */
18234       srv_fatal_semaphore_wait_extend.fetch_sub(1);
18235
18236       if (!valid) {
18237         is_ok = false;
18238
18239         push_warning_printf(thd, Sql_condition::SL_WARNING, ER_NOT_KEYFILE,
18240                             "InnoDB: The B-tree of"
18241                             " index %s is corrupted.",
18242                             index->name());
18243         continue;
18244       }
18245     }

CHECK TABLE .. QUICK でない場合は btr_validate_index に入る。

18281     size_t max_threads = thd_parallel_read_threads(m_prebuilt->trx->mysql_thd);
18282
18283     /* Scan this index. */
18284     if (dict_index_is_spatial(index)) {
18285       ret = row_count_rtree_recs(m_prebuilt, &n_rows, &n_dups);
18286       if ((check_opt->flags & T_EXTEND) && (ret == DB_SUCCESS) &&
18287           !(n_rows < n_rows_in_table || n_dups < n_rows - n_rows_in_table)) {
18288         /* For CHECK TABLE EXTENDED; we also want to make sure that MBR stored
18289         in SPATIAL Index is matching the MBR of geometry stored in Clustered
18290         record. */
18291         m_prebuilt->need_to_access_clustered = true;
18292         n_rows = 0;
18293         n_dups = 0;
18294         ret = row_count_rtree_recs(m_prebuilt, &n_rows, &n_dups);
18295       }
18296     } else {
18297       ret = row_scan_index_for_mysql(m_prebuilt, index, max_threads, true,
18298                                      &n_rows);
18299     }

パラレルでやるっぽいことと、空間インデックスは CHECK TABLE .. EXTEND の時だけ舐めることが分かった。

18309     if (ret == DB_INTERRUPTED || thd_killed(m_user_thd)) {
18310       /* Do not report error since this could happen
18311       during shutdown */
18312       break;
18313     }
18314     if (ret != DB_SUCCESS) {
18315       /* Assume some kind of corruption. */
18316       push_warning_printf(thd, Sql_condition::SL_WARNING, ER_NOT_KEYFILE,
18317                           "InnoDB: The B-tree of"
18318                           " index %s is corrupted.",
18319                           index->name());
18320       is_ok = false;
18321       dict_set_corrupted(index);
18322     }
18323
18324     if (index == m_prebuilt->table->first_index()) {
18325       n_rows_in_table = n_rows;
18326     } else if (!(index->type & DICT_FTS) && (n_rows != n_rows_in_table) &&
18327                (!index->is_multi_value()) &&
18328                (!dict_index_is_spatial(index) || (n_rows < n_rows_in_table) ||
18329                 (n_dups < n_rows - n_rows_in_table))) {
18330       push_warning_printf(thd, Sql_condition::SL_WARNING, ER_NOT_KEYFILE,
18331                           "InnoDB: Index '%-.200s' contains %lu"
18332                           " entries, should be %lu.",
18333                           index->name(), (ulong)n_rows, (ulong)n_rows_in_table);
18334       is_ok = false;
18335       dict_set_corrupted(index);
18336     }
18337   }

row_scan_index_for_mysql の結果でメッセージなど出し分けつつ、18337行目がインデックスのforループの終端。あとは結果を返すだけ。

ということで結論。

  • データ行(クラスタインデックス) だけじゃなくてセカンダリキーもちゃんと見る
  • 空間インデックスがある時は CHECK TABLE .. EXTEND で空間インデックスも見る
  • CHECK TABLE .. QUICK の時にかっ飛ばす処理は btr_validate_index だけだけどこれが結構速くなる上に全部のインデックスを読むには読むので、地味にバッファプールをあっためられる( SELECT /*+ INDEX(t PRIMARY) */ COUNT(*) FROM t をインデックスごとにループさせるのとそんな時間変わらない )

2026/08/25

ALTER TABLEがMetadata Lockで詰まらされた時に検知する何か

TL;DR

  • yt-mdl-checker というスクリプトを書いた
  • performance_schema.metadata_locks から PENDING になっている EXCLUSIVE なロックを引いて、同じテーブルで既に GRANTED になっているやつを KILL するスクリプト
  • クエリ自体は大したことないので、わざわざスクリプト使わなくてもスニペットとして使えるはず

主に「 ALTER TABLE 流したらオンラインALTER TABLEのはずなのに刺さった」対策。たとえ SELECT でも、一度でもそのテーブルに触って COMMIT されていないトランザクションがあれば、その共有MDLが ALTER TABLE の排他MDLをブロックしてしまって、その後のクエリが共有MDLを取れなくなって連鎖的にやられていくケース。

ALTER TABLE 開始時の排他MDLに刺さったらCtrl+Cすればいいだけだけど、オンラインALTER TABLEは一度共有MDLにフォールバックした後 ALTER TABLE 終了処理の中でもう一度排他MDLを取ろうとするので、後者を止めるのは難しい(ずっと張り付いてられるくらいの時間なら張り付いていればいいだけだけれども)ので、なんかALTER TABLEとセットで流せる奴があるといいなとか思った。

使い方は mysql コマンドラインクライアントと似たようなオプションで

$ yt-mdl-checker -hlocalhost -uroot -p'xxx' -i 3 --kill

とかやっておけば、3秒おきにチェックして勝手に KILL してくれそうな感じにしてある。
READMEも書いてます。

https://github.com/yoku0825/ytkit/blob/9b4af50b98b6a16b8f44f084d7220e32a4290c63/README.md#L571-L662

2026/06/26

Group ReplicationでのExecuted_Gtid_Setの進み方

TL;DR

  • たとえシングルプライマリモードだろうと、GTIDは「そのマシンが使うレンジ」が先に割り当てられて、その中からGTIDを払い出していく
  • 元プライマリが使っていたGTIDレンジは、割り当てられたレンジが使い切られない限り、再びプライマリに戻った時に再利用する。

yt-sandbox ってやつでグループレプリケーション環境のコンテナが簡単に立ち上げられるらしいよ(ステマ)

$ yt-sandbox -t gr 8.4
[2816233] NOTE: Generate Sandbox directry into /home/yoku0825/yt-sandbox/golf
[2816233] NOTE: Node1 Container Ipaddress: 172.17.0.2
[2816233] NOTE: Node2 Container Ipaddress: 172.17.0.3
[2816233] NOTE: Node3 Container Ipaddress: 172.17.0.4
Sandbox deployed into /home/yoku0825/yt-sandbox/golf

$ cd /home/yoku0825/yt-sandbox/golf

$ ll
total 28
-rwxr-xr-x. 1 yoku0825 yoku0825 485 Jun 26 08:26 check_group_replication
-rw-r--r--. 1 yoku0825 yoku0825  97 Jun 26 08:26 destroy_all
-rw-r--r--. 1 yoku0825 yoku0825  54 Jun 26 08:26 hosts
lrwxrwxrwx. 1 yoku0825 yoku0825  40 Jun 26 08:26 m -> /home/yoku0825/yt-sandbox/golf/node1/use
drwxr-xr-x. 3 yoku0825 yoku0825 101 Jun 26 08:26 node1
drwxr-xr-x. 3 yoku0825 yoku0825 101 Jun 26 08:26 node2
drwxr-xr-x. 3 yoku0825 yoku0825 101 Jun 26 08:26 node3
-rwxr-xr-x. 1 yoku0825 yoku0825  97 Jun 26 08:26 restart_all
lrwxrwxrwx. 1 yoku0825 yoku0825  40 Jun 26 08:26 s1 -> /home/yoku0825/yt-sandbox/golf/node2/use
lrwxrwxrwx. 1 yoku0825 yoku0825  40 Jun 26 08:26 s2 -> /home/yoku0825/yt-sandbox/golf/node3/use
-rwxr-xr-x. 1 yoku0825 yoku0825  95 Jun 26 08:26 start_all
-rwxr-xr-x. 1 yoku0825 yoku0825  94 Jun 26 08:26 stop_all
-rwxr-xr-x. 1 yoku0825 yoku0825  93 Jun 26 08:26 use_all

シングルプライマリーで上がってくるので、1号機に接続して操作。

$ ./m
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 31
Server version: 8.4.9 MySQL Community Server - GPL

Copyright (c) 2000, 2026, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

golf-1> SHOW BINARY LOG STATUS;
+---------------+----------+--------------+------------------+-------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+-------------------+
| binlog.000001 |      158 |              |                  |                   |
+---------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

golf-1> create database d1;
Query OK, 1 row affected (0.01 sec)

golf-1> create table d1.t1 (num serial, val varchar(32));
Query OK, 0 rows affected (0.02 sec)

golf-1> INSERT INTO d1.t1 VALUES (1, 'one');
Query OK, 1 row affected (0.00 sec)

golf-1>
golf-1>
golf-1> SHOW BINARY LOG STATUS;
+---------------+----------+--------------+------------------+------------------------------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                        |
+---------------+----------+--------------+------------------+------------------------------------------+
| binlog.000001 |      848 |              |                  | 01234567-89ab-cdef-0123-456789abcdef:1-3 |
+---------------+----------+--------------+------------------+------------------------------------------+
1 row in set (0.00 sec)

golf-1> ^DBye

2号機をプライマリーにして書き込み。
100万番単位でGTIDが割り振られるっぽく、100万飛んで1のGTIDが振られる。

【2026/06/26 18:31】
レンジの大きさは group_replication_gtid_assignment_block_size デフォルト100万で決められる。
$ ./s1
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 28
Server version: 8.4.9 MySQL Community Server - GPL

Copyright (c) 2000, 2026, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

golf-2> SELECT group_replication_set_as_primary(@@server_uuid);
+------------------------------------------------------------------+
| group_replication_set_as_primary(@@server_uuid)                  |
+------------------------------------------------------------------+
| Primary server switched to: b91f989d-7138-11f1-aa86-0242ac110003 |
+------------------------------------------------------------------+
1 row in set (0.00 sec)

golf-2>
golf-2> SHOW BINARY LOG STATUS;
+---------------+----------+--------------+------------------+------------------------------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                        |
+---------------+----------+--------------+------------------+------------------------------------------+
| binlog.000001 |      845 |              |                  | 01234567-89ab-cdef-0123-456789abcdef:1-3 |
+---------------+----------+--------------+------------------+------------------------------------------+
1 row in set (0.00 sec)

golf-2>
golf-2> INSERT INTO d1.t1 VALUES (3, 'three');
Query OK, 1 row affected (0.00 sec)

golf-2>
golf-2> SELECT * FROM d1.t1;
+-----+-------+
| num | val   |
+-----+-------+
|   1 | one   |
|   3 | three |
+-----+-------+
2 rows in set (0.01 sec)

golf-2>
golf-2> SHOW BINARY LOG STATUS;
+---------------+----------+--------------+------------------+--------------------------------------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                                |
+---------------+----------+--------------+------------------+--------------------------------------------------+
| binlog.000001 |     1137 |              |                  | 01234567-89ab-cdef-0123-456789abcdef:1-3:1000001 |
+---------------+----------+--------------+------------------+--------------------------------------------------+
1 row in set (0.00 sec)

1号機にプライマリーを戻して書くと、3の次の4が使い回される。

$ ./m
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 34
Server version: 8.4.9 MySQL Community Server - GPL

Copyright (c) 2000, 2026, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

golf-1> SELECT group_replication_set_as_primary(@@server_uuid);
+------------------------------------------------------------------+
| group_replication_set_as_primary(@@server_uuid)                  |
+------------------------------------------------------------------+
| Primary server switched to: b24cf04e-7138-11f1-b6c3-0242ac110002 |
+------------------------------------------------------------------+
1 row in set (0.00 sec)

golf-1>
golf-1> SHOW BINARY LOG STATUS;
+---------------+----------+--------------+------------------+--------------------------------------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                                |
+---------------+----------+--------------+------------------+--------------------------------------------------+
| binlog.000001 |     1137 |              |                  | 01234567-89ab-cdef-0123-456789abcdef:1-3:1000001 |
+---------------+----------+--------------+------------------+--------------------------------------------------+
1 row in set (0.00 sec)

golf-1>
golf-1> INSERT INTO d1.t1 VALUES (4, 'four');
Query OK, 1 row affected (0.00 sec)

golf-1>
golf-1> SHOW BINARY LOG STATUS;
+---------------+----------+--------------+------------------+--------------------------------------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                                |
+---------------+----------+--------------+------------------+--------------------------------------------------+
| binlog.000001 |     1428 |              |                  | 01234567-89ab-cdef-0123-456789abcdef:1-4:1000001 |
+---------------+----------+--------------+------------------+--------------------------------------------------+
1 row in set (0.00 sec)

マルチプライマリなら疑問に思うこともないくらい自然な動作だし、シングルプライマリーでも super_read_only 外したら書けちゃうくらいなので、そのへんの動作は一緒なのであろう。

2026/05/07

MySQL Shellのフルバックアップとmysqlbinlogを合わせてPITR

MySQL ShellのdumpInstanceとMySQL ShellのdumpBinlogs ではなく MySQL ShellのdumpInstanceとmysqlbinlogの組み合わせでのPITR

テスト用コンテナの起動とテスト用のデータ作成とハートビートの書き込み。GTIDは有効な状態。

$ yt-sandbox 8.0
[3254174] NOTE: Generate Sandbox directry into /home/yoku0825/yt-sandbox/bravo
[3254174] NOTE: Node1 Container Ipaddress: 172.17.0.2
Sandbox deployed into /home/yoku0825/yt-sandbox/bravo

$ cd /home/yoku0825/yt-sandbox/bravo
$ ./n1 -e "CREATE DATABASE sbtest"

$ sysbench --mysql-host=172.17.0.2 --mysql-user=root oltp_read_write prepare --table-size=100000 --tables=10
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)

Creating table 'sbtest1'...
Inserting 100000 records into 'sbtest1'
Creating a secondary index on 'sbtest1'...
Creating table 'sbtest2'...
Inserting 100000 records into 'sbtest2'
Creating a secondary index on 'sbtest2'...
Creating table 'sbtest3'...
Inserting 100000 records into 'sbtest3'
Creating a secondary index on 'sbtest3'...
Creating table 'sbtest4'...
Inserting 100000 records into 'sbtest4'
Creating a secondary index on 'sbtest4'...
Creating table 'sbtest5'...
Inserting 100000 records into 'sbtest5'
Creating a secondary index on 'sbtest5'...
Creating table 'sbtest6'...
Inserting 100000 records into 'sbtest6'
Creating a secondary index on 'sbtest6'...
Creating table 'sbtest7'...
Inserting 100000 records into 'sbtest7'
Creating a secondary index on 'sbtest7'...
Creating table 'sbtest8'...
Inserting 100000 records into 'sbtest8'
Creating a secondary index on 'sbtest8'...
Creating table 'sbtest9'...
Inserting 100000 records into 'sbtest9'
Creating a secondary index on 'sbtest9'...
Creating table 'sbtest10'...
Inserting 100000 records into 'sbtest10'
Creating a secondary index on 'sbtest10'...

$ yt-heartbeat -h 172.17.0.2 -uroot -v

まずはMySQL Shellの util.dumpInstance でフルバックアップを取る。

$ date ; mysqlsh mysql://root@172.17.0.2 --js -- util dumpInstance '/tmp/test' ; date
Thu May  7 07:43:06 GMT 2026
Please provide the password for 'root@172.17.0.2':
Save password for 'root@172.17.0.2'? [Y]es/[N]o/Ne[v]er (default No):
Acquiring global read lock
Global read lock acquired

..
Uncompressed data size: 191.90 MB
Compressed data size: 87.48 MB
Compression ratio: 2.2
Rows written: 1000092
Bytes written: 87.48 MB
Average uncompressed throughput: 191.90 MB/s
Average compressed throughput: 87.48 MB/s
Thu May  7 07:43:10 GMT 2026

dumpInstanceは論理バックアップなので、 START TRANSACTION WITH CONSISTENT SNAPSHOT を使っている。よって、このバックアップをリストアした時に復旧できるタイムスライスは「バックアップを開始した時刻」になる。これはバックアップ先の @.json に入っていそう。

$ jq -r .begin /tmp/test/@.json
2026-05-07 07:43:09

もう1個サンドボックスを立ち上げてリストアしてみる。 updateGtidSet=replace にしないと新しいGTIDを払い出しちゃうので指定する。

$ yt-sandbox 8.0
[3255163] NOTE: Generate Sandbox directry into /home/yoku0825/yt-sandbox/charlie
[3255163] NOTE: Node1 Container Ipaddress: 172.17.0.3
Sandbox deployed into /home/yoku0825/yt-sandbox/charlie

$ cd /home/yoku0825/yt-sandbox/charlie
$ ./n1 -e "SET GLOBAL local_infile = ON"

$ date ; mysqlsh mysql://root@172.17.0.3 --js -- util loadDump '/tmp/test' { --updateGtidSet=replace } ; date
Thu May  7 07:49:37 GMT 2026
Please provide the password for 'root@172.17.0.3':
Save password for 'root@172.17.0.3'? [Y]es/[N]o/Ne[v]er (default No):
Loading DDL and Data from '/tmp/test' using 4 threads.

..
Resetting GTID_PURGED to dumped gtid set
11 chunks (1.00M rows, 191.90 MB) for 11 tables in 2 schemas were loaded in 23 sec (avg throughput 8.16 MB/s, 42.54K rows/s)
13 DDL files were executed in 0 sec.
Data load duration: 23 sec
Total duration: 23 sec
0 warnings were reported during the load.

Thu May  7 07:50:04 GMT 2026

$ ./n1 -e "SELECT hostname, server_time FROM ytkit.heartbeat ORDER BY server_time DESC LIMIT 1"  -- 07:43:09.546 のデータが手に入った
+----------+-------------------------+

| hostname | server_time             |
+----------+-------------------------+
| bravo-1  | 2026-05-07 07:43:09.546 |
+----------+-------------------------+

↑うーん、ミリ秒まで @.json に入っていてほしい気もする…。

あとはmysqldumpの時と同じく、「リストア後のGTIDが歯抜けにならないように」(= この場合は「少なくとも必ず b55fb915-49e7-11f1-87a5-0242ac110002:495 とそれ以降のGTID」を含む)バイナリログを適用すればいい。

rsyncか何かで定期的にバイナリログを他の場所に移しておいて(cpで代用)

$ mkdir work
$ sudo cp -ip /home/yoku0825/yt-sandbox/bravo/node1/datadir/binlog.00000* work/
$ ll work/
total 186720
-rw-r-----. 1 mysql mysql       180 May  7 07:38 binlog.000001
-rw-r-----. 1 mysql mysql       180 May  7 07:38 binlog.000002
-rw-r-----. 1 mysql mysql 191192963 May  7 07:54 binlog.000003

mysqlbinlogの —stop-datetime で着地したい時間を指定しつつ mysql コマンドラインクライアントに食わせる。

GTIDモードなので二重適用を恐れる必要はなく、邪魔にならない程度(無視されるとはいえ、GTIDをチェックして空振りさせるので多少の時間は必要で、ぴったり495から始める自信があるなら495から始めても良い)にgtid_executedがオーバーラップするように適用させる。

↑の例だと俺なら binlog.000001 から適用してしまう。000001と000002は実質空っぽだし、000003の07:43:09.546以前のイベントは単に読み捨てられるので。

$ sudo mysqlbinlog --stop-datetime="2026-05-07 07:50:03" work/* | mysql -h172.17.0.3 -uroot

$ ./n1 -e "SELECT hostname, server_time FROM ytkit.heartbeat ORDER BY server_time DESC LIMIT 1"
+----------+-------------------------+
| hostname | server_time             |
+----------+-------------------------+
| bravo-1  | 2026-05-07 07:50:02.848 |
+----------+-------------------------+

$ ./n1 -e "SHOW MASTER STATUS"
+---------------+-----------+--------------+------------------+---------------------------------------------------------------------------------------+
| File          | Position  | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                                                                     |
+---------------+-----------+--------------+------------------+---------------------------------------------------------------------------------------+
| binlog.000001 | 190977368 |              |                  | b55fb915-49e7-11f1-87a5-0242ac110002:1-904,
ecc7abed-49e8-11f1-ad40-0242ac110003:1-24 |
+---------------+-----------+--------------+------------------+---------------------------------------------------------------------------------------+

mysqlbinlogの --stop-datetime は当該時刻「以上」のタイムスタンプ(秒まで)が現れた時点でbreakするので、7:50:03.855860のgitd=’b55fb915-49e7-11f1-87a5-0242ac110002:905’ は適用されない。

$ sudo mysqlbinlog -vv work/binlog.000003 | less
..
# at 191101220
#260507  7:50:03 server id 201  end_log_pos 191101299 CRC32 0xaa35a563  GTID    last_committed=904      sequence_number=905     rbr_only=yes    original_committed_timestamp=1778140204080496   immediate_commit_timestamp=1778140204080496     transaction_length=362
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1778140204080496 (2026-05-07 07:50:04.080496 GMT)
# immediate_commit_timestamp=1778140204080496 (2026-05-07 07:50:04.080496 GMT)
/*!80001 SET @@session.original_commit_timestamp=1778140204080496*//*!*/;
/*!80014 SET @@session.original_server_version=80046*//*!*/;
/*!80014 SET @@session.immediate_server_version=80046*//*!*/;
SET @@SESSION.GTID_NEXT= 'b55fb915-49e7-11f1-87a5-0242ac110002:905'/*!*/;
# at 191101299
#260507  7:50:03 server id 201  end_log_pos 191101382 CRC32 0x768811b1  Query   thread_id=13    exec_time=0     error_code=0
SET TIMESTAMP=1778140203.855860/*!*/;
BEGIN
/*!*/;
# at 191101382
#260507  7:50:03 server id 201  end_log_pos 191101448 CRC32 0x2ce58d7d  Table_map: `ytkit`.`heartbeat` mapped to number 145
# has_generated_invisible_primary_key=0
# at 191101448
#260507  7:50:03 server id 201  end_log_pos 191101551 CRC32 0xde01d932  Write_rows: table id 145 flags: STMT_END_F

BINLOG '
K0T8aRPJAAAAQgAAAAj6YwsAAJEAAAAAAAEABXl0a2l0AAloZWFydGJlYXQABA8SEvwF/AMDAwIA
AgP8/wB9jeUs
K0T8aR7JAAAAZwAAAG/6YwsAAJEAAAAAAAEAAgAE/wAHAGJyYXZvLTGZuc58gyFwmbnOfIMhZioA
YjU1ZmI5MTUtNDllNy0xMWYxLTg3YTUtMDI0MmFjMTEwMDAyOjEtOTA0MtkB3g==
'/*!*/;
### INSERT INTO `ytkit`.`heartbeat`
### SET
###   @1='bravo-1' /* VARSTRING(1020) meta=1020 nullable=0 is_null=0 */
###   @2='2026-05-07 07:50:03.856' /* DATETIME(3) meta=3 nullable=0 is_null=0 */
###   @3='2026-05-07 07:50:03.855' /* DATETIME(3) meta=3 nullable=0 is_null=0 */
###   @4='b55fb915-49e7-11f1-87a5-0242ac110002:1-904' /* BLOB/TEXT meta=2 nullable=0 is_null=0 */
# at 191101551
#260507  7:50:03 server id 201  end_log_pos 191101582 CRC32 0xaa63b7d5  Xid = 16673
COMMIT/*!*/;

..

とこんな感じ。


【2026/05/08 16:28】

この手順と直接関係はないけど、updateGtidSet=replaceなのにgtid_executedに2台ぶんのGTIDが入ってしまっているのでバグレポートした

MySQL Bugs: #120418: unexpected Executed_Gtid_Set after util.loadDump with updateGtidSet: replace

https://bugs.mysql.com/bug.php?id=120418