본문 바로가기
DataOps/Elasticsearch

[Elasticsearch] 8. 엘라스틱서치의 내부 동작 상세

by BenKangKang 2024. 4. 3.

개요

  • 데이터 읽기와 쓰기 작업 요청 들어왔을 때 엘라스틱서치 내부가 어떤 단계를 거쳐 동작하는지 살펴본다.
    • 어떻게 요청의 동시성 제어를 하는지?
    • 샤드에 문제가 생겼을 때 어떻게 복구되는지

8.1 엘라스틱서치의 데이터 분산 처리 과정

8.1.1 쓰기 작업 시 엘라스틱서치 동작과 동시성 제어

쓰기 작업의 3단계

1. 조정 단계(coordination stage)

2. 주 샤드 단계(primary stage)

  1. 요청을 넘겨받은 이후 수행하는 작업들
  • in-sync 복제본
    • 마스터 노드가 관리하는 작업을 복제받을 샤드 목록
    • 주 샤드는 in-sync 복제본에 병렬적으로 요청을 넘긴다.
  • 모든 복제본들이 작업을 성공적으로 수행하고 주 샤드에 응답을 돌려주면 주 샤드가 작업 완료 응답을 보낸다.

3. 복제 단계(replica stage)

  1. 각 in-sync 복제본 샤드는 주 샤드에게 받은 요청을 로컬에서 수행하고 주 샤드에게 작업이 완료됐음을 보고하는 단계

→ 종료는 역순이다.

  • 최초 요청 받아 전달했던 노드에게 작업 완료 결과를 보내야 조정 단계가 종료된다.

메세지 순서의 역전

  • 분산 환경에서 여러 작업을 병렬적으로 보내면 메세지 순서의 역전이 있어날 수 있다.
  • 그 이유를 한번 살펴보자

낙관적 동시성 제어

  • _seq_no
    • 의미
      • 각 주 샤드마다 들고 있는 시퀀스 숫자이며, 매 작업마다 1씩 증가한다.
      • 문서를 색인할 때 이 값을 함께 저장한다.
      • 엘라스틱 서치는 문서 색일할 때 이 값을 함께 저장한다.
      • 엘라스틱서치는 이 값을 역전시키는 변경을 허용하지 않음으로 요청 순서의 역전 적용을 방지한다.
    • 쓰임새
      • 분산 처리에서 _id는 같은데 _seq_no 값이 더 작은 색인 요청을 받으면 작업을 수행하지 않는다.
  • _primary_term
    • 의미
      • 주 샤드가 새로 지정될 때 1씩 증가한다.
      • 주 샤드를 들고 있는 노드에 문제가 발생했을 때 복제본 샤드 중 하나를 주 샤드로 지정하는데, 이전 주 샤드에서 수행했던 작업과 새로 임명된 주 샤드에서 수행했던 작업을 구분하기 위해 필요한 값
    • 쓰임새
      • 어떤 주 샤드에서 수행된 작업인지 구분하기 위해 사용된다.
  • _version
    • 의미
      • 마찬가지로 동시성을 제어하기 위한 메타데이터
      • 이름 그대로 문서의 버전을 표현
    • 차이점
      • 클라이언트가 직접 지정할 수 있다.
        • version_type 을 external or external_gte 로 설정하면 클라이언트가 직접 지정해 색인을 요청할 수 있다.
        • version_type internal 은 버전이 자동으로 붙는 타입
    • 타입
    • 동작
      • 지정하지 않으면 ES 가 자동으로 값을 매긴다.
    • 주의
      • 더 높은 버전으로만 업데이트가 가능하다.
    • 쓰임새
      • 다른 스토리에서 저장된 데이터의 버전을 따로 관리하고 있고, 그 데이터를 엘라스틱 서치로 받아와 2차 스토리지로 동기화하여 사용한다든지 하는 경우 활용하기 좋음

     

8.1.2 읽기 작업 시 엘라스틱서치 동작

  • 쓰기 작업과 다르게 읽기 요청은 복제본 샤드로 넘어갈 수 있다.
    • 문서 단건 조회라면 하나의 샤드에 요청이 넘어가겠지만 검색 등 다수의 샤드에 요청을 넘겨줄 수 있다.
    • 요청을 넘겨받은 각 샤드는 로컬에서 읽기 작업 수행한 뒤 그 결과를 조정 노드로 돌려준다.
  • 샤드에 색인이 완료됐지만 특정 복제본에는 완료되지 않은 상태의 데이터를 읽을 수도 있다

