본문 바로가기

Elasticsearch

Elasticsearch 1편 - Elasticsearch 알아보기

엘라스틱서치에 대해 취업 후 처음으로 공부해보고 실제 관련 업무를 수행하고 있습니다. 이 글을 쓰는 데 목적 또한 중요한 부분을 잊지 말고 기억하기 위한 점이 크니 가볍게 봐주시면 좋을 것 같습니다 :)

Elasticsearch

엘라스틱서치는 다들 들어보셨다시피 오픈소스 분산시스템이며 Apache Lucene 기반으로 만들어졌습니다.
이 문장을 처음에 보고 든 생각은 '그래서 Lucene은 뭐하는 애지?', '분산 시스템이면 분산 코디네이터는 누구지?' 등등이 떠올랐습니다. 그래서 자세히 이에 관하여 찾아보면서 가장 먼저 Lucene과 Elasticsearch의 관계에 대한 궁금증도 자연히 생겨났습니다.

Apache Lucene

Apache Lucene은 오픈소스 검색 라이브러리입니다. 말그대로 라이브러리이기 때문에, 많은 부분을 직접 구현해 사용해야 합니다. 하지만, Lucene은 Full text 색인 및 검색이 가능한 강력한 검색 엔진입니다. Lucene은 기본적으로 역 색인(inverted indexing)을 사용해 문서를 빠르게 검색할 수 있도록 합니다.

1. 역 색인(inverted indexing): 각각의 단어가 어디에 속해 있는지 목록을 유지하는 자료구조를 생성하는 것입니다. 역 색인은 관련성(문서에 많이 나타나는 term 정보는 weight이 큼)에 있어서도 검색엔진에 적합한 자료구조입니다. 좋은 참고자료가 있어 첨부합니다 :)
역 색인은 특정 term을 지닌 도큐먼트의 개수를 지닐 수 있고, 각 도큐먼트의 길이, 도큐먼트의 길이의 평균 등 많은 정보를 지닐 수 있는 특성이 있습니다.
2. 역 색인 Immutability: 디스크에 작성되는 역 색인 인덱스는 Immutable한 특성이 있습니다. 이 장점으로는 4가지가 있습니다.
- Locking 불필요: 인덱스를 업데이트할 필요가 없다면, 멀티 프로세스들이 동시에 이를 바꾸려할 일이 없으므로 locking이 불필요합니다.
- 인덱스가 커널의 파일시스템에 한번 캐시되면, 변경이 없을 것이기에 유지될 것입니다. 파일 시스템 캐시에 충분한 공간이 있는 한 대부분 읽기는 disk hit 대신 memory에서 발생할 것이기에 성능이 크게 향상될 것입니다.
- filter cache 처럼 다른 캐시는 인덱스의 life와 유효하게 유지됩니다. 데이터가 변경되지 않기에 데이터가 변경될때마다 재구성할 필요가 없어집니다.
- 하나의 큰 역 색인(Inverted index)를 작성하면 데이터를 압축해 디스크 I/O와 인덱스를 캐시하는 데 필요한 RAM의 양을 줄일 수 있습니다.
3. Immutable Index는 바꿀 수 없다는 점이 단점입니다. 새 문서 검색을 가능하게 하려면 전체 색인을 재구성해야 합니다. 이 경우 색인에 포함될 수 있는 데이터 양 또는 색인을 업데이트할 수 있는 빈도에 큰 제한이 발생합니다.

Lucene 기반으로 만들어진 Elasticsearch는 역색인 자료구조를 기반으로 문서를 저장합니다. 즉, Elasticsearch는 데이터를 색인, 검색하는 Lucene의 기능들을 쉽게 이용할 수 있도록 해줍니다. 문서를 어떻게 분석(처리)하고 저장할 것인지, 검색 시 여러 query와 filter들이 지원됩니다. Elasticsearch는 REST API를 통해 이러한 기능들과 집계 기능 등을 지원하고, JSON으로 쿼리 합니다.

Elasticsearch vs RDBMS

위에서 Elasticsearch는 데이터를 '문서'로 저장한다고 했습니다. 여기서 '문서'란 무엇일까요? RDBMS는 행-열 구조에 데이터를 저장해왔습니다. 다른 테이블의 다른 행-열에 저장해 join이 필요했던 RDBMS와 달리, Elasticsearch의 '문서'는 'fields-values'로 구성된 JSON 도큐먼트로 구성되어있어 아래와 같이 계층적일 수 있습니다. 문서는 색인과 검색하는 데이터의 가장 작은 단위이며, 필드와 값을 가지고 있습니다.

"ownedBy": "sds"
"jelly": ["haribo", "maigumi"]
"supermarket": {"owner": "sds", "open_at": "07:30:00"}

또한, Elasticsearch의 인덱스는 사용자의 데이터가 저장되는 논리적인 공간을 의미하며 타입은 인덱스 안의 데이터를 유형별로 논리적으로 나눠 놓은 공간을 의미합니다. Elasticseach에서 문서 색인을 만들 때 색인 안의 type에 문서를 넣습니다.

