본문 바로가기
💻 개발 이야기/ELK Stack

[ELK] 엘라스틱서치 샤딩, 이 정도는 알고 사용하자

by Jinseong Hwang 2023. 11. 3.

 

안녕하세요. 황진성입니다.

 

오늘은 Elasticsearch에서 데이터를 나눠 저장하는 단위. 샤드에 대해 알아보고 샤딩을 제대로 사용하기 위해 알고 있으면 좋은 내용들에 대해 알아보겠습니다.

 

이 글을 이해하기 위해서는 엘라스틱서치 노드, 특히 데이터 노드가 무엇인지 알고 있는 것이 좋습니다.

[ELK] 엘라스틱서치 클러스터를 구성하는 다양한 노드에 대해 알아보자

[ELK] 엘라스틱서치 데이터 노드를 효율적으로 관리하는 방법

 

이 글의 내용은 작성일 기준 최신 버전인 Elasticsearch 8.10 문서를 따라 작성됐습니다.

 

샤드란?


Elasticsearch는 여러 대의 노드를 효율적으로 활용해서 분산 처리를 하기 위해 데이터를 샤드(Shard)라는 단위로 나눠서 분산 저장합니다. 데이터를 분산 저장하면 스케일 아웃이 가능하고 작업을 분산 처리해서 처리량을 늘릴 수 있습니다.

 

임의의 도큐먼트가 저장되는 과정을 살펴봅시다.

먼저 클러스터에 데이터 노드가 3개가 있다고 가정합시다. 여기 my_index라는 인덱스가 생성될 때 샤드를 5개 만들었습니다. 5개의 샤드는 3개의 노드에 적절히 분배되고, xxx 도큐먼트가 인덱싱 되면 도큐먼트는 5개의 샤드 중 하나에 저장됩니다.

 

Elasticsearch에서 데이터가 저장되는 곳이 인덱스로 알고 있었는데, 인덱스는 도큐먼트가 저장되는 논리적 단위일 뿐입니다. 실제 도큐먼트의 인덱싱과 검색은 샤드에서 일어납니다. (인덱스는 샤드의 집합이라는 것을 알게 됐습니다.)

 

도큐먼트가 저장될 때 어떤 샤드에 저장되어야 할지 결정을 해야 합니다. 그 결정은 인덱싱 요청을 처음 받는 코디네이터 노드가 처리합니다. 코디네이터 노드가 라우터 역할도 하게 됩니다. 해시 함수 등 일련의 수식을 거쳐 나온 번호의 샤드로 도큐먼트가 저장됩니다.

 

 

프라이머리 샤드와 레플리카 샤드


Elasticsearch는 인덱스를 샤드 단위로 나누어 여러 노드들에 분산 저장합니다. 하지만 데이터가 여러 물리 장비에 분산되어 있는 만큼 노드 하나만 클러스터를 이탈해도 데이터가 손실되어 시스템 장애로 이어질 수 있습니다. 이런 문제를 방지하기 위해 Elasticsearch에서는 데이터의 원본을 프라이머리 샤드(Primary Shard)에 저장하고, 데이터 유실 방지 및 가용성 확보를 위해 데이터 복제본을 레플리카 샤드(Replica Shard)에 저장합니다.

 

프라이머리 샤드 개수와 레플리카 샤드 개수는 사용자가 인덱스를 생성할 때 결정할 수 있습니다.

PUT my_index
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas" 2
  }
}

 

  • number_of_shards : 프라이머리 샤드의 개수를 의미합니다.
  • number_of_replicas : 프라이머리 샤드 1개당 할당할 레플리카 샤드의 개수를 의미합니다.

 

위와 같이 설정한다면 클러스터 내에 인덱스를 저장하기 위해 데이터 원본을 3개의 샤드로 나눕니다. 그리고 각 프라이머리 샤드와 동일한 데이터를 저장하는 레플리카 샤드를 2개씩 사용합니다. 따라서 my_index를 저장하기 위해 총 3 + (2 * 3) = 9개의 샤드가 존재하게 됩니다. 그림으로 나타내면 아래와 같습니다.

 

같은 색의 샤드는 같은 데이터를 저장하고 있습니다.

 

 

레플리카 샤드를 꼭 사용해야 할까?


