엘라스틱서치는 다양한 데이터의 색인이 가능하고, 근실시간 검색을 제공을 목표로 개발되었다. 다수의 서버가 논리적으로 연결되어 수평 확장이 가능하도록 설계된 고가용성 플랫폼이다. 엘라스틱서치는 색인과 검색이 매우 빠르게 일어나는데, 색인 결과가 물리적인 디스크에 생성되는데 사용자에게 실시간에 가까운 검색이 제공될 수 있는 이유는 무엇인지 알아보자.!
색인 작업 시 세그먼트의 기본 동작 방식

- 하나의 루씬 인덱스는 내부적으로 다수의 세그먼트로 구성되어 있으며, 읽기 성능이 중요한 검색엔진에서는 다수의 세그먼트로 나누어져 있는 것이 더 효율적이다.
- 루씬은 검색 요청을 받으면 다수의 작은 세그먼트 조각들이 각각 검색 결과 조각을 만들고, 이를 통합해 하나의 결과로 합쳐서 응답하도록 설계되어 있다.(세그먼트 단위 검색)
- 세그먼트는 역색인 구조를 지닌 파일 자체를 의미하며, 세그먼트 내부는 실제 색인된 데이터가 역색인 구조로 저장되어 있다.

- 색인 작업 요청이 들어오면, IndexWriter에 의해 색인 작업이 이뤄지고, 세그먼트가 생성되며, 커밋 포인트에 기록되며 검색 작업을 요청할 때는 IndexSearcher가 커밋포인트를 이용해 모든 세그먼트를 읽어 검색 결과를 제공한다.
- 시간이 흐를수록 세그먼트의 개수는 늘어나 읽기 성능이 저하되기 때문에, 루씬이 백그라운드에서 주기적으로 세그먼트 파일을 Merge(병합)하는 작업을 수행하고, 모든 세그먼트들을 물리적으로 하나의 파일로 병합한다.
- 루씬은 색인 작업 시, 생성되 세그먼트에 정보를 추가하거나 변경하지 않고, 세그먼트 파일을 생성한다.
- 루씬은 기본적으로 한번 디스크에 저장한 세그먼트는 수정이 불가능하도록 특별하게 관리된다.
- 주기적으로 Merge 작업에 의해 세그먼트가 통합되고 삭제되기 전까지는 수정을 허용하지 않기 때문에, 색인 작업이 수행될 때마다, 세그먼트가 추가로 생성될 수밖에 없는 것이다.
데이터 추가 요청 시, IndexWriter을 동작 방식
최초 색인이 요청된 경우
- IndexWriter가 세그먼트를 생성
- IndexSearcher가 생성된 세그먼트를 읽어 검색을 제공
추가 색인이 요청된 경우
- IndexWriter가 세그먼트를 추가 생성
- 세그먼트가 추가 생성되는 동안 기존 세그먼트만 읽어 검색 결과를 제공
- 세그먼트 생성이 완료되면, 모든 세그먼트를 읽어 검색 결과를 제공
주기적으로 세그먼트 Merge 작업이 일어날 경우
- IndexWriter가 merge 대상이 되는 세그먼트들을 복제한다.
- IndexWriter가 복제한 세그먼트들을 하나의 세그먼트로 합친다.
- 복제본 세그먼트들이 하나로 합쳐지는 동안 IndexSearcher는 원본 세그먼트를 읽어 검색 결과를 제공한다.
- 복제본 통합 작업이 완료되면 원본 세그먼트와 교체하고 교체된 원본 세그먼트들은 삭제한다.
- IndexSearcher는 새로운 세그먼트를 읽어 검색 결과를 제공한다.
세그먼트 불변성
루씬에서 수정을 허용하지 않는 세그먼트의 동작 방식을 불변성이라고 부른다. 대용량 텍스트를 다뤄야 하는 역색인 구조에서는 불변성이 제공하는 여러 장점이 존재한다.
- 동시성 문제 회피
- 불변성이 보장되면 Lock이 필요 없어진다.(수정이 불가능하므로 동시성 문제 회피)
- 시스템 캐시를 적극적 활용
- 데이터가 OS 커널에서 제공하는 시스템 캐시에 한번 생성되면 일정 시간 동안 그대로 유지가 된다.
- 불변성을 보장하기 때문에 시스템 캐시를 삭제하고 재생성할 필요가 없기 때문에 시스템 캐시를 적극 활용 가능하다.
- 높은 캐시 적중률 유지
- 시스템 캐시 수명이 길어진다. 검색 시, 데이터를 항상 메모리에서 읽어 올 수 있다는 의미로 성능 향상을 꾀할 수 있다
- 리소스를 절감
- 역색인을 만드는 과정에서 많은 시스템 리소스가 사용되고, 수정이 허용되면 일부분이 변경되어도 작업 대상이 많아지기 때문에 리소스 소비가 심하다.
세그먼트 불변성으로 생기는 단점도 존재한다.
- 수정이 불가능하다.
- 일부 데이터가 변경되더라도 전체 역색인 구조가 다시 만들어져야 한다.
- 실시간 반영이 어렵다.
- 변경사항 반영 시, 역색인을 새롭게 만드는 작업이 반드시 동반돼야 하고, 변경이 매우 빠르게 발생할 경우에는 실시간 반영 자체가 불가능해진다.
세그먼트의 불변성은 확실히 단점보다는 장점이 더 많다. 상대적으로 읽기 연산의 비중이 큰 루씬은 세그먼트 불변성을 부여함으로써 읽기 연산을 성능을 대폭 끌어올릴 수 있다. 추가(Insert) 연산 같은 경우에도 새로운 세그먼트를 생성해 다수의 세그먼트를 가져가는 전략으로, 불변성을 깨지 않으면서도 나쁘지 않은 성능을 보장한다.
세그먼트 불변성과 업데이트
루씬의 수정 연산
- 세그먼트의 불변성을 유지하기 위해 해당 데이터를 삭제한 후, 다시 추가하는 방식으로 동작한다.
- 기존 데이터는 삭제 처리되어 검색 대상에서 제외되고, 변경된 데이터는 새로운 세그먼트로 추가되어 검색 대상에 포함된다.
- 수정 연산을 내부적으로 삭제 후 추가 방식으로 사용함으로써, 불변성을 지키면서 수정 기능을 제공할 수 있다.
- IndexWriter을 동작 과정
- 세그먼트 삭제 처리를 먼저 수행한다(실제 삭제 X, 삭제 여부 비트 배열 값 변경)
- 수정된 데이터를 새로운 세그먼트로 생성
- IndexSearcher는 모든 세그먼트를 읽어 검색 결과를 제공한다.
루씬의 삭제 연산
- 모든 문서에는 삭제 여부를 표시하는 비트 배열이 내부적으로 존재하는데, 삭제 요청 시 삭제될 대상 데이터의 비트 배열을 찾아 삭제 여부만 표시하고 끝낸다.
- 삭제 표시된 데이터는 실제 제거가 된 데이터가 아니므로 여전히 세그먼트 내부에 물리적으로 남게 되지만, 검색 시 비트 배열에 설정된 삭제 여부 값을 항상 먼저 판단하기 때문에 불변성을 훼손하지 않고도 빠르게 검색 대상에서 제외시킬 수 있다.
- IndexWriter을 동작 과정
- 루씬은 삭제될 데이터가 포함된 세그먼트의 삭제 여부 비트 배열을 확인
- 삭제 여부 비트 배열의 flag를 삭제로 표시
- 세그먼트에 직접적인 변경사항은 없으므로 세그먼트 불변성을 지키며, 캐시도 그대로 유지
- IndexSearcher는 검색 작업 시 삭제 여부 비트 배열을 항상 먼저 확인하고 삭제 여부가 체크된 데이터를 검색 결과에서 제외한다.
- 삭제 표시된 데이터가 삭제되는 시점은 Merge가 수행될 때다.
- 역색인 구조로 된 문서를 삭제하기 위해서는 전체 역색인 구조를 뒤져서 관련된 모든 텀을 제거해야하기 때문에 세그먼트를 다시 생성하는 것과 크게 차이가 없다.
- 그래서, 즉시 삭제하는 것이 아닌 주기적으로 세그먼트가 재생성되는 Merge 작업을 기다렸다가 물리적인 삭제 처리를 함께 진행한다.
루씬을 위한 Flush, Commit, Merge
루씬은 효율적인 색인 작업을 위해 내부적으로 일정 크기의 버퍼를 가지고 있고 이를 메모리 버퍼라고 한다. 만약 이 버퍼가 없었다면, 데이터가 들어올 때마다 동기적으로 작업을 수행해야 하기 때문에 요청 시마다 매번 세그먼트를 만들어야 했을 것이다. 또한 순간적으로 요청이 많아지면 지연이 발생하고 서비스 장애로 이어질 것이다.

- 루씬은 색인 작업이 요청되면 전달된 데이터를 인메모리 버퍼에 순서대로 쌓은 후, 정책에 따라 내부 버퍼에 일정 크기 이상의 데이터가 쌓이거나, 일정 시간이 흐를 경우 버퍼에 쌓인 데이터를 모아 한번에 처리한다(큐 역할로 사용한다.)
- 버퍼에 모여 한번에 처리된 데이터는 세그먼트 형태로 생성되고 즉시 디스크로 동기화된다.(동기화 과정까지 거쳐야 검색 가능)
- 디스크에 물리적으로 동기화하는 과정은 비용이 큰 연산이기 떄문에 세그먼트가 생성될 때마다 물리적인 동기화를 할 경우에는 성능이 나빠질 수 있다.
- 루씬은 이러한 문제를 해결하기 위해 무거운 fsync 방식이 아닌 가벼운 write 방식을 이용해 쓰기 과정을 수행한다.
참고
write()
일반적으로 파일을 저장할 때 사용하는 함수다. 운영체제 내부 커널에는 시스템 캐시가 존재하는데 write() 함수를 이용하면 일단 시스템 캐시에만 기록되고 리턴된다. 이후 실제 데이터는 특정 주기에 따라 물리적인 디스크로 기록된다. 물리적인 디스크 쓰기 작업을 수행하지 않기 떄문에 빠른 처리가 가능한 반면 최악의 경우 시스템이 비정상 종료될 경우에는 데이터 유실이 발생할 수 있다.
fsync()
저수준의 파일 입출력 함수다. 내부 시스템 캐시의 데이터와 물리적인 디스크의 데이터를 동기화하기 위한 목적으로 사용한다. 실제 물리적인 디스크로 쓰는 작업을 수행하기 때문에 상대적으로 많은 리소스가 사용된다.
Flush
- 이러한 인메모리 버퍼 기반 처리 과정을 Flush라고 부른다.
- 데이터의 변경사항을 일단 버퍼에 모아뒀다가 일정 주기에 한 번씩 세그먼트를 생성하고 상대적으로 적은 비용으로 디스크에 동기화하는 작업까지 수행.
- flush 처리에 의해 세그먼트가 생성되면, 커널 시스템 캐시에 세그먼트가 캐시 되어 읽기가 가능해지며, 루씬의 reOpen() 함수를 이용해 IndexSearcher에서도 읽을 수 있는 상태가 된다.
ReOpen()
루씬에서는 indexSearcher가 일단 생성되고 나면 이후 변경된 사항들을 기본적으로 인지하지 못한다. 기존 IndexSearcher를 Close 하고 다시 생성하면 변경된 사항을 인지하는 것이 가능하지만 문서의 추가나 변경이 빈번하게 일어날 경우 많은 리소스가 필요해지기 때문에 권장하지 않는다.
이때 사용할 수 있는 것이 ReOpen()이며, 일정 주기마다 문서가 업데이트된다면 ReOpen() 함수를 이용해 좀 더 효율적으로 리소스를 사용할 수 있다.
https://lucene.apache.org/core/3_0_3/api/core/org/apache/lucene/index/IndexReader.html#reopen0
최신 버전에서는 openIfChanged() 함수를 사용하자
Commit
- 루씬에서는 물리적으로 디스크에 기록을 수행하는 fsync() 함수를 호출하는 작업을 Commit이라고 한다.
- Flush라는 단계가 있어 매번 Commit()을 수행할 필요는 없지만 일정 주기로는 Commit 작업을 통해 물리적 디스크로 기록하는 작업이 필요하다.
Merge
- 늘어난 다수의 세그먼트를 하나로 합치는 작업을 Merge라고 한다.
- 장점
- 검색 성능이 좋아진다(Merge 시 세그먼트 갯수도 줄어들고 검색 횟수도 감소하기 때문)
- 세그먼트가 차지하는 디스크 용량이 줄어든다(삭제된 문서는 Merge 전에는 디스크에 남아 있지만 Merge 작업을 통해 세그먼트를 새롭게 생성하게 되면 디스크에서 삭제되기 때문)
- Merge 작업은 Commit 작업을 반드시 동반해야 하는데, Commit 작업은 비용이 매우 크기 때문에 정책적으로 적절한 주기를 선택하는 것이 중요하다.
- 루씬은 특정 주기로 세그먼트 Merge 작업을 수행하며, 작업 주기는 최적의 성능을 낼 수 있게 설정되며 백그라운드로 수행된다.
| 메서드 | 정의 | 비고 |
| flush | 세그먼트가 생성된 후 검색이 가능해지도록 수행하는 작업 | - write() 함수로 동기화가 수행되기 때문에 커널 시스템 캐시에만 데이터 생성 - 유저 모드에서 파일을 열어서 사용하는 것이 가능해진다. - 물리적인 디스크에 쓰여진 상태는 아니다. |
| commit | 커널 시스템 캐시의 내용을 물리적인 디스크로 쓰는 작업 | - 실제 물리적인 디스크에 데이터가 기록되기 때문에 많은 리소스 필요 |
| merge | 다수의 세그먼트를 하나로 통합하는 작업 | - Merge 과정을 통해 삭제 처리된 데이터가 실제 물리적으로 삭제된다. - 검색할 세그먼트의 갯수가 줄어 검색 성능이 좋아진다. |
Refresh, Flush, Optimize API
루씬의 Flush, Commit, Merge 함수를 그대로 사용하지는 않고 고가용성에 맞춰 적합하도록 개선 및 확장한 Refresh, Flush, Optimize API를 제공한다.
Refresh
- 엘라스틱서치는 각 샤드에 가지고 있는 루씬을 제어할 수 있으며, 주기적으로 인메모리 버퍼에 대해 Flush 작업을 하는데, 이를 엘라스틱서치에서는 Refresh라고 한다.
- 클러스터에 존재하는 모든 샤드에서는 기본적으로 1초마다 Refresh 작업이 수행된다.
- Refresh 주기를 수동으로 조절할 수 있는 API를 제공하지만 주기를 변경하는 것은 권장하지 않는다.
- Commit 보다는 가볍긴하나, 비용이 발생하는 연산이기도 하고 전체적인 성능에도 영향을 줄 수 있는 작업이기 때문이다.
_setting API를 이용 시, Refresh 주기를 변경할 수 있다.
엘라스틱서치에서는 기본적으로 1초 주기로 동작하는데, 1초마다 Refresh 작업을 수행하기 때문에 대량의 데이터를 색인할 경우 성능이 크게 저하될 수 있다. 그러므로 대량의 데이터를 색인할 경우에는 Refresh 작업을 비활성화하고 색인 작업이 완료된 후 원래 설정으로 돌리는 것이 좋다.
//Refresh 연산 비활성화 PUT /movie/_settings { "index": { "refresh_interval": "-1" } } //Refresh 연산 활성화 PUT /movie/_settings { "index" : 1 "refresh_interval" : "1s" }
Flush
- 루씬의 Commit 작업을 수행하고 새로운 Translog를 시작하는 작업이다.
- Translog는 샤드의 장애 복구를 위해 제공되는 특수한 파일이다.
- 엘라스틱서치 샤드는 자신에게 일어나는 모든 변경사항을 Translog에 먼저 기록 후 내부에 존재하는 루씬을 호출한다.
- 시간이 흐를수록 Translog 파일 크기는 계속 커지고 샤드는 1초마다 Refresh 작업을 수행하지만 실제 디스크에 물리적인 동기화는 되지 않기 때문에 주기적인 루씬 Commit을 수행해야 한다.
- 루씬 Commit이 정상적으로 수행되면, 변경사항이 디스크에 물리적으로 기록되고, Translog 파일에서 Commit이 정상적으로 일어난 시점까지의 내역이 삭제된다.
- 엘라스틱서치는 기본적으로 5초에 한 번씩 Flush 작업이 수행되며, API를 통해 해당 주기를 변경할 수 있으나 변경하지 않는 것을 권장한다.
Optimize
- 인덱스 최적화를 위해 제공하는 API이며, 루씬 Merge 작업을 강제로 수행하는 기능이다.
- 파편화된 다수 세그먼트를 하나의 커다란 세그먼트로 통합해 좀 더 빠른 성능을 제공할 목적으로 사용된다.
- 검색 성능을 높이기 위해서는 파편화된 세그먼트의 수를 최소화하는 것이 매우 중요하다.
- 일반적으로 변경이 더 이상 일어나지 않는 오래된 인덱스의 경우 하나의 세그먼트가 되도록 인덱스 내부의 세그먼트들을 강제로 병합하는 것이 성능상 유리하다.
오래된 세그먼트 강제 병합
엘라스틱서치에서 제공하는 max_num_segments 옵션을 이용하면, 샤드의 세그먼트를 설정된 개수로 강제로 병합할 수 있다. 더 이상 변경이 없다면 세그먼트 수를 하나로 강제 병합하는 편이 읽기 측면에서 좋다.
//movie 인덱스의 세그먼트를 하나로 병합
POST /movie/_forcemerge?max_num_segments=1
엘라스틱 서치와 NTR(Near Real-Time)
엘라스틱서치 샤드는 루씬의 기능을 확장해서 제공한다.(루씬의 각종 기능을 Rest API 형식으로 제공하여 손쉽게 사용 가능)
샤드는 "장애 복구 기능을 가진 작은 루씬 기반의 단일 검색 서버"로 보면 되는데, 이러한 작은 검색 서버들이 모여 커다란 엘라스틱서치 클러스터를 구성하는 것이다.
사용자가 엘라스틱서치 인덱스를 검색하면, 인덱스에 포함된 모든 샤드로 동시에 요청이 보내지고 요청을 받은 각 샤드에서 커밋포인트를 이용해 내부에 존재하는 모든 세그먼트들을 순서대로 검색한 후 결과를 전달한다. 샤드로 요청을 보낸 엘라스틱서치는 모든 샤드로부터 검색 결과가 도착할 때까지 기다리고 검색 결과가 도착하면, 하나의 커다란 결과 셋을 만들어 사용자에게 전달한다.
샤드는 내부에는 루씬을 핵심으로 사용하고 있기 때문에 루씬이 가지는 불변성을 그대로 활용할 수 있고, 이를 기반으로 실시간에 가까운 검색 결과를 제공할 수 있다. 또한 루씬에서 제공하는 Flush, Commit, Merge 작업을 확장해 분산처리에 적합하게 제공하기 때문에 대용량 데이터 처리가 가능해지며, 준실시간으로 검색 결과를 제공할 수 있다.
'Elastic Search' 카테고리의 다른 글
| Ch09. 엘라스틱서치와 루씬 이야기- 샤드 최적화 (0) | 2025.10.09 |
|---|---|
| Ch09. 엘라스틱서치와 루씬 이야기- 고가용성을 위한 Translog의 비밀 (0) | 2025.10.08 |
| Ch09. 엘라스틱서치와 루씬 이야기 - 샤드 VS 루씬 인덱스 (0) | 2025.10.07 |
| Ch09. 엘라스틱서치와 루씬 이야기 - 클러스터 관점에서 구성요소 살펴보기 (0) | 2025.10.05 |
| Ch08. 엘라스틱서치 클라이언트 - Transport 클라이언트 (0) | 2025.10.03 |