Search-Engine/Lucene 2019. 5. 25. 21:04

루씬은 색인 요청이 올때마다 새로운 세그먼트가 추가된다. 그리고 일정한 주기로 세그먼트들을 병합하는 과정을 갖는다. 만약 이러한 루씬에 인메모리버퍼가 하는 역할은 무엇일까? 우선 인메모리버퍼가 없는 루씬을 가정한다면, 만약 순간적으로 대용량의 데이터의 색인요청이 많아질 경우 세그먼트(역색인 파일)의 개수가 너무 많아져서 문제가 될 수 있다. 파일이 갑자기 많아지고 이는 당연히 색인에 지연이 생길 것이고 최종적으로 서비스 장애로 이어질 것이다. 하지만 실제적으로 루씬은 색인 작업이 요청되면 전달된 데이터는 일단 인메모리버퍼에 순서대로 쌓이고 버퍼가 일정크기 이상의 데이터가 쌓였다면 그때 한번에 모아서 색인처리를 한다. 즉, 버퍼가 일종의 큐역할을 하는 것이다. 버퍼에 모여 한번에 처리된 데이터는 즉시 세그먼트 형태로 생성되고 디스크로 동기화된다. 하지만 디스크에 물리적으로 동기화하는 일련의 과정은 운영체제 입장에서 비용이 큰 연산이기에 세그먼트가 생성될때마다 물리적인 동기화를 할 경우 성능이 급격히 나빠질 수 있다. 루씬은 이러한 문제점을 해결하기 위해 무거운 fsync 방식을 이용해 디스크 동기화를 하는 대신 상대적으로 가벼운 write 방식을 이용해 쓰기 과정을 수행한다. 이러한 방식으로 쓰기 성능을 높이고 이후 일정한 주기에 따라 물리적인 디스크 동기화 작업을 수행한다.

 

write() 함수

일반적으로 파일을 저장할 때 사용하는 함수다. 운영체제 내부 커널에는 시스템 캐시가 존재하는데 write() 함수를 이용하면 일단 시스템 캐시에만 기록되고 리턴된다. 이후 실제 데이터는 특정한 주기에 따라 물리적인 디스크로 기록된다. 물리적인 디스크 쓰기 작업을 수행하지 않기 때문에 빠른 처리가 가능한 반면 최악의 경우 시스템이 비정상 종료될 경우에는 데이터 유실이 일어날 수도 있다.

 

fsync() 함수

저수준의 파일 입출력 함수다. 내부 시스템 캐시의 데이터와 물리적인 디스크의 데이터를 동기화하기 위한 목적으로 사용된다. 실제 물리적인 디스크로 쓰는 작업을 수행하기 때문에 상대적으로 많은 리소스가 소모된다.

 

이러한 인메모리 버퍼 기반의 처리 과정을 루씬에서는 Flush라고 부른다. 데이터의 변경사항을 일단 버퍼에 모아두었다가 일정 주기에 한번씩 세그먼트를 생성하고 상대적으로 낮은 비용으로 디스크에 동기화 하는 작업까지 수행한다. 일단 Flush 처리에 의해 세그먼트가 생성되면 커널 시스템 캐시에 세그먼트가 캐시되어 읽기가 가능해진다. 커널 시스템 캐시에 캐시가 생성되면 루씬의 openIfChanged()을 이용해 IndexSearcher에서도 읽을 수 있는 상태가 된다.

 

openIfChanged() 함수

루씬에서는 IndexSearcher 객체가 생성되고 나면 이후 변경된 사항들을 기본적으로 인지하지 못한다. 기존 IndexSearcher를 Close하고 다시 생성하면 변경된 사항을 인지하는 것이 가능하지만 문서의 추가나 변경이 빈번하게 일어날 경우 많은 리소스가 필요해지기 때문에 권장하지 않는다. 이때 사용하는 것이 openIfChanged() 함수다. 일정 주기마다 문서가 업데이트된다면 openIfChanged()함수를 이용해 좀더 효율적으로 리소스를 사용할 수 있다.

 