레플리카 샤드를 많이 두게 되면 그만큼 공간도 많이 차지하고 리소스도 많이 사용하게 됩니다. 그럼에도 불구하고 레플리카 샤드를 사용하는 이유가 무엇일까요? 당연하게도 "성능 향상"과 "고가용성"을 위함입니다.

 

성능 향상

Elasticsearch는 분산 검색 엔진입니다. 일반적으로 검색은 다음과 같은 순서로 진행됩니다.

 

  1. (코디네이터 노드 등이) 검색 요청을 받는다.
  2. 실제 검색할 인덱스의 샤드가 위치한 노드로 요청을 전달한다.
  3. 검색 결과를 다시 최초 요청을 받았던 노드로 전달 및 취합한다.

 

이해를 위해 가정해 봅시다.

  • 찾는 데이터가 파랭이, 초록이, 노랭이에 모두 존재한다. (즉, 각각 찾아서 취합해야 한다)
  • 프라이머리 샤드만 하나씩 있고 레플리카 샤드는 없다.

 

만약 파랭이, 초록이 샤드에서는 검색 결과를 0.1초 만에 보내줬지만, 노랭이 샤드에서는 5초를 기다려도 검색 결과가 오지 않는다면 응답 시간이 계속 지연될 것입니다. 이 경우 레플리카 샤드가 있었다면 미리 과부하 된(노랭이 샤드가 있는) 노드2를 제외하고, 노드1 혹은 노드3으로 요청을 보내서 빠른 응답을 유지할 수 있게 됩니다. 또한 사용률이 낮은 노드를 우선 사용하기 때문에 부하 분산이 이뤄지고, 이는 전체적인 클러스터 안정성을 높이는 데도 큰 도움이 됩니다.

 

 

고가용성

위 "성능 향상" 예시와 마찬가지로 가정해 봅시다.

- 찾는 데이터가 파랭이, 초록이, 노랭이에 모두 존재한다. (즉, 각각 찾아서 취합해야 한다)
- 프라이머리 샤드만 하나씩 있고 레플리카 샤드는 없다.

 

만약 노드1에서 장애가 발생해서 사용 불능 상태가 된다면 어떻게 될까요? 파랭이 샤드에 저장된 데이터는 조회할 수 없으며, 심각한 경우에는 파랭이 샤드에 저장된 모든 데이터를 유실할 수 있습니다. 하지만 레플리카 샤드가 있다면 정상적인 노드의 레플리카 샤드가 프라이머리 샤드로 선출된 후 정상적으로 검색 요청에 응답할 수 있게 됩니다. 데이터의 원본/복제본 구분 없이 모든 노드에 균등하게 데이터를 분배했기 때문에 고가용성을 지킬 수 있습니다.

 

 

샤드는 어떻게 할당될까?


앞서 나왔던 예시를 다시 불러와 살펴봅시다.

 

PUT my_index
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas" 2
  }
}

 

인덱스를 생성할 때 프라이머리 샤드 + 복제할 레플리카 샤드의 개수를 설정해 주면 알아서 뚝딱뚝딱 샤드가 여러 노드에 할당되는 것 같았습니다. 하지만 그 속에도 과정이 존재합니다. 샤드 할당의 과정은 Unassigned, Initializing, Started, Relocating의 4단계로 구분할 수 있습니다.

 

1) UNASSIGNED

Unassigned 상태는 샤드는 있지만 아직 노드에 할당되지 않은 상태입니다. 

 

 

2) INITIALIZING

Initializing 상태는 샤드를 노드에 로딩 중인 상태를 의미합니다. Started 상태로 가기 전 잠깐 초기화를 위한 상태입니다. 샤드를 노드에 적재하지 못하는 상황을 제외하곤 모니터링을 해봐도 순식간이기 때문에 발견하기 쉽지 않습니다. 세그먼트들을 바로 검색 가능한 상태로 메모리에 띄우는 과정이지만, 아직 메모리에 완전히 적재된 상태가 아니기 때문에 샤드를 사용할 수는 없습니다.

 

Unassigned 상태의 노드 중 프라이머리 샤드들이 우선적으로 Initializing 상태가 됩니다. 샤드를 생성할 여유가 있는 데이터 노드부터 프라이머리 샤드를 만들기 시작하며, 그 전반적인 과정은 마스터 노드가 결정합니다.

 

