最近、メールで「MySQL Lossless Semi-Synchronous Replication」について質問されることがありました。この質問への答えは多くの人にとって有益であると考えたため、回答をこのブログ記事に書きたいと思います。回答を読めば、トランザクションのコミット、準同期レプリケーション、MySQLのクラッシュリカバリ、ストレージエンジン(InnoDB)クラッシュリカバリの内部挙動について理解できるでしょう。同時に、私がこれまで見聞きしてきたいくつかの誤解についても糾したいと思います。まずはそれら誤解の一つから始めましょう。
こうした誤解の一つは以下のようなものです(これは正解ではありません):準同期レプリケーションが有効になっているスレーブは常に最新の状態になっている(もう一度言いますがこれは正解ではありません)もしあなたが上記のようなことを耳にしたら、これ以上広まってしまわないよう注意してあげてください。たとえ一部のスレーブで準同期レプリケーションが無効になっていたとしても、マスタのクラッシュ後スレーブが最新の状態になっている可能性はあります。私はこの間違った考えが機能の名前に起因していると考えていますが、これ以上は何もできません(命名は難しいですね)。詳細は本記事で後に述べます。
私がメールで受け取った質問の話に戻りましょう。この内容を要約すると以下のようになります。
- 開発環境でMySQL5.7のマスタをクラッシュさせ(kill -9 or echo c > /proc/sysrq-trigger)、スレーブを新マスタに昇格させます
- 旧マスタを復旧させると、新マスタにないトランザクションが旧マスタに存在することが確認されました
- これはロスレス準同期レプリケーション環境では普通に起きることなのでしょうか?
この質問への答えは Yes です:回復した旧マスタ上に新マスタにないトランザクションが存在することは正常です。これは準同期レプリケーションが保証する内容に違反していません。このことを理解するためには、私たちがMySQL5.5/5.6の準同期、そしてMySQL5.7のロスレス準同期の詳細を知らなければいけません。
準同期とロスレス準同期
準同期レプリケーションはMySQL5.5で導入されました。クライアントがCOMMIT応答を受け取った全てのトランザクションは、スレーブに伝播されたことが保証されます。しかし、注意すべき点があります。それは、クライアントはCOMMIT応答を待っている間に、他のクライアントがコミットしたトランザクションのデータを見ることができることです。もしこのタイミングで(スレーブがトランザクションを受け取らずに)マスタがクラッシュした場合、それはトランザクション分離に違反することになります。この事象はファントムリードとして知られています:クライアントが観測したデータが消失する。これではあまり満足できるものではありません。
ロスレス準同期レプリケーションは、上記の問題を解決するためにMySQL5.7で導入されました。ロスレス準同期レプリケーションでは、準同期の約束が守られます(クライアントがCOMMIT応答を受け取った全てのトランザクションは伝播しています)。それに加えて、ファントムリードも発生しません。これがどのように動作しているのか理解するためには、MySQLがトランザクションをコミットする方法を知る必要があります。
MySQLがトランザクションをコミットする方法
MySQLがトランザクションをコミットする時、以下のようなステップを踏みます:
- ストレージエンジン(InnoDB)内でトランザクションの準備をします
- トランザクションをバイナリログに記録します
- ストレージエンジン内でトランザクションが"完了"します
- クライアントに応答を返します
準同期およびロスレス準同期レプリケーションの実装は、上記のプロセスにそれ自身を加えた形になっています。
MySQL5.5/5.6の準同期は、Step #3 と #4 の間に行われます。ストレージエンジン内でトランザクションが完了した後、準同期マスタはスレーブにトランザクションが伝播するのを待ちます。この時、ストレージエンジンからするとトランザクションは"完了"しているため、他のクライアントはそのトランザクションを読むことができます。これがファントムリードが発生する原因です。また、ファントムリードとは無関係ですが、もしこの瞬間にマスタがクラッシュし、バックアップなどから復元した場合、そのトランザクションは"完了"しているためデータベース内に格納されてしまいます。
準同期(およびロスレス準同期)レプリケーションでは、標準的な(非同期)レプリケーションと同じ方法でトランザクションをバイナリログに書きこむことが重要です。言い換えれば、非同期も準同期も両方Step #2 において全く同じ方法をとります。また、一度トランザクションがバイナリログに書かれたら、それは準同期スレーブに限らず全てのスレーブから読み取られます。そのため非同期スレーブは準同期スレーブよりも先にトランザクションを受け取ることができます。このため、マスタがクラッシュした後、準同期スレーブが最新のスレーブであると仮定するのは間違いです。
マスタがクラッシュした後、準同期スレーブが最新のスレーブであると仮定するのは間違いです。
ロスレス準同期では、Step #2 と #3 の間でトランザクションの待ちが発生します。この時、ストレージエンジン内ではトランザクションは"完了"ではなく、その他のクライアントはまだデータを見られません。しかし、たとえこのトランザクションが"完了"していないとしても、この瞬間にマスタがクラッシュし復旧させた場合、トランザクションはデータベース内に残ってしまいます。この理由を理解するためには、MySQLとInnoDBのクラッシュリカバリをさらに深掘りする必要があります。
MySQLとInnoDBのクラッシュリカバリ
InnoDBのクラッシュリカバリの間、"完了"していない(Step #3 に到達していない)トランザクションはロールバックされます。そのため、クラッシュリカバリ後には コミットされていないトランザクション(Step #1 に到達していない)やバイナリログに書かれていないトランザクション(Step #2 に到達していない)はデータベース内には存在しません。しかし、もしInnoDBがバイナリログに書かれているが(Step #2)、"完了"していない(Step #3)トランザクションをロールバックしてしまったら、マスタに存在しないトランザクションがスレーブに到達してしまいます。これはレプリケーションのデータ不整合につながってしまい、良くないことです。
バイナリログに到達したトランザクションはロールフォワードするべきです
上記で記したようなデータ不整合を回避するために、MySQLは自身のクラッシュリカバリをストレージエンジンより先に行います。この復旧は、バイナリログ内の全てのトランザクションに"完了"フラグを立てる作業を含んでいます。したがって、クラッシュしたタイミングでトランザクションがStep #2 と #3 の間にある場合、MySQLクラッシュリカバリ時にストレージエンジン内で"完了"フラグが立てられ、ストレージエンジンのクラッシュリカバリ実行時にロールフォワードされます。このトランザクションがクラッシュ発生時にどのスレーブにも到達していない場合は、クラッシュから回復した後にマスタで見えるようになります。これは準同期を使っていない場合でも起こり得ることですので、注意することが大切です。
準同期を使っていない場合でも、復旧したマスタに余分なトランザクションが発生し得る
回復した旧マスタで見える余分なトランザクションは、MySQLとInnoDBのクラッシュリカバリが原因です。これは、MySQLがトランザクションをコミットする際に、Step #2 と #3 の間に遅延が導入されるため、ロスレス準同期環境では発生する可能性が高くなりますが、タイミングがあってしまえばロスレスではない環境でも発生し得ます。
余分なトランザクションを回避するためのFackbookの工夫
復旧したマスタで余分なトランザクションが発生しないようにするための、独自のトリックがあります。このトリックは、数年前に開催されたPercona LiveのFacabookのセッションで披露されました(申し訳ございませんがセッションへのリンクは見つかりませんでした。もし知っている人がいれば一報ください)。考え方としては、ストレージエンジン内でまだ"完了"していないトランザクションを、ロールバック(ロールフォワードではなく)するようMySQLに強制することです。これはスレーブに置き換えられた旧マスタでのみ行うべきという点に注意する必要があります。スレーブにフェイルオーバーすることなく復旧したマスタ上で実行された場合、スレーブに届いた可能性のあるトランザクションはマスタから消えてしまいます。
"完了"していないトランザクションをロールバックするようMySQLを騙すために、Facebookは旧マスタを再起動する前にバイナリログを切り捨てています。そうすると、MySQLはバイナリログに書き込む前にクラッシュが発生したと勘違いします(Step #2)。したがって、MySQLのクラッシュリカバリではストレージエンジンが"完了"フラグを立てられず、ストレージエンジンのクラッシュリカバリ時にロールバックされます。これにより、回復した旧マスタで余分なトランザクションが発生することを回避できます。しかし、このトランザクションは確実にバイナリログに書かれたため、スレーブにレプリケーションされた可能性があります。そのため、Facebookのやり方では、旧マスタを新マスタよりも後ろに持っていくことで、旧マスタが新マスタよりも新しい状態になってしまうことを避けるようにしています。
私はFacebookが旧マスタを新マスタのスレーブとして再設定していることを知っていますが、これが標準のMySQL可能かどうか、確証はありません。 Facebookが使用しているMySQL亜種にはいくつか機能が追加されており、そのうちの一つがGTIDをInnoDBのRedoログに含めてしまうことです。これにより、旧マスタのリカバリ後、バイナリログが無くなってもデータベースのGTIDのステータスを判別できるようになっています。通常のMySQLでは、バイナリログを消去するとデータベースのGTID情報が失われてしまい、旧マスタを新マスタのスレーブとして再設定できません。しかしながら、InnoDBはクラッシュリカバリ時にバイナリログのポジション、もしくは最後にコミットされたトランザクションを出力するので、準同期レプリケーションであれば旧マスタを Binlog Serverのスレーブとして再設定可能だと思います。
Facebookの使用している準同期レプリケーションについては、以下のURLでさらに知ることができます:
Semi-Synchronous Replication at Facebook The highs and lows of semi-synchronous replication
その他の誤解も解きましょう
この記事を終わる前に、私がしばしば耳にする誤解を正させてください。時折、準同期レプリケーション(or ロスレス準同期)はMySQLの可用性を高めると語る人がいますが、私から言わせてもらおうとそれは間違いです。 準同期もロスレス準同期も、実際には可用性を低下させてしまいます。ここでは可用性が高まることはありません。
ロスレス準同期は高可用性ソリューションではない
準同期とロスレス準同期が通常のレプリケーションよりも可用性が低いという主張は、トランザクションのコミットを妨げる新しいパターンの状況を想定することによって正当化できます。例えば、準同期スレーブが存在しない場合、トランザクションがコミットできなくなります。ロスレス準同期が保証することは、可用性を向上することではなく、クラッシュが発生した場合にコミットされたトランザクションが失われるのを防ぐことです。この代償として、COMMIT待ちの時間が増えたり、COMMITが妨げられる新しいケースが増えます(したがって可用性も低くなります)。
Group Replicationも高可用性ソリューションではない
同様の理由から、Group Replication(or Galera or Percona XtraDB Cluster)も可用性を低下させることがあります。Group Replicationでは、COMMITの遅延が追加されることと引き換えに、コミットされたトランザクションが喪失することを防ぐ仕組みを持っていいます。そこには別のGroup Replicationのコストも存在します。それは、状況によってはCOMMITが失敗してしまうことです(私は通常のMySQLでCOMMITが失敗する状況を知りません、もし知ってたら記事にコメントして下さい)。COMMITが失敗するケースについては私のGroup Replicationのcertificationに関する以前の記事で述べています。この追加コストによって別の興味深い約束が見えてきますが、この記事はGroup Replicationに関するものではないので、ここでは触れません。
Group Replicationは COMMIT が失敗し得るというケースを増やします
これはロスレス準同期やGroup Replicationが、高可用性ソリューションの基礎的な要素として使用できないことを意味していません。ただ、単独では高可用性ソリューションとはならず、他の重要なコンポーネントが必要という意味です。
rpl_semi_sync_master_{timeout,wait_no_slave} 変数の考察
ここまで私はトランザクションがコミットできないケースがあることを紹介しました。その中の1つは、準同期スレーブがない場合、またはそれらのスレーブが(何らかの理由で)トランザクションを確認・応答していない場合です。これを回避するための2つのパラメータがあります。 それは、rpl_semi_sync_master_wait_no_slave と rpl_semi_sync_master_timeoutです。これらについて少し説明しましょう。
rpl_semi_sync_master_wait_no_slave 変数は、十分な数の準同期スレーブが存在しない場合に、準同期の待機状態をバイパスできるようにしてくれます(NySQL5.7の準同期レプリケーションでは1台でも無応答のスレーブがあるとマスタ待ち状態になり、この時の台数はrpl_semi_sync_master_wait_for_slave_count変数で制御できます)。“wait_no_slave” 変数のデフォルト値は ON になっていて、これは十分な 数の準同期スレーブが存在しなくても、ずっと応答を待ち続けるという意味です。これは、準同期の約束を強制するという意味では安全な設定です。この約束(トランザクションがスレーブに伝播する前に、COMMITすることが許可されない)を破るためにパラメータ設定をOFFに変更しても、それは依然残り続けるでしょう(詳細は後述)。しかし、私は完全な準同期レプリケーション環境において、MySQLを無人の待機状態にして実行することはしません。
rpl_semi_sync_master_timeout 変数を使用すると、トランザクションがレプリケーションされなかった場合に、準同期の待機時間をタイムアウトさせることで、MySQLがクライアントのCOMMIT命令を適用することができるようになり、待ち時間を短縮できます。デフォルトは10秒ですが、これは間違っていると思います。マスタが10秒も待機状態になっていた場合、恐らく既に数千ものトランザクションが滞留しており、既にMySQLはビジー状態になっています。MySQLがビジー状態にならないようにしたい場合は、このパラメータを小さくする必要があります。ただし、ロスレスなフェイルオーバーが必要な場合(およびフェイルオーバーに10秒以上かかる場合)、トランザクションをスレーブに複製せずにトランザクションをコミットしてはいけません。この場合は、パラメータを大きくする必要があります。高い設定値か低い設定値、どちらを使うべきか…。
rpl_semi_sync_master_timeout 変数に低い値を設定することは、準同期環境においてはとてもおかしいことのように見えます。DBAが、できるだけ頻繁にコミットする(通常のレプリケーション)か、レプリケーションされたトランザクションのみコミットする(準同期)かを選ぶことができないように思えます。ここでは、両方のメリットを活かす方法はありません:
- 誰かがコミットが高い確率で成功することを望んでいたら、それはDBAが準同期を採用しないことを意味します(そしてこの時のコストはフェイルオーバー時にコミットされたトランザクションが失われることです)
- あるいは、コミットされたトランザクションの高い永続性が望まれている場合は、DBAはコミットの成功率を低くする(およびコミットの待ち時間を増やす)ことを犠牲にして、準同期を採用します
これらのパラメータが役立つ状況が一つあります。非同期レプリケーションから完全な準同期レプリケーションに移行するケースです。この移行の間に、私たちはプロダクション環境で混乱を招かないよう、準同期の新しい制限について学ぶ必要があり、これらのパラメータはここで活躍します。しかし、マスタがクラッシュした時にコミットされたトランザクションを失いことを絶対に避けることが目的の完全な準同期レプリケーションでは、トランザクションをスレーブに複製せずにコミットさせることはお勧めできません。
これに関する最後のコメントとして、完全な準同期レプリケーションのマスタは、スレーブからのACK応答を待つために他のトランザクションを余りにも長くブロックしていると、マスタ自体がクラッシュする可能性があるという風にも考えられます。MySQLがクライアントのブロックを解除する唯一の方法であるため、これは興味深いアイデアです。ただし、これがMySQLの何らかのフォーク(恐らくFacebookからの派生)で実装されているかどうかは分かりません。
この記事が、準同期レプリケーションと、ロスレスな準同期レプリケーションについて明確にできたことを願っています。もし、この記事、および関連するテーマについて質問がある場合は、以下のコメント欄から自由に投稿してください。