본문 바로가기

SearchDeveloper/엘라스틱서치 바이블

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

알아두면 검색 성능은 높여야 하는데 당장 확장할 리소스가 없을 때 써먹을 수 있다.

데이터 분산 처리 과정

쓰기 요청이 들어왔을 때 동작과 동시성 제어

쓰기 전체 흐름

조정 단계(coordination stage)프라이머리 샤드 단계(primary stage)복제 단계(replica stage)

쓰기 요청이 들어오면 라우팅을 통해 인덱스의 몇 번 샤드로 작업을 보낼 지 정하고 (조정 단계),

노드가 프라이머리 샤드한테 작업을 넘겨주고 (프라이머리 샤드 단계),

  • 프라이머리 샤드는 요청이 문제가 없는 지 검증하고
  • 로컬에 요청한 쓰기 작업 수행하고
  • 작업 완료되면 레플리카 (in-sync 복제본) 샤드에 병렬로 요청을 넘긴다
    • 복제받을 레플리카 샤드 목록은 마스터 노드에서 관리하고 있고 in-sync 복제본이라 한다.

각 in-sync 복제본 샤드가 프라이머리 샤드가 요청한 작업 로컬에 수행 후 프라이머리에게 완료 보고 한다. (복제 단계)

복제 단계가 모두 끝나 완료 응답을 주 샤드에게 보내야 주 샤드 단계가 완료되고,

주 샤드가 요청 전달한 노드에게 완료 응답을 보내야 조정1단계가 종료 된다. (동기 방식)

복제본 샤드에 쓸 때 값을 동시에 쓰려고 하면?

문제

1번 클라가 필드값:1 쓰기 요청을 보내고 동시에 2번 클라에서 필드값:2 쓰기 요청을 보냈다면 분산 시스템 특성 상 레플리카 샤드로 쓰기 요청이 들어올 때 두 요청의 순서를 보장할 수 없고 그럼 레플리카에만 필드값:1 로 역전될 수도 있다.

해결

_seq_no__primary_term_ 로 낙관적 동시성 제어 구현 

낙관적 동시성 제어
사용자들이 동시에 데이터를 수정하지 않을 것이라고 가정한다. 따라서 데이터를 읽을때는 Lock을 설정하지 않는다.
그러므로 데이터를 수정하고자 하는 시점에 앞서 반드시 읽은데이터가 다른 사용자에 의해 변경 되었는지를 검사해야 한다.

각 작업에 1씩 증가하는 _seq_no_ 필드를 추가하고 복제본 샤드에서 들고 있는 _seq_no_ 보다 작은 값의 요청이 들어오면 수행하지 않음

  • _seq_no_ 는 각 샤드마다 들고 있다.

또한 이전 프라이머리 샤드에서 수행했던 작업과 새로 임명된 프라이머리 샤드에서 수행했던 작업을 구분하기 위해 _primary_term_ 값을 추가했다. 프라이머리 샤드가 새로 지정될 때 1씩 증가한다.

문서 색인하고 응답 결과로 _seq_no_, _primary_term_확인 가능

샤드마다 _seq_no_ 는 샤드마다 부여되므로 문서가 다른 샤드로 들어갔다면 값이 달라진다.

PUT [index명]/_doc/1?If_primary_term=1&if_seq_no=1

지정한 값이 문서 값과 같을 때만 작업 수행

버전

-_version 값은 1부터 시작해서 문서 수정이나 삭제될 때마다 1씩 증가한다. (version_type:internal)

  • 응답 바디에서 _version 값 확인 가능

-클라가 버전을 직접 지정할 수 있다. (version_type:external, external_gte)

  • 현재 버전보다 낮게 지정하면 에러 난다.

-사용 시나리오: 다른 스토리지에서 데이터 버전을 따로 관리하고 그 값을 ES 에서 받아와 2차 스토리지로 동기화해서 사용할 때 유용하다.

읽기 작업 시 동작

