スイッチ交換でデータベースがすごく苦労した話

suzukito氏:レイヤ3スイッチの交換でデータベースがすごく苦労した話をしたいと思います。

自己紹介です。鈴木と申します。アイスタイルのデータベースエンジニアをやっています。

お話しすることは、スイッチ交換でMySQLのレプリケーションが壊れました。その顛末を共有したいと思います。

まず、ある日、インフラのほうから、こんな依頼が来たんです。「レイヤ3スイッチを交換します。迂回路を用意するので、データベースをそっちに移動できませんか? サービスは止められないので無停止でお願いします。グループレプリケーションってこういう時にも使えますよね」というかたちで来ました。

グループレプリケーションというのは、このMySQL5.7から提供されたマルチマスター構成可能なレプリケーションで、だいたい3台ぐらいの構成でActive-ActiveのHA構成が可能です。3台以上の奇数台での構築が推奨されている構成です。

やりたかったことは、代替のスイッチがあるので、通常のスイッチのほうからこっちに移したい。

こういうふうにつなげて、こちら側の1つだけ停止して残り2台は向こうに持っていって。

スイッチ交換が終わったところで元どおりと。

「いや、よかったな」となるはずだったんですが、実際起きたことはこんな感じでした。

同期しているところで、1台だけしか移せなかったんですね。なぜか。ポートが足りないから1台だけしかつなげなかった。

いや、ちょっと無理かなと思ったんですが、一応検証環境でやってみたら、なんとかもう1回くっついたよと。本番でももう1回くっつくんじゃないかなと思って、それでこっちは同期停止してみたんですよね。

実際に戻してみると、同期が再開しないと。

劇的ビフォーアフターです。最初の頃はこんなかたちの手厚く保護された本番DBだったのが、アフターになってまさかの無保護状態に。たった1台になってしまいました。

(会場笑)

同期再開できなくなった原因

なぜこうなってしまったのかと。同期が再開できなかった原因は、当たり前なんですが、本来更新されないはずだった同期停止側のDBが更新されてしまって、データに差異が生まれてしまったと。

同期再開失敗時のつれないメッセージなんですけれども。start group_replicationってやったんですね。「サーバは、グループのアクティブメンバーになるように正しく構成されていません。エラーログの詳細をご確認ください」というかたちで出てきました。

しょうがないのでエラーログを見てみますと、こんな切ないメッセージが。「このメンバーは、グループ内に存在するトランザクションよりも多くの実行済みトランザクションを持っています。メンバーに、グループに存在しないトランザクションが含まれています。メンバーはグループから離脱します」。

(会場笑)

「ちょ、待てよ!」と。この時間を見てください。午前4時です。午前4時45分にこんな切ないことを言ってくれました。「さよなら、離脱しますよ」と。

(会場笑)

問題発生の原因となったDBの設定なんですが、グループレプリケーションは2種類の動作モードがありまして、1台だけ更新可能なシングルプライマリーモードと、全台更新可能なマルチマスターモード。マルチマスターモードのほうが障害発生時に確実に、LBからの設定、向き先を変えるだけで更新が継続できるので、マルチマスターモードで運用していたんです。

教訓としては、やっぱりグループレプリケーションはシングルプライマリーモードのほうが安全ということが公式も書いてあるので、みなさんこっちのほうを信用しましょう。

作業手順に問題はなかったのかと。ネットワーク分断が起きることは事前にわかっていたので、分断前に手動で同期を停止していたんですね。

同期停止コマンドを実行すると、ちゃんと停止したほうがリードオンリーに切り替わって更新されないはずなんですよ。実際にそれは検証環境を作って検証で確認していたんですね。なのに、実際はデータが更新されてしまって再同期できなくなってしまった。「いったいこれはどういうことだ?」と。

問題の発生の原因になった作業手順なんですが、待機系でまずプロセスリストを表示して、待機系でクエリログを出して一応更新がないかどうか目視していたんですね。同期停止コマンドをそこで実行してリードオンリーに切り替わったと。同期グループから外れました。ここまでは確認しているんです。

が、実際にはここの1と2でちょっと問題がありまして。待機系のほうに実は接続があったんですね。

プロセスリストでは、運用ルール上は存在しないはずの待機系へのクライアント接続があって、告知済みだし更新はしていないだろうと判断して、そのまま放置して作業を続行してしまったと。これは午前3時ぐらいなので、この判断はしょうがないなと思っています。

クエリログも一応見ているんですが、多数のcommitが実行されていたんです。グループレプリケーションでのcommitであろうと判断して、そのまま作業を続行したと。待機系のクライアント接続で実行されているcommitがあったとしても判別できない状態だったと。

そもそもこのクエリログを見ていればわかるんじゃないかという見通しがちょっと甘くて、実際にはすごくいっぱい同期されていて、目視でわからない状態だったと。

教訓なんですが、マルチマスターモードで同期停止を行うときは、確実にクライアントからの接続がない状態をつくり出すべきで、接続があった時点で「この接続を解いてください」とかアプリケーションエンジニアのほうに言って、ちゃんと止めてもらうべきだったなと反省しています。

復旧への道のり

ここから復旧への道のりです。

この午前4時45分。あと2時間以内には全部復旧しないといけないときに、こんなメッセージが出ている状態からどんなふうに戻したかなのですが。

幸いなことに、これ以外にもマスタースレーブレプリケーションしているものが3台あったので、完全に冗長化がない状態ではなかったと。

まず第1に、復旧1回目は、強制的に再同期を行ってみました。その後、グループレプリケーションを解体して、生き残った更新系……これでも直らなかったんですね。

生き残った更新系DBのダンプ&リストアをとりあえずやってみようと。でも、これでもやっぱり直らないと。最終的に、全データベースの物理コピーを実施してやっと直った状態です。

いろいろと起きた挙動としては、まず、強制的な再同期を行うと予想もしなかったコミット遅延が発生しまして、今度は更新が15秒ぐらいかかってしまったと。しょうがないので、グループレプリケーションを解体してコミット遅延の解消と。

生き残った更新系DBのダンプとリストアをやって再同期してみると、今度はマスタースレーブレプリケーションがつながらなくなった。生き残った更新系DBの全部のダンプをとってリストアして再同期してみると、それでもまだ開始できないと。

しょうがないので、最後の手段で、全データベース、物理コピーを実施してやっと復旧した感じです。ここまで2週間ぐらいかかりました。

復旧処理を難しくした原因

処理を難しくした原因なんですが、生き残っているマスターが1台しかない。当初の予定どおり迂回路に2台起動していれば、普及は難しくなかったですね。1台しかないと止めることができないのがやっぱり一番難しかったところです。やっぱりあそこでポートが1個しかない状態で受け入れるべきではなかったと。

(会場笑)

そこから「ポート1個しかないってどういうこっちゃ?」みたいな話です。そのままなんですが、「グループレプリケーションの構成台数は2台以下にしてはいけない」が教訓ですね。

1回目の復旧。強制再同期を行ったんですけど、けっこうデータ量が多いDBだったのでダンプ&リストアに時間がかかるため、グループレプリケーションを解体したくなかったんです。

なので、ネットで検索したところ、Google先生が「このオプションを有効すると強制的に再同期できますよ」と言ってくれたので、「よし、やってみよう」と思ったら、うまくつながったんですよ。まさかその3日後ぐらいにコミット遅延が発生するとは思わなかったんですけれども……。

その後に3日ぐらい経ってみると、更新APIのタイムアウトが多発するようになってしまったと。スローログを見てみるとcommitで遅延が発生していると。commitで遅延が発生するなんて今まで経験がなかったので「なんだろうな?」と思ったんですけど、グループレプリケーションの仕組み的に、3台のデータベースに同期的に実行してcommitを全部実行しているので、そのせいではないかと予測しました。

本当はあんまりやりたくなかったんですが、すべてのデータベースでグループレプリケーションを停止することでcommit遅延を解消して、その結果、耐障害性はかなり下がりました。

それでもラッキーだったのは、マスタースレーブレプリケーションを行う参照系DBが存在したことで「耐障害性はゼロではないですよ」というかたちで上司に報告して、安心してもらいました。

教訓としては、なんでこのオプションがあるのかわからないですが、これは使うべきじゃないですね。このオプションを使ったことによって、二次災害が起きてしまったので、何のためにあるかわからないオプションですね。

復旧2回目です。ダンプ&リストアをやると。これで直るのはもうはっきりわかっていたんですが、時間がかかるのでやりたくなかったんです。

まず、時間がかかるのでやりたくないということで、データベースを絞っちゃったんですよね。ユーザーデータベースだけをダンプして待機系へリストアすると。グループレプリケーションはこれで直りました。

その後、マスタースレーブレプリケーションしているスレーブが3台あるので向き先を変更したところ、つながらないと。レプリケーションが開始できないという問題が発生してしまいました。

この時のログがこれですね。「マスターのSERVER_UUIDを使用すると、スレーブはマスターより多くのGTIDを持ちます」という。最初は「何のこっちゃ?」と思いました。今までレプリケーションをやっていてこんなエラーメッセージを見たことがなかったので、意味がわからなくて。

MySQLデータベースをダンプしていないので、これもダンプすべきだと。もう1回、これも時間かけて全部ダンプをしてみたんですが、それをリストアしてもやはり解消しないと。

結局、GTIDの何に差があったのか

何が原因だったのか。「GTID足りないってどういうこっちゃ?」と。見てみたんですが、今度スレーブ側のほうに原因がありまして。スレーブのほうにはいくつかのUUIDが存在していて、全部で5種類のSERVER_UUIDが入っていたと。

グループレプリケーションがつながるのは当たり前なんですが、このへんは事故が起きたときに接続先をいろいろ変えているので、そのマスターのIDが入ってしまって、そのUUIDが足りないというようなエラーが出ていたということがわかりました。

結局、当たり前なんですが、GTIDの仕組みをちゃんと理解しようという教訓ですね。レプリケーションの復旧は全コピーが基本で確実だということがわかりました。まあ、これも当たり前なんですけれども。

復旧4回目は、3回失敗したために「もう次ので絶対直してくれ」って話になったので、確実にできるものは何ですかってことで、物理コピーをしましょうと。物理コピーするためにはDBを全部止めなきゃいけませんと。

そのために、もともとあった構成からさらにDBを追加するんですね。こうして、いったん一時利用のデータベースのほうにデータを移してあげて、そこにAPIの向き先を向けて、実際物理コピーを実施したあとに向き先を元に戻して、差分を書き戻すという手順を踏みました。

これ、発生した問題を全部まとめて整理すると、2つの問題が発生していまして。グループレプリケーションの問題がまず1つ。同期が再開できないと。2つ目の問題は、マスタースレーブレプリケーションの問題で、特定のマスターとしか同期ができない。結局、物理コピーで全DBをまったく同じ状態にすることで復旧はできました。

教訓を一通りまとめてみたんですが、大事なのはここだけで、レプリケーションの復旧が全コピーが基本ですよと。ほかのことをやってもあまり意味がないので、こんなことになった人は必ず全コピーしてしまいましょう。

教訓のまとめのまとめなんですが、教訓を見直すと当たり前のことしか書いていませんと。一行にまとめると「基本に忠実に」ということで。全コピーしましょう。今日持って帰りたい情報は「全コピーすれば直りますよ」ということです。

朗報なんですが、グループレプリケーションのほうでも、8.0.17のほうからデータベースをクローンする機能ができたとブログに載っていました。こういう機能を最初から持っていれば、もうちょっと簡単に復旧できたのかなと思っています。

最後に一言なんですが、一緒に働く仲間を募集中です。笑いの絶えない明るい職場です。福利厚生は整っています。化粧品が安く買えます。20パーセントオフです。

参加者:おお、すごい。

鈴木:ぜひこのアイスタイル採用サイトにアクセスをお願いいたします。以上になります。

(会場拍手)