Elasticsearch RDBMS
인덱스 데이터베이스
타입 테이블

RDBMS에서는 데이터베이스 안에 여러 개의 테이블을 가질 수 있지만, Elasticsearch 6.x 버전 이후로는 하나의 인덱스에 하나의 타입만 가질 수 있습니다. 각 타입에서 필드의 정의를 mapping이라 부릅니다.

Elasticsearch index 구조

https://fdv.github.io/running-elasticsearch-fun-profit/003-about-lucene/003-about-lucene.html


위 그림은 제가 보았던 모든 그림 중 가장 간결하고 명확해 가져왔습니다. Elasticsearch의 구조에 대해 어렴풋이 이해가 갔지만, 그 내부는 분명 Lucene 기반이라고 했는데... 과연 어디일까에 대한 가려움을 긁어주었다고 할까요...?
Elasticsearch Index는 shards의 컬렉션이며 shard는 Lucene Index 자체입니다.

  • Shard: Elasticsearch가 다루는 가장 작은 단위는 '샤드'입니다. Shard와 Lucene Index는 일대일 대응입니다. 즉 샤드가 루씬 인덱스인 것이죠. Lucene Index는 역 색인을 포함하는 파일들의 모음입니다. 각 Elasticsearch Index는 한 개 혹은 여러 개의 샤드로 구성됩니다. 결국 샤드는 하나의 Lucene Index이므로 Elasticsearch Index는 여러개의 Lucene Index로 구성됩니다.
    샤드는 primary/replica shard로 나뉩니다. replica shard는 primary shard의 복사본이죠. 검색을 위해 사용하거나 primary shard 후보자이기도 합니다. replica shard는 삭제/추가가 용이하지만 primary shard는 실행 시간에 개수의 변경이 불가능합니다.
    클러스터 내 샤드 분산에 대해서는 다음 장에서 살펴보겠습니다 :)
  • Segment: 데이터를 Elasticsearch Shard에 입력할 때, Elasticsearch는 주기적으로 디스크에 불변의(immutable) 루씬 세그먼트 형태로 저장(publish)하며, 이 작업 이후에 조회가 가능해 집니다. 이러한 과정을 우리는 리프레쉬(refresh) 작업이라고 부릅니다.이 과정에 대한 상세 설명은 이 가이드에서 확인하세요. (역자주: 샤드는 한개 혹은 여러개의 세그먼트로 구성됩니다.)

disk에 쓰여진 inverted index, 즉 역색인은 immutable 입니다. 이러한 불변성은 lock 불필요, 커널의 파일 시스템 캐시 메모리 hit율 증감 및 필터 캐시와 같은 다른 캐시 또한 index와 수명주기가 동일해져 캐시 재적재가 필요없다는 장점이 있습니다. 물론, 새 문서를 적재하려면 전체 색인을 rebuild해야 한다는 단점은 있습니다. 그렇게되면 실시간 반영이 어려워질 수 있겠죠.

이러한 단점을 해결하기 위해 Elasticsearch의 기반이 되는 라이브러리인 Lucene은 per-segment 검색 개념을 도입했습니다. 다수의 세그먼트를 생성하는 것입니다. segment 그 자체가 역색인된 문서 모음이며 위에서 설명했듯이 segments가 Lucene Index를 이루고 있습니다. 추가로 Lucene index는 segments 목록을 리스트로 표현한 commit point를 포함합니다. 새 문서는 segment에 쓰여지기 전 먼저 인메모리 indexing 버퍼에 추가되고 buffer는 종종 commit 됩니다.
Refresh 과정을 자세히 들여다 봅시다.
Elasticsearch는 writing & opening new segment 과정을 refresh라고 부르기로 합니다. 디폴트로 모든 shard는 매초 refresh 됩니다. File system cache에 문서가 새로운 세그먼트로 쓰여집니다. 이것이 NRT search가 가능한 이유죠.
Elasticsearch는 시작 중 또는 인덱스를 다시 open할 때 commit point 사용하여 현재 샤드에 속하는 세그먼트를 결정합니다. 매초 refresh 동안, 장애로부터 복구할 수 있도록 정기적으로 full commit을 수행해야 합니다. 만약 commit 중 문서 변경이 일어나면 어떻게 될까요? 그래서 Elasticsearch는 Elasticsearch의 모든 작업을 기록하는 translog 를 추가했습니다. 모든 변경사항은 Lucene을 호출하기 전 shard단에서 translog에 먼저 적재합니다. 전체 과정은 다음과 같습니다.

  • 새 문서가 색인되면 in-memory buffer에 추가되고 translog에 추가됩니다. 
  • refresh(lucene flush) : 1초마다 in-memory buffer의 문서들은 새 segment에 쓰여집니다. (without fsync) & segment search 가능 & in-memory buffer 비워짐(translog 그대로)
    • write()함수로 디스크 동기화. 커널 시스템 캐시에 쓰여짐. 일정 주기에 따라 물리 disk로 기록됨.
  • 새 문서가 계속 색인되면서 translog가 점차 커집니다.
  • 일정 주기로 es flush가 수행됩니다.
    • 디스크에 물리적으로 기록 (fsync)
    • 성공하면 translog의 정상적으로 commit이 일어난 시점까지의 내역이 삭제됨
  • 또는 translog가 너무 커지면 index는 flush (lucene commit)됩니다. 
    • in-memory buffer의 모든 문서들을 새 segment에 작성, buffer cleared
    • commit point가 disk에 flush됩니다.
    • 파일시스템 캐시가 flush 되고 fsync() 함수로 실제 물리 디스크에 변경 내용 기록
    • Translog 기록 삭제