-조정 노드가 클라 요청 받으면 라우팅을 통해 적절한 샤드를 찾아 요청을 넘긴다.

  • 읽기 요청은 레플리카로도 요청이 넘어갈 수 있다.
  • 단건 조회면 한 샤드로만 요청이 넘어가겠지만 검색 등 다수의 샤드에 요청을 넘겨줄 수도 있다.
  • 복제본으로 요청이 넘어가면 아직 반영 완료되지 않은 상태의 데이터도 읽을 수 있다.

-요청 받은 각 샤드는 로컬에서 읽기 작업 한 뒤 결과를 조정 노드에게 돌려준다.

-조정 노드는 결과를 모아 클라에게 응답한다.

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

노드 재기동 시 노드가 들고 있는 레플리카를 프라이머리와 동기화하는 법

노드 재기동 시 노드가 들고 있는 프라이머리를 다운타임 중에 선출된 프라이머리와 동기화하는 법

-각 샤드는 로컬에서 작업 수행하고 몇 번 작업까지 수행했는데 로컬 체크 포인트로 기록한다.

  • Ex. _seq_no_ 1,2,3,5,7 까지 작업 완료한 샤드는 로컬 체크포인트를 3을 업데이트. (3번 작업까지 빠짐없이 연속으로 완료했으므로)

-레플리카는 자기 로컬 체크포인트가 업데이트되면 프라이머리에게 보고한다.

-프라이머리는 각 레플리카로부터 받은 로컬 체크포인트를 비교해 가장 낮은 값을 글로벌 체크포인트로 기록한다.

  • 글로벌 체크포인트까지는 모든 샤드에 그 작업까지 반영이 완료됐음을 의미

-글로벌 체크포인트가 업데이트되면 프라이머리는 레플리카한테 다음 색인 요청시 값을 같이 보내준다.

문제 발생 시

-샤드 간에 글로벌 체크포인트를 비교하고 같다면 복구 작업 없음, 다르다면 필요한 작업만 재처리해서 복구한다. 세그먼트 파일을 통째로 전송하는 것보다 효율적이다.

-문서의 삭제 여부를 알 수 있게 하기 위해 ES 에서 논리적 삭제 도입

논리적 삭제

-루씬 레벨에서 ES가 수행하는 작업은 새 문서 색인, 기존 문서 삭제 두 가지 뿐이다. 업데이트는 문서 삭제 후 새로 생성이기 때문이다. 체크포인트 비교해서 복구하려할 때 새 문서 생성은 정보 들고 있기 때문에 괜찮은데 삭제된 경우는 이미 문서가 삭제돼서 정보가 남아있지 않아 복구할 수 없으므로 최근 삭제한 문서를 일정기간 보존해두고 재처리에 이용하는 논리적 삭제를 도입했다. (샤드 이력 보존, shard history retention leases)

-index.soft_deletes.retention_lease_period 에서 값 설정 (기본값 12시간)

-이 기간 넘겨서 샤드 이력이 만료되면 이후 복구 작업에서는 재처리가 아닌 세그먼트 파일을 통째로 복사한다.

(문서 삭제 후 12시간 후에 샤드 복구 되면 더 느려질 수 있겠군)

ES 검색 동작 흐름

TransportSearchAction

적절한 검색 방법과 대상(인덱스, 샤드)을 확정한다. 클러스터 간 검색이 필요한지도 체크한다.

<검색 대상 정하기>

  • 인덱스명이 alias 나 와일드카드로 들어오면 정확한 인덱스 목록을 확정하고
  • 어떤 샤드로 요청을 보낼 지를 확정한다.
  • 정해진 샤드로 보냈을 때 실패할 수도 있으니 다음으로 보낼 샤드 순서 목록도 작성한다. (by ShardIterator 인터페이스)
    • GET index/_search?preference=_local 로 요청하면 맨 먼저 preference 값을 읽어 로컬 샤드를 최우선으로 작업 수행한다.
      • _only_local, _local, _only_nodes, _prefer_nodes, _shards
    • preference 가 없으면 adaptive replica selection 에 의해 응답속도, 소요 시간, 스레드 풀 상황등을 고려해 응답 빨리 줄 것같은 샤드를 고른다.
      • cluster.routuing.use_adaptive_replica_selection: false 로 지정해 끌 수도 있음
    • pit(point in time) 이 지정됐으면 처음부터 pit 값으로 검색 문맥 가져와 검색 대상 인덱스와 샤드를 가져온다.