세그먼트란?

 세그먼트(Segment)는 Elasticsearch에서 인덱스가 물리적으로 저장되는 가장 작은 단위입니다. 읽기에 최적화된 형태이며 수정은 불가능합니다. 세그먼트는 자체적으로 Lucene 검색이 가능한 구조이며 토큰화된 역인덱스(Inverted Index) 데이터와 원본 데이터가 들어있습니다. Lucene에서 인덱스 검색을 요청하면 각각의 세그먼트들로부터 검색하고 이를 통합해 최종 결과가 나옵니다.
 세그먼트는 리프레시(refresh)될 때마다 생기는데 리프레스는 클러스터의 모든 샤드에서 기본적으로 1초마다 발생하며 리프레시가 되어야 새로 추가된 도큐먼트 검색이 가능합니다. 너무 작은 세그먼트들이 생기면 읽기 성능에 악영향을 미치므로 이를 방지하기 위해 내부적으로 틈틈이 세그먼트들을 병합합니다. Elasticsearch의 기본 데이터 검색 저장 방식이 Lucene을 기반으로 하기 때문에 세그먼트를 정확하게 이해하는 것이 성능 튜닝에 도움이 됩니다.

 

 

3) STARTED

Started 상태는 샤드가 메모리에 완전히 적재된 상태로 이 상태에서만 샤드로 접근 및 검색이 가능합니다. 프라이머리 샤드가 Initializing 상태를 거쳐서 메모리 적재가 완료되면 Started 상태로 변경되며 다른 노드로 레플리카 샤드 할당을 시작합니다. 이때 생성 중인 레플리카 샤드 또한 Initializing 상태를 띄며 메모리에 적재됩니다.

 

 

4) RELOCATING

Relocating은 상태보다는 단계라고 표현하는 것이 좋습니다. 클러스터 내 샤드가 재배치될 때의 단계입니다. 분산 환경에서 노드에 장애가 발생하면 새로운 노드가 추가되거나 삭제되기도 합니다. 이런 경우에 재배치가 필요합니다. 노드 수가 변하지 않더라도, 저장 공간이나 샤드 수 등을 기준으로 주기적 리밸런싱이 일어날 때도 재배치가 필요합니다. Elasticsearch는 자동으로 샤드들 간의 밸런스를 맞추는 기능이 있어서 부하 상태에 따라 샤드를 옮기곤 합니다.

 

다양한 상황이 있겠지만, 아래와 같은 상황이 대표적입니다.

 

  • 새로운 데이터 노드가 추가돼서 레플리카 노드가 이동할 때
  • 기존 데이터 노드가 이탈해서 부족해진 레플리카 노드를 복제 및 생성할 때

 

상황 발생 시 마스터 노드는 재배치 or 리밸런싱을 진행하게 됩니다. 위에서 소개한 것과 마찬가지로 Initializing 상태를 거쳐 Started 상태가 되면 사용 가능해집니다. (예시 상황이라면) 결국은 어떤 문제가 발생해서 프라이머리 샤드 3개 + 레플리카 샤드 6개를 못 맞췄다면 다시 채워주는 과정이라 이해하면 좋습니다.

 

 

샤드 모니터링은 어떻게 할까?


클러스터의 샤드 상태 역시 API를 통해서 확인할 수 있습니다.

 

GET _cat/shards?v
# v 옵션을 사용하면 필드명도 함께 확인하실 수 있습니다!

로컬 PC에서 샤드 상태를 조회

 

다른 프로젝트를 진행하며 products라는 인덱스를 만들었는데, 프라이머리 샤드(p)가 Started 상태이고 레플리카 샤드(r)가 Unassigned 상태인 것을 확인할 수 있었습니다. 저는 인덱스를 생성할 때 따로 샤드 개수 설정을 하지 않았는데 프라이머리 샤드와 레플리카 샤드가 각 1개씩 생성된 것을 확인할 수 있었습니다. 실습한 환경인 Elasticsearch 8.10에서는 따로 설정하지 않으면 프라이머리 샤드 1개 + 레플리카 샤드 1개로 생성됩니다.

 

왜 레플리카 샤드가 Unassigned 상태일까요? 실습 환경은 단일 노드이기 때문에 레플리카 샤드를 운영해도 성능 향상이나 고가용성 보장도 기대할 수 없기 때문에 굳이 사용하지 않습니다.

 

 

GET _cat/indices?v

로컬 PC에서 인덱스 상태를 조회