translog는 아직 디스크로 flush 되지 않은 모든 작업에 대한 persistent record를 제공합니다. Elasticsearch가 시작/복구될 때 마지막 commit point를 사용해 disk로부터 segments를 복구하고 translog의 모든 operations를 재실행해 last commit 이후 모든 변경 사항을 반영합니다. Elasticsearch는 또한 lucene commit 상태에서 변경사항들을 translog에 기록해 다음 commit이 반영될 때 까지 유지해 변경사항 유실을 방지합니다.

쿼리가 요청되면 모든 segments가 오래된 순서부터 차례로(Commit point 사용) 쿼리되며 term 통계는 모든 segments에 걸쳐 집계됩니다.
모든 commit point는 어떤 segments의 어떤 문서가 삭제됐는지를 나타내는 '.del' 파일을 포함하고 있습니다. 즉, 문서가 삭제되면 '.del'파일에 기록되고 query가능하지만 최종 쿼리 결과 반환 전에 결과 목록에서 삭제됩니다.
문서 update시 이전 버전의 문서는 delete 될 것이며 새 문서가 새 segment에 색인됩니다. 마찬가지로 쿼리되지만 최종 쿼리 결과 반환전에 결과 목록에서 삭제됩니다.
기존 segment는 불변이므로 refresh 과정에서 매초 새로운 segment가 추가되니(물론 항상 매초 리프레시가 일어나진 않습니다. 지난 30초간 검색 요청을 한개이상 받은 인덱스에 대해 refresh를 진행합니다. 즉 검색요청을 받지 않았다면 refresh는 일어나지않겠죠) 개수는 늘어날 것입니다. 그러면 모든 segment를 search 요청마다 체크해야 하니 search latency가 느려집니다. 그래서 background에서는 segment merge 작업이 일어나게됩니다. 이때 삭제된 문서가 file system에서 제거되게 됩니다.
Merge 과정은 background에서 수행되어 indexing이나 searching을 interrupt하진 않습니다. commit point에서 작은 segment들이 삭제되고 큰 segment가 추가됩니다.

Shard 크기, 개수 참고

Elasticsearch shards의 크기가 작으면 덩달아 segments도 작아져 오버헤드가 증가합니다.
Elasticsearch에서 각 쿼리는 샤드당 single thread로 실행됩니다. 그러나 동일한 샤드에 대해 여러 쿼리 및 집계가 수행될 수 있듯이 여러 개의 샤드를 동시에 처리할 수도 있습니다. 캐싱을 사용하지 않을 때 최소 query response time은 데이터, 쿼리 유형, 샤드 크기에 따라 달라집니다. 많은 개수의 작은 샤드에 대한 쿼리 요청은 샤드당 처리 속도는 병렬로 빨라질 수 있지만 더 많은 작업을 큐에 넣고 순서대로 처리해야하기에 빠름을 완전히 보장할 수는 없습니다.


참고문헌

https://www.elastic.co/guide/en/elasticsearch/guide/current/inside-a-shard.html

Inside a Shard | Elasticsearch: The Definitive Guide [2.x] | Elastic

In Life Inside a Cluster, we introduced the shard, and described it as a low-level worker unit. But what exactly is a shard and how does it work? In this chapter, we answer these questions: Why is search near real-time? Why are document CRUD (create-read-u

www.elastic.co

https://coding-start.tistory.com/176?category=757916

Elasticsearch - 엘라스틱서치와 루씬의 관계 - 1

엘라스틱서치의 구성요소 엘라스틱서치는 기본적으로 클러스터라는 단위로 데이터를 제공한다. 클러스터는 하나 이상의 물리적인 노드로 이루어져 있으며 각 노드는 모두 데이터 색인 및 검색

coding-start.tistory.com

https://www.elastic.co/kr/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster

How many shards should I have in my Elasticsearch cluster?

편집자 노트: "힙 메모리의 GB당 20 샤드 이하를 목표로 하는 것"에 대한 경험 법칙은 버전 8.3에서 더 이상 사용되지 않습니다. 이 블로그는 새로운 권장 사항을 반영하도록 업데이트되었습니다. E

www.elastic.co

도서: Elasticsearch 실무 가이드