<검색 방법 정하기> - CanMatchPreFilterSearchPhase 부터

CanMatchPreFilterSearchPhase

특정 조건들을 만족하면 본격적인 검색 들어가기 전에 CanMatchPreFilterSearchPhase 를 거쳐 단 하나라도 매치될 가능성이 있는지 점검 및 최적화 작업을 수행한다.

  • 검색 속도를 빠르게 하기 위한 최적화 작업이다.
  • 조정노드에서 하는 점검이다
  • 검색될 문서가 없다면 그 샤드는 검색 대상에서 제거한다.
  • 점검 마치면 각 샤드별 점검 요청을 노드 단위로 묶어 transport 채널로 분산 전송하고, 요청 수신한 노드는 SearchService.canMatch() 실행해 또 점검한다.

*특정 조건
*

  • search_type 이 query_then_fetch 여야 함 (각 샤드레벨에서 유사도 점수 계산 끝내기)
  • 검색 대상 샤드 수가 128개를 초과하지 않는다.
  • 읽기 전용 인덱스를 포함한다.
  • 첫 번째 정렬이 색인된 필드이다

단 하나라도 매치될 가능성이 없는지 점검 및 최적화하는 법

  • 인덱스 메타데이터로 필드 범위상 매칭되는 문서가 확실히 없는지 체크
  • 첫 번째 정렬 기준으로 각 샤드가 갖고 있는 최솟값 최댓값 가지고 상위 문서 보유한 샤드가 먼저 수행되도록 함

AbstractSearchAsyncAction

-검색 대상 샤드 별로 검색 요청 만들어 분산 전송한뒤 응답 수집하고 다음 페이즈로 이동

-search_typequery_then_fetch (샤드 레벨 유사도 점수계산) → SearchQueryThenFetchAsyncAction 클래스에서 제어

-search_typedfs_query_then_fetch (샤드 레벨 점수 취합회 정확한 유사도 점수 계산) → SearchDfsQueryThenFetchAsyncAction 클래스에서 제어

  • DFS: Distributed Frequency Search

-SearchQueryThenFetchAsyncAction, SearchDfsQueryThenFetchAsyncActionAbstractSearchAsyncAction 추상클래스 확장하고 있다.

SearchPhase - CanMatchPreFilterSearchPhase, SearchQueryThenFetchAsyncAction, SearchDfsQueryThenFetchAsyncAction, DfsQueryPhase, FetchSearchPhase, ExpandSearchPhase

검색 동작을 페이즈 단위로 구분하기 위한 SearchPhase 추상클래스를 사용한다. SearchPhase 작업이 끝나면 다음 SearchPhase 로 넘어가는 흐름이다.

조정노드에서 수행하는 페이즈이다.

SearchDfsQueryThenFetchAsyncAction

-search_typedfs_query_then_fetch 일때 수행

-점수 계산에 필요한 정보를 각 샤드에서 가져오기 위해 점수 계산 요청을 만들어 각 샤드들에게 분산 전송한다.

  • 요청을 수신한 노드는 SearchService.executeDfsPhase() 를 호출해 ReaderContext 를 가져오거나 생성한다.
    • pit 요청이라면 기존에 만들어진 ReaderContext 를 사용하고, 아니면 새로 생성해 노드에 일정 시간 저장한다.
    • ReaderContext 에는 그 시점의 샤드 대상으로 검색하는 루씬 IndexSearcher가 담겨있어 이를 재활용하면 pit 기능인 항상 같은 상태의 샤드를 대상으로 검색 가능하다.