인덱스 상태를 조회하면 가장 왼쪽에 health 필드가 있습니다. 여기 나오는 health가 샤드의 상태와 관련이 있습니다.

 

  • red : 하나 이상의 프라이머리 샤드가 클러스터에 정상적으로 적재되지 않은 상태. 제대로 동작하지 않는다.
  • yellow : 프라이머리 샤드는 적재됐지만 하나 이상의 레플리카 샤드가 적재되지 않은 상태. 제대로 동작하지만 프라이머리 샤드가 적재된 노드에 문제 발생 시 동작하지 않을 수 있다.
  • green : 프라이머리 샤드와 레플리카 샤드 모두 정상적으로 적재된 상태.

 

 

샤드 개수는 얼마가 적당할까?


클러스터 내부의 노드 수나 인덱스에 저장될 데이터의 크기에 따라 샤드의 개수가 달라져야 하기 때문에 정답은 없습니다. 각 인덱스마다 프라이머리 샤드와 레플리카 샤드의 수를 조절할 수 있는데, 각 인덱스를 사용하는 프로젝트의 성격에 맞춰서 샤드 개수를 결정하면 됩니다.

 

필요 이상으로 (프라이머리) 샤드를 많이 만드는 것을 보고 오버샤딩(oversharding)이라고 합니다. 오버샤딩은 크게 2가지 문제가 있습니다. 첫째, 각 샤드는 개별적으로 컴퓨팅 리소스를 소비하기 때문에 너무 많으면 시스템 성능에 좋지 않습니다. 둘째, 인덱스를 검색하기 위해서는 모든 샤드에 접근해야 하는데 샤드가 많아지면 모두 접근하는 것만으로도 많은 리소스를 사용하게 됩니다.

 

그렇다고 샤드를 적게 만드는 것도 좋지 않습니다. 샤드 수가 적으면 병렬 처리 효과를 크게 낼 수 없으며 분산도가 떨어져서 결국 전체적인 처리량이 떨어지게 됩니다.

 

결국 해결 방법은 실데이터를 넣고 운영하며 모니터링하며 샤드 수를 한 번씩 바꿔보는 것입니다. CPU 코어 개수 등으로 샤드 개수를 결정해서 온전히 병렬성을 살리기도 하지만, 적절한 샤드 개수는 프로젝트 성격에 따라 달라집니다.

 

 

샤드 크기는 얼마가 적당할까?


샤드 개수를 정하기 위한 정답은 "없다!"가 결론이었습니다. 안타깝지만 샤드 크기를 정하기 위한 정답도 없습니다. (ㅠㅠ) 샤드가 너무 크면 검색 시간이 오래 걸려 성능 문제가 발생하고 샤드가 너무 작으면 담을 수 있는 도큐먼트가 작아진다는 문제가 있습니다. 따라서 샤드 크기 역시 프로젝트 성격에 맞게 실데이터를 넣어서 운영과 모니터링을 해보며 변경하며 결정해야 합니다.

 

처음 샤드를 구성할 때는 이런 점을 알기 힘들기 때문에 통계적 수치로 결정하곤 하는데, 통계적으로 샤드 하나의 크기가 10GB~50GB 정도로 관리하는 것이 좋다고 합니다.

출처 : https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html

 

 

인덱스를 생성하고 운영하다 보면 샤드의 크기는 계속해서 커질 수밖에 없습니다. 따라서 인덱스가 특정 조건에 도달하면 인덱스를 분리하는 식으로 운영할 수 있습니다. ILM(Index Lifecycle Management)이라고 하는데, 8.10 버전 기준 Rollover, Shrink, Force merge, Delete 방식을 제공하고 있습니다. ILM에 대해 더 궁금하다면 아래 링크를 참고해 주세요!

https://www.elastic.co/guide/en/elasticsearch/reference/current/overview-index-lifecycle-management.html

 

 

맺음말

개인적으로 엘라스틱서치 클러스터를 운영하면서 샤드 관리를 하느라 애먹을 일이 거의 없을 것 같긴 합니다. 그래도 공부해 두면 언젠간 요긴하게 쓰이길래 정리해 봤습니다. 작게나마 운영 환경에서 서비스할 일이 생기면 저는 number_of_shards=2, number_of_replicas=1로 설정하고 사용할 것 같네요.

 

언제나 질문 환영합니다 ~

긴 글 읽어주셔서 감사합니다.