よしだのブログ

サブタイトルはありません。

SolrCloud のリカバリー処理

こんにちは!ご無沙汰しております。 この記事は Solr Advent Calendar 2016 の 1日目です!

qiita.com

一日目の出だしにしてはかなり渋め(アドバンスド)な内容かなと思いますが、SolrCloud のリカバリー処理についてコードを読んだり調べてみたので書いてみたいと思います。しかし、特に更新が非常に多い Solr を運用している場合は、最後だけでも是非読んで下さい。読まないと、全台ダウンしちゃいますよ!

今回は、特に文章だらけで申し訳ありませんが、よろしくお願いいたします。

前提

  • SolrCloud をざっと触ったことがあり、複数のレプリカやシャードの構成を組んだり使ったことがあることがある読者を前提に書きます。
  • Solr 5.5 のリカバリー処理について書きます。Solr 6 もそんなに違わないと思いますが。

リカバリー処理とは?

リカバリー処理とは、SolrCloud環境で同一シャード内のレプリカでインデックスデータの同期が失われた場合に、再度同期をとるために Solr によって行われる処理です。 例えば、1シャード3レプリカの3台の構成で、そのうちの1台がダウンしていた場合にインデックスの更新が行われた後、ダウンしていたサーバーが復旧すると、リカバリー処理が行われダウン中に実施されたインデックスの更新が反映されます。 ダウンしていたサーバーが復旧すると Solr Admin の画面で、Recovering となっているとこの処理が行われています。

その前にトランザクションログについて

SolrCloud の構成では、各 Solr のインスタンスごとにトランザクションログ(もしくはアップデートログ、移行 tlog と記載します) を保持しています。この tlog は SolrCloud の Near Realtime Search とデータの確実性を保つために導入されたファイルです。tlog には、データベースのトランザクションログと同様に更新リクエストのインデックス処理前のそのままのデータが記載されています。

SolrCloud ではあるインスタンスに更新データが届くと以下のような処理が行われます。

  1. tlog に書き込む、この際確実にディスクに書き込まれたことを確認する (fsynch)
  2. 書き込まれたことを確認したら、インデックスをメモリ(Java のヒープ上)だけに反映する (soft commit)
  3. (commitされると) メモリ上のインデックスデータを、インデックスのファイルに書き込む (hard commit)

hard commit は、/update?commit=true を送ったりすると行われます。

この時、2 と 3 の hard commit を実行する前にインスタンスが落ちると、インスタンスのヒープ上にしか無い更新データは消えてしまいます。 消えてしまうと困るので、インスタンスが復旧した際に tlog の有無を確認し、あった場合 tlog を再度適用し(リプレイといいます)、インデックスに反映します。ちなみにこの時行われるリプレイでは、いきなりインデックスのファイルに書き込まれます。

これがトランザクションログの役割で、インスタンス1つだけに注目した場合の処理です。以下の記事がこの辺のことを詳細に書いてあるのでご参考に。

lucidworks.com

では、レプリカ1にはインデックスがあって、レプリカ2がディスク障害でインデックスが飛んでしまった場合はどうでしょうか?

リカバリー処理の流れ

インスタンスをまたがったインデックスの障害のケースでも対応できるようにリカバリー処理は作られています。

まず、リカバリーはインスタンスがコレクションに参加した時に必ず行われます。この参加時とは、新規に追加した場合以外にも、ダウンしていたインスタンスが起動した場合も含まれます。この時、リカバリー対象のノードとリーダーノードのデータを比較します。 比較した結果トランザクションログとインデックスに差異がなければ処理は何も行われません。差異があった場合に、実際のリカバリーの処理が行われます。ちなみに、インスタンスを停止してすぐに起動しても一瞬リカバリーに入るのはこのチェックをしているためです。

また、自分自身がリーダーノードである場合も実際の処理は行われません。後述しますが、リーダーノードを正しいデータとしてリカバリーが行われるためです。

対象のノードがフォロワーで、リーダーノードのデータと差異がある場合にのみ、インデックスに対する処理は行われます。実際のリカバリー処理は以下の3ステップで行われます。

1. tlog のリプレイ

まず、tlog がある場合はリプレイします。ここまでは、1台のケースと同じです。

2. PeerSync

次に PeerSync と呼ばれる処理が行われます。

PeerSync とはインスタンスのダウン中に、インデックスの更新リクエストがされ、ダウンしていない同一シャード中の他のサーバーでインデックスが更新された場合に同期するための処理です。ダウンしていたインスタンスでは、PeerSync はリカバリー処理時には必ず実行が可能かチェックされます。具体的な実行が可能かどうかはどのぐらいインデックスに差異があるかを確認し、差異が少なければ PeerSync を行います。