-ReaderContext 생성 이후 샤드 단에서 DfsPhase 를 수행해 쿼리에 매치되는 텀, DF(document frequency), 여러 통계 데이터를 구해 반환한다. → 점수 계산에 필요한 정보

-DfsPhase 응답 받으면 executeNextPhase() 를 호출해 다음 페이즈인 DfsQueryPhase 로 넘어간다.

DfsQueryPhase

-각 샤드가 보내준 DfsPhase 작업결과로 샤드 별 본 검색 요청을 만들어 각 노드로 분산 전송한다.

-이 요청을 받은 노드는 SearchService.executeQueryPhase() 를 호출해 QueryPhase 에서 본격적인 쿼리 매치 작업을 수행한다.

-각 샤드에서 QueryPhase 결과 보내주면 다음 페이즈인 FetchSearchPhase 로 넘어간다.

SearchQueryThenFetchAsyncAction

-search_typequery_then_fetch 일때 수행

-사전 작업(점수 계산에 필요한 정보 얻는 것 같은) 없이 바로 샤드 별 검색 요청 만들어 전송한다.

-이 요청을 받은 노드는 SearchService.executeQueryPhase() 를 호출해 QueryPhase 에서 본격적인 쿼리 매치 작업을 수행한다.

-각 샤드에서 QueryPhase 결과 보내주면 다음 페이즈인 FetchSearchPhase 로 넘어간다.

QueryPhase

-DfsQueryPhaseSearchQueryThenFetchAsyncAction 에서 SearchService.executeQueryPhase() 를 호출하면 넘어오는 페이즈다

-ReaderContext 를 가져오거나 (pit 지정됐을 때, DfsPhase 거쳐왔을 때) 새로 생성한다.

-샤드 요청이 캐시(샤드 레벨에 저장되는 캐시) 가능한 요청인지 확인하고 있으면 응답 바로 반환한다.

-없으면 QueryPhase 작업 수행 후 캐시에 결과를 저장한다. 캐시 불가능한 요청이면 쿼리 매치 작업만 한다.

-execute() 에서 주 작업 (검색, 제안, 집계) 를 수행한다.

  • 검색은 루씬의 IndexSearcher, Query, Collector 이용한다.
  • 제안은 오타교정이나 자동완성에 사용한다. SuggestPhase 에서 수행한다.
  • 집계AggregationPhase 에서 수행한다.

-각 샤드가 수행한 QueryPhase 결과가 조정 노드에 모이면 FetchSearchPhase 로 넘어간다.

FetchSearchPhase 와 FetchPhase

- QueryPhase 결과를 병합해 샤드 별 fetch 요청을 생성하고 분산 전송한다.

-이 요청을 받은 노드는 SearchService.executeFetchPhase() 를 호출한다.

  • ReaderContext 를 확보한 후 FetchPhase.execute() 를 호출해 지정한 번호의 문서 내용을 읽는다.
  • 이후 ReaderContext 가 pit 이나 scroll 것이 아니라 단발성 쿼리 위한 문맥이면 ReaderContext 를 해제 한다.
  • 문서 내용 읽은 fetch 결과를 조정 노드로 반환한다.

-각 샤드별 FetchPhase 결과가 조정 노드에 모이면 ExpandSearchPhase 로 넘어간다.

FetchSubPhase

-FetchPhase 작업 중 문서 내용을 읽어 SearchHit 을 만드는 과정 중의 여러 하위 작업을 수현하는 인터페이스이다.

-FetchSubPhase 구현체는 SearchModule 에 등록되며 커스텀 플러그인으로 등록할 수도 있다.

  • 기본 구현체: FetchSourcePhase (_source 읽기), FetchScorePhase (_score 다시 계산하기), ExplainPhase (_explanation 위한 검색 중간 과정과 유사도 점수 상세 설명 만들기)

ExpandSearchPhase

-collapse 를 위한 페이즈이다.

  • 지정한 필드 값 기준으로 검색 결과를 그룹으로 묶고 그 안에서 다른 기준으로 상위 문서를 지정한 개수만큼 뽑는다.