8.1.2 체크포인트와 샤드 복구 과정

샤드 복구

  • 노드 재시작되면 샤드 복구 작업이 진행되는데, 이때 주 샤드의 내용과 일치하는지 파악할 필요가 있다.
  • p_t 과 s_n 조합해서 샤드와 샤드 사이에 어떤 반영 차이점이 있는지 알 수 있다.
  • 글로벌 체크포인트를 비교해서 필요한 작업만 재처리하여 복구한다
    • 세그먼트 파일을 통째로 전송하는 것보다 훨씬 효율적이다.

주 샤드의 글로벌 체크포인트와 샤드의 로컬 체크 포인트

  • 복제본 샤드가 색인을 완료하면 응답하고 로컬 체크포인트를 1 증가시킨다. 이때 주 샤드는 가장 낮은 로컬 체크포인트를 글로벌 체크포인트로 기록한다.



논리적 삭제와 복구 작업

  • 루씬 레벨에서 엘라스틱서치가 수행하는 쓰기 작업 2가지
    1. 새 문서의 색인
    2. 기본 문서의 삭제
  • 엘라스틱 서치는 논리적 삭제를 도입하여, 최근 삭제한 문서를 일정 기간 보존해 두고 작업 재처리에 활용한다.
    • 루씬에 색인된 문서가 재처리에 필요한 정보를 모두 들고 있어서 재처리 가능

샤드 이력 보존(shard history retention leases)

  • 재처리할 내용을 추적하는 메커니즘
  • 루씬의 세그먼트가 병합되는 도중에도 샤드 이력은 지정한 기간 동안 보존된다.
  • 관련 옵션
    • index.soft_deletes.retention_lease.period
      • default 12시간
    • 이 기간을 넘겨서 샤드 이력이 만료된 이후 수행되는 복구 작업에는 작업 재처리를 이용하지 않느다. → 세그먼트 파일을 통째로 복사하는 방법을 사용한다

8.2 엘라스틱서치의 검색 동작 상세

  • 읽기 동작 중에서도 검색 동작에 대해 상세히 알아본다.
    • 각 레벨에서 매칭되는 문서를 어떻게 판정하고 점수를 계산하는지
    • 어떻게 캐시가 동작하는지

8.2.1 엘라스틱서치 검색 동작 흐름

엘라스틱서치 검색 동작 흐름 개괄

  1. RestSearchAction 클래스의 prepareRequest 메서드에서 들어온 REST 요청을 확인한다. (주샤드)
  2. REST 요청을 엘라스틱서치 내부에서 사용할 요청으로 변경해 엘라스틱서치 내부 클라이언트에게 작업 수행을 의뢰한다. (주샤드)
  3. 이 요청은 이후 TransportSearchAction 인스턴스가 받아서 수행한다. (주샤드 → )
    1. 검색 요청과 현재 클러스터 상태를 분석해 상황에 맞는 적절한 검색 방법과 대상을 확정하고 본격적인 검색 작업을 뒤쪽 페이즈로 넘긴다. (검색 대상으로부터 실제 인덱스 목록을 확정, 어떤 샤드에 검색 요청을 보낼지를 정한다),
    2. 그 뒤 과정은 크게 추상화 2가지 페이즈로 나눈다
  4. query
    1. 쿼리에 매치되는 상위 문서를 검색하는 작업을 수행한다 (조정 노드가 검색 요청을 분선 전송하는 부분부터 요청에 대한 응답을 받을 때까지)
    2. 확정한 검색 대상 샤드가 있는 여러 노드로 검색 요청 분산해 전송
    3. 루씬 레벨의 검색 수행 (데이터 노드)
      1. 상위 문서의 docId를 수집하며 유사도 점수를 계산
      2. 각 샤드는 샤드 내에서 판단한 문서의 docId를 조정 노드에 반환
    4. 조정 노드가 각 샤드 검색 받아 fetch 수행할 문서 확정, 매치된 상위 문서의 내용을 읽어 변환하는 작업을 수행한다.
  5. fetch
    1. 해당문서를 들고 있는 샤드가 위치한 노드에 fetch 요청 분산해 전송
    2. 요청 받은 데이터 노드는 요청 받은 문서의 내용을 읽어 조정 노드에 반환
    3. 조정 노드는 그 응답을 모아 최종 응답을 생성
    4. query 페이즈가 끝난 다음 작업부터 여기까지의 작업이 fetch 페이즈의 작업에 속한다.

