Redis Sentinelの特性と障害シナリオをめぐる二、三の論点
原文は Salvatore Sanfilippo により に公開されました。 このブログを購読する
昨日、分散システムの専門家であるAphyr氏が、ある匿名希望の企業が経験したRedis Sentinelの問題についてツイートした。
「OH、Redis Sentinelについて『マスターをkill -9したら、スプリットブレインが発生して…』」
「その後、古いマスターがデータを失った状態で復活し、その“データがない”状態を他のすべてのノードにレプリケートしてしまった。文字通りバックアップから復元するしかなかった」
OMG、ひどいバグがあるのかと思った。しかしKyleに詳細を尋ねてみると、ユーザーはそもそもマスタープロセスでディスクへの永続化を完全に無効にしていたという。そう、マスターは再起動時にデータセットが空の状態で起動するように、あえて設定されていたのだ。
さて、どうなったか。たちまちTwitter上で騒ぎが始まった。人々はRedisユーザーのことをひどく心配し始めたのだ。かわいそうなRedisユーザー!常に危険にさらされている、と。
もっとも、心配すること自体は確かに賢明なことだが、私は別の道を選びたい。もう少し情報を提供することだ。しかもこのブログ記事を書くのは興味深い。というのも、Kyleは当初ほとんど文脈なしに問題を報告していたものの、数ツイート後には、私見ではあるが、Redis Sentinelで実際に改善すべきだと私が考え、ずっとTODOリストに入れてきた点――今回報告されたインシデントとは無関係な点――を的確に切り出したからだ。
しかしその前に、今回の騒動となったインシデントにおけるRedis/Sentinelの挙動をもう少し詳しく見てみよう。
クラッシュリカバリというシステムモデルへようこそ
現実世界のほとんどの分散システムは、プロセスがランダムに再起動しうるという事実に対して耐性を持つように設計されなければならない。これは、他のプロセスとメッセージを交換できなくなること、すなわちパーティションによって分断されるという問題とは大きく異なることに注意してほしい。こちらはむしろ、状態を失うという問題なのだ。
この問題をより正確に言えば、分散アルゴリズムが再起動後も状態を保持することをプロセスに保証させようとしているのに、それが果たせなかった場合、技術的にはビザンチン障害を経験していることになる。状態は破損し、そのプロセスはもはや信頼できない。
さて、RedisインスタンスとRedis Sentinelインスタンスからなる分散システムでは、再起動したインスタンスが古いデータセットを保持したまま再起動できることが根本的に重要だ。空のデータセットで起動することはビザンチン障害であり、Redis Sentinelはこの種の問題からはリカバリできない。
しかし、少し話を戻そう。実際、このようなインシデントにRedis Sentinelが直接関与していない場合もある。典型的な例は、設定を誤ったマスターがあまりに素早く再起動したために、Sentinelがまったく障害を検知できないケースだ。
- ノードAがマスターである。
- ノードAが永続化を無効にした状態で再起動される。
- SentinelはノードAに到達不能であることを(かろうじて)認識するが、設定されたタイムアウトに達するほどではない。
- ノードAは再び利用可能になるが、完全に空のデータセットで再起動している。
- すべてのスレーブノードB、C、D…は喜んでそこから空のデータセットを同期してしまう。
結局のところ、設定どおりにマスターからはすべてが消去される。そして、データセットの現在の正しい情報源だと信じられているものからレプリケートしているスレーブからも、すべてが消えてしまうのだ。
上記のタイムラインの「3」を、つまりSentinelを方程式から取り除いてみよう。このシナリオ例ではSentinelはまったく動作していないのだから。
そうするとこうなる。N台のスレーブにレプリケートしているRedisマスターがある。マスターが再起動され、空のデータセットで起動するように設定されている。スレーブはそこから(空のデータセットを)再びレプリケートする。
これはRedisユーザーにとって大したニュースではないと思う。これがRedisのレプリケーションの仕組みだ。スレーブは常にマスターの完全なコピーになろうとするのだ。ただ、別のモデルも考えてみよう。
例えば、RedisインスタンスがRDB/AOFファイルに永続化されるNode IDを持つようにできる。ノードは再起動するたびに自身のNode IDをロードする。Node IDが間違っていれば、スレーブはマスターからまったくレプリケートしなくなる。ずっと安全になるだろうか? 実際には、わずかにマシになるだけだ。マスターには別の設定ミスがあるかもしれない。例えば何らかの理由でスナップショットが失敗していたために、再起動後に数週間も古いデータセットをロードしてしまうこともありうる。
つまり、不適切な再起動の後でも、正しいNode IDは保持されているが、データセットがあまりにも古いため、実質的には消去されたのと大差なく、ただ検出がより難しいだけということになる。
しかし、わずかに安全性を高める代償として、運用がより複雑なシステムを抱えることになり、永続化の無効化と似たような運用ミス――ただしそれよりもはるかに分かりにくいミス――によって、IDが一致しないためにスレーブがマスターからレプリケートできなくなる危険も生じる。
そこで話題を変えて、Sentinelが実際に関与する障害モードで、改善が可能なものを見てみよう。
すべてのレプリカが同じわけではない
技術的に言えば、Redis Sentinelが提供する保証は、理解しやすいごく限られたものだ。
- すべてのSentinelは、通信可能になり次第、設定について合意する。実際、各サブパーティション内では常に合意が成立する。
- Sentinelは、Sentinelプロセスの過半数からの承認なしにフェイルオーバーを開始できない。
- フェイルオーバーは厳密に順序付けられる。時間的に後に発生したフェイルオーバーは、より大きな設定「番号」(Sentinelの俗語でいうconfig epoch)を持ち、常に古い設定よりも優先される。
- 最終的に、Redisインスタンスは勝利した論理的な設定(より大きなconfig epochを持つもの)に合致するように設定される。
これは、データセットのセマンティクスが「最後のフェイルオーバーが勝つ」であることを意味する。しかしここで欠けている情報は、フェイルオーバー時にどのスレーブがマスターの後継として選ばれるかということだ。これは結局のところ、根本的な特性である。例えば、Redis Sentinelが(誤った設定で再起動したばかりの)空のスレーブを選んでしまうようなことがあれば、それこそがSentinelの問題だ。Sentinelは、Redisが非同期レプリケーションのシステムであるという制約の中でも、利用可能な中で最良のスレーブを選ぶことでユーザーの最善の利益を図り、もし到達可能な適切なスレーブがなければフェイルオーバー自体を拒否すべきなのだ。
ここは改善が可能な部分であり、現在マスター障害時にスレーブを選定する際に行われているのは次のとおりだ。
- スレーブが再起動され、再起動後にマスターと一度も接続して正常な同期(データ転送)を完了していない場合、そのスレーブはスキップされる。
- スレーブがマスターから切断されている時間が、設定されたタイムアウト(Sentinel群がマスターを障害と判断するために到達不能であるべき時間)の10倍を超えている場合、そのスレーブは不適格とみなされる。
- 残ったスレーブの中から、Sentinelは最も優れた「レプリケーションオフセット」を持つものを選ぶ。
レプリケーションオフセットとは、Redisのマスター・スレーブレプリケーションがレプリケーションチャネル経由で送信されたバイト数を数えるために使う数値だ。これはフェイルオーバーだけでなく、さまざまな場面で役立つ。例えばネットワーク分断後の部分的再同期では、スレーブはマスターに対して「オフセットXから――つまり最後に受信したバイトの次から――データをください」と要求する、といった具合だ。
しかし、このレプリケーション番号を最適な昇格先スレーブを選ぶ文脈で使う場合、2つの問題がある。
- 再起動後にリセットされること。一見すると無害に思える。より大きな数値を持つスレーブを選びたいのだし、そもそも再起動後に接続できないスレーブはスキップされるのだから。しかし実際にはまったく無害ではない。続きを読んでほしい。
- ただの数値に過ぎないこと。つまり、Redisスレーブが特定のマスターからレプリケートしたことを意味しない。
また、スレーブがマスターに昇格する際、マスターのレプリケーションオフセットを引き継ぐことにも注意してほしい。したがって再起動を除けば、この数値は増え続ける。
なぜ「1」や「2」が最適とは言えず、改善できるのか?
このような構成を想像してみよう。ノードA、B、C、D、Eがある。Dが現在のマスターで、Eとともに少数派パーティション側に分断されている。Eは依然としてDからレプリケートしており、彼ら自身から見ればすべて正常だ。
一方、多数派パーティションでは、A、B、Cが互いにメッセージを交換でき、Aがマスターに選出される。
その後Aが再起動し、オフセットがリセットされる。BとCはそこからレプリケートし、再び低いオフセットから始める。
しばらくしてAが障害を起こし、同時にEが多数派パーティションに再合流する。
Eが持つデータセットはBやCのデータセットに比べて更新が古いにもかかわらず、そのレプリケーションオフセットはより大きい。それだけでなく、Eは最近までマスターに接続していたと主張することさえできる。
これを改善するのは簡単だ。各Redisインスタンスは「runid」という、Redisが起動するたびに変わる一意のIDを持っている。これは、誤ったマスターから増分ストリームを取得してしまうのを避けるため、部分的再同期で役立つ。スレーブは、最後に正常にレプリケートしたマスターのrunidを公開すべきであり、Sentinelのフェイルオーバーは、実際にフェイルオーバー対象となっているマスターからレプリケートしていたスレーブだけを選ぶようにすべきだ。
レプリケーションオフセットを特定のrunidに紐付ければ、スレーブがどれだけ最新であるかを測る絶対的な物差しが得られる。2つのスレーブが利用可能で、どちらも古いマスターとの連続性を主張できる場合、レプリケーションオフセットがより大きい方が、保証された最良の選択となる。
しかしこれは、データ自体はそれほど重要ではなく可用性が求められるあらゆるケースで、可用性への懸念も生じさせる。例えばAがクラッシュしたときにEだけが利用可能になった場合、たとえそれが以前Dからレプリケートしていたとしても、何もないよりはましだ。可用性の高いキャッシュが必要で、一貫性がそれほど問題にならないのであれば、Redisクラスターをmemcached風に(N台のマスター間でクライアントサイドのコンシステントハッシングを行う形で)使うのが良い方法だと私は考える。
なお、runidをチェックしなくても、再起動後もレプリケーションオフセットを持続させるだけで、挙動はすでに大幅に改善される。上記の例でいえば、Eが選ばれるのは、スレーブとともに少数派パーティションに隔離されている間に、多数派側の他のスレーブよりも多くの書き込みを受け取っていた場合に限られる。
TLDR: これは修正しなければならない。データセットなしで再起動したマスターの問題とは無関係だが、より正確な実装を持つことは有用だ。ただし、これで抑えられるのは非常に再現しにくい部類の問題だけではある。
これはしばらく前から私のTODOリストに入っていることであり、わずか数回のツイートのやり取りで真の実装上の問題を突き止めたAphyr氏には拍手を送りたい。Aphyr氏が報告した匿名の企業の障害については、深刻な設定ミスから保護しようとすること自体、現時点では現実的ではないと思う。ただ、これはSentinelのドキュメントを改善する必要があることの明確なサインだ。現在のようにシステムの仕組みを説明しようとするものよりも、より段階的に理解できるドキュメントが必要だ。より賢明なアプローチは、共通の健全な設定から始め、「やってはいけないこと」リストを示すことだろう。例えば、空のインスタンスが許容できる場合を除き、永続化を無効にしてはならない、といった具合に。
記事をランダムに読む
コメント
ログインしてコメントする