-본 검색의 hit 수 만큼 새 검색 요청을 만들어 MultiSearchRequest 에 담는다.

-MultiSearchRequest는 로컬, 즉 조정노드 자신이 다시 받아 TransportMultiSearchAction에서 처리한다.

  • 이 다중 검색의 세부 요청들은 다시 TransportSearchAction 이 받아서 독립적인 새 검색 수행하는 것처럼 수행해 조정 노드에 결과를 돌려준다.

-조정 노드가 응답 받으면 결과 모아서 최종 검색 결과 만들어 반환하고 검색 작업을 종료한다.

  • ExpandSearchPhase 의 응답인 InternalSearchResponse 를 최종 응답인 SearchResponse 로 변환하는 작업은 다시 AbstractSearchAsyncAction 으로 돌아와 수행한다.

루씬 쿼리 매칭과 스코어링 과정

QueryPhase 에서 쿼리에 매치되는 상위 문서 수집 작업은 각 샤드 레벨, 즉 루씬 레벨에서 수행한다. 루씬에서 어떻게 검색 작업 하는지 알아보겠다.

[1] IndexSearcher

-루씬 인덱스를 읽기 전용으로 열어 문서를 검색하는 클래스

-최상위 IndexReader 가 하나 있고 각 독립적인 여러 LeafIndexReader 로 구성된다.

  • 각 LeafIndexReader 는 세그먼트 하나의 역색인, 필드, doc_values 등 읽는 추상 클래스다.

[2] QueryBuilder

-ES 레벨의 쿼리 정의하는 인터페이스

  • 쿼리명, 쿼리 DSL 을 파싱하고 직렬화하는 방법 정의

-toQuery(): 루씬 쿼리를 생성함

  • SearchService 에서 검색 문맥 만들 때 호출된다. (ReaderContext 생성 직후, executeSearchPhase(), executeFetchPhase(), executeDfsPhase() 에서)

-AbstractQueryBuilder 추상클래스를 확장해 커스텀 할 수 있다.

[2-1] Query

-루씬 쿼리 정의하는 추상 클래스

-createWeight(): Weight 을 생성한다.

-rewrite(): Query 를 다른 기본(primitive) Query 의 조합으로 재구성해 최적화한다.

  • primitive query: createWeight() 를 오버라이드해 직접 구현 하는 쿼리 (ex. TermQuery, BooleanQuery, MatchNoDocsQuery, PhraseQuery 등)
  • IndexSearch 에서 검색 수행하기 전 호출된다.
  • QueryPhase, DfsPhase 에서 명시적으로 호출되기도 한다.

[2-2] Weight

-Query 내부 동작을 구현하는 추상클래스

-score(): Scorer 를 생성

-bulkScorer(): BulkScorer 생성

-explain(): 쿼리 수행 중간 과정과 유사도 점수 계산 과정을 설명한다. explain:true 로 검색 시 FetchSubPhaseExplainPhase 에서 explain 메소드를 호출한다.

[3-1] Scorer

-유사도 점수 계산 담당하는 추상 클래스

-iterater(): 매치된 문서 순회하는 DocIdSetIterator 반환. 즉 DocIdSetIterator 에서 순회하는 모든 문서는 쿼리에 매치됐다는 뜻이다. Weight-Scorer-DocIdSetIterator 의 순차 과정이 쿼리 매칭되는 문서 고르는 작업이다.

-twoPhaseIterator(): 무거운 매치 작업이면 두 페이즈로 나눠 진행하게 TwoPhaseIterator 를 반환한다. 만약 나눌 필요 없으면 null 을 반환한다.

-score(): 현재 문서(DocIdSetIterator 가 가리키는 문서) 의 유사도 점수 계산해 float 타입으로 반환한다.

-docID(): 현재 문서의 doc id 반환한다. 고유한 32 비트 숫자

[3-2] BulkScorer

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

-디폴트 구현체는 DefaultBulkScorer 이다

  • CollectorDocIdSetIterator, TwoPhaseIterator 이용해 문서 하나씩 순회하며 Scorer 로 점수 계산해서 수집한다.

