BizStationブログ

ビズステーション株式会社の公式ブログです。

MySQL/MariaDB GTID レプリケーション詳細

今回は、MySQL/MariaDB GTID レプリケーションの詳細を説明します。これは、Transactdによるレプリケーションセットアップ(修復)ツールを構築する際に調べたものです。

主に従来のバイナリログとポジションを使ったレプリケーションとGTIDによるレプリケーションの違いについて説明します。ある程度従来のレプリケーションのセットアップなどを理解していることを前提にしています。

Index

なぜGTIDが必要なのか

まず、最初になぜGTIDが必要なのでしょうか?よく言われているのは、

  • CHANGE MASTER TOバイナリログポジションをいちいち指定しなくても良い

といったものですが、さほど大した問題では無いように感じます。本当のところ何のためでしょうか?

それは、MariaDBのドキュメントに明確に書かれています。

  • マスターと複数スレーブの構成で、マスターがダウンしてスレーブのいずれかがマスターに昇格したときに、他のスレーブがマスターを切替えるためCHANGE MASTERを発行するが、その時のポジション指定の問題を解決する

マスターがダウンしたら、スレーブ群の中から、レプリケーションの遅れが最も少ないスレーブをマスターに昇格*1させます。
他のスレーブは新マスターに対してCHANGE MASTERを発行してマスターを切り替えます。問題なのはこのときです。各スレーブごとに旧マスターのバイナリログ名とポジションでどこまでレプリケートしたかはわかっています。しかし、新マスターでいうところのどのファイル名でポジションがいくつなのかはわからないのです。*2
GTIDはこの問題を解決するために生まれました。

MariaDBのGTIDの目的はこれだけです。
MySQLのGTIDにはさらに「マスターとスレーブの一貫性の判断を容易にする」という目的が加えられています。これについては MySQLでGTIDを使う の項目で説明します。)

GTIDが活きるシナリオ

マスターと複数スレーブの構成でマスターを切り替えるフェイルオーバーを行うのであれば、GTIDの恩恵を大いに受けることができます。

正確にはもう少し限定的で、フェイルオーバーを行う際にスレーブ群の中で遅れているスレーブがあってもそれを救済するフェイルオーバーです。
もし、遅れているスレーブを切り捨てるならば、フェイルオーバーした新マスターと他のスレーブは同じデータを持っているので、新マスターのバイナリログをリセットreset masterしスレーブもreset slaveしてポジション指定なしでCHANGE MASTERすれば、従来の方法でも正しく切替ができるからです。

もう一つ、手動でマスターを切替える(スイッチオーバー)際に、すべてのスレーブが同期するのを待たなくて良くなります。同期できているスレーブが1台あればそれを新マスターにすることで遅れているスレーブはその新マスターからデータを受け取ることができるようになります。迅速なスイッチオーバーができます。

上記の必要がなければGTIDの恩恵はなく、従来のバイナリログとポジションを使ったレプリケーションとほぼ同等の機能です。(追記 マルチマスターになると話がややこしくなりますのでここではそれは除いて考えます)

GTIDによるポジションの解決

まず最初に、マスター切り替え時のポジション問題の解決方法について説明します。解決の考え方自体はMariaDBMySQLもほとんど同じです。(実装と扱うためのコマンドは異なります)

従来は、マスターが切り替わったときに、スレーブが新マスターに対して「旧マスターのこのポジション」と要求しても、新マスターのバイナリログはサーバーが異なるので意味がなく、どこのことかわからない状態でした、そのため、新マスターのバイナリログをスキャンし、スレーブが適用済みの位置を自前で探すしかありませんでした。位置を探すのも明確な目印があるわけでもなく、ある程度の長さが一致する部分を探すしかなかったわけです。

