amamanamam

データベースと仲良くなりたいです

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

※過去qiitaに書いてた記事のお引越し https://qiita.com/boro1234/items/797b97fc06e7c688b7b2

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)のコンテナを1台ずつ用意しています。

※なお、レプリカ用のコンテナを用意する際にコンテナの複製を行ったのですが、前章で説明した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レプリケーション用の権限です。

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で確認できます)

mysql> CHANGE REPLICATION SOURCE TO
> SOURCE_HOST = '172.17.0.2',
> SOURCE_PORT = 3306,
> SOURCE_USER =  'repl',
> SOURCE_PASSWORD =  'password',
> SOURCE_AUTO_POSITION = 1;

レプリケーション実験

では実際にレプリケーションの実験をします。 そこで、ソースとレプリカに以下のような超手抜きテーブルを用意します。

mysql> SHOW COLUMNS FROM test;
+-------+------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra          |
+-------+------+------+-----+---------+----------------+
| a     | int  | NO   | PRI | NULL    | auto_increment |
+-------+------+------+-----+---------+----------------+

そしてソース側だけに新たにレコードを1つ追加して、レプリカとの差分を作ります。

-- レプリカ側
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)

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

-- ソース側
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と記述されており、ソース側のトランザクション1番目から10番目が完了していることが分かります。また、Previous_gtidsには1番目から9番目の記述がなされているので、10番目が今回のInsertによるものと推測できます(バイナリフォーマットがSTATEMENTであれば、イベント一覧にSQL文も表示されます)。

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

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

-- レプリカ側
mysql> START REPLICA;

mysqldのログに以下のように記載され、レプリケーションが始まったことが確認できます。

[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

それではレプリカのテーブルにソースと同じ行が追加されているかを確認します。

-- レプリカ側
mysql> select * from test;
+---+
| a |
+---+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
+---+
9 rows in set (0.00 sec)

無事ソース側と同じになりました! show replica statusで確認もしてみます。

-- レプリカ側
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を見てみると10番目のトランザクションも反映済みであることが確認できました。

おまけ(ちょっとソースコード追ってみた)

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

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

そこで、今回は2の部分つまり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コマンドで要求されます。

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が呼び出されることが分かります。関数名の通り、バイナリログをレプリカ側に送信します。

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関数が呼び出されて、バイナリログの送信が実行されています。

// /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といった関数を観測できます)

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

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;
    }
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コマンドパケットの送信や上記で探索されたバイナリログの各イベントの受信が行われているだろうと想像できます。 それはまたの機会に...

参考文献

MySQL 8.0 リファレンスマニュアル::17.1.3 グローバルトランザクション識別子を使用したレプリケーション 詳解 MySQL 詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド MySQL道普請便り GTIDを使用したレプリケーション構成を作成する[1] 漢(オトコ)のコンピュータ道 MySQLレプリケーションの運用が劇的変化!!GTIDについて仕組みから理解する MySQL ソース コード ドキュメント