PeerSync はリーダーのノードが保持している tlog と、自分の tlog を比較し大量の差異が無い場合は、行われていない tlog のみをコピーしリプレイを行いインデックスの同期を行います。tlog をコピーするため、tlog の同期も行われます。PeerSync の同期に成功した後はこの後のレプリケーション処理は行いません。

PeerSync が実行可能かどうかのしきい値は、トランザクションログ1ファイルに最大何件保持するかという設定 numRecordsToKeep に依存しています。numRecordsToKeep の件数を 100% とし、自分の新しいドキュメントの新しい方から 20% のインデックスのバージョンが、リーダーの新しい方から 80% のインデックスバージョンよりも古い場合は PeerSync しません。また、逆に自分のインデックスが新しい場合ももちろん PeerSync しません。わかりやすく書くと、単純な更新処理の場合、tlog 1ファイルに保持する最大件数(numRecordsToKeep)が 100 の場合、ダウン中に更新されたドキュメントが 60件未満なら PeerSync 、それ以上ならレプリケーションが行われます。

なぜ、全て PeerSync でリカバリーが行われないかというと、tlog はインデックスの情報ではなく処理前の更新リクエストがそのまま保持されているので、データのサイズが大きくなりがちで、さらに、リプレイ時にインデックス処理が行われるため比較的時間がかかること、tlog には全データを保持していないことなどの理由があると思われます。

3. レプリケーション

PeerSync で同期できない程度にインデックスに差異があった場合に行われるインデックスの同期処理が、レプリケーションです。

レプリケーションではインデックスのバイナリファイルごとリーダーからコピーし、同期をとる処理です。バイナリファイルはインデックスのセグメントごとに分割されており、差異があるセグメントのインデックスファイルのみコピーしインデックスの同期をおこないます。

レプリケーションでは、セグメントごとにわかれたインデックスファイル単位でコピーするため、Optimize などセグメント全体に作り変えが発生する処理を、サーバーのダウン中に行うと全インデックスのコピーが起きるため、レプリケーションに非常に時間がかかるので注意が必要です。ちなみに、レプリケーションではトランザクションログの同期は行われないので、ある程度の件数が新規に更新され、tlog の同期が取れるようになるまでは、ダウンしていたインスタンスを単純に起動・停止するだけでこのレプリケーションまで処理が進み、ファイルはコピーされずに終了します。

また、リカバリー中の更新リクエストはトランザクションログの buffered update という箇所に別途保存され、レプリケーション後にリプレイされます。リプレイ中にも受け取った更新リクエストは buffered update に追記されリプレイされ、全てのリプレイ処理が完了したのちに status が active になります。

何がリカバリー処理のトリガーとなるか?

リカバリー処理のトリガーとなる現象は、インデックスデータに差異が見つかること以外にもいくつかあります。例えば、更新処理時にリーダーからフォロワーにデータが転送されるのですが、この転送が失敗したり、フォロワーでのインデックス処理が失敗したと判断されると、リーダーからフォロワーにリカバリーを行うよう、リクエストが送信されます。

numRecordsToKeep の値に注意!

このリカバリー処理、よく出来ているのですが、更新リクエストが非常に多いユースケースについては注意が必要です。どういうことかというと、気にせずにデフォルト設定で使用すると、リカバリーがいつまでたっても終わらない、ということが起こりえます。

デフォルトの設定ではトランザクションログ1ファイルあたりに保持できるレコードの件数を示す、numRecordsToKeep の設定が 100 しかありません。更新処理が大量にある場合にリカバリーに入ると、ダウン中に発生する更新処理がしきい値を簡単にオーバーするので PeerSync は行われず、レプリケーションに入ります。レプリケーションでは、比較的時間のかかるインデックスのコピー処理が開始されますが、その間にもどんどん更新リクエストが入ってきます。入ってきた更新処理は、リプレイされインデックスに順次反映されますが、コピー中にたまった更新リクエストの数が多いと、リプレイ中にもどんどんトランザクションログに更新リクエストが貯まるので、終わらないという状態になります。

このリカバリー中のインスタンスは検索はできるのですが、検索結果が古く正しくないので、一般的なシステムではリカバリー中のサーバーには検索リクエストを飛ばさないようにすることが多いです。1台がずーっとリカバリー中だと、その分、外のサーバーに検索リクエストの負荷がかかるので、放おって置くと次々にサーバーがダウンし、全台ダウンすることがあります。

なので、numRecordsToKeep の値を大きめに設定することで、できるだけリカバリー処理を PeerSync で済ませることがベスト・プラクティスとなります。ただし、numRecordsToKeep を大きくするとディスクを大量に食うので注意が必要です。

まとめ

numRecordsToKeep の値を大きくするのはもちろんですが、適切な値を決めるには徹底的な負荷検証とチューニングをお忘れなきよう。

明日のアドカレは近藤さん (tkondo) による、更新処理のパフォーマンスに関するないようになりそうですー!乞うご期待。

qiita.com