そこで、バイナリログに目印としてトランザクションごとに番号を振って、それを一緒に記録するようにしました。この番号がGTIDです。サーバーが異なってもIDが重複しないようにグローバルにユニークな番号です。(Global Transaction ID

また、スレーブはバイナリログの書き込みの際に、マスターから転送されたトランザクションのGTIDを必ず転記するようにします。そうすることで後でマスターになったとしても、スレーブからGTIDさえ教えてもらえればバイナリログのその場所を正しく探し出して返すことができます。

GTIDを使ったレプリケーションの処理をまとめると以下のようになります。

  • マスターはトランザクションごとに重複しないIDを振り、それをバイナリログに記録する
  • マスターはスレーブからのバイナリログ要求にGTIDの情報も加えて転送する
  • スレーブはマスターから受け取ったトランザクションを自身のバイナリログを記録する際にそのID(GTID)を転記する

これらの準備があったうえで、以下のようにすることで、旧マスターでのポジションが新マスターではどこかを解決できるようになります。

  • スレーブはバイナリログログポジションに代えて最後に受け取ったGTIDで要求する
  • マスターはバイナリログ中のGTIDを探し次のトランザクションを返す

具体的なGTID

GTIDはサーバーが異なっても重複しないユニークな番号ですが、MySQLMariaDBでは生成方法が異なります。

MariaDBdomain-id + server-id + サーバーごとのトランザクション番号
MySQLUUID + サーバーごとのトランザクション番号
MySQLのUUIDはWindowsでよく使われるあの長い322e3d16-355f-11e3-9ee3-00155d031304といった番号です。
MariaDBは従来のserver-idの前にdomain-idを付加することでユニークなものとしています。domain-idはmy.cnfで指定しますが何も指定しなければ0です。

実際の番号はそれぞれ以下のように文字列表現されます。

MariaDB 0-1-1234
MySQL 322e3d16-355f-11e3-9ee3-00155d031304:1234
MariaDBは要素をハイフンでつなぎます。MySQLはコロンでつなぎます。

GTIDを使うための設定

GTIDを使うためのmy.cnfの設定を説明します。

MariaDB/MySQL共通 server-id = 1
log-bin=mysqld-bin
binlog-format = ROW
log-slave-updates
MariaDB gtid_domain_id = 1 (書かなければ gtid_domain_id = 0)
MySQL gtid_mode=ON
enforce-gtid-consistency
赤色の値は、固定値ではなく任意に決めることのできる値です。それぞれの意味の詳細はMySQLのDocument等で確認いただければと思います。
MariaDBはGTIDの基本部分を自分で決めることができますが、MySQLは自動でUUIDを生成します。生成したUUIDはauto.cnfに記録されています。スレーブのデータをマスターからコピーする際にこれもコピーしてしまうと、UUIDが重複してしまうので、コピーしないようにします。

では実際の使い方を説明します。MariaDBMySQLでは異なっているのでそれぞれ分けて説明します。

MariaDBでGTIDを使う

MariaDBではバイナリログポジションがGTIDに代わっただけと考えてほぼ差支えありません。多少パラメータの指定方法が違いますが、従来の方法との互換性が高く、非常に使い易くなっています。
また、MariaDB 10.0以降では、server-idが指定されていれば自動でGTIDが振られているため、特別な設定はほとんどありません。GTIDでポジションを指定してもよいし、従来通り指定しても何ら問題ありません。

構成を行う前準備として、マスター/スレーブともにRESET SLAVE ALLRESET MASTERを事前に行い、現在のログをクリアしておくようにお勧めします。フェイルオーバーした際に、無効なログによる誤動作を防止できます。また、マスターの mysql.gtid_slave_posテーブルのレコードを必ず削除しておいてください。MariaDBのバグでRESET SLAVE ALLを行ってもその値をクリアしてくれません。ここに過去の意味のない情報が残っていると、ダウンしたマスターをスレーブ群に加える際誤動作します。

2つのGTIDポジションモード

スレーブがスタート時にマスターにバイナリログ内容の送信を要求する際には、2種類のGTIDポジションがあります。

  1. gtid_slave_pos:スレーブSQLスレッド*3が最後に実行したトランザクションのGTID
  2. gtid_current_pos:最後に処理したトランザクションのGTID

2つの違いを説明します。gtid_slave_posは、マスターから転送されたトランザクションのうち、最後に実行されたもののGTIDです。一方、gtid_current_posは、マスターから転送されたトランザクションだけでなく、スレーブで直接実行されたトランザクションも含めて最後に処理したもののGTIDです。
スレーブで直接実行されたトランザクションがなければ、gtid_slave_posgtid_current_posは同じ値になります。

SQLスレッドエラーの修復などで、スレーブで直接実行されたトランザクションがある場合は、異なった値になり得ます。
スレーブで直接実行されたトランザクションは、当然マスターには存在しません。そのため、gtid_current_posはマスターのバイナリログに存在しないGTIDの可能性があります。その場合はログ転送ができないため、スレーブはI/Oエラーで開始できません。

スレーブの開始時にgtid_current_posを使用する必要はないように思えますが、後述する「ダウンしたマスターをスレーブ群に加える」の際には便利です。

新規セットアップ

まず、マスターとスレーブのデータを事前に(データコピーやmysqldumpを使って)同じ内容にしておくのは従来と同じです。
レプリケーションを開始するバイナリログ名とポジションはCHANGE MASTER TOで指定していましたが、gtid_slave_posグローバル変数にGTIDを設定してからCHANGE MASTER TO master_use_gtid=slave_posとします。
設定すべきGTIDはマスターのselect @@gtid_binlog_pos;で取得します。また、Transactdではdatabase::beginSnapshot(binlogPos)binlogPos->gtidでスナップショットの開始と同時に取得できます。

SET GLOBAL gtid_slave_pos = "0-1-1234";
CHANGE MASTER TO master_host="master-host", master_port=3306, master_user="rep_useer", master_password="6789", master_use_gtid=slave_pos;
START SLAVE;

スレーブで、マスターを新マスターに切り替える

スレーブのSQLスレッドで最後に処理したGTID(このポジションをslave_posと呼びます)がわかれば、それ以降を新マスターに要求すれば済みます。しかし実際にはホストを指定し直すだけで、遅れていたスレーブも簡単に切替できます。なぜなら、gtid_slave_posグローバル変数には最後にSQLスレッドが処理したGTIDが入っているからです。
CHANGE MASTER TO master_host="master-host", master_port=3306でOKです。ポジションの指定は不要です。

スレーブをマスターに昇格させるときは、最後のGTIDを記録しましょう。旧マスターに未レプリケートのデータが残っていたときのためです。
もし、そのようなデータがある場合は、旧マスターが使えるようになったら一時的に新マスターを旧マスターのスレーブとしてそのGTIDで開始します。そうすれば、旧マスターにしかな残っていないデータを新マスターに補てんすることができます。(それができたらこの旧マスターをスレーブ群に追加することができます。)

余談ですが、仮にスレーブが従来のバイナリログ名とポジションを指定していたとします。それでも、CHANGE MASTER TO master_host="master-host", master_port=3306, master_use_gtid=current_posだけでGTIDを使ったマスター切替が行えます。

ダウンしたマスターをスレーブ群に加える

ダウンしたサーバーの回復が完了し、スレーブ群に加える際、このサーバーはスレーブになったことがないので、SQLスレッドで最後に処理したGTID(slave_pos)がありません。最後に処理したGTIDは自身が最後に処理したポジション(current_pos)です。この場合は、CHANGE MASTER TO master_use_gtid = current_posを指定します。ポジションの指定は不要です。
または、最後に処理したGTIDを select @@gtid_binlog_pos;で調べて、新規セットアップと同様に指定してもOKです。

SQLスレッドのエラーを修復する

MariaDBは従来通りの方法が使用できます。sql_slave_skip_counterでイベントをスキップするか、エラーの原因となっている問題を取り除いて再度スタートする方法です。ただし、MariaDB 10.0.12以前はsql_skip_counterが使用できなくなっていましたので、それ以降のバージョンにしましょう。
また、エラーの原因となっている問題を取り除く際、その処理がバイナリログに記録されない方がよい場合は、事前にSET sql_log_bin=OFFとし記録されないようにします。
テーブルやデータベースごとコピーし直したい場合は、以下の記事を参考にしてください。
bizstation.hatenablog.com

MySQLでGTIDを使う

MySQLでの使い方を説明する前に、MariaDBにはないGTIDのもう一つの目的「マスターとスレーブが一貫しているかどうかを判断する」について説明します。

GTIDセット

MySQLのGTIDによるログの転送は、従来のログファイル名とポジションで行っていたポイント指定(位置指定)でなく、マスター・スレーブのそれぞれで処理したすべてのトランザクション番号を保持し、それらが同じかどうかを比較し、不足分を要求するという方法です。
すなわち、ポイントを管理するのではなく、GTIDのセットを管理します。

この不足分を補う方法で「マスターとスレーブが一貫しているかどうかを判断する」を実現しようとしています。「しようとしています」としたのは、抜け道がたくさんあって「マスターとスレーブのデータが同一である」とは保証できないためです。

GTIDセットの表現方法

処理したGTIDをすべて列挙すると大変な量になってしまうため、連続したIDの場合「1-100」といったように最初と最後の番号のみを保持します。
具体的には、ID1~100であれば322e3d16-355f-11e3-9ee3-00155d031304:1-100のように表現します。マスターを切替えた場合などでは、スレーブの処理済みのGTIDセットには、最初のマスターの分と新マスターの分の両方が含まれます。GTIDセットは322e3d16-355f-11e3-9ee3-00155d031304:1-100,b17243ca-11c2-11e6-95ee-00ffcc08618a:1-1200のようにカンマで区切り列挙されます。

GTIDセットの比較

マスターとスレーブで適用されているトランザクションを比較しますが、適用されたGTIDセットをどこで確認するか説明します。

サーバー コマンド 名前
マスター SHOW MASTER STATUS Executed_Gtid_Set 322e3d16-355f-11e3-9ee3-00155d031304:1-100
スレーブ SHOW SLAVE STATUS Executed_Gtid_Set 322e3d16-355f-11e3-9ee3-00155d031304:1-100
Executed_Gtid_Setは、現在存在するバイナリログに記録されているすべてのGTID(グローバル変数gtid_executed)と、既に正しくパージ(削除)(gtid_purged)されたすべてのGTIDの両方を含めたものです。
Executed_Gtid_Setをクリアするには、マスター/スレーブに関わらず RESET MASTERを行います。RESET MASTERは同時にバイナリログを消去します。gtid_purgedは、gtid_executedが空の場合に限って変更可能です。
従って、マスターデータをスレーブにコピーしレプリケーションを開始する場合は、スレーブにてRESET MASTERを行ったあと、gtid_purgedにマスターのExecuted_Gtid_Setの内容を与えることで、双方の適用済みGTIDセットは同じであると判断させます。

GTIDと従来の方法の混在

MySQL 5.6では、GTIDを使う場合はすべてのサーバーでGTIDを使用しなければなりません。一度すべてのサーバーをシャットダウンして有効化する必要があります。5.7からはローリングアップデートと呼ばれる方法で順次有効化できるようです。(これについては未調査です。)

新規セットアップ

まず、マスターとスレーブのデータを事前に(データコピーやmysqldumpを使って)同じ内容にしておくのは従来と同じです。
MySQLのGTIDレプリケーションでは双方のGTIDセットを比較するので、gtid_purgedにマスターのExecuted_Gtid_Setをセットして、実行済みGTIDセット同じであるようにしてから開始します。
設定すべきGTIDは従来と同様マスターのSHOW MASTER STATUSで取得します。また、Transactdではdatabase::beginSnapshot(binlogPos)binlogPos->gtidで、スナップショットの開始と同時にGTIDセットを取得できます。

RESET MASTER;
SET GLOBAL gtid_purged="322e3d16-355f-11e3-9ee3-00155d031304:1-100";
CHANGE MASTER TO master_host="master-host", master_port=3306, master_user="rep_useer", master_password="6789", auto_position=1;
START SLAVE;

Executed_Gtid_Setは非常に長い文字列になる場合があり、従来のバイナリログ名とポジションより設定し易いとは言いがたいものです。
なお、mysqldumpを使用するとダンプファイルの最後の方にSET @@GLOBAL.GTID_PURGED='322e3d16-355f-11e3-9ee3-00155d031304:1-100';の一文を自動で含めてくれます。

スレーブで、マスターを新マスターに切り替える

これはとても簡単で、CHANGE MASTER TO master_host="master-host", master_port=3306, master_user="rep_user", master_password="password", auto_position=1;とするだけです。新マスターのGTIDセットとスレーブのGTIDセットを比較して、不足があれば補ってくれます。ポジションの指定は不要です。

スレーブをマスターに昇格させるときは、最後のバイナリログ名とポジションを記録しましょう。
もし、旧マスターにしかない未レプリケートデータが残っている分があった場合は、あとでその分を新マスターに補います。このとき、新マスターをもう一度旧マスターのスレーブにするだけでレプリケートしてくれるでしょうか? これはだめです。GTIDを使うとセット管理なのでわけのわからないことになってしまいます。ここは、従来のバイナリログ名とポジションを使って不足分だけをレプリケートしてください。

ダウンしたマスターをスレーブ群に加える

これもとても簡単で、CHANGE MASTER TO master_host="master-host", master_port=3306, master_user="rep_user", master_password="password", auto_position=1;とするだけです。新マスターのGTIDセットとダウンした旧マスターのGTIDセットを比較して不足があれば自動で補ってくれます。ポジションの指定は不要です。

SQLスレッドのエラーを修復する

結論から言うと、これはとても厄介です。マスター/スレーブの処理済みGTIDセットは同じでなければならず、不用意にスレーブに変更を加えると、その時はよくても、それがマスターになった際にはその変更が他のスレーブにも転送されてしまいます。内容によってはすべてのスレーブが停止してしまう原因になったりします。
また、違いをセットで比較する性質上sql_slave_skip_counterは使用できません。仮に使用できたとしても、GTIDセットに食い違いがでるのでその分を転送しようとしてしまいます。

そこで、エラーを起こすGTID番号のトランザクションの内容を他の内容に変えてしまう(代替)という方法で、SQLエラーを回避します。
これはMySQLの2つの特性を利用して行うものです。


これらを利用して以下のようにすると、sql_slave_skip_counterと同じように問題のイベントをスキップまたは修復できます。

SET gtid_next="322e3d16-355f-11e3-9ee3-00155d031304:101";
START TRANSACTION
... //ここに回復処理 単にスキップしたければ何もせずcommit
COMMIT
START SLAVE;

ここで、gtid_nextに指定するGTIDがいくつなのか調べる方法を説明します。
SQLエラーを起こした場合SHOW SLAVE STATUSLast_SQL_Errorにその内容が書かれます。しかし、バイナリログ名とポジションは記載されますが、GTIDはありません。Executed_Gtid_Setの末尾には最後に適用されたGTIDが書かれています。例えば322e3d16-355f-11e3-9ee3-00155d031304:1-101であれば322e3d16-355f-11e3-9ee3-00155d031304:101が最後のIDです。通常エラーを起こしたGTIDはこの次のID(102)です。

話は戻りますが、仮に先ほどのエラーを修正したスレーブがマスターになって、他のスレーブにその修正内容を適用するとまずい場合は、すぐに、

FLUSH LOGS;
PURGE BINARY LOGS TO 'mysql-bin.xxxx';

のようにして、ログからトランザクションの内容をパージしましましょう。遅れたスレーブがそのGTIDを要求すると「既にパージ済みで適用できない」といったエラーになってそのスレーブは停止してしまいますが、無用なトランザクションを適用して進んでしまうよりはマシなはずです。*4
もう一つの方法は、特定のテーブルやデータベースに限定できる場合にそれらのみ再コピーし、新規セットアップ同様に

RESET MASTER;
SET GLOBAL gtid_purged="322e3d16-355f-11e3-9ee3-00155d031304:1-100";
START SLAVE;

とすることです。これも以下の記事を参考にしてください。
bizstation.hatenablog.com

MySQLにおける「マスターとスレーブの一貫性の判断が容易」について

GTIDセットの一致を要求するMySQLの実装は、一見すると一貫性が保証されるように見えます。しかし、初期セットアップ時のデータ違いや、SQLスレッドのエラー修復など、運用管理の仕方によって簡単に矛盾した状態が生じます。この矛盾は従来の方法やMariaDBでも全く同様で、GTIDセットによる特別な効果はありません。

それでも、スレーブがバイナリログを書いたあとに(OSはsuccessを返したが)ディスクコントローラエラーなどで実際には記録されていなかった場合には、スレーブを再起動すると、マスターとスレーブのログの不一致を検出できるということはあるかと思います。すべての状況を考察したわけではありませんが、GTIDセットを比べることで得られるのは「スレーブで書いたはずのログに欠落があったとか、バグで転送されていない処理があった」などの検出でしょうか。

真のマスターとスレーブのデータの一貫性の保証は、従来通り双方のすべてのレコードをコンペアするしかないということには変わりないかと思います。

まとめ

  • GTIDのメリットとしてよく言われる「CHANGE MASTER TOバイナリログポジションをいちいち指定しなくてもよい」はスレーブがフェイルオーバーで昇格した新マスターに切り替える際の話であって、新規セットアップなどでは、バイナリログポジションに替わってGTID(セット)を指定する必要がある。(追記 新規レプリケーションセットアップ時にマスターのバイナリログも一緒にコピーすれば、自動でそのログをスキャンして無指定で開始することもできます。ただ、それであればそもそもすべて同じなのでGTIDによる恩恵とは言い難いものです。)
  • GTIDは、マスターと複数スレーブによる、マスターダウン時にフェイルオーバーする構成でメリットがある。
  • MariaDBのGTIDは、従来のバイナリログ名とポジションに番号を振っただけに近く、従来とほぼ同様の運用が行える。(sql_slave_skip_counterも使える)
  • MySQLのGTIDは、単一の位置情報でログを要求するのではなく、マスターとスレーブで適用されたすべてのトランザクション(GTIDセット)の比較で行われる。
  • MySQLで適用済みのGTIDセットを忘れさせるには、RESET MASTERを行う(しかない)。
  • MySQLのGTIDによる「マスターとスレーブの一貫性の判断が容易」は限定的であって、真のマスターとスレーブのデータの一貫性の保証はされない。

おまけ どっちがよいか?

レプリケーションマネージャを作成するために、数百回もレプリケーションを構築し、エラーを発生させ修復するといった作業を行ってきての感想です。

ポイントは「何かあったときの修復し易さ」です。
MySQLでGTIDを使う場合は、マスターデータとスレーブをきっちり同じにしてSQLエラーを絶対に起こさないように運用管理をしっかりしないと、フェイルオーバーしたあとで予期しない全スレーブのSQLスレッド停止なんてことになりかねません。また、そうなるとその修復でも問題が起き、収集がつかなくなります。

対してMariaDBは、もう少しラフに多少なにかあっても従来と同様に修復できます。sql-log-bin=offにして修復するか、もしくはgtid_slave_posモードであればその修復のことは忘れてくれます。

また、GTIDの使い始めもGTIDと従来の方法が混在可能なMariaDBはとても使い易いと思います。

MariaDBはデータの一貫性の保証はありません。MySQLには限定的な一貫性の保証の仕組みがありますが完全ではありません。ミッショクリチカルな用途での一貫性の保証はデータのコンペアなどをするのがベストかと思います。フェイルオーバーを中心に選択するなら、個人的には使いやすいMariaDBがいいです。

*1:昇格というと何か構成が変わるように見えますが、スレーブ機能をリセットし更新処理をそのサーバーに向けるだけのことです。他のスレーブがchange masterでそのサーバーを指定して初めて構成上のマスターになります。

*2:全く遅れがないスレーブ同士であれば、新マスターの show master statusで得られるポジションでOKです。しかし、遅れがあるスレーブだとこれではだめです。

*3:「スレーブSQLスレッド」についてはMySQL :: MySQL 5.6 リファレンスマニュアル :: 17.2.1 レプリケーション実装の詳細を参照してください。

*4:従来の方法やMariaDBの位置による管理の場合は、修復のための変更のあと、すべてのスレーブがより新しいポジションになれば、そのどれかマスター昇格した際にも修復のための変更のログが転送されることはないため、あまり問題にはなりません。