2. 엘라스틱 서치 기본 동작과 구조
- 엘라스틱서치의 구조를 어느정도 먼저 이해한 상태에서 상세 내용을 학습해야 전체 학습에 깊이가 생긴다.
- 이번 장을 마치고 나면 엘라스틱서치가 어떻게 동작하는지 큰 그림을 머릿속에 그릴수 있을것이다.
2.1 엘라스틱서치 기본 동작 빠르게 둘러보기
- 실습 과정에서는 기본적으로 REST API를 호출해 진행한다.
- Kibana에 접속하여 화면 상단의 햄버거 버튼을 클릭해 management 메뉴 아래의 dev tools 하위 메뉴로 들어가면 API의 자동완성도 해주기 떄문에 편리하게 실습할수 있다.
- 문서에서는 Intellij Http Client을 이용해서 테스트를 진행한다.
2.1.1 문서 색인
_id를 지정하여 색인
1
2
3
4
PUT [인덱스 이름]/_doc/[_id값]
{
[문서 내용]
}
1
2
3
4
5
6
7
8
9
10
PUT /my_index/_doc/2
Host: localhost:9200
Content-Type: application/json
{
"title": "Hello world2",
"views": 1234,
"public": true,
"created" : "2023-11-18T18:05:00.000Z"
}
_id를 지정하지 않고 색인
1
2
3
4
PUT [인덱스 이름]/_doc/
{
[문서 내용]
}
1
2
3
4
5
6
7
8
9
10
POST /my_index/_doc
Host: localhost:9200
Content-Type: application/json
{
"title": "Hello world3",
"views": 1234,
"public": true,
"created" : "2023-11-18T18:06:00.000Z"
}
2.1.2 문서 조회
1
2
3
4
GET [인덱스 이름]/_doc/[_id값]
{
[문서 내용]
}
1
2
GET /my_index/_doc/1
Host: localhost:9200
2.1.3 문서 업데이트
1
2
3
4
POST [인덱스 이름]/_update/[_id값]
{
[문서 내용]
}
1
2
3
4
5
6
7
8
9
POST /my_index/_update/1
Host: localhost:9200
Content-Type: application/json
{
"doc": {
"title": "Hello world updated"
}
}
2.1.4 문서 검색
1
2
GET [인덱스 이름]/_search
POST [인덱스 이름]/_search
- 아래 예제는 match 쿼리를 사용한 요청이다.
1
2
3
4
5
6
7
8
9
10
11
GET /my_index/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"match": {
"title": "hello world"
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{
"took": 45,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 0.7026868,
"hits": [
{
"_index": "my_index",
"_id": "1",
"_score": 0.7026868,
"_source": {
"title": "Hello world updated",
"views": 1234,
"public": true,
"created": "2023-11-18T18:04:00.000Z"
}
},
{
"_index": "my_index",
"_id": "2",
"_score": 0.110377684,
"_source": {
"title": "Hello world2",
"views": 1234,
"public": true,
"created": "2023-11-18T18:05:00.000Z"
}
},
{
"_index": "my_index",
"_id": "ZPyw4YsBcsUuSBAASzRQ",
"_score": 0.110377684,
"_source": {
"title": "Hello world3",
"views": 1234,
"public": true,
"created": "2023-11-18T18:06:00.000Z"
}
}
]
}
}
- title 값이 일치하지 않는 항목도 조회된것을 확인할수 있는데,
_score
에서 유사도 점수를 확인할 수 있다. - 단순히 주어진 텍스트와 매칭되는 문서를 찾는 것이 아니라 문서를 분석해서 역색인을 만들어두고 검색어를 분석해서 둘 사이의 유사도가 높은 문서를 찾을수 있다.
2.1.5 문서 삭제
1
DELETE [인덱스 이름]/_doc/[_id값]
1
2
3
DELETE /my_index/_doc/1
Host: localhost:9200
Content-Type: application/json
Response
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"_index": "my_index",
"_id": "1",
"_version": 3,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 4,
"_primary_term": 1
}
2.2 엘라스틱서치 구조 개괄
- 책에 있는 이미지와는 다르지만, 하나의 index의 문서가 여러 node에 복제본을 어떻게 유지하는지에 대한 모습이다.
엘라스틱서치 주요 용어
- 문서(document) : 엘락스틱서치가 저장하고 색인을 생성하는 JSON 문서
- 인덱스 : 문서를 모아 놓은 단위가 인덱스다. 클라이언트는 이 인덱스 단위로 ES에 검색을 요청하게 된다.
- 샤드 : 인덱스는 그 내용을 여러 샤드로 분리하여 분산 저장한다. 또한 ES는 고가용성을 제공하기 위해 샤드의 내용을 복제해둔다. 원본 역할을 담당하는 샤드를 주 샤드(primary shard)라고 하고, 복제본을 복제본 샤드(replication shared)라고 한다.
_id
: 인덱스 내 문서에 부여되는 고유한 구분자다. 인덱스 이름과 _id의 조합은 엘라스틱서치 클러스터 내에서 고유하다.- 타입 : ES는 과거에 하나의 인덱스 안에 있는 여러 문서를 묶어서 타입이라는 논리 단위로 나눴다. 현재는 이 개념은 폐기 됐으며 더 이상 사용되지 않는다.
- 지금은 문서의 묶음을 논리적으로 구분해야 할 필요가 있다면 별도의 인덱스로 독립시켜야 한다. ES6부터 인덱스 하나에 타입 하나만 둘 수 있도록 제한되었다.
- ES7부터 이와 관련된 api가 deprecated되었고, 타입 이름이 들어가야 하던 자리에는 기본값이
_doc
이 들어간 API를 사요해야 한다.
- 노드 : ES 프로세스 하나가 노드 하나를 구성한다. 노드 하나는 여러 개의 샤드를 가진다. 고용성을 제공하기 위해 같은 종류의 샤드를 같은 노드에 배치하지 않는다.
- 클러스터 : ES 노드 여러개가 모여 하나의 클러스터를 구성한다.
- 노드의 역할 : ES 노드는 데이터 노드, 마스터 노드, 조정 노드 등 여러 역할 중 하나 이상의 역할을 맡아 수행한다.
- 데이터 노드 : 샤드를 보유하고샤드에 실제 읽기와 쓰기 작업을 수행하는 노드이다.
- 마스터 노드 : 클러스터를 관리하는 중요한 역할 수행, 마스터 노드는 마스터 후보 노드 중에서 1대가 선출된다.
- 조정 노드 : 클라이언트의 요청을 받아서 데이터 노드에 요청을 분배하고, 클라이언트에게 응답을 돌려주는 노드이다.
2.3 엘라스틱서치 내부 구조와 루씬
- ES는 아파치 루씬을 코어 라이브러리로 사용하고 있다.
- 루씬은 문서를 색인하고 검색하는 라이브러리이다.
2.3.1 루씬 flush
- 문서 색인 요청이 들어오면 루씬은 문서를 분석해서 역색인을 생성한다.
- 최초 생성 자체는 메모리 버퍼에 들어가고, 색인 & 업데이트 & 삭제 등의 작업이 수행되면 루씬은 이러한 변경들을 메로리에 들고 있다가 주기적으로 디스크에 flush 한다.
- 루씬은 색인한 정보를 파일로 저장하기 때문에 루씬에서 검색을 하려면 먼저 파일을 열어야 한다.
- 엘라스틱서치는 내부적으로 루씬의 DirectoryReader라는 클래스를 이용해 파일을 열고 루씬의 색인에 접근할 수 있는 IndexReader 객체를 얻는다.
DirectoryReader.openIfChanged
를 호출해 변경 사항이 적용된 새 IndexReader를 열어 준 뒤 기존 IndexReader를 안전하게 닫는다.- ES에서는 refresh라고 부름.
2.3.2 루씬 commit
- 루씬의 flush는 시스템의 페이지 캐시에 데이터를 넘겨주는 것까지만 보장할 뿐 디스크에 파일이 실제로 안전하게 기록되는것까지는 보장하지 않는다. 따라서 루씬은 fsync 시스템 콜을 주기적으로 커널 시스템의 페이지 캐시의 내용과 실제로 디스크에 기록된 내용의 싱크를 맞추는 작업을 수행한다.
- 이를 루씬의 commit이라고 한다.
- ES의 flush 작업은 이 루씬의 commit을 거친다. 루씬의 flush와 ES의 flush는 다른 개념이므로 혼동하지 말아야 한다.
- ES flush는 refresh 보다 훨씬 비용이 많이 드는 작업이다. 적절한 주기로 실행됨.
- 간혹 fsync 중에도 하드웨어 캐시를 사용하는 일부 저장 장치가 있다. 루씬은 그러한 장비를 사용하는 환경에서는 하드웨어 실패가 나는 상황까지 데이터 정합성을 보장하지는 않는다.
2.3.3 세그먼트
- 앞의 작업을 거쳐 디스크에 기록된 파일들이 모이면 세그먼트라는 단위가 된다.(세그먼트가 루씬의 검색 대상)
- 세그먼트 자체는 불변(immutable)인 데이터로 구성되어 있다.
- 문서 생성시 새로운 세그먼트를 만들지만, 기존 문서를 삭제하는 경우 삭제 플래그만 표시하고, 업데이트의 경우에도 기존 문서에 삭제 플래그를 표시하고, 새 세그먼트를 생성한다.
- 루씬의 검색은 모든 세그먼트를 대상으로 수행되는데 불변인 세그먼트 개수를 무작정 늘려갈 수 없기 때문에 중간중간 적당한 세그먼트 병합을 수행한다.
- 이 과정에서 삭제된 플래그가 표시된 데이터를 삭제하는 작업도 수행된다.
- 세그먼트 병합은 비용이 비싸지만, 병합을 하고나면 검색 성능 향상을 기대해볼 수 있다.
forcemerge
API를 통해 명시적으로 세그먼트 병합을 수행할 수도 있다.- 더 이상 추가 데이터 색인이 없을것이 보장될떄만 수행하는것을 권장한다.
- 크기가 매우 큰 세그먼트가 이미 존재하는 상황에서 추가적인 데이터 변경이 일어나면 추가된 작은 세그먼트는 자동 세그먼트 병합 대상 선정에서 영원히 누락될 수 있음.
2.3.4 루씬 인덱스와 엘라스틱서치 인덱스
- 여러 세그먼트가 모이면 하나의 루씬 인덱스가 된다.
- ES 샤드는 이 루씬 인덱스 하나를 래핑(warp)한 단위이다.
- ES 샤드 여러개가 모이면 ES 인덱스가 된다.
- ES 레벨에서는 여러 샤드에 있는 문서를 모두 검색해볼 수 있다.
ES가 하는 주된 역할
- 새 문서가 들어오면 해당 내용을 라우팅하여 여러 샤드에 분산시켜 저장/색인
- 이후 클라이언트가 검색요청을 보내면 각 샤드를 대상을 검색을 하고, 그 결과를 모아 병합하여 최종 응답을 생성한다.
- 이런 구조를 통해 루씬에서 불가능한 분산 검색을 ES레벨에서는 가능하게 만든것이다.
2.3.5 translog
- 엘라스틱서치에 색인된 문서들은 루씬 commit까지 완료되어야 디스크에 안전하게 기록된다.
- 그렇다고 문서 변경사항이 있을때마다 루씬 commit을 수행하기에는 비용이 많이 든다. 또 변경사항을 모아서 commit한다면, 장애가 발생할 때 미처 commit되지 않은 데이터가 유실될 우려가 있다.
- ES에서는 위와 같은 문제를 방지하기 위해
translog
라는 이름의 작업 로그를 남긴다.
translog 과정
- translog는 색인, 삭제 작업이 루씬 인덱스에 수행된 직후에 기록된다.
- translog 기록까지 끝난 이후에야 작업 요청이 성공으로 승인된다.
ES 장애복구과정에서 translog 활용
- ES에 장애가 발생한 경우 ES 샤드 복구 단계에서 translog를 읽는다.
- translog 기록은 성공했지만 루씬 commit에 포함되지 못했던 작업 내용이 있다면 샤드 복구 단계에서 복구된다.
- translog가 너무 커지면 샤드 복구에 시간이 오래 걸리게 되는데, 이르 방지하기 위해 translog의 크기를 적절히 유지해줄 필요가 있다.
translog와 ES flush
- ES flush는 루씬 commit을 수행하고, 새로운 translog를 만드는 작업이다.
- ES flush가 백그라운드에서 주기적으로 수행되며, translog의 크기를 적절한 수준으로 유지한다.
translog fsync 주기 관련 옵션
- 일반적인 상황에서는 설정하지 않는 옵션이지만,
index.translog.sync_interval
옵션을 설정한 경우 특정 주기에 의해서만 translog fsync를 수행할 수 있다. - translog fsync 기본 값은 5초인데, 이 사이에 문제가 발생하면 데이터가 유실될 수 있다.
Reference
- .
Comments powered by Disqus.