다른 API도 기본적으로 위 흐름과 비슷하다.

생성 작업

  1. RestCreateIndexAction 클래스가 REST API 상세 등록
  2. TransportCreateIndexAction 클래스가 실제 동작을 정의

엘라스틱서치 검색 동작 흐름 상세

  • 검색 동작 흐름을 상세하게

TranspotSearchAction

  • preference 있을 때 검색 대상
    • 어떤 노드와 샤드를 우선해서 검색할 것인지 지정하는 매개변수
    • 원하는 샤드가 있는지 체크하고 없다면 다른 노드의 샤드를 대상으로 지정 → 이름 그대로 선호 옵션
    • 종류
      • _only_local: 로컬에 들고 있는 샤드만을 대상으로 검색 수행
      • _local: 가능하면 로컬에 들고 있는 샤드를 우선으로 검색 수행, 불가능하며 다른 노드의 샤드를 대상으로 지정
      • _only_nodes:<node-id>,<node-id>
        • 지정한 노드 ID가 들고 있는 샤드를 대상으로만 검색을 수행한다.
        • 검색 대상이 될 샤드를 들고 있어야 한다.
      • _prefer_nodes:<node-id>,<node-id>
        • 지정한 노드 ID가 들고 있는 샤드를 우선해서 검색한다
      • _shards:<shard>,<shard>: 샤드 번호를 직접 지정해 해당 샤드 대상으로만 검색을 수행한다.
      • 그 외 문자열
        • 원하는 문자열 자유롭게 지정하 수 있다고한다 → 문자열 해시 값으로 노드 우선순위 지정 → 같은 노드가 요청을 받도록해서 캐시를 최대한 활용하고자 할 때 사용
        • 물론 클러스터 상태 변햇다면 달라진다.
  • preference 없을 때 검색 대상
    • 적절한 노드 선정
      • adaptive replica selection
        • 그간 통게 데이터, 검색 스레드 풀 상황 고려해 응답을 빨리 돌려줄 것으로 예상되는 노드를 선정하는 방법
        • 조정 노드에서 보낸 이전 요청에 대한 응답 속도나, 이전 검색 요청에 시간이 얼마나 소요됐는지 등
      • 관련 옵션
        • cluster.routing.use_adaptive_replica_selection 클러스터 설정을 false 로 지정해 끌 수 있다.
          • false 로 지정하면 랜덤으로 노드 선정

CanMatchPreFilterSearchPhase

  • 검색 대상의 샤드 수가 128개를 초과하거나, 검색 대상이 읽기 전용 인덱스를 포함하거나, 첫 번째 정렬 기준에 색인된 필드가 지정된 경우 수행된다.
  • 이 사전 작업의 비용은 낮은 편이다.
  • 의미
    • 확실히 검색 대상이 될 필요가 없는 샤드를 사전 제거한다
  • 최적화 과정
    • 인덱스의 메타데이터를 이용해 타임스탬프 필드 범위상 매치되는 문서가 없는지 체크
    • 갹 샤드의 최솟값과 최댓값을 가지고 샤드를 정렬해 상위에 올라올 문서를 보유한 샤드가 먼저 수행되도록 최적화하는 등 다양한 방법이 사용된다.

AbstractSearchAsyncAction

  • search_type의 기본값은 query_then_fetch
    • query_then_fetch
      • 각 샤드에서 검색 쿼리 수행하고 매치된 상위 문서 수집할 때도 유사도 점수 계산을 끝내는 가장 일반적인 형태의 검색
    • dfs_query_then_fetch (Distributed frequency search)
      • 모든 샤드로부터 사전에 추가 정보를 모아 정확한 유사도 점수를 계산
      • 이 경우 정확도는 올라가지만 성능이 떨어진다.
      • AbstractSearchAsyncAction 확장하여 DFS 전용 클래스 사용

SearchPhase

  • 검색 동작은 다음 페이즈로 전환되는 부분을 중점으로 보면 흐름을 읽기가 편한다.

SearchDfsQueryThenFetchAsyncAction

DfsQueryPhase

  • 각 샤드에서 보낸 PfsPhase 작업 결과로부터 검색 요청 만들어 다시 각 노드로 분산 전송
  • SearchService executeQueryPhase 메서드 거쳐 QueryPhase 에서 본격적인 쿼리 매치 작업 수행
  • FetchSearchPhase 넘어간다.

SearchQueryThenFetchAsyncAction

QueryPhase

