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)동시성 문제 회피

불변성이 보장된다면 Lock이 필요 없어진다. 다수의 스레드가 동작하는 복잡한 다중 스레드 환경에서 동시성 문제는 매우 중대한 문제이다. 루씬은 세그먼트의 불변성으로 이러한 동시성 문제를 간단히 피해갔다.

2)시스템 캐시 활용

데이터가 OS 커널에서 제공하는 시스템 캐시에 한번 생성되면 일정 시간 동안은 그대로 유지된다. 불변성을 보장하지 않을 경우 수정이 있을 때마다 시스템 캐시를 삭제하고 다시 생성해야하는 비용이 큰 작업을 수행하게 된다. 하지만 불변성이라면 이러한 시스템 캐시를 효율적으로 이용할 수 있다.

3)높은 캐시 적중률

불변성이기 때문에 시스템 캐시의 수명이 길어지므로 캐시의 적중률 또한 높아진다.

4)리소스 절감

역색인을 만드는 과정에서 많은 시스템 리소스(CPU,Memory)가 사용된다. 수정을 허용하게 되면 일부분이 변경되더라도 해당 역색인을 대상으로 작업해야하기 때문에 시스템 리소스가 소모된다.

 

하지만 이러한 불변성에도 단점은 존재한다. 우선 일부 데이터가 변경되더라도 전체 역색인 구조가 다시 만들어져야 하고, 또 다른 문제는 실시간 반영이 상대적으로 어려워지는 것이다. 하지만 이러한 단점을 극복하기 위해 루씬은 하나의 세그먼트를 사용하는 것이 아니라 다수의 세그먼트를 생성하기 때문에 변경 될때 마다 모든 세그먼트를 다시 만드는 것이 아니라 기존 세그먼트는 그대로 두고 추가로 세그먼트를 생성하기에 세그먼트 생성 중에 기존 세그먼트를 이용하여 검색 결과를 제공한다.

 

그렇다면 이런 불변성을 가진 세그먼트에서 수정/삭제연산은 어떻게 처리될까?

 

수정연산 같은 경우 세그먼트의 불변성을 유지하기 위해 해당 데이터를 삭제한 후 다시 추가하는 방식으로 동작한다. 기존 데이터는 삭제 처리되어 검색 대상에서 제외되고 변경된 데이터는 새로운 세그먼트로 추가되어 검색 대상에 포함된다. 삭제 연산 같은 경우는 삭제 여부를 표시하는 비트 배열을 찾아 삭제 여부만 표시하고 끝낸다. 즉, 바로 삭제되는 것이 아니라 단순히 삭제플래그만 남기고 검색 대상에서 제외만 시킨다. 이후 병합 과정이 시작된다면 비로소 삭제플래그값을 가진 데이터가 삭제되는 것이다. 기타 루씬의 Flush, Commit, Merge 작업관련 내용은 아래 링크를 참조하자.

 

 

Lucene - 인메모리버퍼(In-Memory-Buffer) 역할, 세그먼트 병합(Merge)

루씬은 색인 요청이 올때마다 새로운 세그먼트가 추가된다. 그리고 일정한 주기로 세그먼트들을 병합하는 과정을 갖는다. 만약 이러한 루씬에 인메모리버퍼가 하는 역할은 무엇일까? 우선 인메모리버퍼가 없는 루..

coding-start.tistory.com

 

고가용성을 위한 Translog

엘라스틱서치는 분산 시스템이 지원해야 하는 고가용성을 제공하기 위해 내부적으로 Translog라는 특수한 형태의 파일을 유지하고 관리하고 있다. 장애 복구를 위한 백업 데이터 및 데이터 유실 방지를 위한 저장소로써 Tranlog를 활용한다. 분산 코디네이터인 Zookeeper도 클러스터의 고가용성을 위하여 Data Directory에 Translog역할과 거의 동일한 log파일을 작성한다.

 

