Status | Authors | Coach | DRIs | Owning Stage | Created |
---|---|---|---|---|---|
ongoing |
@dgruzd
@DylanGriffith
|
@DylanGriffith
|
@joshlambert
@changzhengliu
| devops enablement | 2022-12-28 |
コード検索にZoektを使用
要約
Zoektはコード検索に特化したオープンソースの検索エンジンです。ZoektはGitLabのAPIとして利用され、実装の細部にとどまりますが、GitLabのユーザーインターフェイスはZoektによって利用可能になるいくつかの新機能を除いて、あまり変わりません。
これは、システムが私たちのスケーリングとコストの期待に実際に応えられるように段階的に展開され、私たちが実行可能な代替であると確信できるまで、Elasticsearchに支えられたコード検索と並行して実行されます。最初のステップは、gitlab-org
内部で利用できるようにし、顧客の関心に基づいて顧客ごとに拡大する予定です。
動機
GitLab のコード検索機能は Elasticsearch に支えられています。Elasticsearchは他のタイプの検索(イシュー、マージリクエスト、コメントなど)には有用であることが証明されていますが、ユーザーがマッチの正確さ(つまり、誤検出のなさ)と柔軟性(例えば、部分文字列マッチや 正規表現をサポートすること)を期待しているコード検索には、設計上良い選択ではありません。私たちは選択肢を調査しましたが、Zoektはコード検索に適したよくメンテンスされた唯一のオープンソース技術です。私たちの調査によると、独自のデータベースを構築するよりも、よくメンテナンスされているオープンソースのデータベースを採用した方が良いと考えています。これは、Zoektの基本的なアーキテクチャが、もし私たちが自分たちで何かを実装しようとしたときに、もう一度実装するようなものであることを、私たちのリサーチが示していることが主な理由です。
私たちの初期のベンチマークでは、Zoekt は私たちの規模でも実行可能であることが示唆されています。しかし、私たちは、Zoekt とのベータインテグレーションを構築し、GitLab.com でグループごとに展開することに投資した方が、より正確なベンチマークを行うよりも、スケーラビリティやコストに関するより良いインサイトを得られると強く感じています。また、まず内部で展開し、その後トライアルに参加したい顧客に展開するため、比較的リスクも低いでしょう。
目標
このインテグレーションの主な目標は、コード検索に次のような要望の高い改善を実装することです:
展開の初期段階は、スケーリングやインフラコストのイシューを可能な限り早期に発見し、解決できるように設計されています。
非ゴール
以下は当初の目標ではありませんが、理論的にはこのソリューションの上に構築される可能性があります:
- 多くのリポジトリで正規表現スキャンを素早く実行できるようにすることで、セキュリティスキャン機能を向上させること。
- 検索インフラのコスト削減 - これはさらなる最適化で可能になるかもしれませんが、初期の見積もりではコストは同程度になると思われます。
- AI/ML検索機能により、ユーザーが検索に興味を持ちそうなものを予測。
- コードインテリジェンスとナビゲーション - コードインテリジェンスとナビゲーション機能は、トリグラムインデックスではなく、構造化データに基づいて構築されるべきですが、(Zoektを使用した)正規表現ベースの検索は、構造化メタデータが有効になっていないコードや、静的解析があまり正確でない動的言語のフォールバックとして適しているかもしれません。なぜなら、ctagsシンボルには正確なナビゲーションのための十分なデータが含まれていない可能性があり、Zoektはプロジェクト間のナビゲーションに必要な依存関係をアンダーサンドしていないからです。
提案
Zoektインテグレーションは、Elasticsearchのコード検索をZoektに置き換えるという実現可能性を示すために作成されました。この青写真は、GitLab.com上でより大規模な顧客展開に必要なステップだけでなく、最小限の実行可能な変更を提供するために必要なすべての詳細について拡張します。
デザインおよび実施内容
ユーザー・エクスペリエンス
ユーザーがZoektを導入しているグループやプロジェクトで詳細検索を行った場合、UIのどこかに “精密検索”(またはその他のUX TBD)に切り替えるトグルが表示され、ElasticsearchからZoektに切り替わります。最終的には、Zoektが長期的な選択肢として適していると判断した場合、Elasticsearchの選択肢を削除したいと考えています。
インデックス作成
Elasticsearchインテグレーションと同様に、GitLabはリポジトリに更新があるたびにZoektに通知します。私たちはgitlab-zoekt-indexer
という新しいインデクサを導入し、リポジトリのクローンを必要とするレガシーインデクサをこれに置き換える予定です。新しいインデクサは、リポジトリのインデックスを作成するためにGitalyに接続するために必要なすべての情報を含むペイロードを期待します。
Rails側のインテグレーションは、リポジトリに更新があるたびにスケジュールされるSidekiqワーカーで、Zoektのこの/indexer/index
エンドポイントを呼び出すだけです。また、ZoektがGitalyに接続するためのGitalyトークンを送信する必要があります。
SSLで接続を暗号化し、Add authentication for GitLab -> Zoekt HTTP callsでベーシック認証を追加してから、新しいインデクサを有効にします。
Sidekiqワーカーは、project_id
に基づいて重複排除を活用することができます。
Zoekt は複数のプロジェクトのインデックスをサポートしていますが、最終的にはユーザーが(デフォルトブランチ以外の)追加ブランチを設定できるようにし、それを Zoekt に送信する必要があるでしょう。このブランチリストを、プロジェクトのインデックスを作成するたびに送信するのか、設定を変更したときにのみ送信するのかを決める必要があります。
複数の Zoekt プロセスが同時に同じリポジトリにインデックスを作成すると、競合状態が発生する可能性があります。そのため、どこかでロック機構を実装して、一度に 1 か所のプロジェクトにしかインデックスを作らないようにする必要があります。Elasticsearch でプロジェクトのインデックスを作成する際に使用している Redis のロックを利用することができます。
検索
検索は Zoekt の/api/search
機能を使って実装されます。Zoektのこのエンドポイントを修正するためのオープンなPRもあります。GitLabは、Elasticsearchと同じように、ユーザーの検索コンテキスト(グループやプロジェクト)に基づいたリポジトリ用の適切なフィルタをすべての検索の先頭に追加します。Zoekt では、検索されたすべてのリポジトリにマッチするクエリ文字列正規表現として実装されます。
Zoekt インフラストラクチャ
各 Zoekt ノードではgitlab-zoekt-indexer
とzoekt-webserver
を実行する必要があります。 これらはそれぞれ異なる責任を持つウェブサーバです。実際の.zoekt
インデックスファイルは、高速検索のために SSD に保存されます。これらのウェブサーバーは同じファイルにアクセスするため、同じノード上で実行する必要があります。gitlab-zoekt-indexer
は.zoekt
インデックスファイルの書き込みを担当します。zoekt-webserver
は、これらの.zoekt
インデックス・ファイルを読み込んで実行する検索への応答を担当します。
ロールアウト戦略
Zoekt のコード検索は、当初gitlab-org
でのみご利用いただけます。その後、より良いコード検索体験を要望する特定の顧客に対して展開を開始します。スケーリングについて学び、改良を加えながら、徐々にGitLab.comの全てのライセンスグループに展開していく予定です。どのグループがインデックス化され、どのグループがインデックス化されていないかを追跡するために、Elasticsearchと同様のアプローチを使う予定です。これは、namespace_id
参照の新しいテーブルzoekt_indexed_namespaces
に基づいて行われます。グループ継承のすべてのレイヤーをチェックするロジックを単純化するために、トップレベルの名前空間へのロールアウトのみを許可します。すべてのライセンスグループにロールアウトしたら、新しくライセンスされたグループを自動的に登録するロジックを有効にします。このテーブルは、以下に説明するように、ネームスペースごとのシャーディングとレプリケーションのデータを格納する場所にもなります。
シャーディングとレプリケーション戦略
Zoektはシャーディングを内蔵しておらず、GitLabのライセンスを持つ全てのお客様に検索機能を提供するためには、複数のZoektサーバーが必要になると予想されます。
シャーディングを実装する明確な方法は2つあります:
- Zoektの上に、あるいはZoektの前に、独立したコンポーネントとして構築する方法です。Zoektに複雑な分散データベースを組み込むことは、プロジェクトにとって良い方向には向かないでしょう。
- GitLab 内でシャードを管理します。GitLabのアプリケーションレイヤーで、インデックスや検索のリクエストを送るシャードを選択します。
同様に、レプリケーションを実装する方法はいくつかあります:
- サーバサイドでZoektレプリカが他のZoektレプリカを認識し、プライマリから更新をストリーミングして同期を保つ方法。
- クライアントサイドのレプリケーションでは、クライアントはすべてのレプリカにインデックス作成リクエストを送信し、任意のレプリカに検索リクエストを送信します。
私たちはGitLabアプリケーション内にシャーディングを実装する予定ですが、レプリケーションはGitLabから全てのレプリカに重複した更新を送るのではなく、Zoektサーバーのファイルシステムレベルで行うのがベストかもしれません。これは、Zoektサーバー上で特定のディレクトリの.zoekt
ファイルの変更を監視し、その更新をレプリカに同期させるような処理です。rsync
なぜなら、ファイルは常に変化しており、同期中にファイルが削除される可能性があるからです。そのため、インデックス作成を遅くすることなく、更新を一括して同期する必要があります。
GitLabにシャーディングを実装することで、デプロイする必要がある追加のインフラコンポーネントを簡素化し、複数のシャーディングのロールアウトと同時に多くの顧客へのロールアウトをより柔軟に制御できるようになります。
プライマリからレプリカへの同期をファイルシステムレベルでZoektノードに実装することで、全体的なリソースの使用を最適化します。ベアリポジトリは単なるキャッシュであるため、インデックスファイルをレプリカに同期するだけでよいのです。これにより
- レプリカのディスク容量
- インデックスを再構築する必要がないため、レプリカのCPU使用率
- レポをクローンするためのGitalyへの負荷
これらの高可用性の実装は後回しにする予定ですが、予備的な計画としては次のようになります:
- GitLabをZoektサーバーのプールで設定します。
- GitLabはグループをランダムにZoektのプライマリサーバに割り当てます。
- Zoektレプリカサーバも存在します
- Zoekt のプライマリサーバは、定期的に
.zoekt
のインデックスファイルをそれぞれのレプリカに同期します。 - プライマリにイシューが発生した場合、レプリカをプライマリに昇格させるプロセスが必要です。どれがプライマリでどれがレプリカかを追跡するためにConsulを使用する予定です。
- プロジェクトのインデックスを作成するとき、GitLab はプライマリのインデックスを更新するために Sidekiq ジョブをキューに入れます。
- 検索時には、検索対象のグループに対してZoektのプライマリサーバかレプリカサーバのどちらかをランダムに選択します。コード検索は “最終的には一貫して “行われ、すべての読み込みは少し古いインデックスを読み込む可能性があるため、どちらが “より最新 “であるかは気にしません。インデックス更新の最大待ち時間の目標を設定し、あまりにも古すぎる場合はローテーションからノードを削除することを検討します。
- グループ検索が常に1つのZoektサーバーを検索できるようにするため、すべてをトップレベルグループごとにシャードします。将来的にグローバル検索がインポートされるようになれば、アグリゲーションが可能になるかもしれません。小規模なセルフマネージドインスタンスでは、単一の Zoekt サーバーを使用することで、アグリゲーションを実装せずにグローバル検索を行うことができます。最大のグループサイズとシングルノードのZoektサーバーのスケーリング制限によっては、1つのグループに複数のシャードを割り当てるアプローチの実装を検討します。
GitLabの外側にある “プロキシ “レイヤーが全てのシャードを管理するのと比べると、選択したパスの欠点は、GitLabから全てのZoektサーバーを管理する複雑さが追加されることです。私たちはこの決定を作業中とみなし、GitLabに複雑さを追加しすぎることがわかったら再評価するつもりです。
GitLab::Zoekt::Shard
モデルを使ったシャーディングの提案
::Zoekt::IndexedNamespace
は名前空間とシャードの間に多対多の関係を実装しているので、これはすでに実装されています。
Consulを使ったレプリケーションとサービスディスカバリー
上記のように Zoekt ノードレベルでレプリケーションを行う場合、zoekt_shards ->
namespaces
から一対多のリレーションを使用するようにデータモデルを変更する必要があります。 これはnamespace_id
カラムをzoekt_indexed_namespaces
で一意にすることを意味します。そして、index_url
が常にプライマリZoektノードを指し、search_url
N個のレプリカとプライマリを持つDNSレコードと search_url
なるようなサービス発見アプローチを実装する必要があります。そして、検索時にsearch_url
レコードからランダムに選択 search_url
します。
イテレーション
- 利用可能
gitlab-org
- モニタリングの改善
- パフォーマンスの向上
- 一部のお客様のみご利用可能
- シャーディングの実装
- レプリケーションの実装
- より多くのライセンスグループで利用可能
- シャードの自動(再)バランシングの実装
- すべてのライセンス・グループに展開するためのコストを見積もり、その価値があるか、あるいはさらなる最適化や計画の調整が必要かどうかを判断。
- すべての認可グループへの展開
- パフォーマンスの向上
- コストを評価し、すべての無料顧客に展開すべきかどうかを決定します。