全文検索エンジンの負荷を分散した話
山田祥允
はじめに
このエントリは全5回を予定する19卒新人ブログリレーの第3回目です。
初めまして。 リクルートテクノロジーズ新卒2年目の山田 祥允です。
主にタウンワークの開発を担当しています。
今回は、タウンワークで用いている全文検索エンジン Solrにおいて、サーバの負荷分散を行なった実例について紹介したいと思います。
背景
タウンワークでは、年々増大していくSolrサーバへの負荷を軽減するため、サーバ数の増加で対応していました。
しかし、既存の構成ではサーバ数を増加することでオンラインアクセスの負荷は分散できても、データの登録処理 (インデクシング) による負荷は分散できませんでした。 これは、全てのサーバに対してデータの登録処理が実行されるため、追加したサーバに対しても結局インデクシングの処理が発生してしまうためです。
そこで、今回はSolrのレプリケーション機能を用いて、 インデクシングの負荷とオンラインアクセスによる負荷を分離することにしました。
Solrの負荷分散
Solr はサーバの負荷を分散する機能として、分散インデックスや分散検索、レプリケーションといった機能を持っています。
分散インデックスというのは、1つの大きなインデックスを小さなインデックスに分割し、複数のサーバで管理する仕組みです。
分散したインデックスに対しての検索は、分散検索を用いて行います。
レプリケーション機能については、後に詳しく述べますが、あるサーバのインデックスを複製し他のサーバに転送できる仕組みです。
分散インデックスと分散検索を使うと、インデックスの大容量化に対して対策を行えたり、インデクシングの負荷分散にはなりますが、複数のサーバーに対する検索となってしまうため検索の負荷が増大してしまいます。
一方で、レプリケーション機能はインデックスの大容量化に対しての対策はできませんが、インデクシングの負荷とオンライン検索の負荷を分離することができます。
よって今回のタウンワークの課題に対してはオンライン検索の負荷はそのままで、インデクシングの処理を切り離すことのできるレプリケーション機能が適していると判断し、レプリケーション機能による負荷分散を行いました。
Solrのレプリケーション機能
それでは、レプリケーション機能について見ていきます。
レプリケーション機能を利用することで、Solrのサーバをインデクシングを担当するマスターと検索を担当するスレーブに分割することができます。
これにより、一方の処理が高負荷になっても他方のパフォーマンスに影響が出ないようになるという利点が得られます。
レプリケーション実施方法
設定ファイル
まずは、設定ファイルによる設定方法について説明します。
solrconfig.xml の ReplicationHandler を設定することでSolrのレプリケーションを行うことができます。
例えば、masterの設定は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 |
<requestHandler name="/replication" class="solr.ReplicationHandler"> <lst name="master"> <str name="replicateAfter">optimize</str> <str name="backupAfter">optimize</str> <str name="confFiles">schema.xml,stopwords.txt,elevate.xml</str> </lst> <int name="maxNumberOfBackups">2</int> <str name="commitReserveDuration">00:00:10</str> <lst name="invariants"> <str name="maxWriteMBPerSec">16</str> </lst> </requestHandler> |
replicateAfter
や backupAfter
といった項目を設定することができます。
各項目の詳細は以下の通りです。
項目名 | 説明 |
---|---|
replicateAfter | インデックスのレプリケーションを行うタイミングを”commit”, “optimize”, “startup”から指定する。 複数指定可能。 |
backupAfter | インデックスのバックアップを行うタイミングを”commit”, “optimize”, “startup”から指定する。 複数指定可能。 |
confFiles | slaveに送信する設定ファイルを指定する。 |
バックアップを保持する世代数。 | |
commitReserveDuration | コミット頻度が高かったり通信速度が遅い場合に、指定した時間より短い間隔でレプリケーションが行われないようにする。 |
maxWriteMBPerSec | 1秒間に最大何MB転送するか指定する。 |
また、slaveは以下のような設定になります。
1 2 3 4 5 6 7 8 9 10 11 |
<lst name="slave"> <str name="masterUrl">http://remote_host:port/solr/core_name/replication</str> <str name="pollInterval">00:00:20</str> <str name="compression">internal</str> <str name="httpConnTimeout">5000</str> <str name="httpReadTimeout">10000</str> <str name="httpBasicAuthUser">username</str> <str name="httpBasicAuthPassword">password</str> </lst> </requestHandler> |
こちらも、 masterUrl
や pollInterval
といった項目を設定できます。
以下のような項目があります。
項目名 | 説明 |
---|---|
masterUrl | masterのURLを指定する。 |
pollInterval | masterへのポーリング間隔を指定する。 00:00:00を指定することでポーリングさせないこともできる。 |
compression | “external”, “internal”の値を指定でき、インデックスファイルを圧縮する。 |
httpConnTimeout | masterに接続する際のタイムアウト値をミリ秒で指定する。 デフォルトは5000。 |
httpReadTimeout | masterからインデックスファイルを取得する際の読み込みタイムアウト値をミリ秒で指定する。 デフォルトは10000。 |
httpBasicAuthUser | masterにBasic認証が設定されている場合ユーザー名を指定する。 |
httpBasicAuthPassword | masterにBasic認証が設定されている場合パスワードを指定する。 |
master/ slave それぞれの設定を行うと、slave がmasterへポーリングし、レプリケーションを実行するようになります。
HTTP API
レプリケーションに関連するAPIも用意されています。
これを用いると、slave 側でpollingを実施するのではなく、好きなタイミングでAPIをコールしてレプリケーションを実施する、といったことが可能になります。
用意されているコマンドは以下の通りです。
実例
ここまでで説明してきたSolrのレプリケーション機能を用いて、実際にインデクシング処理の分離を行なったので紹介します。
構成
今回はマスターを2台(稼働系、待機系), スレーブをN台の構成としました。
稼働系のマスターが落ちた場合、待機系のマスターからレプリケーションを実行します。 この切り替えが可能となるように、スレーブの設定では masterUrl
を指定しませんでした。
また、業務要件上レプリケーションはポーリングではなく、バッチ処理で実施する必要があったため、スレーブの pollInterval
は指定せず、APIコールによるレプリケーションを選択しました。
結果、今回の案件ではsolrconfig.xmlにレプリケーションの設定はせず、APIコールによるレプリケーションのみで実装しました。
実装
具体的な実装はお見せできませんが、以下のような処理を行うバッチを作成しました。
① slaveに対してmasterを指定してレプリケーションを実行させる
② masterのindexversionを取得
③ slaveのindexversionがmasterと一致するか確認
① では masterUrl
② では
③ では indexVersion
ポイントは、 indexVersion
の確認時に master に対しては command=indexversion
のAPIをコールし、 slave に対しては command=details
をコールしている点です。
この理由は、detailsのレスポンスでは以下のように details
直下の indexVersion
と commits
配下の indexVersion
が存在し、この値が異なる場合があるからです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "responseHeader":{ "status":0, "QTime":4}, "details":{ ... [中略] ... "commits":[[ "indexVersion",xxxxxx, "generation",yyy, ... [中略] ... ]], "isMaster":"true", "isSlave":"false", "indexVersion":XXXXXXX, "generation":YYY, ... [中略] ... }} |
command=indexvesion
をコールして返ってくる値は details
直下の indexVersion
の値になります。
しかし、slaveへのindexが完了したことを確認するためには、 commits
配下の indexVersion
がmaster の indexVersion
と一致していることを確認する必要があるのです。
結果
このような構成変更を行ったことで、インデクシングの負荷とオンラインアクセスの負荷を分散させることができました。
レプリケーション自体による負荷の増加が気になるところかと思いますが、ほぼネットワークの負荷のみで、CPU負荷の増大はインデクシング処理の数%程度に収まりました。
また、レプリケーションの実行時間ですが、ほぼネットワークの転送時間と等しく、60GB程度のインデックスの場合、1Gbpsの帯域を使用して1分程度で完了しました。
複数のスレーブへレプリケーションを実施する際、ネットワーク帯域の圧迫が気になるようであれば、一台ずつレプリケーションを実施するという対処法もあるかと思います。
まとめ
今回は実例を交えてSolrのレプリケーション機能を用いた負荷分散について紹介しました。
Solrの負荷分散の1つの選択肢として参考になれば幸いです。