This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. As with all projects, the items mentioned on this page are subject to change or delay. The development, release, and timing of any products, features, or functionality remain at the sole discretion of GitLab Inc.
Status Authors Coach DRIs Owning Stage Created
ongoing @dgruzd @DylanGriffith @DylanGriffith @joshlambert @changzhengliu devops enablement 2022-12-28

코드 검색에 Zoekt 사용하기

요약

우리는 GitLab에 추가적인 코드 검색 기능을 구현할 것입니다. 이 기능은 코드 검색을 위해 특별히 설계된 오픈 소스 검색 엔진인 Zoekt를 기반으로 합니다. Zoekt는 GitLab에서 API로 사용되며 사용자 인터페이스는 Zoekt에서 제공되는 새로운 기능을 제외하고는 크게 변경되지 않을 것입니다.

이 기능은 체계가 우리의 확장 및 비용 기대치를 충족할 것이라 확신하기 위해 단계별로 출시될 것이며, Elasticsearch를 기반으로 한 코드 검색과 함께 실행될 것입니다. 우리가 Zoekt를 대체할 수 있는지 확신할 때까지 Elasticsearch를 사용하면서 시스템이 실제로 충족하는 지 확인할 것입니다. 첫 번째 단계는 내부적으로 gitlab-org에서 사용할 수 있도록 하고, 고객의 관심에 따라 고객별로 점진적으로 확대될 것입니다.

동기

현재 GitLab 코드 검색 기능은 Elasticsearch를 기반으로 합니다. Elasticsearch는 다른 유형의 검색(이슈, 병합 요청, 댓글 등)에 유용하지만 코드 검색에는 적합하지 않습니다. 여기서 사용자들은 정확한 일치(즉, 거짓 긍정이 없는)와 유연성(예: 부분 일치정규 표현식 지원)을 기대합니다. 우리는 여러 옵션을 조사했고, Zoekt가 코드 검색에 적합한 유일한 잘 유지되는 오픈 소스 기술임을 알게 되었습니다. 우리의 연구에 따르면, 우리가 직접 구현하려고 한다면 Zoekt의 기본 아키텍처를 구현하게 될 것이기 때문에 우리는 우리 자신의 기술을 구축하려는 것보다 잘 유지되는 오픈 소스 데이터베이스를 채택하는 것이 더 나을 것으로 믿습니다.

우리의 초기 벤치마킹은 Zoekt가 우리의 규모에서 유효할 것으로 보이지만, 정확한 벤치마킹 노력보다는 Zoekt와의 베타 통합을 구축하고 GitLab.com에서 단계별로 그룹별로 적용하여 확장성 및 비용에 대해 더 나은 통찰력을 제공할 것이라고 강하게 느낍니다. 또한, 처음에는 내부적으로 출시되고 나중에 시행할 고객들에게는 비교적 낮은 위험으로 시행될 것입니다.

목표

이 통합의 주요 목표는 다음과 같은 매우 요청된 코드 검색 개선 사항을 구현하는 것입니다:

  1. 고급 검색에서 정확한 일치(부분 일치) 코드 검색
  2. 고급 전역 검색에서 정규 표현식 지원
  3. 동일 파일에서 여러 줄 일치 지원

출시의 초기 단계는 가능한 한 빨리 스케일링이나 인프라 비용 문제를 파악하고 해결할 수 있도록 설계되었으며, 만일 이 기술이 적합하지 않다면 너무 많이 투자하기 전에 초기 피벗할 수 있도록 합니다.

비목표

다음은 초기에는 목표가 아니지만 이 솔루션 위에 구축될 수 있는 것들입니다:

  1. 빠르게 여러 리포지토리 전체에 대한 정규식 스캔을 수행할 수 있어 보안 스캔 기능을 개선하기
  2. 검색 인프라에 대한 비용 절감 - 추가적인 최적화로 가능할 수 있지만 초기 추정에 따르면 비용은 유사할 것입니다.
  3. 사용자가 관심을 가질 것으로 예측하는 검색을 사용하는 AI/ML 기능
  4. 코드 지능 및 탐색 - 코드 지능 및 탐색 기능은 일반적으로 구조화된 데이터 위에 구축되어야 하지만 정규 표현식을 사용하는(즉, Zoekt를 사용하는)것이 구조화된 메타데이터가 활성화되지 않은 코드나 정적 분석이 매우 정확하지 않은 동적 언어의 경우에 적합한 대비책이 될 수 있습니다. Zoekt는 특히 기존 ctags를 사용한 심볼 추출에도 불구하고 초기에 적합하지 않을 수 있습니다. 왜냐하면 ctags 심볼에는 정확한 탐색을 위한 충분한 데이터가 포함되어 있지 않을 수 있고, Zoekt는 프로젝트 간 탐색에 필요한 종속성을 이해하지 못하기 때문입니다.

제안

Zoekt 통합의 초기 구현은 Zoekt를 Elasticsearch 코드 검색의 대체품으로 사용할 수 있는지의 가능성을 보여주기 위해 생성되었습니다. 이 설계안은 최소한의 변경 사항을 제공하는 데 필요한 모든 세부 정보를 확장하고 이를 GitLab.com에서 더 큰 고객 출시로 확장하는 데 필요한 단계를 설명할 것입니다.

디자인 및 구현 세부 정보

사용자 경험

Zoekt 출시 그룹 또는 프로젝트에 대한 고급 검색을 수행하는 사용자는 UI 어딘가에 “정확한 검색”으로 전환할 수 있는 토글을 제시할 것입니다(또는 기타 사용자 경험에 따라 결정될 것입니다) 이를 통해 Elasticsearch에서 Zoekt로 변경됩니다. 초기 사용자 의견은 사용자에게 이러한 선택을 어떻게 제공할지를 평가하는 데 도움이 될 것이며, 궁극적으로는 Zoekt가 적합한 장기적인 옵션이라고 판단되면 Elasticsearch 옵션을 제거하기를 원할 것입니다.

색인화

우리의 Elasticsearch 통합과 유사하게, GitLab은 저장소에 업데이트가 있는 경우마다 Zoekt에 알립니다. 새로운 인덱서인 gitlab-zoekt-indexer를 도입했으며, 레거시 인덱서를 이 인덱서로 교체할 것입니다. 새로운 인덱서는 저장소를 색인하는 데 필요한 모든 정보가 포함된 페이로드를 Gitaly에 연결하기 위해 예상합니다.

이 통합의 레일즈 측면은 저장소 업데이트가 있을 때마다 예약된 Sidekiq 워커가 될 것이며, 간단히 Zoekt에서 /indexer/index 엔드포인트를 호출할 것입니다. 또한 Zoekt에 Gitaly에 연결할 수 있는 Gitaly 토큰을 전달해야 할 것입니다.

우리는 이 새로운 인덱서를 활성화하기 전에 GitLab -> Zoekt HTTP 호출에 대한 인증 추가에서 SSL로 연결을 암호화하고 기존 인덱서가 GitLab에서 Gitaly 비밀을 받기 때문에 사전에 기존 인덱서를 사용하지 않도록 할 것입니다.

sequenceDiagram participant user as User participant gitaly as Gitaly participant gitlab_sidekiq as GitLab Sidekiq participant zoekt as Zoekt user->>gitlab_git: git push git@gitlab.com:gitlab-org/gitlab.git gitlab_git->>gitlab_sidekiq: ZoektIndexerWorker.perform_async(278964) gitlab_sidekiq->>zoekt: POST /indexer/index {"GitalyConnectionInfo": {"Address": "tcp://gitaly:2305", "Storage": "default", "Token": "secret_token", "Path": "@hashed/a/b/c.git"}, "RepoId":7} zoekt->>gitaly: go gitaly client

Sidekiq 워커는 project_id를 기반으로 중복을 방지할 수 있습니다.

Zoekt는 여러 프로젝트를 색인화할 수 있으며, 결국 사용자가 기본 브랜치 이외의 추가 브랜치를 구성할 수 있도록 허용해야 할 것입니다. 이 정보를 Zoekt에게 전달해야 할 것입니다. 프로젝트를 색인화할 때마다 브랜치 목록을 보내야 하는지 또는 구성이 변경될 때만 보내야 하는지 결정해야 할 것입니다.

동일한 저장소를 동시에 여러 Zoekt 프로세스가 색인화하는 경주 조건이 발생할 수 있습니다. 그러므로 우리는 한 번에 한 곳에서 1개의 프로젝트만 색인화하도록 보장하기 위해 어딘가에 잠금 메커니즘을 구현해야 할 것입니다. 우리는 Elasticsearch에서 프로젝트를 색인화할 때 사용하는 Redis 잠금 동일한 Redis 잠금을 사용할 수 있을 것입니다.

검색

