[엘라스틱서치 바이블] 8. 엘라스틱서치의 내부 동작 상세
- ES의 기본 구조와 핵심 동작 양상, 주요 기능, 서비스 환경을 구축하고 운영하며 장애에 대응하는 방법을 학습한것만으로도 ES를 활용한 개발에 큰 어려움이 없을것이다.
- 다만, 고도화가 필요하다면, 내부 동작을 구체적으로 이해하는것이 중요하다.
8.1 엘라스틱서치의 데이터 분산 처리 과정
- 데이터 읽기와 쓰기 작업 요청이 들어왔을떄 ES 내부가 어떤 단계를 거쳐 동작하는지 살펴본다.
8.1.1 쓰기 작업 시 엘라스틱서치 동작과 동시성 제어
- 쓰기 작업은 조정 단계(coordination stage), 주 샤드 단계(primary stage), 복제 단계(replica stage)의 3단계로 수행된다.
조정 단계
- 먼저 라우팅을 통해 어느 샤드에 작업해야 할지 파악하고, 적절한 주 샤드를 찾아 요청을 넘겨준다.
주 샤드 단계
- 주 샤드에 작업 요청이 넘어오면 요청이 문제 없는지 검증하고, 쓰기 작업을 수행한다.
- 작업이 완료되면 각 본제본 샤드로 요청을 넘긴다.
복제 단계
- 마스터 노드는 작업을 복제받을 샤드 목록을 관리하는데 이를 in-sync 복제본이라 하고, 이 샤드는 주 샤드에게 받은 요청을 로컬에서 수행하고, 주 샤드에게 작업이 완료됐음을 보고하는데 이 과정을 복제 단계라고 한다.
각 단계의 종료 시점
- 각 단계의 실행은 조정 -> 주 샤드 -> 복제 단계로 실행되지만 종료는 역순이다.
- 복제 단계가 완료되어 주 샤드에게 응답을 모두 보내야 주 샤드 단계가 종료된다. 조정 단계도 주 샤드 단계에서 최초 요청을 받아 전달했던 노드에게 작업 완료결과를 보내야 조정 단계가 종료된다.
쓰기 작업의 전체 흐름
낙관적 동시성 제어
- 주 샤드 단계에서 작업을 각 복제본 샤드에 병렬적으로 보내는데, 이때 여러 작업을 병렬적으로 보내면 메시지 순서의 역전이 일어날 수 있다. ES에서는 낙관적 동시성 제어를 통해 이를 처리한다.
- 변경 내용이 복제본 샤드에 완전히 적용되기 전에 다른 클라이언트로부터 주 샤드에 같은 문서의 필드 값을 변경하는 요청이 발생하면 분산시스템 특성상 어떤 요청이 먼저 본제본 샤드로 들어올지는 보장할 수 없다.
- ES에서는 이러한 동시성 문제를 방지하기 위해
_seq_no
가 존재한다._seq_no
는 각 주 샤드마다 들고 있는 시퀀스 숫자값이며, 매 작업마다 1씩 증가한다.- ES에서 문서를 색인할 때 이 값을 함게 저장하는데, eS에서는 이 값을 역전 시키는 변경을 허용하지 않음으로써 요청 순서의 역전 적용을 방지한다.
- 주 샤드를 들고 있는 노드에 문제가 발생하여 해당 노드가 클러스터에서 빠지는 경우에 대한 처리를 위한 방안도 존재한다.
_primary_term
이라는 이전 주샤드에서 수행했던 작업과 새로 임명된 주 샤드에서 수행했던 작업을 구분하기 위한 값이 존재하고, 주 샤드가 새로 지정될 때 1씩 값을 증가시킨다.
- ES에서는 이러한 방법으로 낙관적 동시성 제어(optimistic concurrency control)을 수행한다.
_seq_no
와 primary_term
이 배정되는 과정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
### 인덱스 생성
PUT /concurrency_test
Host: localhost:9200
Content-Type: application/json
{
"settings": {
"number_of_shards": 2
}
}
### 문서 색인
PUT /concurrency_test/_doc/1
Host: localhost:9200
Content-Type: application/json
{
"views": 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"_index": "concurrency_test",
"_id": "2",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
_seq_no
는 주 샤드마다 따로 매긴다. 데이터가 다른 샤드에 색인되면_seq_no
는 0부터 다시 매기는것을 확인할 수 있다.- 로컬에서 테스트할때는 모두 같은 샤드에 색인되는것까지만 확인하긴 했음.
if_primary_term=, if_seq_no
1
2
3
4
5
6
7
PUT /concurrency_test/_doc/1?if_primary_term=1&if_seq_no=1
Host: localhost:9200
Content-Type: application/json
{
"views": 3
}
- 문서 색인시 지정한 값과 같은 seq_no, primary_term인 경우에만 색인 작업이 실행되도록 할 수 있다.
버전(_version)
- 위 색인 테스트에서
_version
이라는 값이 증가하는 것도 확인 할 수 있는데, 이는_seq_no
와_primary_term
처럼 동시성을 제어하기 위한 메타데이터로 모든 무서마다 붙는다. - 기본적으로 1부터 시작해서 업데이트나 삭제 작업을 수행하면 1씩 증가한다.
_seq_no
나_primary_term
과 다른 점은 클라이언트가 문서의_version
값을 직접 지정할 수 있다는 점이다.- version_type을 external / external_gte로 설정하면 클라이언트가 직접
_version
을 지정해 색인할 수 있다.
- version_type을 external / external_gte로 설정하면 클라이언트가 직접
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
### 버전
PUT /concurrency_test/_doc/10
Host: localhost:9200
Content-Type: application/json
{
"views": 0
}
### 버전 external 지정
PUT /concurrency_test/_doc/10?version=15&version_type=external
Host: localhost:9200
Content-Type: application/json
{
"views": 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"_index": "concurrency_test",
"_id": "10",
"_version": 15,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 10,
"_primary_term": 3
}
- version은 long 범위 이내 정수로 지정할 수 있다.
- 마지막 버전보다 낮은 값으로 재색인하려는 경우 version_conflict_engine_exception 이 발생한다.
- version_type은 internal / external(external_gt) / external_gte가 있다.
- internal은 별도로 지정하지 않고 ES에서 자동으로 매겨주는 타입이다.
- external은 기존 문서의 버전보다 더 클 떄에만 색인이 수행된다.
- external_gte는 기존 문서의 버전보다 새 문서의 버전이 더 크거나 같을 떄에만 색인이 수행된다.
8.1.2 읽기 작업시 엘라스틱서치 동작
- 클라이언트로부터 읽기 작업을 요청받은 조정도느는 라우팅을 통해 적절한 샤드를 찾아 요청을 넘긴다.
- 쓰기와 다르게 주 샤드가 아니라 복제본 샤드로 요청이 넘어갈 수 있다.
- 요청을 넘겨받은 각 샤드는 로컬에서 읽기 작업을 수행한 뒤 그 결과를 조정 노드로 돌려준다.
- 조정 노드는 이 결과를 모아서 클라이언트에 응답한다.
- 이때, 주 샤드에는 색인이 완료됐지만, 특정 본제본에는 반영이 완료되지 않은 상탤태로 데이터를 읽을 수 있다는 점을 염두에 둬야 한다.
8.1.3 체크포인트와 샤드 복구 과정
- 문제가 생겨서 특정 노드가 재기동되었다면 그 노드가 들고 있던 샤드에 복구 작업이 진행되는데, 이 과정에서 복구 중인 샤드가 현재 주 샤드의 내용과 일치하는지를 파악할 필요가 있다.
- 재기동되는 동안 주 샤드에 색인 작업이 있었다면, 그 내용을 복구중인 샤드에도 반영해야 할 것이다.
- 문제가 발생했던 노드가 주 샤드를 들고 있었다면 다운타임 중에 선출된 주 샤드가 미처 반영하지 못한 작업 내용이 복구 중인 샤드에 포함돼 있을수도 있다.
- ES에서는 이런 부분을 고려하여 샤드 복구를 수행한다.
수행 과정과 체크포인트 업데이트 흐름
- ES에서는 앞서 다루었던
_primary_term
과_seq_no
을 조합하면 샤드와 샤드 사이에 어떤 반영 차이점이 있는지 알 수 있다. - 각 샤드는 로컬에 작업을 수행하고 나면 몇번 작업까지 순차적으로 빠짐없이 수행을 완료했는지를 로컬 체크포인트로 기록한다.
- 복제본 샤드는 로컬 체크포인트 값이 업데이트되면 이를 주 샤드에 보고한다.
- 주 샤드는 갹 복제본 샤드로부터 받은 로컬 체크포인트를 비교하여 가장 낮은 값을 글로벌 체크포인트 값으로 기록하고, 체크포인트를 비교하여 가장 낮은 값을 글로벌 체크 포인트 값으로 기록한다.
복구 방식
- 문제가 발생해 샤드를 복구해야 할 경우 ES는 샤드 간의 글로벌 체크포인트를 비교한다.
- 주 샤드와 보제본 샤드의 글로벌 체크포인트가 같다면 이 샤드는 추가 복구 작업이 필요없다.
- 글로벌 체크 포인트가 차이가 난다면 두 샤드 간 체크포인트를 확인해서 필요한 작업만 재처리하여 복구한다.
- 재처리할 내용을 추적하는 매커니즘을 ES에서는 샤드 이력 보존(shard history retention leases)라고 부른다.
index.soft_deletes.retention_lease.period
기간을 설정하여 보존 기간을 관리할수 있고, default 12시간이다.- 이 기간을 넘겨서 샤드 이력이 만료된 이후 수행되는 복구 작업에는 작업 재처리를 이용하지 않고, 세그먼트 파일을 통째로 복사하는 방법을 사용한다.
8.2 엘라스틱서치의 검색 동작 상세
- ES 레벨에서 어떻게 검색을 수행하는지 각 샤드 레벨, 즉 루씬 레벨에서는 매칭되는 문서를 어떻게 판정하고 점수를 계산하는지, 캐시는 어떻게 동작하는지 그 내부를 깊게 살펴보자.
8.2.1 엘라스틱서치 검색 동작 흐름
ES 검색 동작 흐름 개괄
검색 뿐만 아니라 다른 API도 아래와 같은 기본적인 흐름을가지고 있음.
- RestSearchAciton : REST 요청을 ES 내부에서 사용할 요청으로 변경하여 다음 단계로 요청하는 과정
- TransportSearchAction : 검색 요청과 현재 클러스터 상태를 분석해 상황에 맞는 적절한 검색 방법과 대상을 확정하고 검색 작업을 다음 단계로 넘기는 과정
- Query Phase : 쿼리에 매칭되는 상위 문서들을 각 샤드들이 판단하여 조정 노드로 반환하는 과정
- Fetch Phase : 각 샤드의 검색 결과를 받아 fetch를 수행할 문서를 확정하는 과정
ES 검색 동작 흐름 상세
TransportSearchAction
- 다양한 방법을 통해 여러 샤드 중에서 어떤 샤드에 우선해서 요청을 보낼것인지 순서를 확정한다.
- 우선순위가 가장 높은 샤드 하나에 요청을 보냈을 때 실패할 수도 있으므로 요청 순서 목록을 작성한다. (ShardIterator)
- 어떤 노드와 샤드를 우선해서 검색할 것인지 지정하는 매개변수
preference
값을 먼저 읽고 처리한다. preference
가 지정되지 않았다면 ES는 해당 샤드를 가진 노드 중 적절한 노드를 선정한다. 조정 노드에서 보낸 이전 요청에 대한 응답 속도나 이전 검색 요청에서 시간이 얼마나 소요됐는지 등 그간의 통계 데이터와 현재 검색 스레드 풀 상황이 어떠한지를 고려해 가장 응답을 빨리 돌려줄것으로 예상되는 노드를 보통 선정한다.- 만약 pit가 지정됐다면 처음부터 pit 값으로 검색 문맥을 가져오고 거기에서 검색 대상 인덱스와 샤드를 가져온다.
preference
_only_local
: 로컬에 들고 있는 샤드만을 대상으로 검색을 수행한다._local
: 가능하면 로컬에 들고 있는 샤드를 우선으로 검색을 수행한다. 불가능하다면 다른 노드의 샤드를 대상으로 지정한다._only_nodes:<node-id>, <node-id>
: 지정한 노드 ID가 들고 있는 샤드를 대상으로만 검색을 수행한다._prefer_nodes:<node-id>,<node-id>
: 지정한 노드 ID가 들고 있는 샤드를 우선해서 검색한다._shards:<shard>, <shard>
: 샤드 번호를 직접 지정해 해당 샤드 대상으로만 검색을 수행한다.custom-string
:_
로 시작하지 않는 문자열 지정으로, 지정한 문자열의 해시 값으로 노드 우선순위를 지정한다. 같은 노드가 요청을 받도록 해서 캐시를 최대한 활용하고자 할 떄 이방법을 사용한다.
CanMatchPreFilterSearchPhase
- 검색 요청의 search_type이 query_then_fetch이고, 몇몇 특정 조건을 만족하면 ES는 본격적인 검색 작업에 들어가기 전에 이 과정을 거치며 몇가지 최적화를 수행한다.
- 검색 대상의 샤드 수가 128개를 초과하거나 검색 대상이 읽기 전용 인덱스를 포함하거나, 첫번째 정렬 기준에 색인된 필드가 지정된 경우 수행된다.
- 검색 대상 샤드에서 주어진 쿼리로 단 하나의 문서라도 매치될 가능성이 있는지 사전에 점검을 수행해 확실히 검색 대상이 될 필요가 없는 샤드를 사전에 제거한다.
- 인덱스의 메타데이터를 이용해 타임스탬프 필드 범위상 매치되는 문서가 확실히 없는지를 체크한다거나 색인된 필드가 지정되어 있으면 각 샤드의 최솟값과 최대값을 가지고 샤드를 정렬해 상위에 올라올 문서를 보유한 샤드가 먼저 수행되도록 최적화하는 등 다양한 방법이 사용된다.
AbstractSearchAsyncAction
- search_type의 기본값인 query_then_fetch로 수행하는 경우 각 샤드에서 검색 쿼리를 수행하고, 매치된 상위 문서를 수집할 때 유사도 점수 계산을 끝내는 가장 일반적인 형태의 검색인데, 이 경우 이 단계에서 전체적인 흐름을 제어한다.(SeachQueryThenFetchAsyncAction)
- search_type을 dfs_query_then_fetch로 지정하면 모든 샤드로부터 사전에 추가 정보를 모아 정확한 유사도 점수를 계산한다.
- 정확도는 올라가지만 성능은떨어진다.(SearchDfsQueryThenFetchAsyncAction)
- dfs : distributed frequency search
SearchPhase
-조정 노드에서 수행하는 작업과 각 샤드 레벨에서 수행하는 작업을 구분해서 봐야 한다.
- QueryPhase, DfsPhase, FetchPhase 등은 각 샤드에서 수행되고 나머지는 조정노드에서 수행된다.
SearchDfsQueryThenFetchAsyncAction
- 점수 계산에 사용할 추가 정보를 각 샤드에서 가져오기 위해 샤드별 요청을 만들어서 분산 전송한다.
- 이 과정에서 각 샤드는 검색 쿼리에 매치되는 텀이나 문서 빈도(document frequency), 여러 통계 데이터 등을 구해 반환한다.
- 이후 조정노드에서 DfsQueryPhase를 수행한다.
DfsQueryPhase
- 각 샤드에서 보낸 DfsPhase 작업 결과로부터 샤드별 본 검색 요청을 만들어 다시 각 노드로 분산 전송한다.
- QueryPhase에서 본격적인 쿼리 매치 작업을 수행하고 이 작업 결과를 수신하면 FetchSearchPhase로 넘어간다.
SearchQueryThenFetchAsyncAction
- query_then_fetch search_type인 경우 사전 작업 없이 바로 샤드별 검색 요청을 만들어 전송한다.
- 샤드별 결과를 수신하면 FetchSearchPhase로 넘어간다.
QueryPhase
- 이 단계에서는 샤드 요청이 캐시 가능한 요청인지 확인하고, 캐시 가능한 요청이라면 캐시에서 값을 불러와 바로 응답을 반환한다.(샤드 레벨 저장 캐시)
- 캐시에 값이 없다면 QueryPhase의 주 작업을 수행하고 캐시에 결과를 저장한다.
- QueryPhase에서는 크게 검색, 제안(suggest), 집계의 세 작업을 수행한다.
- 제안(suggest) 작업은 오타 교정이나 자동완성 등에 사용하는 기능이다.
FetchSearchPhase와 FetchPhase
- 각 샤드가 수행한 QueryPhase 작업의 결과가 조정 노드에 모이면 FetchSearchPhase로 넘어간다.
- FetchSearchPhase 단계에서는 각 샤드에 요청할 fetch 요청을 생성해 분산 전송한다.
- FetchPhase에서는 요청에 지정한 번호의 문서의 내용을 실제로 읽어서 반환한다.
- 각 샤드의 FetchPhase의 작업 결과가 조정 노드에 모이면 ExpandSearchPhase로 넘어간다.
FetchSubPhase
- FetchSubPhase는 문서 내용을 읽어 SearchHit을 만드는 과정에서 수행하는 여러 하위 작업을 처리하는 단계이다.
- 커스텀 플러그인을 통해 직접 만든 FetchSubPhase를 등록할 수도 있다.
- _soruce를 읽거나 _score을 다시 계산하거나, 검색 수행 중간 과정과 부분 유사도 점수를 상세 설명하는 _explanation을 만드는 과정들이 포함된다.
ExpandSearchPhase
- ExpandSearchPhase에서는 필드 collapse을 수행한다.
- 이는 지정한 필드 값을 기준으로 검색 결과를 그룹으로 묶은 뒤 그 안에서 다른 기준으로 상위 문서를 지정한 개수만큼 뽑을 떄 사용하는 특수한 기능이다.
- 필드 collapse을 수행하기 위해 본 검색의 hit 수만큼 새 검색 요청을 만들어서 로컬에 전송하고, 조정 노드 자신이 이를 받는다.
- 이 과정을 마무리하면 결과를 모아서 최종 검색 결과를 만들어 반환하고 검색 작업을 조욜한다.
- 필드 collapse을 사용하지 않는다면 바로 응답을 반환한다.
8.2.2 루씬 쿼리의 매칭과 스코어링 과정
- 위에서 학습한 QueryPhase에서 쿼리에 매칭되는 상위 문서를 수집하는 작업은 각 샤드 레벨, 즉 루씬 레벨에서 수행하는 작업이다.
- 루씬이 어떻게 검색 작업을 수행하는지 흐름을 이해하면 어떤 쿼리가 무거운 쿼리이고 문제가 되는 경우를 이해하기 쉽다.
IndexSearcher
- IndexSearcher 는 루씬 인덱스 내의 문서를 검색할 때 사용하는 클래스
QueryBuilder
- ES 레벨의 쿼리를 정의하는 인터페이스로 쿼리 이름, DSL 파싱, 직렬화 여부 등을 정의한다.
Query
- 루씬 쿼리를 정의하는 추상클래스
Weight
- Query 내부 동작을 구현하는 추상클래스로, IndexSearcher에 의존ㄴ성 있는 작업이나 상태를 담당한다.
Scorer
- 유사도 점수 계싼을 담당하는 추상 클래스로
BulkScorer
- 여러 문서를 대상으로 한 번에 유사도 점수를 계산하는 추상 클래스
- Weight의 BulkScorer 메소드를 따로 오버라이드하지 않았다면 기본 구현인 DefaultBulkScorer 클래스를 반환하여 사용한다.
DocIdSetIterator
- ocIdSetIterator는 매치된 문서의 순회를 담당하는 추상 클래스이다.
- hello 라는 질의어로 term 쿼리를 수행했다고 가정해보면 루씬은 해당 필드의 역색인을 읽어 어떤 문서가 hello term을 가지고 있는지 목록을 불러오고, 이 목록을 DocIdSetIterator로 순회한다.
- 일반적인 경우는 next로 순회할 수 있으나, 일부 문서들을 수집 후보에서 건너뛸 수 있다거나 한 경우 advance(target)을 호출하여 순회한다.
TwoPhaseIterator
- 매치 여부를 판단하는 작업이 무거운 쿼리의 매치 작업을 두 개 페이즈로 나누어 진행하도록 하는 추상 클래스이다.
- 비용이 저렴한 간략한 매치를 먼저 수행해 후보를 좁히고 난 뒤 문서 수집 과정에서 최종 매치를 수행한다.
Collector, LeaftCollector
- Collector는 검색 결과를 수집하는 동작을 정의하는 인터페이스이다.
- 유사도 점수나 정렬 기준 등을 계산하거나 확인하며, 상위 결과를 수집하는 동작 등을 수행한다.
bool 쿼리의 검색 동작 순서와 DocIdSetIterator 순회
- bool 쿼리는 must, filter, must_not, should 절 하위에 다양한 쿼리르 갖고 있는데, 이 중 어떤 쿼리를 먼저 수행된다는 알기 쉬운 정해진 규칙이 없다. 이를 루씬의 주요 동작과 연결시켜 상세히 알아보자.
rewrite, cost
- ES는 검색 요청을 받으면 내부적으로 쿼리를 루씬의 여러 쿼리로 쪼갠 뒤 조합해 재작성한다.(
Query.rewrite()
) - 그 뒤 쪼개진 각 쿼리를 수행시 얼마나 비용이 소모되는지 내부적으로 추정한다. 비용의 효과를 추정하고 유리할 것으로 생각되는 부분을 먼저 수행한다. (
DocIdSetIterator.cost()
) - bool 쿼리를 담당하는 BooleanWeight는 쿼리의 세부 내용에 따라 다양한 종류의 최적화된 Scorer를 만들어서 반환한다.
conjunction 검색과 DocIdSetIterator 순회
- conjunction 검색은 AND 성격의 검색으로 주어진 쿼리의 매치 조건을 모두 만족해야 최종 매치된 것으로 판정한다.
- disjunction 검색은 OR 성격의 검색으로 주어진 쿼리의 매치 조건 중 하나만 만족해도 최종 매치된 것으로 판단한다.
- 위 2가지 방식 모두 하위 Query마다 Weight에 이어 Scorer를 미리 만들어 둔 다음 하위 Scorer들을 가지고 최종 Scorer를 만든다.
- conjunction 방식에서는 각 항목에 대한 DocIdSetIterator을 뽑고, cost순으로 정렬 한 뒤 제일 작은 cost를 가진 Iterator순으로 순회한다.
- 첫번쨰 항목에 대한것을 수행하고, doc을 기준으로 다음것을 수행하여 두번쨰 항목에서도 가리키는지를 확인하고, 모든 항목에 대해서 같은 doc을 바라보는지 확인하여 모두 일치하다면 매치된것으로 판단한다.
disjunction 검색과 건너 뛰기
- disjunction 검색도 하위 Scorer와 Iterator을 이용해 최상위 레벨의 Scorer와 Iterator를 생성한다.
- OR 검색이므로 개념적으로 결과 중 최솟값을 반환한다.
- 구현은 하위 Iterator를 현재 doc ID를 기준으로 하는 최소 힙을 이용한다.
- 유사도 점수를 계산해서 상위 k개의 문서를 수집하면 되는 상황이라면 현재까지 수집된 k번쨰 문서보다 점수 경쟁력이 떨어지는 문서나 문서의 블록을 적극적으로 건너띄는 여러 가지 최적화 방법을 함께 사용한다.
쿼리 문맥과 필터 문맥
- 쿼리 문맥의 쿼리든 필터 문맥의 쿼리든 수행 순서와는 관련이 없다.
- DocIdSetIterator 인스턴스생성이 끝난 후에야 LeafCollector가 순회하며 데이터를 수집하고, 그 안에서 socre를 계산한다.
- 이러한 흐름 자체가 쿼리 문맥, 필터 문맥 상관 없이 하위 쿼리에 매치되는 후보군 자체를 일단 다 뽑는다는 것을 의미한다.
- 필터 문맥의 쿼리는 이때 score를 호출하여 점수를 계싼하는 비용을 아끼는것일뿐 먼저 수행되는것은 아니다.
- 매치되는 단일 문서마다 유사도 점수를 계산하며 수집한다는 것은 하위 쿼리의 동작 일부가 병렬적으로 수행된다는 것을 의미한다.
8.2.3 캐시 동작
- 검색 성능이 아쉽다면 캐시를 잘 활용하고 있는지 검토해 볼 필요가 있다.
샤드 레벨 요청 캐시
- 샤드 레벨 요청 캐시(request cache)는 검색 수행 시 query 페이즈에서 수행된 작업을 샤드 레벨에 저장하는 캐시이다.
캐시 수행 위치
- SearchQueryThenFetchAsyncAction 작업 이후 QueryPhase 작업에 들어갈 떄 동작한다.
- ShardSearchRequest와 대상 인덱스 설정을 보고 캐시 가능한 요청인지 여부를 파악한다.
캐시 조건
- ES 검색 수행 시 요청 캐시를 활용하려면 다음 조건을 만족해야 한다.
- search_type이 query_then_fetch이어야 한다.
- scroll 검색이 아니어야 한다.
- profile 요청이 아니어야 한다. ( Profile API )
- 검색 요청의 개별 구성 요소 실행에 대한 자세한 타이밍 정보를 제공받기 위한 요청, 디버깅용
- now가 포함된 날짜 시간 계싼 표현이나 Math.random() 같은 명령이 포함된 스크립트가 아니어야 한다.
- api 호출시 requestCache 매개변수를 명시적으로 지정한 경우 true이어야 한다.
- 매배견수를 지정하지 않는 경우
index.requests.cache.enable
인덱스 설정이 true여야 한다.(default : true) - 검색 요청의 size 매배견수가 0이어야 캐시 대상이 될 수 있다.
- 명시적으로 requestCache를 true로 지정하면 이 여부와 관계 없이 캐시를 수행한다.
캐시 키
- 요청이 캐시 대상이라는 사실을 확인하고 나면 캐시 키를 만든다.
- 인덱스, 샤드 번호와 검색 요청의 본문 내용 등 사실상 완전히 같은 검색 요청이어야 캐시가 적중되는 구조이다.
- 인가되지 않은 사용자가 캐시에 올라간 데이터를 받아가지 않게 하기 위한 추가적인 로직이 있음.
캐시 대상 값
- 캐시 키를 만든 후에는 이 키로 캐시에 올라간 데이터가 있는지 찾는다.
- 캐시가 적중했다면 쿼리 매치 작업을 수행하지 않고 캐시된 값을 바로 이용한다.
- 제안 결과, 집계 결과, 매치된 문서 수, 최대 점수, 매치된 상위 문서 등 모든 응답 값이 캐시된다.
샤드 레벨 요청 캐시 활용 방향
- 샤드 레벨 요청 캐시의 주 목적 자체가 size를 0으로 지정해 요청하는 집계 결과를 캐시하는 것이 목적이다.
- 매치된 상위 문서가 무엇인지, 유사도 점수가 얼마인지는 캐시에서 가져올 수 있지만, 그 문서의
_source
는 FetchPhase를 통해서 가져와야 한다. 그러므로 집계를 지정하지 않은 일반적인 검색 요청의 쿼리 매치 작업 결과를 캐시하는 것은 집계 작업을 캐시하는 것보다 효율성이 떨어진다.(기본 동작이 size = 0 인 요청하는 캐시하는 이유) - 서비스 특성상 ES 클러스터의 주 사용 용도가 집계가 아닌 일반 검색이고, 동일한 쿼리가 여러 번 인입될 가능성이 있다면 요청 캐시를 적극적으로 사용하는 것도 좋다.
- 이 경우 requestCache 매개변수를 true로 지정해서 요청하는 방식을 사용하자.
- FetchPhase은 무조건 수행해야 하기는 하지만, QueryPhase의 작업을 생략할 수 있는 것만으로도 성능 차이가 클 수 있다.
캐시가 적재되는 위치
- ES 노드가 기동하는 과정에서 Node 클래스, IndicesService 인스턴스가 생성되고, 이 내부의 IndicesRequestCahe 인스턴스를 멤버로 관리하며 실제 Cache는 여기서 관리된다.
- 즉 샤드 레벨 요청 캐시는 노드와 생명주기를 같이 한다고 보면 된다.
캐시 상태 확인
1
GET /_status/request_cache/human
- 위와 같이 인덱스별 혹은 전체 인덱스의 캐시 적중, 부적중, 퇴거(eviction) 회수, 캐시 크기 등의 주요 정보를 확인 할수 있다.
캐시 크기 지정과 캐시 무효화
- 기본 설정으로는 총 힙의 1%까지 샤드 레벨 요청 캐시로 사용한다.
config/elasticsearch.yml
의indices.requests.cache.size
을 변경하고 재기동하여 이 값을 변경할 수 있다.- ex.
indices.requests.cache.size:2%
- ex.
- 샤드 레벨 요청 캐시는 인덱스 refresh를 수행할 때마다 무효화된다. 수동으로 무효화하려면 아래 API를 호출할 수 있다.
POST [인덱스 이름]/_cache/clear?request=true
노드 레벨 캐시
- 노드 레벨 캐시는 필터 문맥으로 검색 수행시 쿼리에 어떤 문서가 매치됐는지를 노드 레벨에 저장하는 캐시이다.
캐시 수행 위치
- QueryPhase에서 검색 수행 시
IndexSearcher.search()
을 수행하고, 여기서 유사도 점수를 계산하지 않는 쿼리라면 쿼리 캐시를 적용하는 CachingWeightWrapper로 감싸 최종 Wieght를 반환하고, 검색을 수행하는 과정에서 캐시된 DocIdSet을 순회하는 Iterator을 반환한다.
캐시 대상 값
- 캐시에 적재하는 값인 DocIdSet 추상 클래스는 쿼리에 매치된 문서 목록을 나타낸다.
- 여러 DocIdSet의 구현체가 있지만, 비트 배열로 구현한다. (ex. 4번 문서가 매치된 경우 배열의 비트를 1로 처리한다.
캐시 키
- 캐시의 키는 Query이다. Query의 equals와 hashCode 구현상 같은 Query로 취급되는 경우 캐시를 적중시킬 수 있다.
bool 쿼리에서 노드 레벨 쿼리 캐시 활용
- 쿼리 문맥과 필터 문맥을 모두 가지고 있는 bool 쿼리는 쿼리 중 일 부분인 필터 문맥 부분만 잘 분리하여 캐시한다.
- 여러 bool 쿼리의 내용을 바꿔가며 수행하더라도 필터 문맥의 하위 쿼리 중 일부가 겹친다면 부분적으로 캐시의 혜택을 볼 수 있다.
- 쿼리 문맥은 노드 레벨 쿼리 캐시가 안된다고 보면 된다.
캐시 조건
- 캐시를 활용하기 위해서는 유사도 점수를 계산하지 않는 쿼리여야 한다는 점 외에도 조건이 많다.
- 1만개 이상의 문서가 있으며 동시에 샤드 내 문사ㅓ의 3% 이상을 보유하고 있는 세그먼트가 대상일 떄만 캐시한다.
- 내부 테스트 목적으로 설정의 해당 조건을 아예 제거할수는 있지만, 1만과 3%의 숫자 값은 바꿀 수 없다.
- 이 외에도 쿼리 캐시 없이도 충분히 빠르다고 판단하는 쿼리는 캐시하지 않는다.
- 최근 해당 쿼리를 얼마나 수행했는지 이력을 추적하여 루씬 내부적인 기준으로 무겁다고 판단한 쿼리는 2회 이상, 그 외는 4~5회 이상 반복 수행해야만 캐시를 수행한다.
락 획득
- 노드 레벨 쿼리 캐시는 읽기 작업과 쓰기 작업에 모두 락 획득을 필요로 한다.
- 읽기 작업 도중 락 획득은 무작정 기다리지는 않고, 락 획득 시도가 실패하면 바로 일반 검색 작업을 진행한다.
쿼리 캐시 상태 확인
1
GET /_stats/query/_cache?human
- 이 요청을 통해 인덱스별 혹은 전체 인덱스의 캐시 적중, 부적중, 퇴거 회수, 캐시 크기 등 주요 노드 레벨 쿼리 캐시의 상태를 확인할 수 있다.
캐시 크기 지정
- 기본 설정으로는 총 힙의 10%까지 노드 레벨 쿼리 캐시를 사용한다.
config/elasticsearch.yml
의indics.queries.cache.size: 5%
와 같이 지정해서 값을 변경할 수 있다.
쿼리 캐시 무효화
- 쿼리 캐시도 인덱스 refresh를 수행할떄 캐시가 무효화되고, 수동으로 무효화 하는 경우 아래 API를 호출할 수 있다.
- 파라미터가
request / query
로 구분해서 달리 처리할 수 있다.
- 파라미터가
1
POST [인덱스 이름]/_cache/clear?query=true
ES의 캐시 적재 위치 구성
샤드 레벨 요청 캐시와 노드 레벨 캐시 비교
요청 캐시 | 쿼리 캐시 | |
---|---|---|
수행 위치 | QueryPhase 작업 들어가기 직전 | 래핑한 Weight에서 BulkScorer, Scorer 등 생성시 |
키 | SharedSearchRequest | Query |
값 | 매치된 상위 문서와 점수의 목록, 제안, 집계 등을 포함한 QueryPhase의 수행 결과 | 매치된 문서 목록을 나타내는 비트 배열 |
주요 캐시 조건 | 1. 요청에 requestCache를 명시적으로 지정, 2. index.requests.cache.enabled=true && size가 0인 집계 쿼리, 그 외 확정적인 검색 등 기본 조건 | 필터 문맥에서 쿼리를 많이 자주 수행해야 하고, 빠른 쿼리는 제외되며 세그먼트에 일정 이상의 문서가 있는 경우 |
적재 위치 | IndicesService.IndicesRequestCache | IndicesService.IndicesQueryCache |
캐시 접근 범위 | 샤드 레벨 | 노드 레벨 |
운영체제 레벨 페이지 캐시
- 위 캐시 외에 OS 레벨에서 기본적으로 동작하는 페이지 캐시가 있다.
- OS는 디스크에서 데이터를 읽은 후 데이터를 메모리에 넣어 두었다가 다시 이 데이터를 읽을 일이 있으면 디스크가 아니라 메모리에서 읽어 반환한다.
- ES는 페이지 캐시를 잘 활용하고 있으므로 시스템 메모리의 절반 이상은 캐시로 사용하도록 설정하는 것이 좋다.
Comments powered by Disqus.