エンジニアリング

Elasticsearchのバージョニングサポート

Elasticsearchの重要な原則の1つは、データを最大限に活用できるようにすることです。従来、検索は読み取り専用であり、検索エンジンは単一の情報源から取得されたデータを使用して読み込まれていました。使用量が増え、Elasticsearchがアプリケーションの中心になると、複数のコンポーネントでデータを更新する必要が生じます。複数のコンポーネントは同時実行につながり、同時実行は競合を引き起こします。Elasticsearchのバージョニングシステムは、このような競合に対処します。

バージョニングの必要性 - 例

具体的な状況を説明するために、人々がTシャツのデザインを評価するために使用するWebサイトがあるとします。このWebサイトはシンプルです。すべてのデザインが一覧表示され、ユーザーは上向きの親指か下向きの親指のアイコンを使用して、デザインを評価できます。このWebサイトでは、すべてのTシャツについて、現在の良い評価と悪い評価のバランスが表示されています。

各検索エンジンのレコードは次のようになります。

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 999
}'

ご覧のように、各Tシャツのデザインには名前が付いていて、現在の結果を把握するための票数カウンターが表示されています。

シンプルさとスケーラビリティを保つため、Webサイトは完全にステートレスです。誰かがページを見て投票ボタンをクリックすると、サーバーにAJAXリクエストが送信され、elasticsearchはカウンターを更新するように命令されます。そのために、単純な実装では、現在の投票値を取得し、その値に1を加算してelasticsearchに送信します。

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 1000
}'

このアプローチには重大な欠点があります。投票数を失う可能性があることです。アダムとイブが同時に同じページを見ているとします。現在のところ、ページには999票が表示されています。2人ともそのデザインが好きなので、良い評価の投票のボタンをクリックします。そして、Elasticsearchはドキュメントを更新するために上記のリクエストのまったく同一のコピーを2つ受け取り、そのリクエストを実行します。つまり、総投票数は1001票のはずですが、1000票になっています。おっと。

もちろん、 update apiではもっと賢く処理することができ、投票を特定の値に設定するのではなく、投票に1を加算することが可能であるという事実を指示することができます。

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update' -d'
{
"script" : "ctx._source.votes += 1"
}'

このようにすることで、Elasticsearchは、まず内部的にドキュメントを取得し、更新を実行して、再度インデックスを作成することになります。これで正常に処理される可能性は大幅に高くなりましたが、それでも前と同じ潜在的な問題が生じます。ドキュメントを取得してから再びインデックスを作成するまでのわずかな間に、問題が生じる可能性があります。

上記のシナリオに対応し、さらに複雑なシナリオにも対応できるように、Elasticsearchにはバージョニングシステムが組み込まれています。

Elasticsearchのバージョニングサポート

Elasticsearchに保存されているすべてのドキュメントには、関連するバージョン番号があります。バージョン番号は1~2 63-1の範囲の正の数です。初めてドキュメントにインデックスを作成すると、ドキュメントはバージョン1を取得し、Elasticsearchが返す応答でそれを確認できます。例えば、これは、このブログ記事の最初のcURLコマンドの結果です。

{
"ok": true,
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 1
}

このドキュメントへのすべての書き込み処理では、 それがindex、update、またはdeleteのいずれであっても、Elasticsearchはバージョン番号に1を加算します。この加算はアトミックであり、処理が正常に返された場合に発生することが保証されています。

また、Elasticsearchはget操作の応答でドキュメントの現在のバージョンを返します(これらはリアルタイムであることを覚えておいてください)。そして、 すべての検索結果とともにそれを返すように命令されます。

楽観ロック

つまり、例に戻ると、潜在的に2人のユーザーが同時に同じドキュメントを更新しようとするシナリオに対するソリューションが必要でした。従来の方法では、これはロックで解決されます。ドキュメントを更新する前に、そのドキュメントをロックし、更新を実行して、ロックを解除します。ドキュメントがロックされていれば、誰もドキュメントを変更できないことが保証されます。

多くのアプリケーションでは、誰かがドキュメントを変更する場合、変更が完了するまで他の誰もそのドキュメントを読むことができないことも意味します。この種類のロックは機能しますが、それには問題点もあります。高スループットシステムにおいては、これには主に次の2つの欠点があります。

  • 多くの場合、単純にその必要がありません。正しく実行されれば、競合はめったに発生しません。もちろん、競合の可能性はありますが、システムが実行する処理のごく一部に限られます。
  • ロックは、実際に気にしているということが前提になっています。たとえシステムが一瞬で変わることを知っていたとしても、Webページをレンダリングするだけなら、多少古くても一貫性のある値を取得することにおそらく問題はないでしょう。読み込みは、書き込みが完了するまで待つ必要がありません。

Elasticsearchのバージョニングシステムでは、楽観ロックと呼ばれる別のパターンを簡単に使うことができます。毎回ロックを取得する代わりに、どのバージョンのドキュメントを検索するのかをElasticsearchに指示します。その間にドキュメントに変更がなければ、処理は成功し、ロックされません。ドキュメントに何か変更があり、新しいバージョンになった場合、Elasticsearchがそれを通知するため、適切に対処できます。

上の検索エンジン投票の例に戻ると、このようになります。シャツのデザインに関するページをレンダリングするときに、ドキュメントの現在のバージョンをメモします。これは、 ページに対して実行するgetリクエストの応答とともに返されます。

curl -XGET 'http://localhost:9200/designs/shirt/1'
{
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 4,
"exists": true,
"_source": {
"name": "elasticsearch",
"votes": 1002
}
}

ユーザが投票した後、その間に何も変更がなければ、Elasticsearchに新しい値(1003)だけをインデックス化するように指示できます。(追加の versionクエリ文字列パラメーターに注意してください)

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=4' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