[4-1] DocIdSetIterator

-’매치된' 문서의 순회 담당하는 추상 클래스

-cost(): 순회하는데 드는 비용 추정값 (보통 매치된 문서 수)

-advance(target): target 값 이상으로 첫 번째 매치되는 문서. DocIdSetIterator 순회를 전진시킨다.

-nextDoc(): 매치된 다음 문서로 DocIdSetIterator 순회를 전진시킨다. 보통 advance(docID() + 1)

  • 루씬은 필드 역색인 읽어 hello 와 매치되는 문서id 0,3,5,6,10 을 불러온다.
  • target 초기값은 -1 이며 advance(0) 부터 시작해 0번 문서가 매치되면 유사도 점수 계산하고 수집한다.
  • +1 씩 하며 더 이상 매치된 문서 없으면 NO_MORE_DOCS 상수 반환하고 종료한다.
  • 다른 쿼리 조합하거나 최적화 가능한 상황이면 바로 advance(7) 을 호출해 7번 미만 문서는 수집할 필요 없다는 걸 알리고, 바로 10번 문서로 doc id가 이동한다.

[4-2] TwoPhaseIterator

-무거운 매치 여부 판단하는 작업을 두 페이즈로 나누어 진행하는 추상 클래스

-비용 저렴한 매치 먼저 수행해서 후보를 좁히고 그 후에 문서 수집 과정에서 최종 매치를 한다.

-approximation(): 간략하게 문서 매치 만족하는 문서 순회하는 DocIdSetIterator 를 반환한다.

-matches(): 무거운 작업이다. DocIdSetIterator 의 문서 돌면서 match() 가 수행된다.

-matchCost(): matches() 에 드는 비용 추정해서 반환한다.

[5] Collector, LeafCollector

-유사도 점수나 정렬 기준 등 계산하거나 확인하며 검색 결과 수집하는 인터페이스

-getLeafCollector(): LeacCollector 인스턴스 만들어 반환한다.

bool 쿼리 동작 순서와 DocIdSetIterator 순회

bool쿼리 안의 must, filter, must_not, should 하위의 다양한 쿼리들이 어떤 순서로 수행되는지 정해진 규칙이 없다. 이에 대해 좀 더 알아보겠다.

rewrite, cost

-ES 쿼리를 루씬의 여러 쿼리로 쪼갠고 조합해 재작성할 때 Query.rewrite() 메서드를 사용한다.

-쿼리를 쪼갠 후 각 쿼리 수행 시 드는 비용을 내부적으로 추정해 유리할 것으로 판단되는 쿼리를 먼저 수행한다. 비용은 DocIdSetIterator.cost() 로 확인한다.

-여러 하부 쿼리를 병렬로 수행하기도 한다.

-Bool 쿼리의 Weight 구현체: BooleanWeight

  • 쿼리 세부 내용에 따라 다양한 종류의 최적화된 Scorer 를 만들어 반환한다.
  • conjunction 검색과 disjunction 검색을 위한 Scorer 가 만드는 DocIdSetIterator 를 살펴보자
    • conjunction 검색: AND 검색. 쿼리 매치 조건 모두 만족해야 한다.
    • disjunction 검색: OR 검색. 하나만 만족해도 된다.
    • 둘 다 하위 Query 마다 Weight, Scorer 를 미리 만들어둔 다음 하위 Scorer 로 최종 Scorer 를 만든다.

conjunction 검색

-AND 검색

hello / rocket / punch 모두 만족하는 문서 찾아야 함

  • 먼저 앞에 두개 동시에 만족하는 문서로 이동하고
  • 다음 세번째 문서도 체크해서 세 개 동시에 만족하는 문서를 찾는다.

TwoPhaseIterator 방식이라면 최상위 TwoPhaseIterator 가 하위 TwoPhaseIteratormatchCost 순으로 정력해둔다음 matches() 가 호출되면 matchCost 가 낮은 하위 TwoPhaseIterator.matches() 부터 호출한다.

 

