GTIDを用いたレプリケーションについて

この記事は、[Lancers（ランサーズ） Advent Calendar 2022](https://qiita.com/advent-calendar/2022/lancers)の の４日目の記事です。
昨日の記事は[@high-g](https://qiita.com/high-g)さんによる[アウトプットしつづけることの大切さ](https://engineer.blog.lancers.jp/%e3%82%a2%e3%83%89%e3%83%99%e3%83%b3%e3%83%88%e3%82%ab%e3%83%ac%e3%83%b3%e3%83%80%e3%83%bc/%e3%82%a2%e3%82%a6%e3%83%88%e3%83%97%e3%83%83%e3%83%88%e3%81%97%e3%81%a4%e3%81%a5%e3%81%91%e3%82%8b%e3%81%93%e3%81%a8%e3%81%ae%e5%a4%a7%e5%88%87%e3%81%95/) でした！

Productivityチームの久保です。最近MySQLにハマりつつあります。
以前、MySQLのレプリケーション機能について調べている中で、GTIDという概念に触れる機会がありました。
そこで、今回はGTIDについて調査した内容を幾つか紹介できればと思います。「GTIDって言葉は聞いたことあるけど、実際何か知らないぞい」という方のお役に立てれば幸いです。

※Productivityチームについては[こちら](https://engineer.blog.lancers.jp/devops/announcing-the-launch-of-the-productivity-team/)をご参照ください。

## 目次
・GTIDについての説明・概要
・GTIDを用いたレプリケーションの実験
・おまけ（ちょっとソースコード追ってみた）

## GTIDについての説明・概要
この章ではGTIDについて簡単に解説します。

### まずGTIDとは何か？
**GTID**(Global Transaction ID)とはその名の通り、各トランザクションに与えられた一意識別子を指します。
GTIDは`source_id:transaction_id`といった構成となっています。ここで`source_id`はserver-uuidすなわちサーバーの一意識別子、`transaction_id`はトランザクションがコミットされた順番で振られる番号を表します。したがって、GTIDはソース・レプリカ含めて全てのサーバ・全てのトランザクションで一意となります。
GTIDはトランザクションがコミットされると割り当てられ、そしてバイナリログに書き込まれます。

### では、そのようなGTIDは何に使われる？
GTIDはレプリケーションの文脈で用いられます。先に示したようにGTIDによってトランザクションを一意に判別できます。そのため、GTIDを参照すればレプリカ側はソースの更新をどこまで反映できたか・どこから更新する必要があるかを判断することができます。

GTIDが登場する以前は、レプリカのバイナリログとソースのバイナリログの関係性を示す情報が存在しませんでした。つまり、レプリカのバイナリログを見たとしても、それぞれの情報がソースのどの更新を表しているのかを判断することができないという状態でした。そのため、レプリケーションを設定する際にはバイナリログポジションを指定して、「ここから見てね」と教えてあげる必要がありました。

そこでGTIDの登場し、個々のレプリカ・ソース間で自動で連携でき、円滑にレプリケーションやレプリカのソース昇格が行えるようになったのです。

## GTIDを用いたレプリケーションの観測
ここまでで簡単にGTIDに関する概要を説明しました。
では実際にGTIDの設定をしてみてレプリケーションの様子を観察してみることにします。

### 環境
実験環境についてですが、ubuntu:2204のdockerコンテナを用意し、MySQLをソースインストール・ビルドしたものを用意しています。また、MySQLのバージョンは`8.0.28-debug`です（おまけの章で触れますが、ソースデバッグを後に行うことを見越しています）。 そして、レプリケーションの実験をするので、ソース用（`IP:172.17.0.2,port:3306`）とレプリカ用(`IP:172.17.0.4,port:3306`)のコンテナを１台ずつ用意しています。

※なお、レプリカ用のコンテナを用意する際にコンテナの複製を行ったのですが、前章で説明したserver-uuidもコピーされて重複が起きてしまいます。そのため複製後は`auto.cnf`削除＆再起動する必要があります。
※ソースデバッグの環境構築の過程については、別の機会に記事にしようかと思ってます。


### レプリケーション設定

#### my.cnfの設定
レプリケーションを行うために、初めに`my.cnf`で以下のようにサーバー変数の追加・変更をしてmysqldを起動します。

```
# バイナリログの有効化。右辺はバイナリログファイルのプレフィクスであり、この場合'mysql-bin.000001'のようなファイル名になる
log-bin=mysql-bin　　

# サーバーを区別するための簡易的なID。ソース・レプリカで異なるIDを付与する。
server-id=1

# レプリカがソースから受け取った情報を自身のバイナリログに書き込むための設定。
log-slave-updates

# GTIDの有効化。
gtid-mode=ON

# GTID整合性に反するステートメントの禁止。
enforce-gtid-consistency=true
```
※`enforce-gtid-consistency=true`によって禁止されるステートメントの例はいくつかありますが、バージョンとバイナリログフォーマットの組み合わせによって禁止されるかどうかは異なります。例えば一時テーブルの作成（ `CREATE TEMPORARY TABLE`）はMySQL8.0.13以降でかつフォーマットがROWまたはMIXEDであれば、一時テーブル作成ステートメントがバイナリログに書き込まれないため、GTIDと併用可能となります。

#### ソース・レプリカ個別の設定

次にソース・レプリカそれぞれに個別の設定を行います。

`start　replica`（レプリケーション開始）してからのレプリケーションの過程はざっくりと
1. レプリカ側がソース側にバイナリログを要求
2. ソース側がレプリカ側にバイナリログを送信
3. レプリカ側が受け取ったバイナリログを元に更新等を行う

の順番で行われます。そこで1,2の接続のために、ソース側でレプリカ用のユーザを用意します。ちなみに`REPLICATION SLAVE`はレプリケーション用の権限です。
```sql
mysql> CREATE USER  'repl'@'172.17.0.4' IDENTIFIED BY 'password';
Query OK, 0 rows affected (0.07 sec)

mysql> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'172.17.0.4';
Query OK, 0 rows affected (0.01 sec)
```

そしてレプリカ側で1の接続先を設定します。
GTIDが登場する前は、この設定時にバイナリログポジションを明記する必要がありました。
今回はGTIDを使用するので、バイナリログポジションを明記はせずに`SOURCE_AUTO_POSITION`というオプションを明記します。

※ちなみに、初め私は`CHANGE MASTER TO`構文を使っていたのですが、この表現は非推奨となっています。（実際、`CHANGE MASTER TO`構文を用いると、非推奨であることが`show warnings`で確認できます）
```sql
mysql> CHANGE REPLICATION SOURCE TO
> SOURCE_HOST = '172.17.0.2',
> SOURCE_PORT = 3306,
> SOURCE_USER =  'repl',
> SOURCE_PASSWORD =  'password',
> SOURCE_AUTO_POSITION = 1;
```



### レプリケーション実験

では実際にレプリケーションの実験をします。
そこで、ソースとレプリカに以下のような超手抜きテーブルを用意します。
```sql
mysql> SHOW COLUMNS FROM test;
+-------+------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra          |
+-------+------+------+-----+---------+----------------+
| a     | int  | NO   | PRI | NULL    | auto_increment |
+-------+------+------+-----+---------+----------------+
```
そしてソース側だけに新たにレコードを１つ追加して、レプリカとの差分を作ります。
```sql
--　レプリカ側
mysql> select * from test ;
+---+
| a |
+---+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
+---+
8 rows in set (0.00 sec)
-- ソース側
mysql> select * from test ;
+---+
| a |
+---+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
+---+
9 rows in set (0.00 sec)
```

ここで、この時点でのソースとレプリカのバイナリログを確認してみます。

```sql
-- ソース側
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                         |
+------------------+----------+--------------+------------------+-------------------------------------------+
| mysql-bin.000012 |      472 |              |                  | 0beb1885-169e-11ed-9554-0242ac110002:1-10 |
+------------------+----------+--------------+------------------+-------------------------------------------+
1 row in set (0.00 sec)

mysql> show binlog events in 'mysql-bin.000012';
+------------------+-----+----------------+-----------+-------------+--------------------------------------------------------------------+
| Log_name         | Pos | Event_type     | Server_id | End_log_pos | Info                                                               |
+------------------+-----+----------------+-----------+-------------+--------------------------------------------------------------------+
| mysql-bin.000012 |   4 | Format_desc    |         1 |         126 | Server ver: 8.0.28-debug, Binlog ver: 4                            |
| mysql-bin.000012 | 126 | Previous_gtids |         1 |         197 | 0beb1885-169e-11ed-9554-0242ac110002:1-9                           |
| mysql-bin.000012 | 197 | Gtid           |         1 |         276 | SET @@SESSION.GTID_NEXT= '0beb1885-169e-11ed-9554-0242ac110002:10' |
| mysql-bin.000012 | 276 | Query          |         1 |         351 | BEGIN                                                              |
| mysql-bin.000012 | 351 | Table_map      |         1 |         401 | table_id: 91 (test.test)                                           |
| mysql-bin.000012 | 401 | Write_rows     |         1 |         441 | table_id: 91 flags: STMT_END_F                                     |
| mysql-bin.000012 | 441 | Xid            |         1 |         472 | COMMIT /* xid=17 */                                                |
+------------------+-----+----------------+-----------+-------------+--------------------------------------------------------------------+

-- レプリカ側(一部省略)
mysql> show replica status\G
*************************** 1. row ***************************
             Replica_IO_State:
                  Source_Host: 172.17.0.2
                  Source_User: repl
                  Source_Port: 3306
                Connect_Retry: 60
              Source_Log_File: mysql-bin.000011
          Read_Source_Log_Pos: 747
               Relay_Log_File: f00ed9936c1e-relay-bin.000027
                Relay_Log_Pos: 735
        Relay_Source_Log_File: mysql-bin.000011
           Replica_IO_Running: No
          Replica_SQL_Running: No
....
           Retrieved_Gtid_Set: 0beb1885-169e-11ed-9554-0242ac110002:1-9
            Executed_Gtid_Set: 0beb1885-169e-11ed-9554-0242ac110002:1-9
                Auto_Position: 1
....        
1 row in set (0.00 sec)
```
ソース側の`Executed_Gtid_Set`には`0beb1885-169e-11ed-9554-0242ac110002:1-10`と記述されており、ソース側のトランザクション１番目から１０番目が完了していることが分かります。また、`Previous_gtids`には１番目から９番目の記述がなされているので、１０番目が今回のInsertによるものと推測できます（バイナリフォーマットがSTATEMENTであれば、イベント一覧にSQL文も表示されます）。

そして、レプリカ側の`Executed_Gtid_Set`には`0beb1885-169e-11ed-9554-0242ac110002:1-９`と記述されており、ソース側のトランザクション１番目から９番目を反映完了していることが分かります。

ここまででソースとレプリカに差分があることをバイナリログレベルで確認できました。
では最後に、レプリカ側で以下によってレプリケーションを開始します。
```sql
-- レプリカ側
mysql> START REPLICA;
```

mysqldのログに以下のように記載され、レプリケーションが始まったことが確認できます。
```sql
[Repl] Slave I/O thread for channel '': connected to master 'repl@172.17.0.2:3306',replication started in log 'mysql-bin.000011' at position 747
```

それではレプリカのテーブルにソースと同じ行が追加されているかを確認します。
```sql
-- レプリカ側
mysql> select * from test;
+---+
| a |
+---+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
+---+
9 rows in set (0.00 sec)
```
無事ソース側と同じになりました！
`show replica status`で確認もしてみます。

```sql
-- レプリカ側
mysql> show replica status\G
*************************** 1. row ***************************
             Replica_IO_State: Waiting for source to send event
                  Source_Host: 172.17.0.2
                  Source_User: repl
                  Source_Port: 3306
                Connect_Retry: 60
              Source_Log_File: mysql-bin.000012
          Read_Source_Log_Pos: 472
               Relay_Log_File: f00ed9936c1e-relay-bin.000029
                Relay_Log_Pos: 688
        Relay_Source_Log_File: mysql-bin.000012
           Replica_IO_Running: Yes
          Replica_SQL_Running: Yes
....         
           Retrieved_Gtid_Set: 0beb1885-169e-11ed-9554-0242ac110002:1-10
            Executed_Gtid_Set: 0beb1885-169e-11ed-9554-0242ac110002:1-10
                Auto_Position: 1
....
1 row in set (0.00 sec)
```
`Executed_Gtid_Set`を見てみると１０番目のトランザクションも反映済みであることが確認できました。

## おまけ（ちょっとソースコード追ってみた）
前章でGTIDを用いたレプリケーションを観測できましたが、せっかくなのでソースコードを見てみたいと思う訳です。
前章で見たレプリケーションの過程は以下でした。
1. レプリカ側がソース側にバイナリログを要求
2. ソース側がレプリカ側にバイナリログを送信
3. レプリカ側が受け取ったバイナリログを元に更新等を行う

ここで、2の処理を担当するソース側のスレッドは**BinlogDumpスレッド**と呼ばれ、
1,3におけるソースに接続・要求と受信・リレーログへの保存処理を担当するレプリカ側のスレッドを**レプリケーション I/Oスレッド**、そしてそのリレーログの内容を実行するSQLスレッドの作成をするスレッドを**レプリケーションSQLスレッド**と呼びます。

そこで、今回は２の部分つまりBinlogDumpスレッドでの処理を少しだけ追ってみることにします（1,3の部分や2のもっと深そうな部分は別の機会に）

では、ソース側をデバッグしてみます。
gtidやbinlogなどでソースコードをgrepして、それっぽい関数を見つけてブレークポイントを置き、レプリケーションを開始します。

```
(gdb) b mysql_binlog_send
Breakpoint 1 at 0x5598b10686a2: file /mysql-8.0.28/sql/rpl_source.cc, line 994.

(gdb) bt
#0  mysql_binlog_send (thd=0x7f42b000cbc8, log_ident=0x7f42b000cb92 "@replica_uuid = '7f9472e2-6875-11ed-9bb7-0242ac110004'", pos=139924397411152, slave_gtid_executed=0x7f42b000cb18, flags=32578)
    at /mysql-8.0.28/sql/rpl_source.cc:994
#1  0x00005598b10685b7 in com_binlog_dump_gtid (thd=0x7f42b0001330, packet=0x7f42b00066a1 "\002", packet_length=70) at /mysql-8.0.28/sql/rpl_source.cc:982
#2  0x00005598afce46fc in dispatch_command (thd=0x7f42b0001330, com_data=0x7f42e85f5bc0, command=COM_BINLOG_DUMP_GTID) at /mysql-8.0.28/sql/sql_parse.cc:2152
#3  0x00005598afce1b2b in do_command (thd=0x7f42b0001330) at /mysql-8.0.28/sql/sql_parse.cc:1352
#4  0x00005598afee7375 in handle_connection (arg=0x5598b81e6ed0) at /mysql-8.0.28/sql/conn_handler/connection_handler_per_thread.cc:302
#5  0x00005598b1d5e137 in pfs_spawn_thread (arg=0x5598b804b840) at /mysql-8.0.28/storage/perfschema/pfs.cc:2947
#6  0x00007f4336329609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#7  0x00007f4335a77133 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
```


/mysql-8.0.28/sql/sql_parse.ccの`dispatch_command`の引数を見てみると`COM_BINLOG_DUMP_GTID`というコマンドをレプリカ側から受け取っていることが分かります。これはバイナリログの要求を表します。そのコマンドに応じて`com_binlog_dump_gtid`関数を呼びます。
※因みにgtidを使用しない場合は`COM_BINLOG_DUMP`コマンドで要求されます。
```c++:sql_parse.cc
case COM_BINLOG_DUMP_GTID:
      // TODO: access of protocol_classic should be removed
      error = com_binlog_dump_gtid(
          thd, (char *)thd->get_protocol_classic()->get_raw_packet(),
          thd->get_protocol_classic()->get_packet_length());
      break;
```

そして、/mysql-8.0.28/sql/rpl_source.ccの`com_binlog_dump_gtid`で受信した情報を取得し、最終的に`mysql_binlog_send`が呼び出されることが分かります。関数名の通り、バイナリログをレプリカ側に送信します。

```c++:rpl_source.cc
bool com_binlog_dump_gtid(THD *thd, char *packet, size_t packet_length) {
  DBUG_TRACE;
  /*
    Before going GA, we need to make this protocol extensible without
    breaking compatitibilty. /Alfranio.
  */
  ushort flags = 0;
  uint32 data_size = 0;
  uint64 pos = 0;
  char name[FN_REFLEN + 1];
  uint32 name_size = 0;
  char *gtid_string = nullptr;
  const uchar *packet_position = (uchar *)packet;
  size_t packet_bytes_todo = packet_length;
  Sid_map sid_map(
      nullptr /*no sid_lock because this is a completely local object*/);
  Gtid_set slave_gtid_executed(&sid_map);

  assert(!thd->status_var_aggregated);
  thd->status_var.com_other++;
  thd->enable_slow_log = opt_log_slow_admin_statements;
  if (check_global_access(thd, REPL_SLAVE_ACL)) return false;

  READ_INT(flags, 2);
  READ_INT(thd->server_id, 4);
  READ_INT(name_size, 4);
  READ_STRING(name, name_size, sizeof(name));
  READ_INT(pos, 8);
  DBUG_PRINT("info", ("pos=%" PRIu64 " flags=%d server_id=%d", pos, flags,
                      thd->server_id));
  READ_INT(data_size, 4);
  CHECK_PACKET_SIZE(data_size);
  if (slave_gtid_executed.add_gtid_encoding(packet_position, data_size) !=
      RETURN_STATUS_OK)
    return true;
  slave_gtid_executed.to_string(&gtid_string);
  DBUG_PRINT("info",
             ("Slave %d requested to read %s at position %" PRIu64 " gtid set "
              "'%s'.",
              thd->server_id, name, pos, gtid_string));

  kill_zombie_dump_threads(thd);
  query_logger.general_log_print(thd, thd->get_command(),
                                 "Log: '%s' Pos: %" PRIu64 " GTIDs: '%s'", name,
                                 pos, gtid_string);
  my_free(gtid_string);
  mysql_binlog_send(thd, name, (my_off_t)pos, &slave_gtid_executed, flags);
```

/mysql-8.0.28/sql/rpl_source.ccの`mysql_binlog_send` では`Binlog_sender`クラスの`run`関数が呼び出されて、バイナリログの送信が実行されています。

```c++:rpl_source.cc
// /mysql-8.0.28/sql/rpl_source.cc
void mysql_binlog_send(THD *thd, char *log_ident, my_off_t pos,
                       Gtid_set *slave_gtid_executed, uint32 flags) {
  Binlog_sender sender(thd, log_ident, pos, slave_gtid_executed, flags);

  sender.run();
}
```

`run`関数の中身では、while文のループを介してバイナリログを順次読み取っています。
また`send_binlog`関数の中で、同様にwhileループを介してバイナリログ内のイベントを読み取ってレプリカへ送信している様子を伺うことができます(`send_binlog`関数の中で`send_event`といった関数を観測できます)

```c++:rpl_binlog_sender.cc
void Binlog_sender::run() {
  DBUG_TRACE;
  init();

  unsigned int max_event_size =
      std::max(m_thd->variables.max_allowed_packet,
               binlog_row_event_max_size + MAX_LOG_EVENT_HEADER);
  File_reader reader(opt_source_verify_checksum, max_event_size);
  my_off_t start_pos = m_start_pos;
  const char *log_file = m_linfo.log_file_name;

  ...

  while (!has_error() && !m_thd->killed) {
    
    ....

    if (reader.open(log_file)) {
      set_fatal_error(log_read_error_msg(reader.get_error_type()));
      break;
    }

    THD_STAGE_INFO(m_thd, stage_sending_binlog_event_to_replica);
    if (send_binlog(reader, start_pos)) break;
```
なお、冒頭の`init`関数の一部では、`check_start_file`関数を通してバイナリログの探索を行っています。
実際、`find_first_log_not_in_gtid_set`内ではレプリカから送られてきたGTIDを元に、それが含まれているバイナリログの探索を行なっています。具体的にはバイナリログの新しいものから順に`Previous_gtids_log_event`を読み取りって合致するものを取得しています（すなわち、レプリカが反映すべきイベントが記されたログファイルを取得している）

※`find_first_log_not_in_gtid_set`の`case GOT_PREVIOUS_GTIDS:`辺りにブレークポイントを置いて、filenameの変数を確認すると`"./mysql-bin.000021"`のように想定したバイナリログがゲットできていることを確認できます。

```c++
void Binlog_sender::init() {
  DBUG_TRACE;
  THD *thd = m_thd;
  
  ....

  if (check_start_file()) return;

  ....

int Binlog_sender::check_start_file() {
  char index_entry_name[FN_REFLEN];
  char *name_ptr = nullptr;
  std::string errmsg;

  ....

  if (mysql_bin_log.find_first_log_not_in_gtid_set(
            index_entry_name, m_exclude_gtid, &first_gtid, errmsg)) {
      set_fatal_error(errmsg.c_str());
      return 1;
    }
```

```c++:binlog.cc
bool MYSQL_BIN_LOG::find_first_log_not_in_gtid_set(char *binlog_file_name,
                                                   const Gtid_set *gtid_set,
                                                   Gtid *first_gtid,
                                                   std::string &errmsg) {
  DBUG_TRACE;
  LOG_INFO linfo;
  auto log_index = this->get_log_index();
  std::list<std::string> filename_list = log_index.second;
  int error = log_index.first;
  list<string>::reverse_iterator rit;
  Gtid_set binlog_previous_gtid_set{gtid_set->get_sid_map()};

  ....

　　　　rit = filename_list.rbegin();
  error = 0;
  while (rit != filename_list.rend()) {
    binlog_previous_gtid_set.clear();
    const char *filename = rit->c_str();
    DBUG_PRINT("info",
               ("Read Previous_gtids_log_event from filename='%s'", filename));
    switch (read_gtids_from_binlog(filename, nullptr, &binlog_previous_gtid_set,
                                   first_gtid,
                                   binlog_previous_gtid_set.get_sid_map(),
                                   opt_source_verify_checksum, is_relay_log)) {
      ....

      case GOT_GTIDS:
      case GOT_PREVIOUS_GTIDS:
        if (binlog_previous_gtid_set.is_subset(gtid_set)) {
          strcpy(binlog_file_name, filename);
          /*
            Verify that the selected binlog is not the first binlog,
          */
          DBUG_EXECUTE_IF("replica_reconnect_with_gtid_set_executed",
                          assert(strcmp(filename_list.begin()->c_str(),
                                        binlog_file_name) != 0););
          goto end;
        }
```
これらを踏まえて、レプリカ側では`COM_BINLOG_DUMP_GTID`コマンドパケットの送信や上記で探索されたバイナリログの各イベントの受信が行われているだろうと想像できます。それはまたの機会に...

## 終わりに

言葉の理解→実験→ソースリーディングの過程を経て、とても理解が深まりました（そして純粋に楽しかった）。
ソースデバッグにはまだ不慣れですが、これからもこの調査の仕方を継続してみようと思います。
有識者の方々からのご指摘やフィードバックお待ちしておりますmm

明日の担当は、[@igara](https://qiita.com/igara)さんです。お楽しみに！

## 参考文献
[MySQL 8.0 リファレンスマニュアル::17.1.3 グローバルトランザクション識別子を使用したレプリケーション](https://dev.mysql.com/doc/refman/8.0/ja/replication-gtids.html)
[詳解 MySQL](https://www.amazon.co.jp/%E8%A9%B3%E8%A7%A3-MySQL-Sasha-Pachev/dp/4873113431)
[詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド](https://www.amazon.co.jp/%E8%A9%B3%E8%A7%A3MySQL-5-7-%E6%AD%A2%E3%81%BE%E3%82%89%E3%81%AC%E9%80%B2%E5%8C%96%E3%81%AB%E4%B9%97%E3%82%8A%E9%81%85%E3%82%8C%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%81%AE%E3%83%86%E3%82%AF%E3%83%8B%E3%82%AB%E3%83%AB%E3%82%AC%E3%82%A4%E3%83%89-NEXT-ONE/dp/4798147400/ref=pd_lpo_1?pd_rd_w=peEEl&content-id=amzn1.sym.d769922e-188a-40cc-a180-3315f856e8d6&pf_rd_p=d769922e-188a-40cc-a180-3315f856e8d6&pf_rd_r=4RTKBXXTFCHRWHBD4EGZ&pd_rd_wg=jCIAW&pd_rd_r=9ee2a7c1-18de-43f4-aa0d-dc33c7882509&pd_rd_i=4798147400&psc=1)
[MySQL道普請便り GTIDを使用したレプリケーション構成を作成する［1］](https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0024#sec2_h3)
[漢(オトコ)のコンピュータ道 MySQLレプリケーションの運用が劇的変化！！GTIDについて仕組みから理解する](http://nippondanji.blogspot.com/2014/12/mysqlgtid.html?m=1)
[MySQL ソース コード ドキュメント](https://dev.mysql.com/doc/dev/mysql-server/latest/)