엘라스틱서치 샤드는 내부에 Translog라는 특수한 파일을 가지고 있다. 샤드에 데이터 변경사항이 생길 경우 Translog 파일에 먼저 해당 내역을 기록한 후 내부에 존재하는 루씬 인덱스로 데이터를 전달한다. 루씬으로 전달된 데이터는 인메모리 버퍼로 저장되고 주기적으로 처리되어 결과적으로 세그먼트가 된다. 엘라스틱서치에서는 기본적으로 1초에 한번씩 Refresh(루씬의 Flush) 작업이 수행되는데, 이를 통해 추가된 세그먼트의 내용을 읽을 수 있게 되고 검색에 사용된다. 하지만 해당 작업이 일어나더라도 Translog파일에 기록된 내용은 삭제되지 않고 계속 유지된다. 이처럼 Translog는 엘라스틱서치 샤드에 일어나는 모든 변경사항을 담고 있는 특수한 형태의 로그인 것이다. 이러한 특성을 이용해 엘라스틱서치는 Tranlog의 내역을 바탕으로 장애복구를 수행한다.

하지만 Translog 파일에 로그가 계속해서 누적될 수는 없다. 특정 시점이 되면 Tranlog 내부의 로그중 불필요한 과거의 로그는 삭제된다. 이 특정 시점은 엘라스틱서치의 Flush(루씬의 Commit)이 수행될때이다. 엘라스틱서치의 Flush는 내부적으로 fsync() 함수를 이용해 실제 물리적인 디스크에 변경 내역을 기록한다. 그리고 작업이 성공적으로 마무리되고 물리적으로 디스크 동기화에 성공하면 누적되어 있던 Tranlog 파일의 내용이 비로소 삭제된다. Flush가 일어난다는 것은 디스크에 물리적으로 기록된다는 것이고 이는 영구적으로 보관된다는 것을 의미하기 때문에 이 시점까지의 로그는 더는 필요하지 않게 된다.

 

그렇다면 엘라스틱서치에서 Translog가 장애복구를 위해서 필요한 것은 알겠지만 구체적으로 어떻게 장애복구에 이용될까? 예를 한번 들어보자. 엘라스틱서치는 1초 마다 Refresh(루씬의 Flush)를 한다. 하지만 이는 물리적인 디스크에 쓰여진 상태가 아니기에 불안정하기에 주기적으로 Flush(루씬의 Commit)를 해야한다. 하지만 Flush 작업은 매우 무겁고 긴 시간 동안 일어날 수 있다.

 

[상황 1]

엘라스틱서치의 Flush에 의해 루씬 Commit 작업이 시작됐고 완료되지 못한 상태에서 샤드에 장애발생.

[해결방법]

샤드가 강제로 종료될 경우 진행 중이던 루씬 Commit 작업이 롤백되기 때문에 샤드가 정상적으로 재실행되면 Translog의 로그 내역을 이용해 간단히 복구 가능하다. 왜냐하면 엘라스틱서치의 Flush가 완료되지 않아서 Translog는 아직 삭제되지 않은 상태이기 때문이다.

 

[상황 2]

변경사항이 순간적으로 많아져서 루씬 Commit이 긴 시간 동안 일어나게 되고 그동안 많은 데이터 변경 요청이 한꺼번에 샤드로 들어옴.

[해결방법]

루씬 Commit 작업이 수행되는 시간이 길어진다고 해서 Commit이 일어나는 동안 샤드로 전달된 변경사항이 Commit 작업이 끝날때까지 반영이 되지 않는다면 실시간 검색을 지원한다는 의미가 거의 없다. 그래서 엘라스틱서치는 Commit이 일어나는 동안 들어온 변경사항을 루씬의 인메모리 버퍼로 전달하지 않고 Translog에 임시로 저장해두고 다음 Commit에 반영될 때까지 유지한다.

 

이러한 Translog는 클러스터 장애복구에 아주 중요한 역할을 하는 로그 파일이다. 하지만 이러한 Translog도 파일이 커지면 복구하는데 장애유발을 할 수 있으므로 적절한 주기로 엘라스틱서치의 Flush(루씬 Commit)를 하여서 적절한 크기의 이하의 Translog 파일을 유지하는 것도 중요하다.

posted by 여성게
: