고급 검색 개발 지침

이 페이지에는 Elasticsearch를 개발하고 활용하는 방법에 대한 정보가 포함되어 있습니다.

Elasticsearch를 활성화하고 초기 색인을 수행하는 방법에 대한 정보는 Elasticsearch 통합 설명서에서 확인할 수 있습니다.

심층 분석

2019년 6월, Mario de la Ossa는 GitLab Elasticsearch 통합에 관련된 도메인 특화 지식을 공유하려면 향후에 이 코드베이스의 일부에서 작업할 수 있는 모든 사람들에게 Deep Dive(회사 구성원만: https://gitlab.com/gitlab-org/create-stage/-/issues/1)를 주최했습니다. YouTube 녹화Google 슬라이드, PDF에서 확인할 수 있습니다. 이 심층 분석에서 다루는 내용은 GitLab 12.0 당시 정확하며, 특정 세부 사항은 변경되었을 수 있지만 여전히 좋은 소개 역할을 할 것입니다.

2020년 8월, 두 번째 Deep Dive가 진행되었으며, GitLab별 다중 인덱스 지원에 중점을 둔 구조에 초점을 맞췄습니다. YouTube 녹화슬라이드를 확인할 수 있습니다. 이 심층 분석에서 다루는 내용은 GitLab 13.3 당시 정확했습니다.

지원되는 버전

버전 요구 사항을 참조하세요.

Elasticsearch 쿼리에 중대한 변경을 수행하는 개발자는 지원되는 모든 버전에서 기능을 테스트해야 합니다.

개발 환경 설정

Elasticsearch GDK 설정 지침을 참조하세요.

도움이 되는 Rake 작업

  • gitlab:elastic:test:index_size: 현재 색인이 사용하는 공간 및 색인에 있는 문서 수를 알려줍니다.
  • gitlab:elastic:test:index_size_change: 색인 크기를 출력하고, 다시 색인을 실행한 다음 다시 색인 크기를 출력합니다. 색인 크기 개선을 테스트할 때 유용합니다.

추가로, 테스트를 위해 큰 리포지터리나 복수의 복제가 필요한 경우 다음 지침을 따르세요

작동 방식

Elasticsearch 통합은 외부 색인 생성기에 의존합니다. 우리는 Go로 작성된 색인 생성기를 제공합니다. 사용자는 레이크 작업을 통해 초기 색인을 트리거해야 하지만 이 작업이 완료된 후에는 GitLab 자체가 after_ 콜백을 통해 필요한 때에 다시 색인화를 트리거합니다. 이 콜백은 /ee/app/models/concerns/elastic/application_versioned_search.rb에서 상속된 create, update 및 destroy에 의해 처리됩니다.

초기 색인 작업이 완료되면 프로젝트 이외의 모든 모델에 대한 create, update 및 delete 작업은 Redis ZSET에서 추적됩니다. 정기적인 sidekiq-cron ElasticIndexBulkCronWorker가 이 큐를 처리하여 Bulk Request API를 사용하여 한 번에 많은 Elasticsearch 문서를 업데이트합니다.

검색 쿼리는 ee/app/models/concerns/elastic에서 찾을 수 있는 관련 기능에 의해 생성됩니다. 이러한 관련 기능은 액세스 제어를 담당하며, 보안 버그의 역사적인 원인이 되었으므로 주의를 기울여야 합니다!

사용자 정의 라우팅

사용자 정의 라우팅은 프로젝트와 관련된 문서 유형에 대해 Elasticsearch에서 사용됩니다. 라우팅 형식은 project_<project_id>입니다. 라우팅은 인덱싱 및 검색 작업 중에 설정됩니다. 사용자 정의 라우팅을 사용하는 경우의 이점과 상충점 중 일부는 다음과 같습니다.

  • 프로젝트 범위의 검색이 훨씬 빠릅니다.
  • 글로벌 및 그룹 범위의 검색에 너무 많은 샤드가 포함될 경우 라우팅이 사용되지 않습니다.
  • 샤드 크기 불균형이 발생할 수 있습니다.

기존 분석기 및 토크나이저

다음 분석기 및 토크나이저가 ee/lib/elastic/latest/config.rb에 정의되어 있습니다.

분석기

path_analyzer

블롭의 경로를 색인화할 때 사용됩니다. path_tokenizerlowercase, asciifolding 필터를 사용합니다.

아래에 있는 path_tokenizer 설명 참조.

sha_analyzer

블롭 및 커밋에서 사용됩니다. sha_tokenizerlowercase, asciifolding 필터를 사용합니다.

나중에 있는 sha_tokenizer 설명 참조.

code_analyzer

블롭의 파일 이름 및 콘텐츠를 색인화할 때 사용됩니다. whitespace 토크나이저 및 word_delimiter_graph, lowercase, asciifolding 필터를 사용합니다.

whitespace 토크나이저는 토큰이 어떻게 분할되는지를 보다 세밀하게 제어하기 위해 선택되었습니다. 예를 들어 문자열 Foo::bar(4)Foobar(4)와 같은 토큰을 올바르게 검색하려면 토큰이 생성되어야 합니다.

토큰을 분할하는 방법에 대한 설명은 code 필터 참조.

토크나이저

sha_tokenizer

이는 edgeNGram 토크나이저를 사용하여 SHAs를 임의의 하위 집합별로 검색할 수 있게 하는 사용자 정의 토크나이저입니다(최소 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은 동시에 작동할 수 있는 여러 인덱스를 지원하도록 작업 중입니다. 스키마 변경 시에 관리자는 새 인덱스를 만들고 그에 대해 다시 색인할 수 있게 될 것이며, 검색은 여전히 이전 안정적인 인덱스로 이어질 것입니다. 모든 데이터 업데이트는 두 인덱스로 전달될 것입니다. 새 인덱스가 준비되면 관리자는 활성 상태로 표시할 수 있으며, 이로써 모든 검색을 그로 전송하고 이전 인덱스를 제거할 수 있을 것입니다.

이는 예를 들어 새 서버로의 마이그레이션에도 도움이 됩니다.

현재 우리는 이 새로운 디자인으로 마이그레이션하는 과정에 있습니다. 현재 모든 것은 단일 버전으로 작동하도록 강제되어 있습니다.

아키텍처

elasticsearch-rails에서 제공되는 전통적인 설정은 내부 프록시 클래스를 통해 통신하는 것입니다. 개발자는 모델별 로직을 해당 모델에 포함할 모듈에 작성할 것입니다. (예: ‘SnippetsSearch’). __elasticsearch__ 메서드는 프록시 객체를 반환할 것입니다. 예를 들면:

  • Issue.__elasticsearch__는 ‘Elasticsearch::Model::Proxy::ClassMethodsProxy’ 의 인스턴스를 반환할 것입니다.
  • Issue.first.__elasticsearch__는 ‘Elasticsearch::Model::Proxy::InstanceMethodsProxy’의 인스턴스를 반환할 것입니다.

이러한 프록시 객체는 일반적으로 Elasticsearch 서버로 직접 대화합니다 (다이어그램 상단 부분 참조).

Elasticsearch Architecture

계획된 새 디자인에서 각 모델은 해당 모델별 로직이 위치한 하위 클래스화된 프록시 객체 쌍을 가질 것입니다. 예를 들어, Snippet은 ‘Elasticsearch::Model::Proxy::ClassMethodsProxy’의 하위 클래스인 SnippetClassProxy를 가질 것입니다. Snippet은 ‘Elasticsearch::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’)에 별칭으로 포함되어 있습니다. 이러한 클래스를 참조할 때 항상 ‘최신’ 네임스페이스를 직접 사용하지 말고 실제 버전(예: ‘V12p3’)을 사용하세요.

버전 이름은 기본적으로 GitLab 릴리스 버전을 따릅니다. 설정이 12.3에서 변경된 경우 우리는 ‘V12p3’라는 새 네임스페이스를 만들 것입니다 (p는 “point”의 약자입니다). 버전을 다르게 명명해야 하는 경우 문제를 제기하세요.

현재 버전이 ‘v12p1’이고 ‘v12p3’에 대한 새 버전을 생성해야 하는 경우 다음 단계를 수행합니다:

  1. 전체 ‘v12p1’ 폴더를 ‘v12p3’로 복사합니다.
  2. ‘v12p3’ 폴더의 파일에 대한 네임스페이스를 ‘V12p1’에서 ‘V12p3’로 변경합니다(여전히 ‘최신’에 별칭이 됩니다).
  3. ‘v12p1’ 폴더를 삭제합니다.
  4. 전체 ‘latest’ 폴더를 ‘v12p1’로 복사합니다.
  5. ‘v12p1’ 폴더의 파일에 대한 네임스페이스를 ‘최신’에서 ‘V12p1’로 변경합니다.
  6. 필요한 대로 ‘latest’ 폴더의 파일을 수정합니다.

성능 모니터링

Prometheus

GitLab은 모든 웹/API 요청 및 Sidekiq 작업에 대한 요청 수와 타이밍과 관련된 Prometheus 메트릭을 노출합니다. 이는 Elasticsearch의 타이밍이 다른 작업을 수행하는 데 소요된 시간에 비해 전반적인 성능에 어떤 영향을 미치고 있는지 비교하는 데 도움이 됩니다.

색인 대기열

GitLab은 또한 색인 대기열에 관한 Prometheus 메트릭을 노출합니다. 이는 성능 병목 현상을 진단하고 GitLab 인스턴스 또는 Elasticsearch 서버가 업데이트의 양을 따라갈 수 있는지 결정하는 데 도움이 됩니다.

로그

모든 색인은 Sidekiq에서 발생하므로 Elasticsearch 통합에 관련된 로그는 대부분 sidekiq.log에서 찾을 수 있습니다. 특히, Elasticsearch에 요청을 하는 모든 Sidekiq 작업자는 Elasticsearch에 대한 요청 수와 쿼리/작성에 소요된 시간을 기록할 것입니다. 이는 클러스터가 색인을 따라잡고 있는지 이해하는 데 도움이 됩니다.

Elasticsearch 검색은 페이지를 로드하거나 API 요청을 하는 일반적인 웹 작업자를 통해 이루어집니다. 이에 따라 Elasticsearch로의 요청 수와 소요된 시간이 기록될 것입니다. 이러한 로그는 또한 데이터베이스 및 Gitaly 요청에 소요된 시간을 포함할 것이며, 이는 성능이 낮은 검색의 어떤 부분을 진단하는 데 도움이 될 수 있습니다.

성능 문제를 진단하는 데 도움이 될 추가적인 Elasticsearch에 관련된 로그가 elasticsearch.log에 전송됩니다.

성능 바

Elasticsearch 요청은 성능 바에 표시될 것입니다. 이는 개발 중에 지역적으로 사용하거나 배포된 GitLab 인스턴스에서 모두 사용할 수 있으며, 검색 성능이 나쁜 이유를 진단하는 데 유용합니다. 여기에는 구체적인 쿼리가 표시되는데, 이는 검색이 느린 이유를 진단하는 데 유용합니다.

상관 ID 및 X-Opaque-Id

우리의 상관 ID는 Rails에서 Elasticsearch로 모든 요청으로 전달되며, 이는 요청을 Elasticsearch에서 클러스터의 작업들과 연결하는 데 우리에게 도움을 줍니다.

문제 해결

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 파일에 추가하십시오:

# 디스크 할당자를 끕니다
cluster.routing.allocation.disk.threshold_enabled: false

또는

# 자체 제한을 설정하십시오
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가 자동으로 해제될 것입니다.

_출처: “Disk-based Shard Allocation Elasticsearch Reference” 5.66.x_

재해 복구/데이터 손실/백업

GitLab에서의 Elasticsearch 사용은 늘 보조 데이터 리포지터리로만 활용됩니다. 즉, Elasticsearch에 저장된 모든 데이터는 언제든지 다른 데이터 소스, 특히 PostgreSQL 및 Gitaly에서 얻을 수 있습니다. 따라서 Elasticsearch 데이터 리포지터리가 어떤 이유로 손상된 경우에도 모든 것을 처음부터 다시 인덱싱할 수 있습니다.

Elasticsearch 인덱스가 굉장히 크다면 처음부터 다시 인덱싱하는 데 너무 많은 시간이 소요될 수 있거나 다운타임을 초래할 수 있습니다. Elasticsearch 인덱스가 동기화되지 않았다고 판단되는 시간 범위 내에 발생한 모든 업데이트를 로그에서 확인하여 차이를 찾아내고 재동기화하는 자동 메커니즘이 내장되어 있지는 않지만, 유용한 도구 중 하나는 해당 기간에 발생한 모든 업데이트 로그를 살펴보는 것입니다. 이 정보는 매우 저수준이며 GitLab 코드베이스에 익숙한 운영자에게 유용합니다. 이 정보는 다른 사용자에게 유용할 수 있으니 여기에 문서화되어 있습니다. 이론적으로 재생할 필요가 있는 관련 로그는 다음과 같습니다:

  1. 어떠한 리포지터리 업데이트가 동기화된 모든 비-리포지터리 업데이트는 elasticsearch.log에서 track_items를 검색하여 찾을 수 있으며, 이러한 항목을 다시 ::Elastic::ProcessBookkeepingService.track!를 통해 전송함으로써 다시 재생할 수 있습니다.
  2. 발생한 모든 리포지터리 업데이트는 elasticsearch.log에서 indexing_commit_range를 검색하여 찾을 수 있으며, 이를 재생하려면 로그에서 가장 오래된 from_shaIndexStatus#last_commit/last_wiki_commit을 재설정한 다음, 프로젝트를 다시 인덱싱하기 위해 ElasticCommitIndexerWorker를 트리거해야 합니다.
  3. 발생한 모든 프로젝트 삭제는 sidekiq.log에서 ElasticDeleteProjectWorker를 검색하여 찾을 수 있으며, 이를 재생하기 위해 다른 ElasticDeleteProjectWorker를 트리거해야 합니다.

위의 방법과 정기적인 Elasticsearch 스냅숏을 찍는 것으로, 처음부터 모든 것을 인덱싱하는 것에 비해 다양한 종류의 데이터 손실 문제에서 상대적으로 짧은 시간 내에 복구할 수 있어야 합니다.