高度な検索開発者のためのガイドライン

このページでは、Elasticsearch の開発者向けの情報を提供します。

Elasticsearch を有効にして最初のインデックス作成を行う方法についてはElasticsearch インテグレーションドキュメント を参照してください。

ディープダイブ

2019年6月、Mario de la OssaがGitLabElasticsearchインテグレーションに関するDeep Dive(GitLabチームメンバー限定:https://gitlab.com/gitlab-org/create-stage/-/issues/1 )を開催し、将来コードベースのこの部分に携わる可能性のある人に彼のドメイン固有の知識を共有しました。詳しくは 録画は YouTube で、スライドはGoogle SlidesPDFで見ることができます。このディープダイブで扱った内容は全てGitLab 12.0時点でのもので、具体的な詳細は変更されているかもしれませんが、それでも良い入門書として役立つはずです。

2020年8月には、マルチインデックスをサポートするためのGitLab特有のアーキテクチャに焦点を当てた2回目のディープダイブが開催されました。その YouTubeでの録画と スライドが利用可能です。このディープダイブで取り上げた内容は全てGitLab 13.3時点でのものです。

サポートされているバージョン

バージョン要件を参照してください。

Elasticsearch のクエリに大きな変更を加える開発者は、サポートされているすべてのバージョンでその機能をテストしてください。

開発者の環境設定

Elasticsearch GDKのセットアップ手順を参照してください。

役立つ Rake タスク

  • gitlab:elastic:test:index_size:現在のインデックスが使用している容量と、インデックス内のドキュメント数を表示します。
  • gitlab:elastic:test:index_size_change:インデックスサイズを出力し、インデックスを再作成し、再度インデックスサイズを出力します。インデックスサイズの改善をテストする際に便利です。

さらに、テスト用に大きなリポジトリや複数のフォークが必要な場合は、以下の手順を検討してください。

どのように動作しますか?

Elasticsearch インテグレーションは外部のインデクサに依存しています。私たちはGoで書かれたインデクサを出荷しています。ユーザーは Rake タスクによって最初のインデックス作成をトリガーする必要がありますが、これが完了した後は、GitLab 自身が/ee/app/models/concerns/elastic/application_versioned_search.rbから継承された作成、更新、破棄時のafter_ コールバックによって、必要に応じてインデックス作成をトリガーします。

最初のインデックス作成が完了すると、プロジェクト(#207494 を参照)を除くすべてのモデルの作成・更新・削除オペレーションが RedisZSETで追跡されます。通常のsidekiq-cron ElasticIndexBulkCronWorker はこのキューを処理し、Bulk Request APIを使って一度に多くの Elasticsearch ドキュメントを更新します。

検索クエリはee/app/models/concerns/elastic にある関係によって生成されます。これらの懸念はアクセス制御も担当しており、歴史的にセキュリティバグの原因となっていますので、細心の注意を払ってください!

カスタムルーティング

Elasticsearch では、プロジェクトに関連するドキュメントタイプに対してカスタムルーティングを使用します。ルーティングの形式はproject_<project_id> です。ルーティングはインデックス作成と検索オペレーション中に設定されます。カスタムルーティングを使用する利点とトレードオフの例をいくつか挙げます:

  • プ ロ ジ ェ ク ト ス コ ープ さ れた検索は非常に高速にな り ます。
  • グローバル検索やグループ検索でヒットするシャードの数が多すぎる場合は、ルーティングは使用されません。
  • シャードサイズの不均衡が発生する可能性があります。

既存のアナライザとトークナイザ

ee/lib/elastic/latest/config.rb では、以下のアナライザおよびトークナイザが定義されています。

アナライザ

path_analyzer

blobのパスにインデックスを付けるときに使用します。path_tokenizerlowercaseasciifolding フィルタを使用します。

例については、以下のpath_tokenizer の説明を参照してください。

sha_analyzer

blob とコミットで使用されます。sha_tokenizerlowercaseasciifolding フィルタを使用します。

例については、後述のsha_tokenizer の説明を参照してください。

code_analyzer

blobのファイル名とコンテンツをインデックスするときに使用されます。whitespaceword_delimiter_graphlowercaseasciifolding フィルタを使用します。

トークンの分割方法をより制御できるように、whitespace トークナイザーが選択されました。たとえば、文字列Foo::bar(4) を適切に検索するには、Foobar(4) のようなトークンを生成する必要があります。

トークンの分割方法についてはcode フィルタをご覧ください。

トークナイザー

sha_tokenizer

これは、edgeNGram トークナイザ を使用して、SHA をその任意のサブセット(最小 5 文字)で検索できるようにするカスタム・トークナイザです。

使用例:

240c29dc7e になります:

  • 240c2
  • 240c29
  • 240c29d
  • 240c29dc
  • 240c29dc7
  • 240c29dc7e

path_tokenizer

これはカスタム・トークナイザーで、path_hierarchy トークナイザーreverse: true を使用し、入力として与えられたパスの多寡にかかわらず検索でパスを見つけることができるようにします。

使用例:

'/some/path/application.js' になります:

  • '/some/path/application.js'
  • 'some/path/application.js'
  • 'path/application.js'
  • 'application.js'

注意点

  • 検索は独自のアナライザを持つことができます。アナライザーを編集する際は、忘れずにチェックしてください。
  • Character トークン・フィルターとは対照的に)フィルターは常に元の文字を置き換えます。これらのフィルターは正確な検索を妨げます。

複数インデックスによるダウンタイムなしの再インデックス作成

note
複数インデックスの機能はまだ完全に実装されていないため、これはまだ適用できません。

現在のところ、GitLabは単一のバージョンの設定しか扱うことができません。設定やスキーマを変更した場合、すべてを一からインデックスし直す必要があります。インデックスの再作成には時間がかかるため、検索機能のダウンタイムが発生する可能性があります。

ダウンタイムを避けるために、GitLabは同時に機能する複数のインデックスをサポートするように取り組んでいます。スキーマが変更されるたびに、管理者は新しいインデックスを作成し、そのインデックスを再作成することができます。データの更新は両方のインデックスに転送されます。新しいインデックスの準備ができたら、管理者はそのインデックスをアクティブにすることができます。

これは、AWSへの移行など、新しいサーバーへのマイグレーションにも便利です。

現在、私たちはこの新しいデザインにマイグレーションしているところです。今のところ、すべてが1つのバージョンで動作するようにハードワイヤードされています。

アーキテクチャ

elasticsearch-rails によって提供される伝統的なセットアップは、内部プロキシ・クラスを通して通信することです。開発者はモデル固有のロジックを、モデルに組み込むためのモジュール(例えばSnippetsSearch )に記述します。__elasticsearch__ のメソッドは、例えばプロキシオブジェクトを返します:

  • Issue.__elasticsearch__ のインスタンスを返します。Elasticsearch::Model::Proxy::ClassMethodsProxy
  • Issue.first.__elasticsearch__Elasticsearch::Model::Proxy::InstanceMethodsProxy のインスタンスを返します。

これらのプロキシオブジェクトは Elasticsearch サーバと直接通信します(図の上半分を参照)。

Elasticsearch Architecture

新しい設計では、各モデルには対応するサブクラス化されたプロキシオブジェクトのペアがあり、その中にモデル固有のロジックが配置されます。例えば、SnippetElasticsearch::Model::Proxy::ClassMethodsProxy のサブクラスであるSnippetClassProxy を持ちます。SnippetElasticsearch::Model::Proxy::InstanceMethodsProxyのサブクラスであるSnippetInstanceProxy を持ちます。

__elasticsearch__ はプロキシオブジェクトの別のレイヤーを表し、複数の実際のプロキシオブジェクトを追跡します。これはメソッドコールを適切なインデックスに転送します。例えば

  • model.__elasticsearch__.search は安定したインデックスに転送されます。
  • model.__elasticsearch__.update_document はすべてのインデックスに転送されます。

バージョンごとのグローバル設定はElastic::(Version)::Config 。そこでマッピングを変更できます。

新しいバージョンのスキーマの作成

note
複数インデックスの機能はまだ完全に実装されていないため、これはまだ適用できません。

ee/lib/elastic/v12p1 のようなフォルダには、異なるバージョンの検索ロジックのスナップショットが格納されています。Git の履歴を継続的に管理するために、最新バージョンはee/lib/elastic/latest の下にありますが、そのクラスは実際のバージョン (例えばee/lib/elastic/v12p3) の下にエイリアスされています。これらのクラスを参照するときは、Latest 名前空間を直接使用せず、実際のバージョンを使用してください (たとえば、V12p3)。

バージョン名は基本的に GitLab のリリースバージョンに従います。12.3で設定が変更された場合、V12p3 (pは “point “の略)という新しい名前空間を作成します。バージョン名を変更する必要がある場合は、イシューを発行してください。

現在のバージョンがv12p1 で、v12p3 の新しいバージョンを作成する必要がある場合、手順は次のようになります:

  1. v12p1 のフォルダ全体をv12p3
  2. v12p3 フォルダー下のファイルの名前空間をV12p1 からV12p3 に変更します(エイリアスはLatest のままです)。
  3. v12p1 フォルダーを削除します。
  4. latest のフォルダ全体をv12p1
  5. v12p1 フォルダー下のファイルの名前空間をLatest から次のように変更します。V12p1
  6. 必要に応じて、latest フォルダ下のファイルを変更します。

パフォーマンス・モニタリング

Prometheus

GitLabは、すべてのWeb/APIリクエストとSidekiqジョブのリクエスト数とタイミングに関連するPrometheusメトリクスをエクスポートし、パフォーマンスの傾向を診断したり、Elasticsearchのタイミングが他のことに費やした時間と比較してパフォーマンス全体にどのような影響を与えているかを比較したりするのに役立ちます。

インデックスキュー

GitLabはインデックスキューのPrometheusメトリクスもエクスポートし、パフォーマンスのボトルネックを診断したり、GitLabインスタンスやElasticsearchサーバーが更新量に追いついているかどうかを判断するのに役立ちます。

ログ

インデックス作成はすべてSidekiqで行われるため、Elasticsearchインテグレーションに関連するログの多くはsidekiq.log 。特に、何らかの方法でElasticsearchにリクエストを行うすべてのSidekiqワーカーは、リクエスト数とElasticsearchへのクエリ/書き込みにかかった時間をログに記録します。これは、クラスターがインデックス作成に追いついているかどうかを把握するのに役立ちます。

Elasticsearchの検索は通常のウェブワーカーがリクエストを処理することで行われます。ページの読み込みやAPIリクエストを行い、それがElasticsearchへのリクエストとなった場合、リクエスト数とproduction_json.log にかかった時間がログに記録されます。これらのログにはDatabaseとGitalyのリクエストにかかった時間も記録され、検索のどの部分のパフォーマンスが低いかを診断するのに役立ちます。

また、elasticsearch.log に送信される Elasticsearch 固有のログには、パフォーマンスの問題を診断するのに役立つ情報が含まれている可能性があります。

パフォーマンスバー

Elasticsearch のリクエストはPerformance Bar に表示されます。これは開発者のローカルでも、デプロイされた GitLab インスタンスでも、検索パフォーマンスの低下を診断するために使用できます。これは正確なクエリが表示されるので、検索が遅い原因を診断するのに便利です。

相関 ID とX-Opaque-Id

相関 IDは Rails から Elasticsearch へのすべてのリクエストでX-Opaque-Id ヘッダとして転送され、GitLab でクラスター内のすべてのタスクを追跡できるようになります。

トラブルシューティング

取得flood stage disk watermark [95%] exceeded

次のようなエラーが表示されることがあります。

[2018-10-31T15:54:19,762][WARN ][o.e.c.r.a.DiskThresholdMonitor] [pval5Ct]
   flood stage disk watermark [95%] exceeded on
   [pval5Ct7SieH90t5MykM5w][pval5Ct][/usr/local/var/lib/elasticsearch/nodes/0] free: 56.2gb[3%],
   all indices on this node will be marked read-only

これは、ディスク容量のしきい値を超えたためです。デフォルトの95%のしきい値に基づいて、十分なディスク容量が残っていないと判断されます。

さらに、read_only_allow_delete の設定は、trueに設定されます。 これは、forcemergeなどのインデックス作成をブロックします。

curl "http://localhost:9200/gitlab-development/_settings?pretty"

これをelasticsearch.yml ファイルに追加してください:

# turn off the disk allocator
cluster.routing.allocation.disk.threshold_enabled: false

または

# set your own limits
cluster.routing.allocation.disk.threshold_enabled: true
cluster.routing.allocation.disk.watermark.flood_stage: 5gb   # ES 6.x only
cluster.routing.allocation.disk.watermark.low: 15gb
cluster.routing.allocation.disk.watermark.high: 10gb

Elasticsearch を再起動すると、read_only_allow_delete は自動的にクリアされます。

ディスクベースシャードアロケーション|Elasticsearchリファレンス」5.6および6.xより

ディザスタリカバリ/データロス/バックアップ

GitLabにおけるElasticsearchの使用は、セカンダリデータストアとしてのみです。つまり、Elasticsearchに保存されたデータはすべて、他のデータソース、特にPostgreSQLとGitalyから常に再導出することができます。そのため、何らかの理由でElasticsearchのデータストアが破損した場合でも、ゼロからインデックスを作成し直すことができます。

Elasticsearchのインデックスがとてつもなく大きい場合、ゼロからインデックスを作成し直すのは時間がかかりすぎたり、ダウンタイムが長すぎたりする可能性があります。Elasticsearchインデックスの同期が取れなくなった場合に、自動的に矛盾を見つけて再同期する仕組みは組み込まれていませんが、有用なツールの1つは、見逃していると思われる時間範囲に発生したすべての更新のログを調べることです。この情報は非常に低レベルで、GitLabのコードベースに精通しているオペレーションにしか役に立ちません。他の人の役に立つかもしれないので、ここに記録しておきます。理論的には、何が再生される必要があるのかを知るために使うことができる関連ログは次のとおりです:

  1. 同期されたリポジトリ以外の更新は、track_items を検索することでelasticsearch.log から見つけることができます。::Elastic::ProcessBookkeepingService.track!
  2. 発生したすべてのリポジトリ更新は、indexing_commit_rangeを検索することでelasticsearch.log で見つけることができます。これらを再生するには、IndexStatus#last_commit/last_wiki_commit をログの最も古いfrom_sha にリセットし、次のようにプロジェクトの別のインデックスをトリガーする必要があります。ElasticCommitIndexerWorker
  3. 発生したすべてのプロジェクト削除は、ElasticDeleteProjectWorker を検索することでsidekiq.log で見つけることができます。これらの更新は、別のElasticDeleteProjectWorkerをトリガーすることで再生できます。

上記の方法と定期的なElasticsearch スナップショットの取得により、ゼロからインデックスを作成するよりも比較的短時間で様々な種類のデータ損失問題から復旧できるはずです。