하지만 최악의 경우에는 Flush만으로는 100% 데이터의 유실을 보장할 수 없다고 했다. 즉, fsync() 함수를 이용하여 언젠가는 반드시 동기화를 해야한다. 이러한 작업을 Commit이라고 한다. 매번 Commit하는 것이 아니고 Flush 작업을 몇번 한 이후에 일정 주기로 Commit작업을 통해 물리적인 디스크로 기록 작업을 수행해야한다.

 

아무리 루씬이 세그먼트 단위 검색을 지원하지만 시간이 지날수록 세그먼트 수가 많아지면 커밋 포인트의 부하도 증가하고 여러개의 세그먼트를 검색해야하기 때문에 검색 성능도 저하된다.그래서 일정주기 동안 여러개의 세그먼트는 하나의 세그먼트로 병합이 된다. 이러한 병합 처리에 여러 장점이 존재한다.

 

 

병합의 장점

  • 검색 성능 향상 : 검색 요청이 들어오면 루씬 내부에 존재하는 모든 세그먼트를 검색해야하는데, 각 세그먼트는 순차적으로 검색되므로 세그먼트를 병합하여 세그먼트 수를 줄이면 순차 검색 횟수도 줄어든다.
  • 디스크 용량 최소화 : 삭제되는 문서의 경우 병합 작업 전에는 삭제 플래그 값을 가지고 삭제되지 않고 물리적인 디스크에 남아있는다. 이러한 삭제 플래그를 가진 문서는 병합 작업을 시작해야 비로소 삭제된다.

 

이러한 병합 작업은 Commit 작업을 반드시 동반해야한다.

 

루씬 Flush 작업

  • 세그먼트가 생성된 후 검색이 가능해지도록 수행하는 작업
  • write() 함수로 동기화가 수행됬기 때문에 커널 시스템 캐시에만 데이터가 생성된다.이를 통해 유저 모드에서 파일을 열어 사용하는 것이 가능해진다.
  • 물리적으로 디스크에 쓰여진 상태는 아니다.

루씬 Commit 작업

  • 커널 시스템 캐시의 내용을 물리적인 디스크로 쓰는 작업
  • 실제 물리적인 디스크에 데이터가 기록되기 때문에 많은 리소스 필요

루씬 Merge 작업

  • 다수의 세그먼트를 하나로 통합하는 작업
  • Merge 과정을 통해 삭제 플래그 값을 가진 데이터가 실제 물리적으로 삭제 처리된다.
  • 검색할 세그먼트의 수가 줄어들기 때문에 검색 성능이 향상된다.

 

posted by 여성게
:

 

엘라스틱서치의 구성요소

엘라스틱서치는 기본적으로 클러스터라는 단위로 데이터를 제공한다. 클러스터는 하나 이상의 물리적인 노드로 이루어져 있으며 각 노드는 모두 데이터 색인 및 검색 기능을 제공하는 일종의 물리적인 서버와 같다. 내부에는 루씬 라이브러리를 사용하고 있으며 루씬은 엘라스틱서치의 근간을 이루는 핵심 모듈이다.

 

1)클러스터

클러스터는 데이터를 실제로 가지고 있는 노드의 모음이다. 엘라스틱서치에서는 관련된 모든 노드들을 논리적으로 묶어서 클러스터라고 부른다. 또한 노드들은 같은 클러스터 내부의 데이터만 서로 공유가 가능하다. 같은 클러스터를 구성하는 노드들을 같은 클러스터 이름으로 설정해야한다. 엘라스틱서치는 설정된 클러스터 이름을 이용해 같은 클러스터의 구성원으로 인식된다. 같은 클러스터 내부의 노드는 평소 데이터 색인이나 검색작업을 함께 수행하게 되고 장애가 발생했을 때 데이터 복구를 위한 다양한 작업도 서로 협력해서 함께 진행한다.

 

Cross Cluster Search