검색은 Zoekt의 /api/search 기능을 사용하여 구현될 것입니다. 또한 Zoekt에서 이 엔드포인트를 수정하는 오픈 PR도 있으며 이를 수정될 때까지 fork에서 작업하는 것도 고려해볼 수 있습니다. GitLab은 사용자의 검색 컨텍스트(그룹 또는 프로젝트)에 기반하여 저장소에 대한 적절한 필터를 모든 검색에 앞선다. Zoekt를 위해 이는 검색된 모든 저장소와 일치하는 쿼리 문자열 정규식으로 구현될 것입니다.

Zoekt 인프라

각 Zoekt 노드는 gitlab-zoekt-indexerzoekt-webserver를 실행해야 할 것입니다. 이들은 각기 다른 책임을 갖는 웹 서버입니다. 실제 .zoekt 색인 파일은 빠른 검색을 위해 SSD에 저장될 것입니다. 이러한 웹 서버들은 동일한 파일에 액세스하기 때문에 동일한 노드에서 실행되어야 할 것입니다. gitlab-zoekt-indexer.zoekt 색인 파일을 작성하는 데 책임이 있으며, zoekt-webserver는 이러한 .zoekt 색인 파일을 읽어 수행하는 검색에 대한 응답을 담당합니다.

전개 전략

처음에는 Zoekt 코드 검색이 gitlab-org에서만 사용 가능할 것입니다. 그 후에는 코드 검색 경험을 향상시키기를 요청한 특정 고객에게 서비스를 제공하기 시작할 것입니다. 스케일링에 대해 배우고 개선 사항을 만들어 나가면서 GitLab.com의 모든 라이선스 그룹에 서서히 전개할 것입니다. 우리는 Elasticsearch와 유사한 접근법을 사용하여 어떤 그룹이 색인화되었고 어떤 그룹이 색인화되지 않았는지 추적하는 데 사용하게 될 것입니다. 이는 namespace_id를 참조하는 새로운 zoekt_indexed_namespaces 테이블을 바탕으로 합니다. 우리는 모든 계층의 그룹 상속을 확인하는 논리를 단순화하기 위해 최상위 그룹에만 전개할 것입니다. 모든 라이선스 그룹으로 전파를 허용할 것입니다. 새로 라이선스가 부여된 그룹은 자동으로 등록되도록 로직을 활성화할 것입니다. 이 테이블은 또한 아래에서 설명하는 네임스페이스별 분할 및 복제 데이터를 저장하는 장소가 될 수 있습니다.

분할 및 복제 전략

Zoekt에는 내장된 분할 기능이 없으며, GitLab 라이선스 고객에게 검색 기능을 제공하기 위한 규모를 확보하기 위해 여러 Zoekt 서버가 필요할 것으로 기대됩니다.

분할을 구현하는 명확한 두 가지 방법이 있습니다.

  1. Zoekt 위에 또는 Zoekt 앞에 독립적인 구성 요소로 구축합니다. Zoekt에 분산 데이터베이스의 모든 복잡성을 빌드하는 것은 프로젝트에 대한 좋은 방향은 아니므로 이는 아마도 올바른 방향이 될 것입니다. 대부분은 올바른 샤드로 요청을 중계하는 독립적인 인프라 조각이 될 것입니다.
  2. GitLab 내부에서 샤드를 관리합니다. 이는 GitLab 내부의 애플리케이션 계층이며, 적절한 샤드를 색인화 및 검색 요청에 보내기 위해 선택하게 될 것입니다.

마찬가지로, 복제를 구현하는 몇 가지 방법이 있습니다.

  1. Zoekt 복제본 간에 업데이트를 스트림하는 것을 인식하는 Zoekt 복제본에서 서버 측으로
  2. 클라이언트 측 복제, 클라이언트가 모든 복제본에 색인화 요청을 보내고 검색 요청은 모든 복제본에 보냅니다.

우리는 샤딩 구현을 GitLab 애플리케이션 내부에서 계획하고 있지만, 복제는 GitLab에서 모든 복제본으로 중복된 업데이트를 보내는 대신에 Zoekt 서버 파일시스템 수준에서 제공하는 것이 리소스 사용을 최적화시킬 것입니다. 각 Zoekt 노드에서 기본 -> 복제본 동기화 프로세스가 필요할 것입니다. 파일의 변경을 모니터링하고 복제본에 대한 업데이트를 실시하는 Zoekt 서버에서 어떤 프로세스가 필요할 것입니다. 이는 rsync보다 약간 더 복잡해야 하며, 파일이 지속적으로 변경되고 파일이 삭제되는 경우에도 어떤 방식으로든 업데이트를 동기화해야 할 것입니다.

