免責事項
この記事はSujatha Sivakumar氏によるMySQL High Availabilityの投稿「STOP SLAVE Improvements for Multi-Threaded Slaves」(2016/3/4)をユーザが翻訳したものであり、Oracle公式の文書ではありません。
マルチスレッドスレーブを利用しているときは、STOP SLAVEコマンドは応答が返るまで時間がかかります。スレーブがワーカーのキューの処理が完了するのを待機するためです。このブログ投稿ではMySQL 5.6.26およびそれ以降のバージョンで加えられている改良に関しての内容となり、改良の内容はマルチスレッドスレーブ(MTS)を利用している時でもSTOP SLAVEコマンドが素早く応答するようにしたものです。
背景
ここでのマルチスレッドスレーブは、トランザクションをスレーブに適用している最中のトランザクションをまたがる並列化のことを述べています。これによりレプリケーションが、とりわけマルチコアアーキテクチャ上では、一層スケーラブルなものとなります。MTSモードにおいてはレシーバースレッド(IOスレッド)はイベントおよびトランザクションをマスターから受取り、ローカルのリレーログにキューイングします。適用スレッド(SQLコーディネーター)はこれらのイベントを読込みワーカーが実行するようにスケジュールします。次の画像がワーカーが2つの場合にワーカーのキューがどのようになるのかを示しています。
画像1: シナリオ: ワーカーが2つの場合のMTS
ワーカーを突然(KILLのように)停止した場合、スレーブの実行ストリームにギャップが発生します。ワーカーが上記の画像1のような状態のときに、KILLコマンドがコーディネーターおよびワーカーの両者に実行された場合、実行中のトランザクションが完了したあとにスレーブは停止するでしょう。このシナリオではトランザクションT5は実行されず、これによってMTSの実行シークエンスにギャップが発生することになります。 GTIDを利用すると効率的にこのギャップを埋めることができます。自動で位置取りをするレプリケーションプロトコルは、まず最初にMTSが適用しなかったトランザクションのイベント処理を行い、さらにその後の全てのトランザクションの処理も行い、これに関しては既に適用済である可能性すらあります。幸運なことに、GTIDが有効化されていればトランザクションが2回適用されることに対する心配はいりません(2回目の実行では何も変更されず、単にスキップされるため)。GTIDが有効化されていなければ、トランザクションは再度適用されデータの不整合につながります。
画像2: MTSコーディネーターおよびワーカーのKILL
MTSにおけるSTOP SLAVEコマンドの実行は、確実にギャップのない状態でスレーブを停止させます。 MySQL 5.6.26より前のバージョンではこの状態を達成するために、STOP SLAVEコマンドは全てのワーカーが割り当てられたタスクを完了するまで待機していました。画像1のシナリオで考えてみましょう。このシナリオでSTOP SLAVEコマンドの実行すると下記の画像3の状態となります。
画像3: 修正前のSTOP SLAVE
STOP SLAVEコマンドはトランザクションT8とT9が実行されるまで待機していました。上記の処理によってギャップのない場所で停止することを保証しますが、他方でSTOP SLAVEコマンドが完了するまでとても長く時間がかかります。ワーカーのキューが多数ある場合は、コマンドが応答するまでさらに時間がかかることになります。
しかしながら、MySQL 5.6.26およびそれ以降のバージョンでは、STOP SLAVEコマンドはワーカーが全てのキューの完了を待つのではなく、代わりに最近傍のギャップのない状態を判別しその場所で停止します。 次の画像(画像4)は改良後のワーカーキューの状態を表します。STOP SLAVEコマンドはトランザクションT8とT9の完了をまたないように改良されています。
画像4: 修正後のSTOP SLAVE
改良されたSTOP SLAVEアルゴリズムの動作
MTSコーディネータースレッドはSTOPコマンドを受け取ると、全てのワーカーに稼動状態(running_status)をSTOPと設定することでSTOPするよう通知します。STOPコマンドが受け取られると、全てのワーカーは完了した(あるいは実行中の)グループインデックスの最大値を確認します。グループインデックスはそれぞれのグループおよびトランザクションがワーカーに割り当てられる際にインクリメントされるカウンタです。ワーカーに実行する必要があるイベントがあった場合、現在のグループインデックスが使われ、ワーカーのキューが空であった場合は最後に実行したグループのインデックスがグループのインデックスの最大値(max_group_index)の計算に使われます。
max_group_indexの値が決まれば、グループインデックスの最大値と同じかそれより小さい値のグループインデックスの値を持つワーカーは自身のグループを適用します。グループインデックスの最大値より大きいグループインデックス値を持つワーカー、あるいはキューが空の場合は、単純に終了します。各ワーカーは1度だけmax_group_indexに寄与していることに注意してください。この処理によってスレーブは最近傍のギャップのない状態で停止することが保証できます。次の例で改良されたSTOP SLAVEアルゴリズムがどのように動作するかを実演します。
デモシナリオ
2つのデータベース、d1とd2があり、次のテーブルがこれらのデータベースにあるとします。トランザクションとスレーブ上でのそれぞれのグループインデックスの値(g_index)は下記の通りです。
--マスターへの接続
CREATE DATABASE d1; -- g_idx:1
CREATE DATABASE d2; -- g_idx:2
CREATE TABLE d1.t (a INT PRIMARY KEY, name text) ENGINE=INNODB; --g_idx:3
CREATE TABLE d2.t (a INT PRIMARY KEY, name text) ENGINE=INNODB; --g_idx:4
--ここでスレーブをマスターと同期
スレーブ上で直接、2行の挿入を行うトランザクションを開始します。
--スレーブへの接続
BEGIN;
INSERT INTO d2.t VALUES (2, 'Slave local'); # マスターからくるT3を停止させるため
INSERT INTO d1.t VALUES (3, 'Slave local'); # マスターからくるT6を停止させるため
マスター上で次のトランザクションを実行します。これはスレーブにレプリケーションされます。d1.tとd2.tには主キー制約があるため、スレーブはT3とT6のトランザクションをブロックします。
--マスターへの接続
INSERT INTO d1.t VALUES (1, 'T1'); g_idx:5
INSERT INTO d2.t VALUES (1, 'T2'); g_idx:6
INSERT INTO d2.t VALUES (2, 'T3'); g_idx:7 # スレーブ上でブロックされる
INSERT INTO d2.t VALUES (3, 'T4'); g_idx:8
INSERT INTO d1.t VALUES (2, 'T5'); g_idx:9
INSERT INTO d1.t VALUES (3, 'T6'); g_idx:10 # スレーブでブロックされる #ギャップのない状態 --#1
INSERT INTO d2.t VALUES (4, 'T7'); g_idx:11 # これはSTOP SLAVE後に実行されるべきではない
INSERT INTO d2.t VALUES (5, 'T8'); g_idx:12 # これはSTOP SLAVE後に実行されるべきではない
INSERT INTO d1.t VALUES (4, 'T9'); g_idx:13 # これはSTOP SLAVE後に実行されるべきではない #ギャップのない状態 --#2
--スレーブへの接続1
--STOP SLAVEを送る。これはワーカーが終了するのを待機する
--スレーブへの接続2
ROLLBACK #一時的に停止しているT3とT6を解放する
上記の一連の操作をマスターおよびマルチスレッドスレーブに実行したとしましょう。スレーブ上のコーディネーターが上記の全トランザクションがワーカーのキューに割り当てるのをいくばくか待つでしょう。次にSTOP SLAVEコマンドを実行しT3とT6の一時停止を解放します。W1およびW2ワーカーはmax_group_index値の計算に参加します。W1は現在トランザクションT6(g_idx: 10)を実行中でW2はT3(g_idx: 7)を実行中のため、W1は10、W2は7を返します。これらの値からmax_group_indexが判断され10と設定されます。それからスレーブはこの改善されたアルゴリズムで最初のギャップのない状態(グループインデックス10)で停止します。
ワーカーが処理を継続しmax_group_indexの計算に再度参加できたら、つまりW2がビジー状態でW1がT6を完了させた後、T9をとってmax_group_indexの計算に再参加するとギャップのないウィンドウがT9まで広がります。このようにスレーブは最近傍のギャップのない点で停止できなくなります。これはSTOP SLAVEコマンドを遅くします。したがって各ワーカーはmax_group_indexの計算に1回だけ参加すべきです。この新しいアルゴリズムでスレーブは#1の位置に停止できます。
上記の変更はMySQL 5.6.26のBug#75525の一部として行われました。
簡単なデモ
MTSをslave_parallel_workers=2で有効化します。
ステップ1: マスター上で2つのデータベースと2つのテーブルを作成します。
--マスター上のコネクション
mysql> CREATE DATABASE d1; CREATE TABLE d1.a(i int) ENGINE=INNODB;
Query OK, 1 row affected (0.00 sec)
Query OK, 0 rows affected (0.08 sec)
mysql> CREATE DATABASE d2; CREATE TABLE d2.a(i int) ENGINE=INNODB;
Query OK, 1 row affected (0.00 sec)
Query OK, 0 rows affected (0.09 sec)
ステップ2: スレーブ上で各INSERTに遅延を発生させるためのsleepとともにトリガーを作成します。
--スレーブへの接続
mysql> CREATE TRIGGER d1.iai AFTER INSERT ON d1.a FOR EACH ROW DO SLEEP(1);
Query OK, 0 rows affected (0.01 sec)
mysql> CREATE TRIGGER d2.iai AFTER INSERT ON d2.a FOR EACH ROW DO SLEEP(1);
Query OK, 0 rows affected (0.02 sec)
ステップ3: マスター上で2つのデータベースt1とt2に1000タプルをINSERTする小さなプロシージャを実行します。
--マスターへの接続
mysql> USE d1;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql>DELIMITER //
mysql>CREATE PROCEDURE simple_insert()
-> BEGIN
-> DECLARE id INT;
-> SET id = 1;
-> WHILE id INSERT INTO d1.a (i) VALUES (id);
-> INSERT INTO d2.a (i) VALUES (id);
-> SET id = id + 1;
-> END WHILE;
-> END//
Query OK, 0 rows affected (0.00 sec)
mysql> DELIMITER ;
mysql> CALL simple_insert();
Query OK, 1 row affected (11.68 sec)
ステップ4: マスター上のステップ3が完了し次第、スレーブ上でSTOP SLAVEを実行します。
修正前
--スレーブへの接続
mysql> STOP SLAVE;
Query OK, 0 rows affected (4 min 18.66 sec)
修正後
--スレーブへの接続
mysql> STOP SLAVE;
Query OK, 0 rows affected (0.77 sec)
結論
Bug#75525の修正以前は、マルチスレッドスレーブ上でのSTOP SLAVEコマンドは常に全ワーカーのキューが終了するのを待機してからSTOPしていました。これはSTOP SLAVEコマンドが完了まで長時間かかることを意味していました。MySQL 5.6.26のBug#75525の修正以後は、STOP SLAVEは最近傍のギャップのない状態を判別しその点で停止するようになりました。試してみてフィードバックをください!