Hatena::ブログ(Diary)

rkajiyamaの日記 このページをアンテナに追加

2013-03-01

抄訳 シリーズブログ Failover and Flexible Replication Topologies in MySQL 5.6

| 抄訳 シリーズブログ Failover and Flexible Replication Topologies in MySQL 5.6を含むブックマーク

MySQL開発者のSven Sandbergのブログ MySQL Replication Ideas からの記事を翻訳してみました。一部は省略してあるのでぜひ原文も確認してください。

追記:なお、原文にあるGTID_DONEはMySQL 5.6.9からGTID_EXECUTEDに変わっています。

他にもいくつかGTID関連のパラメタもMySQL 5.6 GA前に変わっていますのでご注意ください。

MySQL :: MySQL 5.6 Reference Manual :: 17.1.4.5 Global Transaction ID Options and Variables

Global Transaction Identifiers – why, what, and how / GTID - なぜ、なに そして どう使う

MySQL 5.6では新たにGTIDが導入されました。GTIDには数々の用途が想定されますが、我々がGTIDを導入した最大の目的はシームレスなフェールオーバーのためです。これによって、マスターで障害が起きた際には、スレーブの一台が新しいマスターとなります。手動での操作やサービス停止時間を最小限に抑えてこの切り替えを可能とします。

このページはGTIDに関するブログポストの最初です。ここでは、複数の利用例や、いかにフェールオーバーを簡単に行えるかを見ていきます。

以降のブログポストでは、このパワフルな機能のより高度な使い方や、詳細な実装やメカニズムを解説します。これによって、レプリケーション環境のトラブルシューティングや障害解析、フェールオーバー設計、その他の機能の理解が進めばと思います。また外部ツールがGTIDをいかに利用できるかなどを見ていきます。

Use cases / (レプリケーションの)利用例

ツリー型:

http://1.bp.blogspot.com/-RsWiQG1PP9A/UGZHqZvFNvI/AAAAAAAAACg/ZCaNRp0epGI/s1600/1-tree.png

(略)

What GTIDs are not / GTIDでできないこと

GTIDでできないことを明確にしておきましょう。サーバの障害を検知することはできません。外部のユーティリティが必要です。また、障害が発生した後での新しい構成を決定することもできません。クライアントに別のサーバ(生き残ったスレーブなど)を知らせることもできません。これらはそもそも実装されていません。GTIDそのものはこれらの処理を行うためのものではありません。GTIDは、問題のある状態の判断や処理についてはサーバの外部のユーティリティなどで行われることを想定しています。

この後のブログポストでは、これらの判断をどのように行えるかを紹介します。またmysqlfailoverユーティリティで実際にフェールオーバー処理などを実装しています。

Anatomy of a Global Transaction Identifier / GTID詳解

実際のGTIDの利用例を見てみましょう。最初のレプリケーションの利用例を見てみます。

http://2.bp.blogspot.com/-t__m-4W411g/UGZHupvqrwI/AAAAAAAAAC4/zCqtes1sb50/s1600/2-tree-failover.png

サーバAがクラッシュしたため、BかCのどちらかを新しいマスターにしたいとします。

レプリケーションは非同期のため、BとCは全てのトランザクションのコピーや実行が済んでいない可能性があります。片方が他方よりも先行していることも。例えばBの方がCよりも先行している場合、Bを新しいマスター選びます。そして、Bには存在していてCに適用されてないトランザクションからCのレプリケーションを再開します。

では実際にどう動くか。マスターでトランザクションをコミットすると、2つのコンポーネントからなるIDが生成されます。

前半は、異なったサーバで実行されたトランザクションが異なったGTIDを持つために、後半は同じサーバで実行された複数のトランザクションが異なったGTIDを持つために。

文字列としては、GTIDは“UUID:N”のように表現されます。

例: 22096C54-FE03-4B44-95E7-BD3C4400AF21:4711

Identifiers are replicated / IDもコピーされる

GTIDはバイナリログにて、それぞれのトランザクションの前に記録されます。

例:

http://2.bp.blogspot.com/-voF9rqzdV0k/UGZHwzIZDeI/AAAAAAAAADg/_9X-eKz9S98/s1600/8-binlog-with-gtids.png

バイナリログの内容は、GTIDを含めてスレーブに転送されます。スレーブはこのGTIDを読み、スレーブでコミットを行っても同じGTIDを維持します。そのため、スレーブでは新たにGTIDを生成することはありません。スレーブとマスターでは、GTID生成の観点からはコミット時の挙動が異なります。(より正確に言うと、スレーブのスレッドクライアント処理をするスレッドでは挙動が違う) スレーブ内部での詳細なメカニズムは重要なため、次のブログポストにて詳解します。