disconjunction 검색

-OR 검색

-하위 Scorer 와 DocIdSetIterator를 이용해 최상위 ScorerDocIdSetIterator 를 생성한다.

-or 이므로 advance(target) 결과 중 최소값을 반환한다.

쿼리 문맥과 필터 문맥

- 두 문맥 간 수행 순서는 완련이 없다.

-(1단계) DocIdSetIterator 인스턴스 생성 끝나면 LeafCollectorDocIdSetIterator를 순회하며 collect() 호출하고

-(2단계) 그 안의 Scorer.score() 호출한다.

→ 일단 매치되는 후보군을 먼저 다 뽑고 유사도 계산은 나중에 한다. 필터 문맥 쿼리는 점수 계산 비용을 아끼는 것이지 먼저 수행된다는 의미는 아니다.

-매치되는 단일 문서 마다 유사도 점수 계산하고 수집하는건 하위 쿼리 일부가 병렬적으로 수행된다는 뜻

캐시 동작

샤드 레벨 요청 캐시 (request cache)

- query 페이즈에서 수행된 작업을 샤드 레벨에 저장한다.

캐시 수행 위치

SearchQueryThenFetchAsyncAction 작업 이후 QueryPhase.execute() 수행 전에 SearchService.executeSearch() 에서 캐시 사용 가능한지 파악한다.

캐시 조건

-search_type 이 query_then_fetch 여야함

-scroll 검색이 아니여야 함

-profile 요청이 아니어야 함

-now, Math.random(), new Date() 같은 확정되지 않은 조건이 들어가지 않아야함

-샤드 레벨 요청 캐시 설정했는지 확인

  • 명시적 index.requests.cache.enable 값이 없으면 size:0 이어야 캐시 먹힘
  • index.requests.cache.enable:true 면 size:0 이어도 캐시 먹힘

캐시 key

-요청이 캐시 대상이면 캐시 키를 만든다

-(1단계) ShardSearchRequest 의 인덱스, 샤드 번호, 검색 요청 본문 내용을 바이트로 직렬화한다.

-(2단계) ES 기동 과정에서 기본 내장 모듈 Security 플러그인이 등록한 캐시 키 변경 로직을 적용한다. 인가되지 않은 사용자의 캐시 접근을 비허용하기 위함

캐시 value

QueryPhase.execute()수행해 얻은 결과를 바이트로 직렬화

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

활용 시나리오

-size:0, 즉 집계 결과 캐시에 유용하다.

  • size:0 이 아닐 때 매치된 상위 문서랑 유사도 점수는 캐시에서 가져올 수 있지만 FetchPhase 는 캐싱 안 하기 때문에 _source 는 FetchPhase 를 수행해서 가져와야 한다. → 집계 작업 보다 캐시 효율성이 떨어짐
  • 그래도 부담이 큰 같은 요청이 여러 번이라면 size:0 아니라도 유용하다. DocIdSetIterator 순회 작업, 유사도 계산 작업, 상위 문서 수집 작업을 생략할 수 있기 때문이다.

캐시 적재 위치

-ES 노드 기동되면 Node 클래스 생성자 호출 → IndicesService 인스턴스가 생성 → IndicesRequestCache 인스턴스를 생성 → Cache 생성 => 이 Cache 인스턴스에 샤드 레벨 요청 캐시가 적재됨

  • CacheIndicesService 안에 있기 때문에 노드와 생명 주기가 같다. 하지만 캐시 키안에 샤드 정보가 포함돼있고 캐시 호출이나 적재 작업은 모두 샤드 레벨해서 수행되고 다른 샤드가 적재한 값이 호출될 일은 없기 때문에 샤드 레벨 캐시로 취급한다.

캐시 상태 확인

GET _stats/request_cache?human

 "shop-info-20231212-0000" : {
      "uuid" : "xaIKewddQuKCjiQFfLmdHg",
      "primaries" : {
        "request_cache" : {
          "memory_size" : "0b",
          "memory_size_in_bytes" : 0,
          "evictions" : 0,
          "hit_count" : 395,
          "miss_count" : 884
        }
      }}