일반적으로 검색 시 하나의 클러스터 데이터만 검색하는 것이 원칙이긴 하지만 최초 설계 시 전혀 관련성 없어 보이던 데이터들을 시간이 지나서 데이터가 점점 많이 쌓일 수록 데이터 연관성이 생길 수도 있다. 엘라스틱서치에서는 이처럼 다양한 필요에 따라 다수의 클러스터를 한 번에 검색할 수 있는 Cross Cluster Search라는 기능을 제공한다.

 

 

Cross-cluster search | Elasticsearch Reference [7.1] | Elastic

The cross-cluster search feature allows any node to act as a federated client across multiple clusters. A cross-cluster search node won’t join the remote cluster, instead it connects to a remote cluster in a light fashion in order to execute federated sear

www.elastic.co

 

2)노드

물리적으로 실행된 런타임 상태의 엘라스틱서치를 노드라고 부른다. 노드는 위에서 설명한 클러스터를 이루는 구성원의 일부이며 실제 데이터를 물리적으로 가지고 있는 단일 서버이기도 하다. 실행 시 노드는 클러스터에 의해 UUID가 할당되고 클러스터 내에서는 할당된 UUID로 서로를 식별한다. 기본 값으로 부여되는 UUID를 원하지 않는다면 직접 이름을 설정할 수도 있다.(하지만 이름은 유일해야한다.) 노드는 내부에 다수의 인덱스를 가지고 있으며, 각 인덱스는 다수의 문서를 가지고 있다. 색인 작업을 통해 엘라스틱서치로 전송한 데이터는 인덱스라는 논리적인 자료구조 속에 문서라는 단위로 저장된다. 같은 클러스터 내부에서 존재하는 모든 노드는 서로 다른 노드와 수시로 정보를 주고 받는다. 기본적으로 모든 노드는 마스터 노드와 데이터 노드의 역할을 동시에 수행할 수 있도록 설정되어있지만 실제 대용량의 운영환경에서는 각각 용도에 맞는 노드를 적절히 분리하여 클러스터링하는 것이 좋다.

 

2)-1 노드의 형태

  • 마스터 노드(Master Node) : node.master 설정이 true로 설정된 노드다. 클러스터의 제어를 담당한다.
  • 데이터 노드(Data Node) : node.data 설정이 true로 설정된 노드다. 데이터를 보유하고 CRUD, 검색, 집계 등 데이터 관련 작업을 담당한다.
  • 인제스트 노드(Ingest Node) : node.ingestrk true로 설정된 노드다. 색인 전 전처리 작업을 담당한다.
  • 코디네이팅 노드(Coordinating Node) : 검색이나 집계 시 분산 처리만을 목적으로 설정된 노드다. 대량의 데이터를 처리할 경우에 효율적으로 사용할 수 있는 노드이다.

3)인덱스

엘라스틱서치 인덱스는 유사한 특성을 가지고 있는 문서를 모아둔 문서들의 모임이다. 클러스터 내부에 생성되는 모든 인덱스는 클러스터 내에서 유일한 인덱스명을 가져야한다. 또한 인덱스명은 모두 소문자로 설정해야 한다. 또한 과거 버전과는 다르게 현재 버전들은 하나의 인덱스에 하나의 타입만 생성해야 한다.

 

4)문서

문서는 검색 대상이 되는 실제 물리적인 데이터를 뜻한다. 문서는 인덱스를 생성할 수 있는 기본적인 정보 단위이고 엘라스틱서치에서는 JSON형식으로 문서를 표현한다.

 

5)샤드

