Redis PSYNC2のバグ事後検証
4日前、あるユーザーがRedisのGitHubリポジトリに重大な問題を報告しました。問題は新しいRedis 4.0のPSYNC2レプリケーションプロトコルに関するもので、非常に深刻なものでした。PSYNC2によってRedisのレプリケーションには多くの改善がもたらされます。たとえばフェイルオーバー後や、スレーブを制御して再起動した後であっても、データセット全体ではなく差分だけを交換して再同期できるようになります。今回の問題は、この後者の機能に関するものでした。PSYNC2では、RDBファイルにレプリケーション情報が追加されます。スレーブを再起動するとレプリケーションのメタデータが再び読み込まれ、スレーブはPSYNCを試行して、マスターとのハンドシェイクを行い、最後に接続が切断されてからの差分を受け取れるようになります。
Redisの運用という観点では、これはすべて朗報です。しかし、PSYNC2自体はRedis 4.0.0の安定版に導入された時点からかなり堅牢だった一方で、再起動したスレーブに関わる機能は、信頼性に大きな問題を抱えていました。この機能には2つの問題がありました。1つ目は、PSYNC2への追加が最後の段階で行われ、当初の設計文書には含まれていなかったことです。PSYNC2で行った作業を明らかに拡張したもの、という程度の扱いで、仕様の他の部分と同じレベルでバグや問題の可能性を精査していませんでした。2つ目は、この機能が当初見えるよりも複雑だったことです。再起動後にスレーブ側のレプリケーションが持つ状態をすべて正しく復元するのは、実際には難しいのです。しかも、状態の一部を復元できなくても、ほとんどの場合は明らかなバグとして現れないため、統合テストでは見つけにくいものでした。特定の条件が発生して初めて、一部の状態が欠けていることによる問題が現れます。たとえば、スレーブのレプリケーション状態において現在選択されているDBを正しく再構成できない場合、複数のRedis DBに対して書き込みが行われたときにだけ問題が発生します。そのようなバグによって現在選択されているDBが正しく保存されないのも、特殊な条件下に限られます。
多くのRedisコントリビューターの協力、そして特にAlibabaのRedis開発者であるGitHubユーザーの@soloestoyの協力のおかげで、私たちは最近、PSYNC2に関する潜在的な問題のいくつかを改善する作業を進めていました。これまでに書いたすべてのパッチを十分にテストしたうえで、その成果は数日後にRedis 4.0.3へ取り込まれる予定でした。しかしissue #4483を受け取ったとき、Redis 4.0.3のリリースを急がなければならないと理解しました。そこに書かれていた問題は、それまでに発見していたRedis 4のPSYNC2関連の問題よりも、はるかに深刻だったからです。
issue #4483で説明されていたバグは、かなり単純でありながら非常に厄介なものでした。スレーブを再起動し、RDBからレプリケーション状態を再読み込みすると、マスターのレプリケーション・バックログに、EVALSHAコマンドという形でLuaスクリプトの実行が含まれていることがあります。しかし、再起動後にはスレーブのLuaスクリプトエンジンからすべてのスクリプトが消去されているため、そのようなコマンドを処理できません。その結果、Luaスクリプトによって発生した書き込みがスレーブで処理されなくなります。ただし、そのスクリプトが「commands replication」を使っている場合は別です。これはデフォルトではありません。デフォルトでは、スクリプトそのものがレプリケーションされます。
私は少しパニックになり、問題にいくつかの代替パッチを書いて投稿しました。最終的には、以前のバージョンとのRDB互換性を壊さずに済むものが採用されたため、私はすぐにRedis 4.0.3をリリースしました。しかし、ここでミスをしました。自宅ではなくオフィスで仕事をするようになって、数週間が経っていました。普段は夜に仕事をしないのですが、4.0.3のために、その日は帰宅してから自宅のノートPCを開き、パッチを4.0ブランチにマージして、いくつかテストを行いました。翌日オフィスに戻ると、別のコンピューターで作業を続けました。しかし、自宅でマージしたコミットの1つをリポジトリにプッシュしないまま、取り込めていなかったことに気づきませんでした。
つまり私は、PSYNC2の修正をすべて含んでいるものの、レプリケーションのバグに対する最も重要な修正だけが抜けたRedis 4.0.3をリリースしてしまったのです。私はすぐに、レプリケーションの修正を含む新しいパッチレベル版、Redis 4.0.4をリリースしました。これはすでによくない状況でした。Redisのアップグレードは予定を組んで行う作業であり、私のリリース準備のミスのために2回もアップグレードしたい人はいません。しかし、さらに悪いことが起きました。4.0.4に追加した修正、つまりPSYNC2を実行する再起動後のスレーブにおけるスクリプトのレプリケーションに関する修正に、別のエラーがありました。そのエラーは、すべてのレプリケーション統合テストを完全にすり抜けました。修正では、Luaスクリプトを後で再読み込みできるように、スレーブのメモリ上にあるスクリプトを直接RDBへ保存していました。しかし、スクリプトを読み込む関数が、スクリプトがすでにメモリ上にある場合にアサートすることを考慮していませんでした。そのため、スレーブがマスターから完全同期を受けると、RDBファイルの読み込みを開始した直後にクラッシュしました。すでにメモリ上に存在するスクリプトが重複していたためです。あるユーザーがTwitterでこの問題をすぐに報告してくれたので、私は報告を受けてから4.0.5が利用可能になるまで45分足らずで問題を修正しました。しかし、この一連の修正提供の失敗によって生じた潜在的な問題を、それですべて軽減できるわけではありません。
以上の問題は、いくつかの理由によって引き起こされました。
- PSYNC2で変更されたコードの複雑さと量を考えると、Redis 4.0は安定版として早く出しすぎました。次回は、リリースを遅らせることになっても、もっと時間をかけます。新しいメジャーバージョンのRedisについては、GA版を出荷する前にこうしたバグを見つけられるよう、最後のリリース候補により長い期間を設けます。
- 私はissue #4483の修正を提供しようとして、急ぎすぎました。バグが重大だったとしても、何が起きているのか、そして修正自体が引き起こす可能性のある問題を確認するために、時間をかけるべきでした。さらに、あまりに焦っていたため、4.0.3を構成するコミット一式を十分注意して確認せず、修正の1つが欠けたままになり、新しいリリースが必要になりました。
- Redis 4.0には非常に厳密なPSYNC2の単体テストと統合テストがあります。各段階でデータの一貫性を確認しながら継続的なフェイルオーバーをシミュレートするテストも含まれています。しかし今回のバグを通じて、PSYNC2とLuaスクリプトのレプリケーションを組み合わせたテストを一度も行っていなかったことがわかりました。ここは改善しなければなりません。また、スレーブのRDB再起動後に行われるPSYNC2レプリケーションも強化する必要があります。
まずは「3」の改善から始めます。そして今後は、新しいリリースを出す前に「1」と「2」の教訓を必ず考慮します。今回、私のミスに対処しなければならなかった皆さんに、心からお詫びします。そして、信じられないほど多くの助力と支援をしてくれた@soloestoyに、心から感謝します。
記事をランダムに読む