이러한 고가용성 측면에 대한 구현은 나중에 미루겠지만, 기본 계획은 다음과 같습니다.

  1. GitLab은 Zoekt 서버 풀로 구성됩니다.
  2. GitLab은 그룹에 임의의 Zoekt 주 서버를 할당합니다.
  3. Zoekt 주 서버는 정기적으로 해당 복제본에게 .zoekt 색인 파일을 동기화할 것입니다.
  4. 본래의 복제본을 주 서버로 승격하는 프로세스가 필요할 것입니다. 우리는 이를 추적하기 위해 Consul을 사용하게 될 것입니다.
  5. 프로젝트를 색인화할 때 GitLab은 주 서버의 색인을 업데이트하기 위해 Sidekiq 작업을 대기열에 추가할 것입니다.
  6. 검색할 때는 Zoekt 주 서버 또는 복제본 서버 중 하나를 무작위로 선택하여 검색할 것입니다. 우리는 어떤 쪽이 “더 최신”인지 상관하지 않으며 코드 검색은 “최종 일관성”이 될 것입니다. 모든 읽기가 약간 오래된 색인을 읽게 될 것이며 인덱스 업데이트의 최대 대기 시간을 제한하고 너무 오래된 노드는 회전에서 제거할 수 있도록 고려할 것입니다.
  7. 우리는 모든 것을 최상위 그룹별로 분할할 것이며, 그룹 검색이 항상 단일 Zoekt 서버를 검색하도록 보장할 것입니다. 앞으로 중요한 경우 전역 검색을 위해 집계가 가능할 수 있을 것입니다. 작은 영구 설치들은 전역 검색을 위한 집계가 구현되지 않고도 단일 Zoekt 서버를 사용할 수 있도록 할 것입니다. 가장 큰 그룹 크기와 단일 노드 Zoekt 서버의 확장 제한에 따라 그룹이 여러 샤드로 할당될 수 있도록 구현 방법을 고려할 수 있습니다.

선택한 경로의 단점은 GitLab에서 모든 이러한 Zoekt 서버를 관리하는 복잡성이 추가된다는 것입니다. 이 결정은 계속 진행 중인 작업으로 보고 복잡성을 추가하는 경우 GitLab에 너무 많은 복잡성을 추가하는 것으로 판명될 경우 재평가할 것입니다.

GitLab ::Zoekt::Shard 모델을 사용한 샤딩 제안

이미 ::Zoekt::IndexedNamespace로 구현되어 있으며, 네임스페이스와 샤드 간의 다대다 관계를 구현합니다.

자체 등록 Zoekt 노드를 사용한 샤딩 제안

이 제안은 대부분 GitLab Runner의 아키텍처에서 영감을 받아 만들어졌으며, 통신이 양방향임이 주요 차이점입니다. 저희는 Zoekt Sharding and Replication에서의 토의 끝에 이에 도달했습니다.

고려한 대체 옵션

Zoekt 클러스터 상태를 관리할 위치에 대해 Raft와 Zoekt의 독자적인 데이터베이스를 포함하여 여러 가지 옵션을 고려했습니다. 저희는 Zoekt 대신 GitLab에 의해 전체 클러스터 상태를 관리하는 것에 많은 이점이 있다고 판단하여 Zoekt 노드를 가능한 한 순진하게 유지하기로 결정했습니다.

주요 이점은 다음과 같습니다:

  1. GitLab의 배포 주기가 여러 프로젝트 전체에 걸쳐 많은 버전 업데이트를 필요로 하는 Zoekt보다 빠릅니다.
  2. 이미 우리에게 익숙한 Postgres, Redis, Sidekiq 등을 비롯한 상태 관리를 위한 많은 도구가 이미 GitLab에 존재합니다.
  3. 이 프로젝트에서 주로 작업하는 엔지니어들은 Go보다는 Rails에 대한 경험이 훨씬 많으며, 다른 검색 기능은 대부분 Rails 코드로 작성되고 있으므로 Rails 코드를 더 많이 작성합니다.

이러한 이점 중 일부는 다른 팀이 소유한 다른 프로젝트에 대해 올바른 선택이 아닐 수 있습니다.