IndexSearcher

  • 루씬 인덱스내의 문서를 검색할 때 사용하는 클래스
  • search(query, collector):
    • 엘라스틱서치는 QueryPhase에서 IndexSearcher의 search(query, collector) 메서드를 호출해 검색을 수행한다.

QueryBuilder

  • 엘라스틱서치 레벨의 쿼리를 정의하는 인터페이스
  • 쿼리 이름은 무엇인지, 이 쿼리의 DSL 은 어떻게 파싱하고 어떻게 직렬화할지 등을 정의한다.
  • toQuery: 엘라스틱서치 쿼리로 루씬의 Query를 생성한다.

Query

  • 루씬 쿼리를 정의한느 추상 클래스
  • createWeight
    • Weight 생성
  • rewrite

Weight

  • Query 내부 동작을 구현하는 추상 클래스
  • score
    • Scorer를 생성한다
  • bulkScorer
    • BulkScorer 생성
    • IndexSearch의 search 메서드 내부에서 호출된다.
  • explain
    • 쿼리 수행의 중간 진행 과정과 유사도 점수 계산 과정을 자세히 설명한다.

Scorer

  • iterator
    • 매치된 문서의 순회를 담당하는 DocIdSetIterator 를 반환한다.
  • twoPhaseIterator
    • 무거운 매치 작업을 두 개 페이지로 나누어 진행하도록 하는 TwoPhaseIterator 반환
  • score:
    • 현재 문서의 유사도를 점수로 계산하여 반환
    • float,
  • docId
    • 현재 문서의 doc ID 반환
    • docId 2개
      • 로컬 docId
        • 세그먼트마다 기준 docId 오프셋이 있음
      • 글로벌 docId
        • 로컬 doc ID 합산

BulkScorer

  • 여러 문서를 대상으로 한 번에 유사도 점수를 계산하는 추상 클래스

DocIdSetIterator (DISI)

  • 매치된 문서의 순회를 담당하는 추상 클래스
  • cost
    • 순회에 필요한 비용의 추정값 반환
    • 적당한 근사값 혹은 하드코딩된 상수를 반환
  • docID:
    • 현재 문서의 doc ID를 반환한다
  • advance(target)
    • doc ID target 값 이상인 첫 번째 매치되는 문서로 DocIdSetIterator 순회를 전진시킨다.
  • nextDoc
    • 매치된 다음 문서로 DocIdSetIterator 전진시킨다.

Collector, LeafCollector

  • 검색 결과를 수집하는 동작을 정의하는 인터페이스
    • 유사도 점수나 정렬 기준 등을 계산하거나 확인하며 상위 결과를 수집하는 동작 등을 수행한다.

conjunction(결합, 연결) 검색과 DocIdSetIterator 순회

  • conjunction 검색
    • AND 성격의 검색으로 주어진 쿼리의 매치 조건을 모두 만족해야 최종 매치된 것으로 판단한다.
  • disjunction 검색
    • OR 성격 검색으로주어진 쿼리 중 하나만 만족해도 매치된 것으로 판단한다.

8.2.3 캐시 동작

  • 검색 성능이 아쉽다면 캐시를 잘 활용하고 있는지 검토해야한다.

샤드 레벨 요청 캐시

  • The request cache is a cache that stores a task performed in the query phase at a shard level when performing a search.

수행 위치

  • QueryPhase 작업에 들어갈 때 동작한다.
  • QueryPhase execute 가 수행되기 전 SearchService 의 executeSearch 메서드에서 캐시 작업이 수행된다.
  • ShardSearchRequest와 대상 인덱스 설정을 보고 캐시 가능한 요청인지 여부를 파악한다

조건

  • search_type이 query_then_fetch
  • scroll 검색이 아니어야 한다
  • profile 요청이 아니어야 한다.
  • now 들어간 시간, 표현이나 random 같이확정되지 않은 형태의 조건이 들어간 검색도 캐싱 대상에서 빠진다.
  • 검색 API 호출할 때 requestCache 매개변수를 명시적으로 true 혹은 false 확인
  • 매개변수 명시적으로 지정하지 않는다면 index.requests.cache.enable 인덱스 설정 확인
    • default 는 true

  • 요청이 캐시 대상이라는 사실을 확인하면 캐시 키를 만든다.
  • 2단계
    1. ShardSearchRequest 주요 내용을 바이트로 직렬화
      1. 인덱스, 샤드 번호와 검색 요청 본문 내용 포함
      2. 즉 같은 검색 요청이어야 캐시 적중
    2. SearchPlugin 인터페이스의 getRequestCacheKeyIdfferentiator 메서드를 오버라이드한 플러그인에서 지정한 캐시 키 변경 로직을 적용