内部的には、Elasticsearchは2つのバージョン番号を比較するだけです。これはロックの取得と解除に比べればはるかに軽い処理です。誰もドキュメントを変更していなければ、処理はステータスコード 200 OKで成功します。しかし、誰かがドキュメントを変更した(その結果、内部バージョン番号が増加した)場合、処理はステータスコード409 Conflictで失敗します。Webサイトが正しく応答できるようになりました。新しいドキュメントを取得し、投票数を増やし、新しいバージョン値を使って再試行します。これが成功する可能性は高くなります。そうでなければ、単にその手順を繰り返すだけです。

このパターンは、非常に一般的であるため、Elasticsearchの updateエンドポイントで実行できます。retry_on_conflictパラメーターを設定することで、バージョンが競合した場合に処理を再試行するよう指示できます。スクリプト化された更新と組み合わせると特に便利です。たとえば、このcURLはElasticsearchに対して、失敗する前に最大5回までドキュメントの更新を試みるように指示します。

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update?retry_on_conflict=5' -d'
{
"script" : "ctx._source.votes += 1"
}'

なお、バージョンチェックは完全に任意です。特定のフィールド(例: votes)を更新しているときにチェックを強制的に実行し、その他のフィールド(一般的にはnameなどのテキストフィールド)を更新するときには無視するように選択できます。すべてはアプリケーションの要件とトレードオフ次第です。

すでにバージョン管理システムを導入している場合

内部サポートに加え、Elasticsearchは他のシステムで管理されているドキュメントのバージョンにも対応しています。たとえば、データを別のデータベースに保存し、そのデータベースがバージョニングを行う場合や、アプリケーション固有のロジックがあり、そのロジックがバージョニングの動作を決定する場合などです。このような場合でも、Elasticsearchのバージョニングサポートを使用し、 externalバージョンタイプを使用するように命令できます。Elasticsearch は、ドキュメントが変更されるたびにバージョンが増えることが保証されているかぎり、どのような数値バージョニングシステム(1:263-1の範囲)でも機能します。

Elasticssearchに外部バージョニングを使用するように指示するには、 データを変更するリクエストでversion_typeversion以下に例を示します。

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=526&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

バージョニングを他の場所で行っているということは、Elasticsearchがそのすべての変更を把握しているとは限りません。これは、バージョニングの実装方法にも微妙な影響を与えます。

上のインデックスコマンドを考えてみましょう。 internalバージョニングでは、「現在のバージョンが526に等しい場合にのみ、この文書の更新をインデックスする」という意味です。バージョンが一致する場合、Elasticsearchはその数値を1つ増やしてドキュメントを保存します。しかし、外部のバージョニングシステムでは、この要件を強制的に実行できません。もしかしたら、そのバージョニングシステムは毎回1ずつ増分されるわけではないのかもしれません。もしかしたら、任意の数値の分だけ加算されるのかもしれません(時間ベースのバージョニングを考えてみてください)。あるいは、すべてのバージョン変更をElasticsearchに通知することが難しいのかもしれません。これらのすべての理由から、externalバージョニングサポートの動作は若干異なっています。

version_typeがexternalに設定されていると、Elasticsearchは指定されたバージョン番号を保存し、加算しません。また、Elasticsearchは、完全一致をチェックする代わりに、現在保存されているバージョンがインデックスコマンドのバージョンと同じかそれ以上の場合にのみ、バージョン競合エラーを返します。これは、事実上、「この情報を保存するのは、その間に他の誰も同じか、より新しいバージョンを提供していない場合に限る」ということを意味します。具体的には、保存されているバージョン番号が526より小さい場合、上記のリクエストは成功します。526以上ではリクエストは失敗します。

重要:externalのバージョニングを使用する場合は、インデックス、更新、削除の呼び出しでは、常に最新のversion(およびversion_type)を追加するようにしてください。忘れてしまうと、Elasticsearchはそのリクエストを処理するために内部システムを使用し、バージョンが誤って増分されてしまいます。

削除について最後に一言加えておきます。

データの削除は、バージョニングシステムにとって問題です。いったんデータが消えてしまえば、新しいリクエストが日付の入ったものなのか、それとも実際に新しい情報が含まれているのかについて、システムが正しく把握する方法はありません。たとえば、レコードを削除するために次の処理を実行するとします。

curl -XDELETE 'http://localhost:9200/designs/shirt/1?version=1000&version_type=external'

その削除処理は、ドキュメントのバージョン1000でした。もしそれについて知っている情報をすべて捨ててしまえば、同期していない後続のリクエストは正しくない処理を実行することになります。

curl -XPOST 'http://localhost:9200/designs/shirt/1/?version=999&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 3001
}'

そのドキュメントが存在したことを忘れてしまったら、この呼び出しが許可され、新しいドキュメントが作成されるでしょう。しかし、この処理のバージョン(999)は、実際には、これは古い情報であり、ドキュメントは削除されたままであるべきだということを示しています。

簡単だと思われるかもしれませんが、本当にすべてを削除するのではなく、削除処理、参照したドキュメントID、そのバージョンを覚えておく必要があります。この方法は、確かにこの問題を解決しますが、それには問題があります。ドキュメントにインデックスを作成して削除することを繰り返せば、すぐにリソースが尽きてしまいます。

Elasticsearch検索はこの2つのバランスを取っています。削除のレコードは残りますが、1分後には忘れられます。これを削除ガベージコレクションと言います。ほとんどの実践的なユースケースでは、システムが追いつき、遅れたリクエストが到着するのには60秒で十分です。これがうまくいかない場合は、 インデックスでindex.gc_deletesを他の時間スパンに設定して変更できます。