고급 검색 개발 지침

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

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

깊이 파고들기

2019년 6월, Mario de la Ossa는 미래에 해당 코드베이스에서 작업할 수 있는 사람들에게 도메인 특화 지식을 공유하기 위해 GitLab Elasticsearch 통합에 대한 깊이 파고 (GitLab 팀 멤버 전용: https://gitlab.com/gitlab-org/create-stage/-/issues/1)를 주최했습니다. YouTube에서 녹화본Google 슬라이드, PDF에서 슬라이드를 찾을 수 있습니다. 이 깊이 파고에서 다룬 모든 내용은 GitLab 12.0 시점에서 정확했지만, 특정 세부 사항이 변경되었을 수 있지만 여전히 좋은 소개 자료로 사용될 것입니다.

2020년 8월에는 GitLab 특정 아키텍처에 대한 다중 인덱스 지원을 중점으로 둔 두 번째 깊이 파고가 주최되었습니다. YouTube에서 녹화본슬라이드를 볼 수 있습니다. 이 깊이 파고에서 다룬 모든 내용은 GitLab 13.3 시점에서 정확했습니다.

지원되는 버전

버전 요구 사항을 참조하십시오.

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

개발 환경 설정

Elasticsearch GDK 설정 지침을 참조하십시오.

도움이 되는 Rake 작업

  • gitlab:elastic:test:index_size: 현재 인덱스가 사용하는 공간과 인덱스에 있는 문서 수를 보여줍니다.
  • gitlab:elastic:test:index_size_change: 인덱스 크기를 출력하고, 재색인한 다음 다시 인덱스 크기를 출력합니다. 인덱싱 크기를 개선하는 테스트 시 유용합니다.

또한, 테스트를 위해 대형 저장소나 복수의 포크가 필요한 경우 다음 지침을 따르십시오.

작동 방식

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

초기 색인이 완료된 후에 프로젝트를 제외한 모든 모델의 생성, 업데이트 및 삭제 작업은 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_tokenizerlowercaseasciifolding 필터를 사용합니다.

예시는 아래의 path_tokenizer 설명을 참조하세요.

sha_analyzer

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

나중에 예시를 위해 sha_tokenizer 설명을 참조하세요.

code_analyzer

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

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

토큰 분할 방법에 대한 설명은 code 필터를 참조하세요.

참고: Elasticsearch의 code_analyzer는 모든 코드 케이스를 고려하지 않습니다.

토크나이저

sha_tokenizer

이것은 SHAs를 입력의 하위 집합에 따라 검색 가능하게 하는 edgeNGram 토크나이저를 사용하는 사용자 정의 토크나이저입니다 (최소 5자).

예시:

240c29dc7e는 다음과 같이 변환됩니다:

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

path_tokenizer

입력된 경로의 어떤 부분이든 주어진 경우 경로를 찾을 수 있도록하기 위해 reverse: true와 함께 path_hierarchy 토크나이저를 사용하는 사용자 정의 토크나이저입니다.

예시:

'/some/path/application.js'는 다음과 같이 변환됩니다:

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

유의사항

  • 검색에는 독자적인 분석기가 있을 수 있습니다. 분석기를 편집할 때는 확인하는 것을 잊지 마세요.
  • Character 필터(토큰 필터와 대조적인)는 항상 원래의 문자를 대체합니다. 이러한 필터는 정확한 검색을 방해할 수 있습니다.

여러 인덱스로의 제로 다운타임 재색인

참고: 이것은 아직 적용되지 않았습니다. 여러 인덱스 기능이 완전히 구현되지 않았습니다.

현재 GitLab은 단일 버전의 설정만 처리할 수 있습니다. 어떠한 설정/스키마 변경이든 모든 것을 처음부터 다시 색인화해야 합니다. 재색인하는 데 시간이 오래 걸릴 수 있으므로, 이는 검색 기능 다운타임을 유발할 수 있습니다.

다운타임을 피하기 위해 GitLab은 동시에 작동할 수 있는 여러 인덱스를 지원하기 위해 노력하고 있습니다. 스키마가 변경될 때마다, 관리자는 새 인덱스를 생성하고 그에 재색인을 할 수 있으며, 검색은 기존의 안정적인 인덱스로 계속됩니다. 어떠한 데이터 업데이트도 두 인덱스로 전달됩니다. 새로운 인덱스가 준비되면, 관리자는 활성화하여 모든 검색을 새 인덱스로 이동시킬 수 있고, 이전 인덱스를 제거할 수 있습니다.

이것은 새 서버로의 마이그레이션에도 도움이 됩니다. 예를 들어, AWS로부터 이동 또는 AWS로의 이동일 때도 도움이 됩니다.

현재 이 새 디자인으로의 마이그레이션 작업 중에 있습니다. 현재 모든 것은 한 가지 버전으로 작동하도록 설정되어 있습니다.

아키텍처

elasticsearch-rails에서 제공하는 전통적인 설치 방식은 내부 프록시 클래스를 통해 통신하는 것입니다. 개발자는 모델별 로직을 모듈에 작성하여 해당 모델에 포함합니다. 예를 들어 SnippetsSearch를 위해 모델별 로직을 작성합니다. __elasticsearch__ 메서드는 다음과 같은 프록시 개체를 반환합니다.

  • Issue.__elasticsearch__Elasticsearch::Model::Proxy::ClassMethodsProxy의 인스턴스를 반환합니다.
  • Issue.first.__elasticsearch__Elasticsearch::Model::Proxy::InstanceMethodsProxy의 인스턴스를 반환합니다.

이러한 프록시 객체들은 직접적으로 Elasticsearch 서버와 통신합니다 (다이어그램의 상단 부분 참조).

Elasticsearch 아키텍처

계획된 새 디자인에서 각 모델은 해당 모델별 로직이 있는 동일한 클래스의 하위 클래스인 프록시 객체의 쌍을 가지게 됩니다. 예를 들어, 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 폴더에 있지만, 해당 클래스들은 실제 버전(ex, ee/lib/elastic/v12p3)에 별칭을 붙입니다. 이러한 클래스를 참조할 때는 절대 Latest 네임스페이스를 직접 사용하지 말고 실제 버전(ex, 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 폴더 내의 파일을 필요에 따라 수정합니다. ## 성능 모니터링

프로메테우스

GitLab은 모든 웹/API 요청 및 Sidekiq 작업에 대한 요청 수와 타이밍과 관련된 프로메테우스 메트릭을 내보냅니다. 이는 성능 트렌드를 진단하고 Elasticsearch의 시간이 전반적인 성능에 미치는 영향을 다른 작업을 하는 시간과 비교하는 데 도움이 될 수 있습니다.

인덱싱 대기열

또한 GitLab은 인덱싱 대기열에 대한 프로메테우스 메트릭을 내보내어 성능 병목 현상을 진단하고 GitLab 인스턴스나 Elasticsearch 서버가 업데이트의 양에 대응할 수 있는지를 결정하는 데 도움이 될 수 있습니다.

로그

모든 인덱싱 작업은 Sidekiq에서 진행되므로 Elasticsearch 통합과 관련된 중요한 로그는 sidekiq.log에서 찾을 수 있습니다. 특히, Elasticsearch에 요청을 수행하는 모든 Sidekiq 워커는 Elasticsearch에 대한 쿼리/쓰기를 수행한 횟수와 소요된 시간을 로깅합니다. 이는 클러스터가 색인화에 대해 거부하고 있는지 이해하는 데 유용할 수 있습니다.

Elasticsearch 검색은 요청을 처리하는 웹 워커를 통해 수행됩니다. 페이지를 로드하거나 API 요청을 수행하면 Elasticsearch에 요청을 수행하게 되며, 이후 해당 요청의 수와 소요 시간이 production_json.log에 기록됩니다. 이 로그에는 또한 데이터베이스 및 Gitaly 요청에 소요된 시간도 포함되어 있어 어떤 부분이 성능이 낮은지 진단하는 데 도움이 될 수 있습니다.

성능의 이유로 Elasticsearch에 특화된 추가적인 로그가 있으며, 이는 성능 이슈를 진단하는 데 도움이 될 수 있는 내용을 담고 있는 elasticsearch.log로 전송됩니다.

성능 막대

Elasticsearch 요청은 성능 막대에서 표시됩니다. 이는 개발 중에도 그리고 배포된 GitLab 인스턴스에서도 낮은 검색 성능을 진단하는 데 사용할 수 있습니다. 여기에는 사용되는 정확한 쿼리가 표시되어 검색이 왜 느린지 진단하는 데 유용합니다.

상관 ID와 X-Opaque-Id

저희의 상관 ID는 Rails에서 Elasticsearch로 모든 요청으로 전달되며, 이를 통해 클러스터에서 요청에 관련된 작업을 GitLab에서 추적할 수 있게 하는 X-Opaque-Id 헤더로 설정됩니다.

문제 해결

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 전용
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을 찾아 해당 업데이트를 다시 유발시켜야 합니다.

위의 방법을 사용하고 정기적으로 Elasticsearch 스냅샷을 취함으로써, 모든 것을 처음부터 색인하는 것에 비해 다양한 종류의 데이터 손실 문제에서 비교적 짧은 시간 내에 복구할 수 있어야 합니다.