Global Transaction Identifiers in Action / GTIDの実際の使われ方

実行済みのGTIDは、gtid_donegtid_executedというグローバル変数で確認できます。この変数MySQL 5.6で追加された参照専用の変数です。gtid_donegtid_executedはそのサーバでコミットされたGTIDの「範囲」を文字列で格納しています。例えば、下記のIDがトランザクションによって生成されたとします。

    0EB3E4DB-4C31-42E6-9F55-EEBBD608511C:1
    0EB3E4DB-4C31-42E6-9F55-EEBBD608511C:2
    4D8B564F-03F4-4975-856A-0E65C3105328:1
    0EB3E4DB-4C31-42E6-9F55-EEBBD608511C:3
    4D8B564F-03F4-4975-856A-0E65C3105328:2

するとgtid_donegtid_executedの値は以下のようになります。

    "0EB3E4DB-4C31-42E6-9F55-EEBBD608511C:1-3,
    4D8B564F-03F4-4975-856A-0E65C3105328:1-2"

その他の例:

mysql> SELECT @@GLOBAL.GTID_EXECUTED;
+------------------------+
| @@GLOBAL.GTID_EXECUTED |
+------------------------+
|                        |
+------------------------+

mysql> CREATE TABLE tbl (a INT);
mysql> SELECT @@GLOBAL.GTID_EXECUTED;
+----------------------------------------+
| @@GLOBAL.GTID_EXECUTED                 |
+----------------------------------------+
| 4D8B564F-03F4-4975-856A-0E65C3105328:1 |
+----------------------------------------+
mysql> INSERT INTO tbl VALUES (1);
mysql> INSERT INTO tbl VALUES (2);
mysql> INSERT INTO tbl VALUES (3);
mysql> SELECT @@GLOBAL.GTID_EXECUTED;
+------------------------------------------+
| @@GLOBAL.GTID_EXECUTED                   |
+------------------------------------------+
| 4D8B564F-03F4-4975-856A-0E65C3105328:1-4 |
+------------------------------------------+

この変数は、スレーブがマスターに追いついているかどうか、追いついていない場合にはどのトランザクションが未実行かを確認するためにも使えます。

master> SELECT @@GLOBAL.GTID_EXECUTED;
+------------------------------------------------+
| @@GLOBAL.GTID_EXECUTED                         |
+------------------------------------------------+
| 4D8B564F-03F4-4975-856A-0E65C3105328:1-1000000 |
+------------------------------------------------+
slave> SELECT @@GLOBAL.GTID_EXECUTED;
+-----------------------------------------------+
| @@GLOBAL.GTID_EXECUTED                        |
+-----------------------------------------------+
| 4D8B564F-03F4-4975-856A-0E65C3105328:1-999999 |
+-----------------------------------------------+

この場合には、マスタで行われたトランザクションの4D8B564F-03F4-4975-856A-0E65C3105328:1000000がスレーブではまだ実行されていないことを表します。必要な場合には、mysqlbinlogコマンドなどを使って、このトランザクションの内容を確認できます。

The Last Ingredient: New Replication Protocol / 新しいレプリケーションプロトコル

ここまでGTIDがどのように生成され、レプリケーション構成の中でどのように展開されるかを見てきました。続いてはフェールオーバーのための新しい重要な要素、改良されたレプリケーションを見ていきます。

プロトコル:従来はスレーブがマスターに接続すると、バイナリログ名とオフセット(バイナリログのポジション)をリクエストし、それ以降全てを転送していました。新しいプロトコルでは、

1. スレーブがマスターに接続すると、スレーブで実行&コミット済みのGTIDの範囲をマスターに送信する

2. マスターは、「それ以外」のトランザクションを全てスレーブに渡す ※スレーブから送られたGTID以降ではなく、スレーブで実行されていないトランザクション全て

:下記が新しいレプリケーションプロトコルの例

http://4.bp.blogspot.com/-xHF6fk3in_c/UGZHxLtuqcI/AAAAAAAAADo/eLObx289fGs/s1600/9-new-protocol.png

ここではバイナリログの内容を個別に表現しています。特定のSQLが1つのトランザクションで実行されていますがここでは重要ではありません。説明をシンプルにしたかっただけで、実際には複数のクエリが1つのトランザクションに含まれることが多いでしょう。またUUIDの数字そのものなども重要ではないので、GTIDは単に省略してid1, id2などとしてあります。