캐시 설정

총 힙의 몇 %까지 샤드 레벨 요청 캐시로 사용할 것인지 (디폴트 1%)

config/elasticsearch.yml

indices.requests.cache.siize: 2%

수동으로 무효화 하기 (캐시 지우기 같음)

GET [인덱스명]/_cache/clear?request=true

샤드 레벨 요청 캐시 refresh 수행 시 무효화된다. (역시! 인덱스 데이터가 변경되면 클리어되는 거였군)

노드 레벨 쿼리 캐쉬

-필터 문맥으로 검색 수행시 어떤 문서가 매치됐는지 노드 레벨에 저장

캐시 수행 위치

-QueryPhase 에서 IndexSearcher.search() 할 때 Query.createWeight() 호출해 Weight 를 생성한다.

-유사도 계산 안 하는 Query 면 ChachingWeightWrapper 로 감싸 최종 Weight 를 반환한다.

-Weight 가지고 검색 수행할 때 BulkScorerScorer 가 캐시된 DocIdSet 을 순회하는 DocIdSetIterator 를 반환한다.

캐시 조건

-유사도 점수 계산하지 않는 쿼리여야 함

-문서 수 1만개 이상이어야 함

-샤드 내 문서의 3%이상 보유한 세그먼트가 대상이다

-캐시 없어도 충분히 빠를 쿼리는 캐시 하지 않음 (TermQuery, MatchAllDocQuery, MatchNoDocsQuery, DocValuesFieldExistQuery(doc_values 필드 대상 exists 쿼리) ..)

-락 획득해야함

  • 캐기 읽기, 쓰기 작업 모두 락 획득 필요하다. 락 획득 실패하면 일반 검색으로 진행한다.

캐시 적재 위치

-ES 기동시 IndicesService 인스턴스 생성 → IndicesQueryCache 인스턴스 생성 → IndicesQueryCache 내부에 노드 레벨 쿼리 캐시 적재됨

-한 노드 내 모든 IndexSearcherIndicesQueryCache 인스턴스 공유함.

-IndicesService 는 노드 생명 주기와 같으므로 노드 레벨 캐시다.

캐시 Key

캐시 키는 Query다. equals & hashcode 같이 같은 Query 로 취급할 수 있는 쿼리가 들어와야 적중한다.

캐시 Value

쿼리에 매치된 문서 목록. DocIdSet 추상클래스이다.

캐시 상태 확인

GET _stats/query_cache?human

 "shop-menu-20231212-0000" : {
      "uuid" : "a-pU2-woRSGqpEWx_MQfaA",
      "primaries" : {
        "query_cache" : {
          "memory_size" : "59.2mb",
          "memory_size_in_bytes" : 62099607,
          "total_count" : 5180281,
          "hit_count" : 714121,
          "miss_count" : 4466160,
          "cache_size" : 17873,
          "cache_count" : 71356,
          "evictions" : 53483
        }
      },

캐시 설정

총 힙의 몇 %까지 노드 레벨 요청 캐시로 사용할 것인지 (디폴트 10%)

config/elasticsearch.yml

indices.requests.cache.siize: 5%

수동으로 무효화 하기 (캐시 지우기 같음)

GET [인덱스명]/_cache/clear?query=true

샤드 레벨 요청 캐시 refresh 수행 시 무효화된다. (역시! 인덱스 데이터가 변경되면 클리어되는 거였군)

요청 캐시(샤드 레벨) VS. 쿼리 캐시 (노드 레벨)

페이지 캐시 (운영체제 레벨)

디스크에서 데이터 읽고 메모리에 넣어뒀다가 다시 읽을 일 있으면 메모리에서 읽어 반환. 이를 위해 시스템 메모리의 절반 이상은 캐시로 사용하게 설정하는 것이 좋다.

 

레퍼런스

엘라스틱서치 바이블 8장