대상 값

QueryPhase 통해 얻는 거의 모든 데이터

  • 제안 결과
  • 집계 결과
  • 매치된 문서 수
  • 최대 점수
  • 매치된 상위 문서

캐시 활용 방향

  • QueryPhase 만 캐싱하는 것, FetchPhase(실제 결과) 캐시가 아니어서 상위 문서가 무엇인지는 캐시하지만 실제 데이터는 다시 패칭해서 가져온다.

적재 위치

  • 엘라스틱서치 노드가 기동하는 과정에서 Node 클래스의 생성자가 호출되며, 여기서 IndicesService 인스턴스가 생성된다.
  • 즉 샤드 레벨 요청 캐시 보유하고 있는 곳도 실질적으로 노드와 생명주기가 같은 IndicesService

상태 확인

  • *GET _stats/request*cache?human
  • 전체 인덱스의 캐시 적중, 부적중, 퇴거(eviction) 회수, 캐시 크기 등의 주요 정보를 확인할 수 있다.

크기 지정과 무효화

  • 기본 설정으로는 총 힙의 1%까지 샤드 레벨 요청 캐시로 사용한다
  • config/elasticsearch.yml 에 indices.request.cache.size 에 %를 지정하고 노드 재기동하면 이 값을 변경할 수 있다. (예 2%)
  • 무효화
    • 샤드 레벨 요청 캐시는 인덱스 refersh 수행할 때 무효화된다.
    • 수동으로 무효화 → POST [인덱스이름]/_cache/clear?request=true

노드 레벨 쿼리 캐시

  • 노드 레벨 쿼리 캐시는 필터 문맥으로 검색 수행 시, 쿼리에 어떤 문서가 매치됐는지를 노드 레벨에 저장하는 캐시다

위치

  • QueryPhase 검색 수행 시 IndexSearcher의 search 메서드 호출 여기서 createWeight 호출해 weight 생성

대상 값

  • DocIdSet 추상클래스, 즉 쿼리에 매치된 문서 목록을 캐시에 적재한다.
  • 주로 비트 배열로 구현한다. → 4번 문서가 매치된 경우 배열의 4번 비트를 1로 올린다.
  • 문서 수가 검색 대상 문서 수의 1%를 초과하면 밀도 높은 배열로 취급하며, 그때는 DocIdSet 구현체로 FixedbitSet 을 사용한다.

캐시 키

  • Query, 즉 Query의 equals와 hashCode 구현상 같은 Query로 취급할 수 있는 쿼리가 들어와야 캐시를 적중시킬 수 있다.
  • 캐시 적중률을 높이려면 이 점을 염두에 두어야 한다.

조건

  • 유사도 점수를 계산하지 않는 쿼리여야 한다.
  • 1만 개 이상의 문서가 있으며, 동시에 샤드 내 문서의 3이상을 보유하고 있는 세그먼트가 대상일 때만 캐시한다.

락 획득

  • 노드 레벨 캐시 읽기, 쓰기 작업 모두 락 획득을 필요로 한다.
  • 다만 읽기 작업은 최초 락 획득 실패하면 바로 일반 검색 작업 진행
    • 일반 작업은 락 획득을 무작정 기다릴 수 없다.

위치

상태 확인

  • *GET _*stats/query_cache?human
  • 인덱스별 혹은 전체 인덱스의 캐시 적중, 부적중, 퇴가 회수, 캐시 크기 등의 주요 정보 확인 가능

캐시 크기 지정과 캐시 무효화

  • 기본으로는 총 힙의 10% 까지 노드 레벨 쿼리 캐시 사용
  • 설정 파일의 indices.query.cache.size: 5%
  • 무효화
    • POST [인덱스 이름]/_cache/clear?query=true

샤드 레벨 요청 캐시와 노드 레벨 쿼리 캐시 비교

운영체제 레벨 페이지 캐시

  • 운영체제 레벨에서 기본적으로 동작하는 페이지 캐시가 있다.
  • 디스크에서 데이터 읽은 후 데이터를 메모리에 넣어 두었다가 이 데이터 읽을 때 메모리에서 읽어 반환
  • ES는 페이지 캐시를 잘 활용함으로 시스템 메몰 절반 이상은 캐시로 사용하도록 설정하는 것이 좋음

 

 

댓글