고수준 제안

  1. Zoekt 노드는 3가지 추가 인수와 함께 시작됩니다: 해당 주소, 샤드 이름 및 GitLab URL.
  2. 우리는 샤드 이름을 별도로 유지하여 샤드를 다른 주소로 마이그레이션할 수 있도록 합니다.
  3. Zoekt가 k8s에서 실행 중인 경우 주소로 hostname --fqdn (예: gitlab-zoekt-1.gitlab-zoekt.default.svc.cluster.local)을 전달할 수 있습니다. 베어메탈에서 Zoekt를 실행하는 고객은 별도로 구성해야 합니다.
  4. Zoekt는 아마도 내부 API를 사용하여 GitLab에 연결할 것입니다. 우리는 내부 트래픽을 유지하고 추가적인 트래픽 비용을 피하기 위해 별도의 GitLab URL을 사용하고자 할 수도 있습니다.
  5. GitLab은 ‘last_seen_at’ 및 샤드의 이름(::Zoekt::Shard를 확장할 수 있음)을 유지할 룩업 테이블을 유지할 것입니다. 우리는 또한 레플리카와 프라이머리의 개념을 소개해야 할 것입니다.
  6. Zoekt 노드(이 경우에는 색인 생성기)는 구성된 GitLab URL로 새로운 작업을 얻기 위해 주기적인 요청을 보낼 것입니다. GitLab은 새로운 노드를 등록하거나 룩업 테이블에서 기존 레코드를 업데이트할 것입니다.
  7. 작업이 완료된 후, zoekt-indexer는 작업이 완료되었음을 나타내기 위해 GitLab에 콜백을 보낼 것입니다.
  8. 특정 시간이 지나도록 GitLab이 요청을 받지 못하면 네임스페이스를 다른 샤드로 재할당하고 누락된 샤드를 사용할 수 없게 표시할 수 있습니다.
  9. 검색을 실행할 때, 우리는 프라이머리 및 레플리카에 라운드로빈 요청을 보낼 수 있습니다. 우리는 리트라이도 구현하고자 할 수 있습니다. 예를 들어, 프라이머리에 대한 요청에 실패하면, 우리는 레플리카에 대한 추가 요청을 즉시 또는 그 반대로 보낼 수 있습니다. 여기에 해당하는 이슈가 있습니다: Consider circuit breaker for Zoekt code search.
  10. 초기에는 효율적인 샤드 간 색인 파일 이동 및 복사를 구현하기 전에 복제를 건너뛰고자 할 수 있습니다(예: rsync).
  11. 리밸런싱은 아마도 인덱싱된 네임스페이스에 충분한 레플리카와 사용 가능한 저장 공간 여부에 고려하는 크론 Sidekiq 워커에서 발생할 것입니다.

k8s에서 실행할 고레벨 명령어 예시:

./gitlab-zoekt-indexer -index_dir=/data/index -shard_name=`hostname` -address=`hostname --fqdn`

상태풀 세트에 더 많은 레플리카를 추가하면 자동으로 주소 및 샤드 이름을 처리해야 할 것입니다. 예를 들어:

  • gitlab-zoekt-0 / gitlab-zoekt-0.gitlab-zoekt.default.svc.cluster.local
  • gitlab-zoekt-1 / gitlab-zoekt-1.gitlab-zoekt.default.svc.cluster.local
  • ..

색인 생성기가 받을 수 있는 가능한 작업들:

  • index_repositories(ids: [1,2,3,4])
  • delete_repositories(ids: [5,6])
  • copy_index(from: 'gitlab-zoekt-0', to: 'gitlab-zoekt-1', repo_id: 4)

Consul을 사용한 복제 및 서비스 검색

위에서 설명한대로 Zoekt 노드 수준에서 복제를 계획한다면, 우리는 zoekt_shards -> namespaces로부터 일대다 관계를 사용하는 데이터 모델을 변경해야 합니다. 이는 zoekt_indexed_namespaces에서 namespace_id 열을 고유하게 만드는 것을 의미합니다. 그런 다음 index_url이 항상 프라이머리 Zoekt 노드를 가리키고,search_url이 N개의 레플리카 및 프라이머리를 가지는 DNS 레코드인 서비스 검색 접근 방식을 구현해야 합니다. 그런 다음, 검색할 때 search_url 레코드에서 무작위로 선택합니다.

이터레이션

  1. gitlab-org에서 이용 가능하게 만들기
  2. 모니터링 개선
  3. 성능 향상
  4. 특정 고객에게 이용 가능하게 만들기
  5. 샤딩 구현
  6. 복제 구현
  7. 라이선스 그룹에 많은 사용자에게 이용 가능하게 만들기
  8. 샤드의 자동 (재)균형 조정 구현
  9. 모든 라이선스 그룹에 롤아웃하는 비용 추정 및 가치 여부 결정 또는 추가 최적화 또는 계획 조정이 필요한지 결정
  10. 모든 라이선스 그룹에 롤아웃
  11. 성능 개선
  12. 비용 평가 및 모든 무료 고객에게 롤아웃할지 여부 결정