この例では、Cは“id1...id2”をBに送り、Bはid3のtrx3をCに送っています。もっとトランザクション続けばBに以降を転送し続けます。

SQL:以下のSQL文で新しいプロトコルを利用することを設定できます。

CHANGE MASTER TO MASTER_AUTO_POSITION = 1;

重要なポイントは、手動でGTIDを指定する必要は全く無い点です。スレーブは自動的に実行済みのGTIDをマスターに送信します。MASTER_AUTO_POSITION = 1を設定した場合は、MASTER_LOG_FILEまたはMASTER_LOG_POSを設定することはできません。

Failover / フェールオーバー

GTIDを使ってフェールオーバーを行う準備ができました。

ツリー型:最初の例の場合。先ほどの絵にバイナリログの情報を追加しました。

http://1.bp.blogspot.com/-4ey5LjLEFOg/UH5vD2L30dI/AAAAAAAAAFI/PYhRnBzHXw0/s1600/10b-tree-failover-new-protocol-fixed.png

この絵のように、マスターでは3つのトランザクションが実行されていて、Bでは全てが適用済み、Cには1つだけが転適用された状態だします。そしてAで障害が起きたとします。この場合にはBを新しいマスターとし、CをBのスレーブとします。新しいレプリケーションプロトコルを使うと、Cはid1をBに送ると、Bはid2のtrx2(およびそれ以降にBでコミットされたトランザクション)をCに送ります。

Circle topology(略)

Summary / まとめ

シームレスなフェールオーバーを可能とするため、MySQL 5.6にGTIDが導入されました。GTIDはそのトランザクションが初めてコミットされた時に生成され、別のサーバにレプリケートされた場合には同じIDが維持されます。新しいレプリケーションプロトコルは、フェールオーバーをほぼ自動化します。データベース管理者がGTIDを知っておく必要はありません。管理者が行うべきはどれが新しいマスターかをスレーブに知らせることです。監視で重要になってくるのは@@GLOBAL.GTID_DONE @@GLOBAL.GTID_EXECUTEDです。

抄訳 Advanced use of Global Transaction Identifiers

|  抄訳 Advanced use of Global Transaction Identifiersを含むブックマーク

引き続きMySQL開発者のSven Sandbergのブログ MySQL Replication Ideas の翻訳版です。原文も確認してください。

Details of Re-execution and Empty Transactions / トランザクションの再実行と空のトランザクション