인덱스에는 매우 많은 양의 문서가 저장될 수 있다. 일반적으로 하나의 하드웨어에서 제공되는 리소스 이상의 데이터를 저장할 수 없지만 물리적인 한계를 뛰어넘기 위해 샤드라는 개념을 도입했다. 이를 이용하면 데이터를 분산 저장하는 가능하다. 엘라스틱서치에서는 인덱스를 생성할 때 기본적으로 5개의 샤드로 데이터가 분산되도록 생성되고 설정에 의해 샤드의 개수를 원하는 만큼 변경할 수도 있다. 하나의 샤드는 인덱스의 부분 집합이다. 하지만 해당 부분집합으로만으로도 독립적인 검색 서비스가 가능하다. 실제로 인덱스에 질의를 요청하면 인덱스가 가지고 있는 모든 샤드로 검색요청을 보내고 각 샤드의 결과를 취합하여 하나의 결과로 제공한다.

 

6)레플리카

샤드의 복제본을 레플리카라고 한다. 엘라스틱서치에서는 인덱스를 생성할 때 기본적으로 1개의 레플리카를 생성한다. 장애 복구만이 아니고 검색에도 활용되기 때문에 이를 이용하면 읽기 분산에 유리해진다. 엘라스틱서치는 노드의 장애시 페일오버 메커니즘을 레플리카를 이용하여 제공하고 있다. 인덱스가 생성될 때 샤드 개수와 레플리카 개수를 자유롭게 설정할 수 있다. 하지만 인덱스가 생성된  이후에는 샤드 개수를 변경하는 것이 불가능하다. 만약 샤드의 개수를 늘리고 싶다면 샤드개수를 늘린 인덱스를 새로 만들고 기존 인덱스에서 새로 생성한 인덱스로 ReIndex하는 방법 밖에 없다. 데이터가 아주 크다면 Reindex 시간이 아주 길기 때문에 애초에 최적의 샤드의 개수를 정하는 것이 중요하다. 이에 반해 레플리카 개수는 인덱스를 생성한 후에도 자유롭게 변경하는 것이 가능하다.

 

6)-1 엘라스틱서치의 고가용성

엘라스틱서치에서는 샤드나 노드에 장애가 발생할 경우 즉각적인 복구가 가능하기 때문에 안정적인 클러스터 운영이 가능하다. 페일오버 메커니즘을 레플리카를 이용하기 때문에 원본 샤드가 존재하지 않는 노드에 레플리카 샤드를 생성한다. 또한 검색 시 샤드와 레플리카에서 병렬로 실행될 수 있기 때문에 검색 성능이 좋아지는 결과도 있다.

 

7)세그먼트

문서들은 빠른 검색에 유리하도록 설계된 특별한 자료구조로 저장된다. 루씬에 데이터가 색인되면 데이터는 토큰 단위로 분리되고 특수한 형태의 세그먼트라는 단위로 저장이 된다. 이러한 세그먼트는 읽기에 최적화된 역색인이라는 형태로 변환되어 물리적인 디스크에 저장된다. 검색엔진의 특성상 쓰기 연산보다 읽기 연산의 비중이 비교적 높기때문에 읽기에 최적화된 역색인이라는 구조로 저장하는 것이다.

 

 

엘라스틱서치와 RDB비교

엘라스틱서치 RDB
인덱스 데이터베이스
샤드 파티션
타입 테이블
문서
필드
매핑 스키마
Query DSL SQL

 

엘라스틱서치는 루씬 라이브러리를 샤드 내부에 가지고 있으며, 이 루씬 라이브러리가 핵심 모듈이라고 설명했다. 루씬은 검색 라이브러리이고 이 라이브러리에서 중요한 클래스가 바로 IndexWriter & IndexSearcher이다. 간단히 전자는 데이터를 색인하는 역할이고 후자는 검색하는 역할이다. 이 두개를 가지고 색인과 검색 역할을 제공하는 루씬 인스턴스를 루씬 인덱스라고 하는데, 사실 하나의 엘라스틱서치 샤드는 하나의 루씬 인덱스라고 설명할 수 있다. 즉, 샤드가 독립적으로 검색을 제공하는 이유이기도 하다. 이말은 각 샤드마다 데이터를 위한 물리적인 파일을 가지게 될 것이다. 이것을 간단히 표로 표현하면

 

 

엘라스틱서치 인덱스 구조

