Elastic Search는 내부적으로 Lucene을 사용해 검색 기능을 수행하기에 Lucene의 동작원리를 이해하면 Elastic Search를 이해하는 데 크게 도움이 된다.
9.1 Cluster 관점에서 구성요소 (물리적 레이아웃)
Elastic Search의 구조 (하향식)
- 최상위 개념 : Cluster
- 최하위 개념 : Segment
Cluster
데이터를 실제로 가지고 있는 노드의 모음
- Elastic Search에서 데이터를 제공하는 기본 단위
- Cluster는 관련된 모든 노드들을 논리적으로 확장해서 구성, 하나 이상의 물리적인 노드로 구성 됨
- 각 노드는 모두 데이터 색인 및 검색 작업 가능, 데이터 복구를 위한 작업도 가능한 일종의 물리적인 서버
- 같은 Cluster 내부의 데이터만 서로 공유가능하기에 연관된 모든 노드가 하나의 Cluster 구성원으로 연결되는 것이 매우 중요
- 연결 방법? 같은 Cluster를 구성하는 노드들은 같은 Cluster 이름으로 설정하면 된다. Cluster이름을 이용해 같은 구성원임을 인식하기 때문
- 내부에 Lucene 라이브러리가 있어 핵심 모듈을 구성
다수의 클러스터를 한 번에 검색할 수 있는 ES의 기능
- 사용 이유?
- 일반적으로는 검색 시 하나의 클러스터 데이터만 검색하는 것이 원칙
- 하지만, 시간이 흐르다보면 데이터가 점점 커지고 복잡해져
최초 설계 시 전혀 연관성이 없어보이던 데이터들의 연관이 필요해지거나 데이터가 너무 커져서 어쩔 수 없이 클러스터를 분리해야 하는 일 생김
- 필연적으로 다수의 클러스터를 함께 검색해야 하는 상황이 빈번하게 발생할 때 사용
[참고]
과거에는 Tribe Node
로 다수의 클러스터를 검색할 수 있는 기능이 제공되었으나, 현재는 deprecated 되어 사용 X
Node
물리적으로 실행된 Runtime 상태의 Elastic Search
- Node는 논리적인 Cluster를 이루는 구성원의 일부
- 실제 데이터를 물리적으로 가지고 있는 단일 서버
- Cluster 내에서 Node 식별 방법?
- (1) Node는 실행 시 Cluster에 의해 UUID가 할당되고
(2) Cluster 내에서는 할당된 UUID로 서로를 식별한다.
- Node는 내부에 다수의
Index
를 가지고 있으며, 각 Index는 다수의 Document
를 가진다.
- indexing을 통해 Elastic Search로 전송한 데이터는
Index
라는 논리적인 자료구조 속에 Document
라는 형태로 저장된다.
- 같은 Cluster 내부에 존재하는 모든 Node는 서로 다른 Node와 수시로 정보를 주고 받는다.
- 안정적인 Cluster 운영을 위해 Node를 서비스 목적에 맞게 역할을 분리하여 운영하는 것을 권장
- Node의 역할별 형태
- Master Node
: 클러스터의 제어 담당
- Data Node
: 데이터 저장, CRUD, 검색, 집계 등 데이터 작업
- Ingest Node
: indexing 전 전처리 작업
- Tribe Node
: 여러 클러스터를 제한적으로 연결하여 동시에 검색 수행. 현재는 Cross Cluster Search 기능으로 대체
- Coordinating Node
: 검색이나 집계 시 분산처리, 대량의 데이터 처리 시 효율적인 노드
Index
유사한 특성을 가지고 있는 문서를 모아둔 문서들의 컬렉션
- Elastic Search Index의 특징
- Cluster 내부에 생성되는 모든 Index는 유일한 Index명을 가져야 한다. (Index명을 이용해 데이터 생성, 수정, 삭제, 검색이 이루어지기 때문)
- Index명은 반드시 모두 소문자로 설정
- 많은 양의 Document 저장 가능
[Elastic Search Index와 Lucene Index의 차이]
Lucene에서는 Indexing(Full-Text를 분석) 결과를 물리적인 디스크로 저장하며 이를 Index라고 한다.
Elastic Search Index와 다르다. 구분 필요!!
Document
검색 대상이 되는 실제 물리적인 데이터
- Document는 Index를 생성할 수 있는 가장 기본적인 정보 단위
- ES에서는 JSON 형식으로 표현
- Index에 원하는 만큼의 많은 Document 저장 가능
- 실제로는 물리적인 shard 형태로 나눠져 다수의 노드로 분산 저장
Shard
데이터 저장 시 물리적인 한계를 뛰어넘기 위해 분산 저장하는 방법
- Shard는 Index의 전체 데이터를 분산해서 가지고 있는 일종의 부분집합
- 전체 데이터의 부분집합이지만 각 Shard는 자신이 가지고 있는 데이터만으로도 독립적으로 검색 서비스 가능
- 실제로 Index에 query를 요청하면,
(1) Index가 가지고 있는 모든 Shard로 검색 요청 전달
(2) 각 Shard에서 1차적으로 검색 한 후
(3) 그 결과를 취합하여 최종 결과로 제공
- 색인 생성 시점에서 Shard 개수 지정 가능, 색인 후에는 Shard의 수 수정 불가 (기본적으로 5개의 Shard)
- 이유? 각 Shard는 내부에 독립적인 루씬 라이브러리를 가지고 있고 루씬은 단일 머신 위에서만 동작한다. 이때문에
샤드 내부의 루씬
입장에서는 다른 샤드의 존재를 알지 못하고, 자신이 전체 데이터 중 일부만 가지고 있다는 사실도 알 수가 없다.
따라서 샤드(프라이머리)의 개수를 변경한다는 것은 각각 독립적인 루씬이 가지고 있는 데이터들을 모두 재조정해야하지만 루씬 내부의 세그먼트는 불변성 때문에 루씬 내부의 세그먼트를 쪼개서 보내고 합치는 과정 불가 (ReIndex 밖에 방법이 없다)
- Shard는 데이터 원본이다. Shard가 Node에 저장된 상태에서 새로운 Node가 추가된다면, 기존에 존재하던 Shard들은 각 노드에 균일하게 relocationg 된다.
[샤딩이 중요한 이유]
1. 지속적으로 증가하는 콘텐츠를 수평적으로 분할하여 하드웨어의 한계 극복
2. 여러 node에서 shard를 통해 분산 처리 되므로 성능, 처리량 향상
Replica
Shard의 복제본.
장애 복구를 위해 존재하며, 평상 시에는 검색 시 읽기 속도 향상을 위해 사용
- Index 생성 시 기본적으로 1개의 Replica set를 생성한다.
- 레플리카 샤드는 운영 중에도 개수 변경 가능. 프라이머리 샤드를 단순히 복제만 하면 되기 때문 -> 최초 서비스 오픈 시 복제본의 수를 최소화 해서 서비스를 운영하는 것이 좋다. 노드의 장애나 데이터량에 따른 읽기 분산을 지속적으로 머니터링하고 이를 바탕으로 탄력적으로 복제본의 수를 조절하는 것을 권장
- Failover 매커니즘 제공 시 replica 사용하여 안정적인 클러스터 운영 보장
- 분산 환경에서 특정 노드가 오프라인으로 변경될 경우 장애 처리 방법
- 최초 인덱스 생성 시 settings 속성을 이용해 샤드와 레플리카의 개수 정의 가능
- 레플리카가 많아질수록 색인 성능 비례하여 감소한다. 따라서 읽기 분산이 중요한 경우 색인 성능을 일부 포기하고 레플리카 세트 수를 늘리고, 빠른 색인이 중요한 경우에는 읽기 분산을 일부 포기하고 레플리카 수를 최소화
PUT /movie
{
"settings" : {
"index" : {
"number_of_shards" : 5,
"number_of_replicas" : 1
}
}
}
- 예제의 경우 샤드 개수는?
- 5개의 프라이머리 샤드 + 5개의 레플리카 샤드 = 10개의 샤드
Segment
루씬 내부에 존재하는 역색인 구조의 자료구조
역색인 구조로 되어있어 읽기에 최적화
하나의 루씬 내부에서만 존재할 수 있고 확장 불가
Lucene index
검색과 색인 기능을 가진 최소한의 검색엔진
IndexWriter로 색인하여 segment 생성
IndexSearcher로 segment 검색
자신이 가진 segment 내에서만 검색 가능
ElasticSearch shard
ES에서 제공하는 가장 작은 단위의 검색엔진
내부적으로 루씬을 확장하여 검색엔진 역할 수행
다수의 샤드가 협력해서 존재하는 모든 세그먼트 검색 가능
9.2 Elastic Search shard vs Lucene index
하나의 ES shard는 하나의 Lucene index라고 할 수 있다.
ES는 독립적인 Lucene index를 ES shard라는 형태로 확장해서 제공한다.
- ES shard : 실제 데이터를 가지고 있는 최소 단위의 검색 모듈
- 모든 shard가 가지고 있는 segment들을 논리적으로 통합하여 검색 가능
- Lucene index가 가지는 물리적 한계를 극복하여 데이터를 무한으로 확장
- Lucene index : IndexWriter와 IndexSearcher를 가지고 색인과 검색을 동시에 제공하는 루씬 인스턴스
- 자기 자신이 가지고 있는 segment 내에서만 검색 가능
- 데이터 저장 시 물리 머신이 제공하는 리소스의 한계를 뛰어넘을 수 없다는 단점 존재
- 그렇다면, ES index는?
- 물리적으로 분산된 ES shard를 논리적인 관점에서 하나의 거대한 데이터로 바라본 것
9.3 Elastic Search가 근실시간(Near Real-Time) 검색을 제공하는 이유
ES는 처음부터 근실시간 검색 제공
을 목표로 설계되었다.
검색 과정
- 사용자가 ES index를 검색하면 index에 포함된 모든 shard(lucene index)로 동시에 요청 전송
- 각 shard에서는 commit point를 이용해 내부에 존재하는 모든 segment들을 순서대로 검색 후 결과 전달
- ES는 모든 shard로부터 도착한 검색 결과를 하나의 커다란 결과 셋으로 만들어 최종적으로 사용자에게 전달
어떻게 색인 결과가 물리적인 디스크에 생성되는데도 불구하고 실시간에 가까운 검색이 가능할까?
(1) segment 구조
- 루씬은 검색 요청을 받으면 다수의 작은 segment 조각들이 각각 검색 결과 조각을 만들어내고 통합하여 결과를 응답하도록 설계된 검색방식(Per-Segment Search)을 가진다.
- 역색인 구조이기에 빠른 읽기 검색
- segment 불변성 (수정 절대 비허용)
- 역색인 구조에서의 불변성이 가지는 장점
- 동시성 문제 회피 : 다중 스레드 환경에서 불변성이 보장되기에 Lock 필요없어짐
- 시스템 캐시 활용 : 데이터가 시스템 캐시에 한번 생성되면 일정 시간 동안은 그대로 유지 (데이터 변경될 때마다 시스템 캐시를 삭제하고 다시 생성해야 하는 비용 제거)
- 높은 캐시 적중률 유지 : 검색 시 데이터를 항상 메모리에서 읽어올 수 있어 시시ㅡ템 캐시의 수명이 길어짐
- 리소스 절감 : 역색인을 만드는 과정에서 많은 시스템 리소스(CPU, memory I/O) 사용. 수정이 허용되었다면 일부분이 변경되더라도 다시 역색인 해야하기에 시스템 리소스 절감
- 단점
- 수정 불가 : 일부 데이터가 변경되더라도 새로 역색인을 만들어야 한다.
- 실시간 반영의 상대적 어려움 : 변경사항을 반영하려면 반드시 새로운 역색인을 만들어야하는데 변경이 매우 빠르게 일어날 경우 실시간 반영 자체가 불가능
- 단점 극복 방법!
- 다수의 segment를 생성해서 제공
- 변경이 일어날 때마다 segment를 다시 만드는게 아니라,
기존 segment는 그대로 두고 추가로 segment를 생성!
그리고 검색 요청 시에는 생성된 모든 segment를 읽어서 검색결과를 제공
- 불변성을 해치지 않는 update, delete
- update(수정) : 기존 데이터를 삭제하여 검색 대상에서 제외, 변경 데이터는 새로운 segment로 추가하여 검색 대상에 포함
- delete(삭제) : 삭제 여부 비트 배열을 찾아 삭제여부만 표시 (물리적 삭제X)
Merge 작업이 수행될 때 물리적으로 삭제
(2) Commit Point 자료구조
- 여러 segment의 목록 정보를 가지고 있어 색인, 검색 시 Commit Point 활용
- IndexSearcher가 Commit Point를 이용해 모든 segment를 읽어 검색결과를 제공
- 루씬의 IndexSearcher는 검색 시 Commit Point를 이용해 가장 오래된 segment부터 차례대로 검색한 후 각 결과를 하나로 합쳐서 제공
- 최초 Indexing 작업 요청이 들어오면 IndexWriter에 의해 색인 작업 이루어지고 결과물로 하나의 segment가 생성.
- 색인 작업을 할 때마다 새로운 segment가 추가로 생성되고 Commit Point에 기록된다. -> 기존 segment에 정보를 추가하거나 변경하지 않고 매번 새로운 segment를 만들기에 segment의 개수는 점점 늘어난다. (다수의 파일을 열어 읽어야 하는 성능 저하 방지)
- segment 개수가 시간이 흐를수록 빠르게 늘어나기에 주기적으로 background에서 segment 파일을
Merge
-> 읽기 성능 저하 방지
Lucene을 위한 Flush, Commit, Merge
Lucene Flush
- segment가 생성된 후 검색이 가능해지도록 수행하는 작업
- write() 함수로 동기화가 수행되었기에 커널 시스템 캐시에만 데이터가 생성
- 일단 시스템 캐시에만 기록되고 리턴. 이후 실제 데이터는 특정한 주기에 따라 물리 디스크에 기록. 시스템 비정상 종료 시 데이터 유실 가능성
- 이를 통해 유저 모드에서 파일을 열어서 사용하는 것이 가능
- 물리적으로 디스크에 쓰여진 상태는 아니다 (<-> fsync() 함수)
Lucene Commit
- 커널 시스템 캐시의 내용을 물리적인 디스크로 쓰는 작업
- 실제 물리적인 디스크에 데이터가 기록되기에 많은 리소스 필요
Lucene Merge
- 다수의 segment를 하나로 통합하는 작업
- Merge 과정을 통해 삭제 처리된 데이터가 실제로 물리적으로 삭제 처리 된다
- 검색할 segment의 개수가 줄어들어 검색 성능 향상, 디스크 용량 경량화
ES를 위한 Refresh, Flush, Optimize API
Lucene index가 가지는 대부분의 기능을 확장해 API로 제공.
대표적인 튜닝 포인트인 Flush, Commit, Merge를 확장하여 제공
ES Refresh
Lucene Flush -> ES Refresh
- 실시간 검색에 가깝게 동작하기 위해 주기적으로 인메모리 버퍼에 대해 Flush 작업 수행 (1초마다)
ES Flush
Lucene Commit -> Flush
- Lucene의 Commit 작업 수행 후 새로운 Translog 시작 (검색은 가능하지만 아직 디스크에 물리적으로 동기화 되지 않았기에 Commit 시 변경사항 Translog에 기록)
- fsync() 함수 사용하여 실제 물리 디스크에 변경 내용 기록
- Flush 작업 성공적으로 마무리되면 (물리 디스크 동기화 성공) 누적되어있던 Translog 파일 내용 삭제
ES Optimize API
Lucene Merge -> Optimize API
인덱스 최적화를 위해 강제 병합할 수 있는 Optimize API 제공
9.4 고가용성을 위한 Translog의 비밀
ES는 분산 환경에서 절대 고장나지 않는 시스템을 제공하기 위해 내부적으로 Translog라는 특수한 형태의 파일을 유지하고 관리한다.
Translog
: ES shard에서 일어나는 모든 변경사항을 담고 있는 로그
"장애 복구"를 위한 백업 데이터 및 데이터 유실 방지를 위한 임시 저장소로써 Translog 적극 활용
Flush 작업이 매우 무거운 작업이고 상대적으로 긴 시간동안 일어날 수 있기에 장애 대비 필요
Translog 동작 순서
- 데이터가 추가되면 Translog에 기록, 동시에 인메모리 버퍼에 추가
- Refresh가 수행되면 인메모리 버퍼에서는 사라지지만 Translog에는 계속 남아있다
- 더 많은 데이터가 추가되고 지속적으로 segment가 추가된다
- Translog가 일정 크기 이상으로 커지면 Flush 수행
- Lucene의 Commit Point가 디스크에 Flush
- 시스템 캐시의 내용이 디스크에 Flush
- Translog의 기록이 비로소 삭제
-
shard에 변경사항이 생길 때
- Translog 파일에 먼저 내역 기록 후 lucene index로 데이터 전달
- 루씬으로 전달된 데이터는 인메모리 버퍼로 저장되고, 주기적으로 처리되어 결과적으로 segment가 된다.
-
ES에서는 기본적으로 1초에 한번씩 Refresh 작업 수행 -> 이를 통해 추가된 segment의 내용을 읽을 수 있게 되고 검색에 사용되고 Translog에 내용은 쌓여간다.
하지만, Translog 파일에 로그가 계속해서 누적될 수는 없기에 특정 시점이 되면 불필요한 과거의 로그는 삭제된다.
-
ES Flush 작업이후 물리적으로 디스크 동기화에 성공한 시점에 누적되어있던 Translog 내용 삭제
- 물리적으로 기록됨은 영구적으로 보관됨을 의미하기에 이 시점까지의 로그가 더는 필요하지 않다
복구 방법
- 마지막 Lucene Commit 이후의 모든 내역을 재실행해서 segment를 재생성
- Translog 파일의 크기가 커질수록 장애 발생 시 복구에 걸리는 시간이 비례해서 늘어난다.
- 복구를 진행하는 도중에 재생성할 segment가 많아 다시 장애가 발생하는 경우가 생기는 등 전체 클러스터가 마비되는 대형 장애 유발 가능성이 있다. 그렇기에 데이터 크기나 양에 따라 적절한 정책을 세우고 Translog 크기를 관리하는 것이 무엇보다 중요
문제 발생 케이스
케이스 1
Flush에 의해 Lucene Commit 작업이 시작되었는데, 완료되지 못한 상황에서 샤드에 장애가 발생한다면?
해결 방법
샤드가 강제로 종료될 경우 Lucene Commit 작업이 rollback되기에
샤드가 정상적으로 재실행되고 나면 Translog 내용을 이용해 간단히 복구 가능
Translog의 내역을 바탕으로 순차적으로 인메모리 버퍼 복구, Refresh가 수행되면 다음 Flush 시점에 Lucene Commit이 수행
케이스 2
변경사항이 순간적으로 많아져서 Lucene Commit이 긴 시간동안 일어나게 되고 그동안 많은 데이터 변경 요청이 한꺼번에 샤드로 들어온다면?
해결 방법
Lucene Commit 작업이 수행되는 시간이 길어진다고해서 Commit이 일어나는 동안 샤드로 전달된 변경사항이 Commit 작업이 끝날 때까지 반영되지 않는다면 실시간 검색을 지원하는 것이 의미가 퇴색된다.
그래서 ES는 Commit이 일어나는 동안에 들어온 변경사항을 Lucene의 인메모리 버퍼로 전달하지 않고 Translog에 임시로 저장해두고 다음 Commit에 반영될 때까지 유지한다.
샤드는 이러한 특별한 상황에 대한 임시저장소로서 Translog가 사용된다는 사실을 이미 알고 있기 때문에
segment 검색 전, Translog에서 임시로 저장한 변경사항이 없는지 먼저 확인한다. -> 데이터 유실 방지
9.5 Elastic Search shard 최적화
레플리카 샤드의 복제본 수는 얼마가 적당?
프라이머리 샤드와 레플리카 샤드 모두 동일한 세그먼트 과정을 거쳐 만들어진다.
레플리카가 많아질수록 색인성능 떨어지기에 서비스 운영 시작 시 최소화 하여 운영하고 탄력적으로 개수를 조절해 나가는 것을 권장
클러스터에서 운영 가능한 최대 샤드 수?
이론상 클러스터에는 인덱스가 무한대로 생성될 수 있기에 특별한 제한이 없다.
하지만, 개별 인덱스를 생성할 때 설정 가능한 샤드의 수는 현재 1024개로 제한
모든 샤드는 마스터 노드에서 관리되기에 샤드가 많아질수록 마스터 노드의 부하도 덩달아 증가.
샤드의 물리적인 크기와 복구 시간
마스터 노드는 장애 발생 시 샤드 단위로 복구를 수행하기 때문에! 마스터 노드 입장에서는 샤드가 가지고 있는 데이터 건수보다 데이터의 물리적인 크기가 더욱 더 중요하다.
노드에 장애가 발생하면 장애가 발생한 primary shard와 동일한 데이터를 가지고 있는 replica shard가 순간적으로 primary shard로 전환되어 서비스 된다.
그와 동시에 primary shard로 전환된 shard와 동일한 shard가 물리적으로 다른 장비에서 replica shard로 새롭게 생성된다. 그리고 서비스는 한동안 이대로 유지된다.
시간이 지나 장애가 발생한 노드가 복구되면 복구된 노드로 일부 샤드들이 네트워크를 통해 이동한다. 이러한 이동 과정을 통해 전체 클러스터의 균형을 맞춘다.
References:
[책] 엘라스틱서치 실무 가이드 (저 권택환, 김동우
외 3명, 위키북스)
[블로그] https://sung7074.tistory.com/192