前回のブログポスト (オリジナル版 http://svenmysql.blogspot.co.uk/2012/10/failover-and-flexible-replication.html / 抄訳版 http://d.hatena.ne.jp/rkajiyama/20130301) では、GTIDがどのように生成され展開されていくかや、新しいレプリケーションプロトコル、フェールオーバーに役立つ要素などについて解説してきました。

今度は、GTIDがどのように機能するのかの詳細を解説します。特に、スレーブのスレッドが同じトランザクションを誤って繰り返し実行しない仕組みを確認します。また、mysqlbinlogでのメカニズムも確認します。トランザクションを空にするコンセプトと、どのように安全にトランザクションを抑制(実行せずにスキップする)かを確認しましょう。

The replication thread / レプリケーションスレッド

前回解説した通り、マスターはGTIDをイベントとして、トランザクションの内容の前にバイナリログに記録します。そして、スレーブのスレッドがGTIDを読むと、スレーブのサーバ変数gtid_nextをそのGTIDで変更します。例えば

スレーブのスレッドが読んだGTIDが4d8b564f-03f4-4975- 856a-0e65c3105328:4711の場合、以下のSQL文を発行します。

SET GTID_NEXT = 4d8b564f-03f4-4975-856a-0e65c3105328:4711;

これによって、スレーブはGTIDを自動生成するのではなく、4d8b564f-03f4-4975-856a-0e65c3105328:4711を利用します。

mysqlbinlog

上記のSQL文は、SUPER権限を持ったどのクライアントからでも実行可能です。

同じ仕組みはmysqlbinlogコマンドでも使われています。mysqlbinlogがGTIDのイベントを読み込むと、自動的にSET GTID_NEXT文を出力します。それによって、mysqlbinlogからの出力を実行するクライアントは、トランザクション内容だけではなくGTIDも同じまま実行が可能となります。

Transactions Must Only Execute Once / トランザクションは一度だけ実行されなければならない

もしGTID_NEXTで指定されたトランザクションが既に実行されていたらどうなるでしょうか?同じトランザクションが再度実行されてはなりません。まずデータの整合性がとれなくなる可能性がありますし、そもそも異なったトランザクションが同じGTIDを持つことはありえません。また、フェールオーバーの際に別のエラーを引き起こしかねません。

そのため、SET GTID_NEXTが実行されると、該当するトランザクションが既に実行されているか、@@GLOBAL.GTID_DONE @@GLOBAL.GTID_EXECUTEDからサーバがチェックします。

Empty Transactions – Making the Slave Skip Transactions / 空のトランザクション - スレーブでトランザクションをスキップする

既に実行されたトランザクションサーバがスキップするのは、データを破壊してしまうような状況を避けるためだけではありません。GTIDを利用したフィルタリングや、データベース管理者が安全にトランザクションをスキップする、またはレプリケーションを特定の位置から安全にスタートすることを可能とします。(ここでいう安全とは、これらの操作を実行する分かりやすい方法で、次のフェールオーバーなどでデータを誤って破壊するような処理は実装していないことを指します)

下記の例をベースに説明を始めます:

http://3.bp.blogspot.com/-pGvywWPHGp4/UHrkkBFb_EI/AAAAAAAAAEY/jTYy4qhX47k/s1600/1-master-slave.png

AがマスターでBがスレーブです。Aは3つのトランザクションを実行し、Bに全てが転送され適用されています。ここに新たなスレーブCをAに追加するというシナリオです。そしてトランザクションtrx1とtrx2はCで実行したくないとします。この2つのトランザクションをスキップする理由としては、Cは一部のテーブルのみを持つサブセットとする場合でtrx1とtrx2はCが持っていないテーブルに対するトランザクションであるケースや、またはtrx1はミスでtrx2で取り消し処理を行っているケース、またはこれらが非常に大きなトランザクションなので実行したくないケース、さらにはCにとって不要なケースなどが考えられます。

こういった場合には、Cはtrx3からトランザクションを開始したいと考えるでしょう。

http://2.bp.blogspot.com/-1Ks2HilOvFI/UHrklP0RsKI/AAAAAAAAAEc/R9IJ5gf8i2M/s1600/2-master-slave-filter-slave.png

ここでフェールオーバーの際には何が起きるでしょうか?例えばAが停止してしまい、Bを新しいマスターに、CをBのスレーブにします。前回のブログポスト[]での説明の通り、新しいレプリケーションプロトコルでフェールオーバーが可能となっています。CがBに接続すると、CはIDの範囲をBに送り、その範囲に含まれないトランザクションをCに返します。この場合、Cはid3をBに送り、id1のtrx1とid2のtrx2を受け取り、Cでこれらのトランザクションがコミットされます。せっかくスキップしたはずのトランザクションがフェールオーバーによって実行されてしまいます。

この挙動はデータベースを壊してしまいかねない問題だけではありません。(C上でトランザクションが実行される順番が変わっています) エラーメッセージも出さずにデータを壊してしまい、フェールオーバーが発生するというそれだけでもデータベース管理者にとって問題のある状況を悪化させます。さらに、この問題のトランザクションは非常に古い可能性もあります。場合によっては何年も前のトランザクションで、データベース管理者自身もなぜスキップしたのか自体を思い出せずに、デバッグや問題解決を難しい物にしてしまいます。

ここでポイントになるのは、より安全な方法でこれらの状況を回避し、データベース管理者の悪夢を回避することができる点です。

GTID_DONEGTID_EXECUTEDに既に存在するGTIDをGTID_NEXTに設定すると、サーバはそのトランザクションをスキップすることを思い出してください。そこで、GTIDで指定されたトランザクションをスキップするため、まず始めに行うのは同じGTIDを再実行しないように設定することです。

例:

mysql> SET GTID_NEXT = “4d8b564f-03f4-4975-856a-0e65c3105328:4711”;
mysql> COMMIT;

通常、COMMIT文単体では何も起こりません。しかし、GTID_NEXTにGTIDを設定した場合、サーバは空のトランザクションを記録し、バイナリログにはBEGINとCOMMITの間に何も書かれません。

http://4.bp.blogspot.com/-s5iWSPMMfGM/UHrkmlIZzjI/AAAAAAAAAEo/p2iDpiLER-A/s1600/3-empty-transaction.png

これによって、該当のトランザクションは確実にスキップされます。もう同じトランザクションが再度実行されることはありません。

Empty Transactions and Failover / 空のトランザクションとフェールオーバー

先ほどの例にもどって、空のトランザクションを使ってtrx1とtrx2をスキップすると何が起こるかを見ていきましょう。CがスレーブとしてAに接続する前に、GTID id1とid2を空のトランザクションとしてCで実行します。

http://1.bp.blogspot.com/-FpfQExOm-bw/UHrkn-OQeLI/AAAAAAAAAEs/x9Rme2pBSzE/s1600/4-master-slave-filter-slave-with-empty-transaction.png

その後、新しいレプリケーションプロトコルを使ってCをAに接続します。CはGTID_DONE GTID_EXECUTEDをAに送ると、Aはそれ以外の全てをCに送ります。Cは"id1,id2"をAに送り、Aはid3のtrx3を返します。そしてレプリケーションが続きます。

ここまでは我々が必要としているが満たされています。Cはtrx1とtrx2をスキップしレプリケーションを開始しています。ただしid1とid2は存在しています。

では、フェールオーバーが起きるとどうなるでしょうか?Aが停止してしまい、Bを新しいマスターに、CをBのスレーブにします。Cは"id1-id3"をBに送り、Bはそれ以外のトランザクションを返します。trx1とtrx2はCには送られてきません。これらのGTIDは既にCで空のトランザクションで実行されているためです。

この例では、重要なポイントを強調しています。GTIDがサーバの状態も表しています。二つのサーバが同じデータを持っていても、それぞれのバイナリログに含まれるGTIDの範囲が異なる場合は、完全に同じとは見なされません。そして空のトランザクションなどを使うことで問題を起こさないようにできます。

Replication Filters and Empty Transactions / レプリケーションフィルタリングと空のトランザクション

別のシナリオでは、空のトランザクションレプリケーションフィルタリングを行う上で重要な役割を担います。このフィルタリングを使って、マスターのデータベースの一部だけをスレーブに持たせる設計が可能です。もしスレーブのサーバを --replicated-ignore-db=mydatabase オプション付きで起動すると、スレーブはマスターから送られてくるバイナリログをチェックし、mydatabaseに関する処理は全てスキップします。

AがマスターでBとCが直接のスレーブだった場合を改めてみていますが、Cがmydatabaseに関する変更点をフィルタリングしているとします。

http://2.bp.blogspot.com/-KZIJODjOpGQ/UHrko13o-OI/AAAAAAAAAE4/kdOvWb2VdOo/s1600/5-replicate-ignore.png

さらにtrx1とtrx2がmydatabaseに対する処理の場合、Cはそれらをスキップします。そしてCでは自動的に空のトランザクションをGTIDのid1とid2として記録します。

もしこの時点でAが停止すると、Bが新しいマスターとなり、CはBのスレーブとなります。このとき空のトランザクションid1とid2がCのGTID_EXECUTEDに記録されています。これによってCはBへの接続時にid1とid2をBに送るため、Bはtrx1とtrx2を再び送信することはありません。

これはパフォーマンスの観点から重要です。特にmydatabaseに対して実行されるトランザクションがmydatabaseそのものと同じぐらい大きい場合など、もしCで空のトランザクションを実行しないと、Bは大量のデータをCに転送する可能性があります。そしてフェールオーバーが既にスキップされたはずの(そして実行されることのない)大量のトランザクションを無駄に受け取ることとなります。

GTID_NEXT is only settable by SUPER / SUPER権限のみがGTID_NEXTを設定可能

繰り返しますがGTID_NEXTを設定するにはSUPER権限が必要です。なぜこうなっているのか。GTID_NEXTが設定されると、マスターで実行された任意のトランザクションを抑制(スキップ)できます。どのユーザがマスターでトランザクションをコミットしても同じです。従って、SUPER権限を持つような特別なユーザのみがGTID_NEXTを設定できるようになっている必要があります。

Summary / まとめ

ここまで見てきたように、スレーブのスレッドはSET GTID_NEXTを実行して、次に実行されるべきトランザクションのGTIDを特定します。mysqlbinlogコマンドでも同様のことができ、またデータベース管理者も必要であれば同じことができます。

既にコミットされたトランザクションは、GTID_NEXTで指定されたものと同じGTIDを持つ場合、スキップされます。

まだ実行されていないトランザクションをスレーブ上でスキップするには、スキップしたいトランザクションのGTIDで空のトランザクションをコミットしておきます。