Elasticsearch Index
Elasticsearch shard Elasticsearch shard Elasticsearch shard Elasticsearch shard
Lucene Index Lucene Index Lucene Index Lucene Index
segment segment segment segment segment segment segment segment

 

루씬 인덱스는 독립적으로 자기가 가지고 있는 세그먼트 내에서만 검색이 가능하다. 하지만 엘라스틱서치는 이러한 루씬 인덱스를 가진 샤드들을 하나의 엘라스틱서치 인덱스로 묶여 있으므로, 다수의 루씬 인덱스에서 동시에 검색이 가능한 것처럼 기능을 제공하게 되는 것이다.

 

 

색인 작업 시 세그먼트의 동작 방식

하나의 루씬 인덱스는 내부적으로 다수의 세그먼트로 구성돼 있다. 읽기 성능이 중요한 검색엔진에서는 하나의 세그먼트로 검색 요청을 처리하는 것보다 다수의 세그먼트를 생성해서 나눠서 처리하는 것이 훨씬 효율적일 것이다. 루씬은 검색 요청을 받으면 다수의 작은 세그먼트 조각들이 각각 검색 결과 조각을 만들어 내고 이를 통합하여 하나의 결과로 합쳐서 응답하도록 설계돼 있다. 이러한 검색 방식을 세그먼트 단위 검색이라고 한다. 세그먼트는 역색인 구조를 지닌 파일 자체를 의미하는데 세그먼트 내부에는 실제로 색인된 데이터가 역색인 구조로 저장돼있다.

루씬은 세그먼트들을 관리하기 위한 용도로 커밋 포인트라는 자료구조를 제공한다. 커밋 포인트는 여러 세그먼트의 목록 정보를 가지고 있으며, 검색 요청 시 이를 적극적으로 활용한다. 최초 색인 작업 요청이 루씬에 들어오면 IndexWriter에 의해 색인 작업이 이루어지고 결과물로 하나의 세그먼트가 생성된다. 그 후 색인 작업이 추가로 요청될 때마다 새로운 세그먼트가 추가로 생성되고 커밋 포인트에 기록된다. 즉, 색인 작업이 일어날 때마다 세그먼트 개수는 점점 늘어난다. 하지만 너무 많은 세그먼트가 생성되면 읽기 성능이 저하될 수 있기 때문에 루씬은 백그라운드에서  주기적으로 세그먼트 파일을 병합하는 작업을 수행하고 이를 통해 모든 세그먼트들을 물리적으로 하나의 파일로 병합한다.

즉, 일정시간이 지나고 추가 색인 작업이 없는 상태라면 최종적으로 하나의 커다란 세그먼트만 남는다. 

색인 작업이 계속 될때마다 세그먼트는 계속 추가적으로 생성된다. 이말은 즉, 세그먼트는 불변성을 가지게 되고 병합과정 이외에는 절대 수정되지 않는다.

 

루씬의 색인 동작과정

  • 최초 색인 요청 - 1) IndexWriter가 세그먼트를 생성 -> 2)IndexSearch가 생성된 세그먼트를 읽어 검색결과 제공
  • 추가 색인 요청 - 1) IndexWriter가 세그먼트를 추가 생성 -> 2)세그먼트가 추가 생성되는 동안 기존 세그먼트만 읽어 검색 결과 제공 -> 3)세그먼트 생성이 완료되면 생성된 모든 세그먼트를 읽어 검색 결과 제공
  • 주기적으로 세그먼트 병합 작업이 일어날 경우 - 1)IndexWriter가 병합 대상이 되는 세그먼트들을 복제 -> 2)IndexWriter가 복제한 세그먼트들을 하나의 세그먼트로 병합 -> 3) 병합 과정 중에는 기존 원본 세그먼트로 검색 결과 제공 -> 4) 병합작업 완료시 원본 세그먼트와 병합 세그먼트를 교체하고 원본 세그먼트 삭제 

 

여기까지 내용을 끊고 다음 포스팅에서 이어서 설명합니다.

posted by 여성게
: