Redis PSYNC2バグのポストモーテム
原文は Salvatore Sanfilippo により に公開されました。 このブログを購読する
4日前、RedisのGitHubリポジトリに重大なIssueが投稿されました。問題はRedis 4.0で新たに導入されたレプリケーションプロトコルPSYNC2に関するもので、非常に深刻なものでした。PSYNC2はRedisのレプリケーションに多くの利点をもたらします。フェイルオーバー後や、さらにはスレーブを制御された形で再起動した後でも、データセット全体ではなく差分だけを交換して再同期できることなどが含まれます。今回の問題は、この後者の機能に関するものでした。PSYNC2ではRDBファイルにレプリケーション情報が付加されます。スレーブが再起動されると、このレプリケーションメタデータが再び読み込まれ、スレーブはPSYNCを試みることができるようになります。マスターとハンドシェイクを行い、切断以降の差分を受信しようとするのです。
運用の観点からは、これはすべて良いニュースでした。しかし、PSYNC2自体はRedis 4.0.0の安定版リリース以来かなり堅牢だった一方で、スレーブの再起動を伴うこの機能については、明らかに信頼性が不足していました。この機能には二つの問題がありました。一つ目は、これがPSYNC2への土壇場での追加であり、当初の設計書には含まれていなかったことです。PSYNC2で行った作業の当然の延長のようなものでしたが、仕様の他の部分と同じレベルで潜在的なバグや問題が精査されることはありませんでした。二つ目は、この機能が一見したよりも複雑であることです。再起動後にスレーブ側のレプリケーションが持つ状態を、すべて正確に復元するのは容易ではありません。しかも、状態の一部を復元し損ねても、ほとんどの場合は目立ったバグとして現れないため、結合テストで発見することが困難です。特定の条件が揃ったときに初めて、状態の欠落が問題として表面化するのです。例えば、スレーブのレプリケーション状態において現在選択されているDBを正しく再構築できない場合、異なるRedis DBへの書き込みが発生したときにのみ問題が起きますし、そうしたバグは特殊な条件下でのみ、現在選択されているDBが正しく保存されないという形で現れるのです。
多くのRedisコントリビューターの助けを借りて、特にAlibabaでRedisの開発に携わるGitHubユーザーの@soloestoy氏の多大な協力により、ここ最近、私たちはPSYNC2の潜在的な問題を数多く改善してきました。これらの修正はすべて、ここまでに作成したパッチを徹底的にテストした上で、数日以内にRedis 4.0.3に含まれる予定でした。しかし、Issue #4483を受け取ったとき、この問題がこれまでに見つかっていた他のRedis 4のPSYNC2の問題よりもはるかに深刻であることを理解し、Redis 4.0.3のリリースを急がなければならないと判断しました。
Issue #4483で報告されたバグは、かなり単純でありながら非常に厄介なものでした。スレーブが再起動し、RDBからレプリケーション状態が再読み込みされた後、マスターのレプリケーションバックログにはEVALSHAコマンドの形式でLuaスクリプトの実行が含まれている可能性があります。しかし、スレーブ側では再起動後にLuaスクリプトエンジンからすべてのスクリプトがフラッシュされてしまうため、これらのコマンドを処理できません。その結果、Luaスクリプトに起因する書き込みがスレーブで処理されないことになります。ただし、そのスクリプトがデフォルトではない「コマンドレプリケーション」を使っている場合は例外です。デフォルトではスクリプト自体がレプリケートされるからです。
私は少しパニックに陥り、いくつかの代替パッチを作成してIssueに投稿しました。最終的には、古いバージョンとのRDBの非互換を必要としないパッチが採用されたため、すぐにRedis 4.0.3をリリースすることにしました。しかし、そこで私はミスを犯してしまいました。ここ数週間、私は自宅ではなくオフィスで仕事をしていました。普段は夜間に作業することはないのですが、4.0.3については帰宅後に自宅のノートPCを開き、パッチを4.0ブランチにマージしてテストを行いました。翌日オフィスに戻って別のPCで作業を続けたのですが、自宅でマージした一つのコミットをリポジトリにプッシュし忘れていることに気づきませんでした。
つまり、私はレプリケーションのバグに対する最も重要な修正を除いた、他のすべてのPSYNC2修正を含む形で4.0.3をリリースしてしまったのです。すぐに、そのレプリケーション修正を含む新しいパッチバージョンであるRedis 4.0.4をリリースしました。これ自体すでに褒められたことではありません。Redisのアップグレードは計画的に行われるもので、リリース準備のミスのせいで二度もアップグレードしたい人はいないはずです。しかし、最悪の事態はまだこれからでした。4.0.4で追加した、再起動後にPSYNC2を行うスレーブでのスクリプトレプリケーションに関する修正には、すべてのレプリケーション結合テストをすり抜けてしまうエラーが含まれていました。この修正では、後で再読み込みできるように、スレーブのメモリにあるLuaスクリプトを直接RDBに保存するようにしていました。しかし、スクリプトを読み込む関数が、スクリプトがすでにメモリに存在する場合にアサートしてしまうことが考慮されていませんでした。そのため、スレーブがマスターからフル同期を受け取ってRDBファイルの読み込みを始めると、すでにメモリに存在する重複したスクリプトが原因で即座にクラッシュしてしまうのです。あるユーザーがTwitterですぐに報告してくれ、報告からRedis 4.0.5の提供まで45分以内で修正しましたが、この一連の修正提供の失敗が引き起こした潜在的な問題が軽減されるわけではありません。
上記の問題は、いくつかの理由によって引き起こされました。
- Redis 4.0は、PSYNC2の複雑さとコード変更量を考えると、安定版としてリリースするのが早すぎました。次回からは、リリースが遅れることになっても、より時間をかけ、Redisの新しいメジャーバージョンでは最後のリリース候補の期間を長く取り、GA版を出荷する前にこうしたバグを見つけられるようにします。
- Issue #4483の修正を提供しようとあまりにも急ぎすぎました。たとえ重大なバグであったとしても、何が起きているのか、そして修正自体が引き起こしうる潜在的な問題を確認するために、時間をかけるべきでした。さらに、あまりにも急いでいたため、4.0.3を構成するコミットの集合を十分に注意深く確認せず、一つの修正が抜け落ち、新たなリリースが必要になる事態を招いてしまいました。
- Redis 4.0には、データの一貫性を各ステップでチェックしながら継続的なフェイルオーバーをシミュレートするテストを含め、非常に厳格なPSYNC2のユニットテストと結合テストがありますが、今回のバグによって、テストではPSYNC2とLuaスクリプトのレプリケーションを組み合わせたケースが一度も試されていないことが判明しました。これは改善しなければなりません。また、スレーブのRDB再起動後のPSYNC2レプリケーションに関するテストも強化する必要があります。
私はまず「3」の改善から着手し、今後は新しいリリースのたびに「1」と「2」の教訓を考慮していきます。今回、私のミスによってご迷惑をおかけしたすべての方に心からお詫び申し上げます。そして、並外れた助けとサポートをいただいた@soloestoy氏に深く感謝します。
記事をランダムに読む
コメント
ログインしてコメントする