by Evan Elias and Santosh Praneeth Banda
Global Transaction ID (GTID)は、MySQL 5.6の新機能の中でも最も使わずにはいられない機能の一つだ。このおかげで、フェイルオーバやポイントインタイムリカバリ、階層を持ったレプリケーションなどに非常に有益だし、クラッシュセーフなマルチスレッドレプリケーションの必須条件にもなっている。この数ヶ月で、我々はFacebookの全ての本番用MySQLインスタンスで、GTIDを有効にした。その中で、この機能の適用方法や操作について、たくさんの知見が得られた。たくさんのサーバサイドの修正事項については、WebScaleSQLを通じてソースを公開していくつもりで、他のスケールのコミュニティもそこから学ぶことができ、利益を得られるだろうと信じている。
背景
伝統的なMySQLレプリケーションは、相対座標に基づいていた。つまり、各スレーブはマスタの現在のバイナリログファイルのポジションを追いかけているだけだった。GTIDは、各トランザクションに一意な識別子を割り当てることでこの仕組みを拡張してあり、それによって各MySQLサーバはどのトランザクションが既に実行済みかを追跡するのだ。この仕組みのおかげで「自動ポジショニング」の機能が使えるようになった。つまり、スレーブがマスタインスタンスへ接続するとき、CHANGE MASTER文でbinlogファイル名やポジションを指定する必要がなくなったのだ。
自動ポジショニングは、フェイルオーバをよりシンプルに、高速に、間違いが起きにくくする。MHA(Master High Availability)のような外部ツールを必要とせずに、マスタ障害後にスレーブを同期するのを簡単なことにしてしまう。事前に計画済みのマスタの昇格も、ポジションを合わせるために全スレーブを停止することなしにできるようになったので、簡単になった。データベース管理者は、間違ったポジションを手動で指定してしまう可能性を心配する必要もなくなった。人為的ミスがあった時ですら、サーバは実行済みのトランザクションを無視してくれるほど賢くなったのだ。
階層構造の異なるマスタにスレーブを向けるようにする時、多段レプリケーション(スレーブのスレーブ)を含む複雑なトポロジも、GTIDで非常にシンプルに扱える。GTIDが有効なバイナリログのストリームをどのレプリカセットからも取得でき、また相対的なポジションを必要とせずにバイナリログをリプレイできるので、GTIDによってバイナリログのバックアップとリカバリも簡単になった。
さらに、GTIDを準同期レプリケーションと組み合わせることで、クラッシュしたマスタをスレーブとして安全にリカバリするのを自動的に行うよう実装した。マスタがクラッシュすると、これを検知して30秒以内にデータ損失なしでスレーブを昇格できる。その後、オリジナルのマスタがリカバリ可能で、自動化の仕組みによってデータの一貫性を確認できたら、オリジナルのマスタを入れ替えるのにデータのコピーをしなければならなかったところを、GTIDによって自動的に新マスタを向けるようにできるのだ。
デプロイの流れ
MySQL 5.6のGTIDを、既に運用している大規模環境に適用するのは、非常に難しい。gtid_modeはダイナミック変数ではないし、マスタとスレーブでその設定を合わせなくてはならないのだ。MySQLのドキュメントに書かれているオフィシャルなデプロイ計画では、マスタをread_onlyにし、レプリカセット内の全MySQLインスタンスを同じポジションにした状態で同時に停止し、my.cnfのgtid_mode変数を有効にして、全サーバを起動するというものだ。このようなプロセスは、高可用性の考え方とそもそも相容れないもので、大規模な本番環境では実行不可能なものだ。
我々はこの問題を回避するために、fb-mysqlに、gtid_modeがマスタとスレーブで一致していなければならない制約を緩和するよう手を入れた。gtid_mode=OFFのマスタがgtid_mode=ONのスレーブを許可するようにし、read_only=ONのサーバ(つまりスレーブ)ではトランザクションに対するGTIDの割り当てを抑制するようにしたのだ。これにより、各レプリカセットに対して、以下のような高可用性を保ったデプロイ戦略が取れるようになった。
- 各スレーブ1台ずつ、gtid_modeを有効にするためにMySQLを再起動する。その後、全てのスレーブがgtid_mode=ONの状態になる。しかしこの時点ではまだマスタはgtid_mode=OFFのままだ。
- 通常のマスタ昇格を行う。つまり、スレーブの向き先を変更し、オリジナルのマスタを新マスタに向ける。オリジナルのマスタを起動した時はレプリケーション開始に失敗するが、これはgtid_mode=OFFだからである。
- オリジナルのマスタを、gtid_modeを有効にするために再起動する。これで新しいマスタからのレプリケーションが可能になり、全スレーブがgtid_mode=ONになる。
十分な予防措置と検証のロジックによって、多数のレプリカセットへのこのプロセスの展開も安全に実行できるようになった。デプロイプロセスのピーク時は、この展開スクリプトを数百のレプリカセットに同時に実行していた。
fb-mysqlへの変更点
GTIDをデプロイし、本番環境でそれが動作するようにするため、MySQLとその自動化の仕組みに厖大な変更を加える必要があった。デプロイ方法の変更はさておき、最初のテストの間にも、GTIDの深刻なバグやパフォーマンス劣化に遭遇した。
いくつかのケースでは、新しい自動ポジショニングのプロトコルでスレーブが接続してくると、MySQLサーバが全バイナリログをスキャンしてしまったり、クラッシュリカバリの間にGTID_PURGEDやGTID_EXECUTEDといったグローバル変数が初期化されたりした。バイナリログを開くと、その先頭にあるprevious_gtid_eventsを読み込む必要があるのだ。fb-mysqlでは、各バイナリログファイル名に対応したprevious_gtid_eventsをバイナリログインデックスファイルが持つようにフォーマットを変えることで、この問題を修正した。つまりfb-mysqlでは、いちいちバイナリログを直接開くのではなく、バイナリログインデックスファイルに書かれたprevious_gtid_eventsを読みだすわけだ。これでパフォーマンスが改善された。
また我々は、信頼性の低い設定(sync_binlog != 1でinnodb_flush_log_at_trx_commit != 1)でGTIDを有効にしたスレーブは、クラッシュセーフではないことにも気づいてしまった。完全に信頼性を持たせた設定では、シングルスレッドのレプリケーションにおいては、バイナリログとInnoDBトランザクションログをトランザクション後すぐにディスクに書き込まなくてはならないが、これはスレーブのログ適用パフォーマンスにネガティブな影響を与えてしまう。フェイスブックの規模においては、操作上のオーバヘッドを避けるため、クラッシュセーフであるためのあらゆる機能は、重要な意味を持つ。従ってfb-mysqlでは、GTID情報を一貫性を持った状態で保存するために、新しいトランザクションテーブル(mysql.slave_gtid_info)を追加するよう修正することに決めた。
GTIDは、多くのレプリケーションの複雑さを簡単にしてくれる強力な機能でもある。例えば、relay_log_recovery=1の時にマルチスレッドスレーブが動かないことを突き止めたが、一方でrelay_log_recovery=1は、信頼性の低いレプリケーションの設定(sync_relay_log != 1)でのクラッシュセーフレプリケーションには必須でもある。fb-mysqlでは、relay_log_recovery=1をGTIDが有効なマルチスレッドスレーブでも使えるようにした。これにより、マルチスレッドスレーブがクラッシュした時にできる実行済みトランザクションのずれが、GTIDの自動ポジショニングで自動的に修正されるようになった。
準備
GTIDのデプロイを始めるにあたっては、その前にいくつかのステップを踏む必要があった。そのうちの大きなステップが、全ての自動化の仕組みでGTIDと自動ポジショニングを使うようにアップデートしたことだ。自動ポジショニングを使わずにgtid_modeを有効にするのは、クラッシュセーフなスレーブを使うのに有害でしかないので、これらを同時に展開するのは重要なことになる。もっとも工数がかかった変更は、GTIDが既に有効なのか、あるいは今回初めて有効にするかを切り替える仕組みをマスタ昇格のロジックに組み込んだことだ。
もう1つ重要な前提条件は、GTID互換でない文が実行されないようにすることだ。MySQLにはenforce_gtid_consistencyというオプションがあり、これによってGTID互換でない文はエラーを出すようになる。安全上、gtid_modeが有効な時はこのオプションも常に有効にすべきだ。しかし、GTIDを有効にする手順を開始する前に、アプリケーションを監査して、事前にこのようなクエリパターンを修正しておく必要がある。我々のような規模でこれを可能にするため、このような文用にユーザ統計カウンタを追加し、さらにMySQLのエラーログにこのような分の全情報を書きこむオプションをMySQLに追加した。これによって、数千の異なるワークロードに渡って、およそ20のこれらの文のパターンが使われていることを簡単に特定できた。
最後に、必要とされるレアケースのために、文をスキップするのを手助けするようなスクリプトも書いた。長い歴史を持つsql_slave_skip_counterはgtid_modeが有効な時は使えないのだ。その代わりDBAは、gtid_next変数をいじり、空のトランザクションを挿入しなくてはならない。これは、緊急時には辛い作業だし、DBAチームがGTIDの知識を蓄積している途中ならなおさらだ。そんなわけで、このスクリプトを作っておいたのは賢明だ。
GTIDに関連するMySQLへの変更点や自動化の取り組みについてのより深い技術的なことは、Percona Live MySQL Conference 2014のスライドを見てほしい。
まとめ
フェイスブックのGTIDのデプロイは、MySQLエンジニアリングチーム、データベースオペレーションチーム、データパフォーマンスチームといった、チームを超えた共同作業だった。フェイスブック規模の環境へのGTIDのデプロイは、MySQLサーバへの大掛かりな改善や、自動化の仕組みの変更、展開のためのカスタムスクリプトといった、相当な努力が必要な作業だった。展開プロセスの初期では、GTIDサポートに関連するMySQLの数々のバグとパフォーマンス問題を明らかにし、報告し、そして修正した。それによって、現在では我々の用途においては非常に安定しており、ここ数カ月は新しい問題に直面していないことを、喜んでここに記せるのだ。これらと同じ修正点を、今後数週間でMySQLのブランチであるWebScaleSQLにも加えていくつもりなので、そこから他の人々も学び何らかの利益を得ることができるだろう。
このような様々な努力はあったものの、GTIDのデプロイはかけた時間に見合うだけの価値があることが証明されたと言える。この機能によって直近の利益を得ることができるし、近い将来のより一層の自動化の改善の基礎にもなるのだ。
GTIDのバグ一覧
- #69059 GTID lack a reasonable deployment strategy
- #69097 Mysqld scans all binary logs on crash recovery
- #68386 Master scans all binlogs when slave reconnects with auto positioning
- #70659 Make crash safe slave work with gtid + less durable settings
- #69943 Transactions skipped on slave after "stop/start slave" using GTID replication
- #71575 Master logs two consecutive GTIDs causing slaves to miss the first GTID
- #72313 Stop sql_thread, start sql_thread causes a trx to log with a different GTID
- #72314 Stop io_thread, start io_thread with GTID may cause data inconsistencies
- #72635 Data inconsistencies when master has truncated binary log with GTID after crash
- #73032 Setting gtid_purged may break auto_position and thus slaves
- #70711 Mysqlbinlog prints invalid SQL from relay logs when GTID is enabled
- #73397 Make MTS work with relay_log_recovery=1 when GTID is enabled