새소식

Database

Elasticsearch - 샤드, 인덱스 운영 전략

  • -

샤드 운영 전략

 

샤드는 다음과 같은 옵션으로 개수를 설정할 수 있습니다.

 

  • number_of_shard : 프라이머리 샤드 개수
  • number_of_replicas : 레플리카 개수

 

만약 인덱스에 위 옵션을 3, 2로 지정했다면 해당 인덱스는 프라이머리 샤드 3개에 대한 복제본이 2개씩 생기므로 프라이머리 3개, 레플리카 6개로 총 9개의 샤드가 생성됩니다.

 

number_of_shard는 인덱스가 데이터를 몇 개의 샤드로 쪼갤 것인지를 지정하는 값이므로 신중하게 설계해야 합니다. 한 번 지정하면 reindex 같은 동작을 통해 인덱스를 통째로 재색인하는 등 특별한 작업을 수행하지 않는 한 바꿀 수 없기 때문입니다. 샤드 개수를 어떻게 지정하느냐는 엘라스틱서치 성능에도 영향을 미칩니다. 클러스터에 샤드 숫자가 너무 많아지면 클러스터 성능이 떨어지나 인덱스당 샤드 숫자를 적게 지정하면 샤드 하나의 크기가 커집니다. 샤드 크키가 커지면 장애 상황 등에서 샤드 복구에 많은 시간이 소요되므로 클러스터 안정성이 떨어지게 됩니다. 공식 문서는 다음과 같은 가이드를 권장합니다.

 

  • 샤드 하나의 크기는 10GB ~ 40GB
  • 노드 heap 1GB당 20개 이하의 shard 구성

 

샤드 하나의 크기는 20GB만 되어도 느리다는 감각이 느껴지므로 보통 수 GB 내외로 조정하고 32g heap 기준으로 노드당 640개의 샤드를 가지게 되는데 이보다는 조금 더 적은 샤드를 갖는 구성을 하는 것이 좋습니다.

 

노드 대수가 n 대라면 number_of_shards를 n 배수로 지정해 모든 노드가 작업을 고르게 분산받도록 설정할 수 있습니다. 서비스 중요도가 높은 인덱스나 성능을 타이트하게 조정해야 하는 인덱스라면 이 부분을 고려해서 값을 지정해야 합니다. 하지만 모든 인덱스를 이렇게 처리할 필요는 없습니다. 추후 선형적 확장을 위해 서버가 추가 투입되면 공식이 깨지기 때문입니다. 또한 엘라스틱서치 클러스터에서 활발하게 작업 중인 인덱스의 수가 충분히 많다면 단일 인덱스에 대해 모든 노드가 일을 하고 있는지를 과도하게 신경 쓸 필요가 없을 수도 있습니다. 한 인덱스에 참여하지 않는 노드이더라도 다른 인덱스의 작업에 리소스를 사용할 것이기 때문입니다.

 

구축 시나리오 예시

 

일 100GB 데이터 분석용 클러스터

 

요구사항

  • 하루에 100GB 정도의 데이터를 저장하면서 보관 기간이 한 달인 분석 엔진 클러스터
  • 인덱스 이름 패턴 : es-YYYY-MM-dd
  • 프라이머리 샤드 기준 하루에 색인되는 인덱스 용량 : 100GB * 1일
  • 인덱스 보관 기간 : 30일
  • 레플리카 샤드 개수 : 1개
  • 클러스터에 저장되는 전체 예상 용량 : 100GB * 30(일) * 2(레플리카, 프라이머리)
  • 데이터 노드 한대에 저장 가능한 용량 : 2TB
  • 클러스터에 저장될 인덱스의 총 개수 : 30개
    • 하루에 한개의 인덱스가 생성되므로

 

6TB가 최대 필요 용량으로 예상되어 노드를 3개만 두어 맞추면 안 됩니다. es는 기본적으로 노드의 디스크 사용률을 기준으로 샤드를 배치하기 때문에 노드의 디스크 사용률이 low watermark 기본값인 85%가 넘으면 해당 노드에 샤드 할당을 지양한다. 따라서 노드의 최대 데이터 적재 용량을 80%로 잡고 시나리오를 작성해야 합니다.

 

구축

  • 데이터 노드 5대
    • 6TB의 용량이 전체 용량의 80%여야 하므로 전체 용량을 7.5TB로 산정해야 합니다.
    • 데이터 노드 한 대가 장애가 발생했다고 가정했을때, 레플리카 샤드로 인해 한대의 노드 장애에 대해서는 클러스터의 yellow 상태를 보장합니다. 하지만 오랜 기간 장애가 이어질 수 있으므로 해당 노드에 저장된 문서들을 다른 노드에서 충분히 받아줄 수 있을 만큼의 용량을 확보해야 합니다.
    • 데이터 노드가 4대일 때에는 총 8TB가 확보되어 6TB의 데이터를 수용하는데 문제가 없지만 노드 한대에서 장애가 발생해서 클러스터에 노드가 3대만 남게 되면 모든 데이터 노드의 디스크가 가득 차게 됩니다. 하지만 5대로 구성하면 총 10TB가 확보되어 한대가 장애가 발생하더라고 8TB까지 수용할 수 있어 장애 복구가 시급하지 않습니다.
  • 프라이머리 샤드 10개
    • es에서는 샤드 하나의 크기를 20~40G 정도로 할당을 권고합니다.
    • 노드 간 볼륨 사용량 불균형을 막기 위해 데이터 노드의 n배로 샤드의 개수를 산정해야 합니다.
      • 데이터노드가 5대이므로 2배인 10개의 프라이머리 샤드를 구축한다.
      • 10개 * 30일 * 2(레플리카)를 하면 총 600개의 샤드가 생성된다.
    • 인덱스 하나를 기준으로 노드 한 대에 할당되는 샤드 개수 : 10 * 2(레플리카) / 5(노드수) = 4개
    • 인덱스 전체를 기준으로 노드 한 대에 할당되는 샤드의 총 개수 : 10 * 2(레플리카) * 30일 / 5(노드수) = 120개

 

데이터 노드의 개수가 5개이므로 2 배수인 10개의 프라이머리 샤드 개수로 산정한다면, 하루에 색인되는 용량은 총 100GB이므로 10개의 프라이머리 샤드로 구성된다면 샤드 하나의 크기는 10GB 정도로 할당됩니다. 하루에 레플리카 샤드를 포함하여 총 20개의 샤드를 생성하게 되고, 데이터 노드 5대가 샤드를 4개씩 나눠 갖게 됩니다. 30일이 지나면 600개의 샤드가 생성되고, 데이터 노드 5대가 샤드를 120개씩 나눠갖게 됩니다.

 

일 1GB의 데이터 분석과 장기간 보관용 클러스터

 

요구사항

  • 하루에 1GB 정도의 데이터를 저장하면서 보관 기간이 3년인 분석 엔진 클러스터
  • 인덱스 이름 패턴 : es-YYYY-MM-dd
  • 프라이머리 샤드 기준 한 달에 색인되는 인덱스의 용량 : 1gb * 30 = 30GB
  • 인덱스 보관 기간 : 3년
  • 레플리카 개수 : 1개
  • 클러스터에 저장되는 전체 예상 용량 : 30GB * 36개월 * 2(레플리카) = 2.16TB
  • 데이터 노드 한 대에 할당할 수 있는 용량 : 2TB
  • 클러스터에 저장될 인덱스의 총 개수 : 36개

 

구축

  • 데이터 노드 3대
    • 용량은 노드 2대면 충분하지만 장애의 경우를 대비해 3대로 구축
    • 3대를 구축하니 용량의 여유분이 많이 발생하므로 레플리카를 2로 수정하여 안정성을 높일 수 있습니다.
    • 30GB * 36개월 * 3(레플리카) = 3.24TB
  • 인덱스를 구성하는 프라이머리 샤드 개수 : 6개
    • 인덱스 하나당 할당되는 샤드 개수 : 6 * 3 = 18개
    • 총 샤드 개수 : 6개(샤드) * 36(개월) * 3(레플리카) = 648개
    • 한 노드에 할당되는 샤드 수 : 648 / 3 = 216개

 

데이터 노드의 개수가 3개이므로 2배수인 6개를 프라이머리 샤드 개수로 산정한다면, 한 달에 에 색인되는 용량은 총 30GB이므로 6개의 프라이머리 샤드로 구성된다면 샤드 하나의 크기는 5GB 정도로 할당됩니다. 한 달에 레플리카 샤드를 포함하여 총 18개의 샤드를 생성하게 되고, 데이터 노드 3대가 샤드를 6개씩 나눠 갖게 됩니다. 36개월이 지나면 648개의 샤드가 생성되고, 데이터 노드 3대가 샤드를 216개씩 나눠갖게 됩니다.

 

일 100GB의 데이터 분석과 장기간 보관 클러스터

 

요구사항

  • 하루에 100GB 이상의 큰 데이터를 저장하면서 보관 기간이 1년인 클러스터
  • 인덱스 이름 패턴 : es-YYYY-MM-dd
  • 프라이머리 샤드 기준 하루에 색인되는 인덱스 용량 : 100GB
  • 인덱스 보관 기간 : 1년
  • 레플리카 샤드 개수 : 1개
  • 클러스터에 저장되는 전체 용량 : 100GB * 365일 * 2(레플리카) = 73TB
  • 데이터 노드 한 대에 할당할 수 있는 용량 : 2TB
  • 클러스터에 저장될 인덱스 총 개수 : 365개

 

시나리오대로라면 디스크 저장 공간이 총 73TB가 필요하고, 데이터 노드가 37대는 되어야 모든 용량을 저장할 수 있습니다. 장애 상황까지 고려하면 이보다 훨씬 많은 노드가 필요합니다. 보통 이렇게 오랜 기간 저장하는 데이터는 모든 데이터를 자주 분석하지 않습니다. 최근 1일, 1주, 1개월 혹은 3개월 등의 기준으로 데이터를 조회하며, 1년 치 데이터를 조회하는 경우는 일 년에 몇 회 정도입니다. 이렇게 1년에 몇 번 조회하지 않는 데이터를 위해 많은 비용을 지불하는 낭비를 막기 위해 데이터 노드를 hot/warm data 형태로 구성할 수 있습니다. 자주 조회하게 될 최근 데이터는 hot 영역, 자주보지 않지만 연간 분석을 위해 가끔 조회하게 될 데이터는 warm에 저장하는 방식을 취하면 데이터 보관 요구와 비용절감을 모두 할 수 있습니다.

 

보통 빠른 응답을 위해 SSD 디스크 사용을 권고하지만 warm 구성에 사용할 디스크는 상대적으로 저렴하면서 고용량의 저장 공간을 제공하는 SATA 디스크를 사용합니다.

 

구축

  • hot 노드 인덱스 보관 기간 : 1개월
  • hot 노드에 저장되는 전체 예상 용량 : 100GB * 2(레플리카) * 30일 = 6TB
  • warm 노드 한 대에 할당 가능한 용량 : SATA 10TB
  • warm 노드 인덱스 보관 기간 : 11개월
  • warm 노드 저장되는 전체 예상 용량 : 100GB * 2(레플리카) * 335일 = 67TB

 

위 조건을 바탕으로 노드 산정합니다.

  • hot 노드 : 5대
    • 6TB/10TB * 100 = 60%
    • 한 대 노드 장애시, 6TB/8TB = 75%
  • warm 노드 : 10대
    • 67TB/100TB * 100 = 67%
    • 한 대 노드 장애시, 67TB/90TB * 100 = 74%

위 조건으로 샤드 개수 산정

  • 프라이머리 샤드 개수 : 10개 (hot 노드의 n배)
    • 클러스터 전체 인덱스에 의해 생성되는 샤드 총 개수 : 10 * 365일 * 2(레플리카) = 7300개
  • hot 노드 한 대에 할당되는 샤드 총 개수 : 10 * 2(레플리카) * 30일 / 5대 = 120개
  • warm 노드 한 대에 할당되는 샤드 총 개수 : 10 * 2(레플리카) * 335일 / 10대 = 670개

 

보통 hot과 warm 노드의 개수가 다르기 때문에 샤드 개수를 잘 산정하지 않으면 hot에 있을 때 균등하게 배치된 샤드가 warm으로 넘어갈 때 불균등하게 배치될 수 있습니다. 이때 재분배되더라도 균등하게 분배되게 하려면 hot 노드의 개수와 warm 노드 개수의 최소 공배수로 샤드를 설정하여 인덱스를 생성해야 합니다.

 

위 시나리오에서 hot은 5대, warm은 10대의 최소 공배수는 10입니다. 하루에 샤드는 총 20(10+10)개가 생성되고 노드 5대에 각 4개씩 샤드가 분배됩니다. 한 달이 지나고 warm 노드로 이동할 때는 10대에 각 2대씩 분배하게 됩니다.

 

노드 설정은 다음 세가지 사항을 지정해야 합니다.

  • 데이터 노드에서 해당 노드를 hot으로 사용할지, warm 노드로 사용할지
  • 최초 인덱스 생성 시 hot 노드로 샤드가 할당될 수 있도록 설정
  • 한 달이 지난 이후 인덱스의 설정을 warm 노드로 할당하도록 설정

 

마지막으로 분석 엔진에 유용한 성능 확보 방법

  • 색인이 끝난 인덱스는 forcemerge api로 검색 성능 확보
  • 색인이 끝난 인덱스는 read only로 설정하여 shard request cache가 초기화되어 삭제되지 않도록 설정
  • 한 달이 지난 인덱스는 hotdata 노드에서 warmdata 노드로 샤드 재배치

 

검색 엔진으로 활용하는 클러스터

 

요구사항

  • 100ms 내에 검색 결과가 제공되어야 한다.
  • 검색 엔진에 사용할 데이터는 500GB
  • 검색 요구사항이 변경되어 매핑이 변경될 수 있다.

 

구축
검색 엔진에 사용되는 데이터는 보통 준비된 상태에서 서비스하며, 인덱스의 필드가 추가되거나 필드 데이터 타입이 변경되는 등의 매핑 변경이 있을 때 인덱스를 새롭게 생성 후 재색인하여 운영합니다.

 

es는 기본적으로 1쿼리 1샤드 1스레드를 기준으로 검색 요청을 처리합니다. 각 노드에 샤드가 1개씩 있고, 3대의 노드(A, B, C)에는 4개의 cpu 코어가 있다고 가정해 봅시다. 사용자의 검색 요청을 받은 노드 C가 검색 스레드 풀에서 스레드 하나를 꺼내 자신이 가지고 있는 샤드에 검색 요청에 해당하는 문서가 있는지 찾아보면서 동시에 A, B 노드에 문서가 있는지 찾아달라는 요청을 보냅니다. 쿼리는 모든 노드에 동일하게 요청하며 각각의 노드는 자신의 검색 스레드 풀의 스레드 하나를 사용해서 샤드에서 문서를 검색합니다. 하지만 노드에 샤드가 1개 이상 있다면 검색요청은 스레드 풀에 있는 사용 가능한 스레드를 모두 사용해서 각각의 샤드에서 문서를 찾습니다. 즉, 검색 요청의 처리를 완료하기 전까지 다른 검색 쿼리가 처리되지 못하고 검색 스레드 큐에 위치합니다. 만약 큐가 가득 차면 rejected 현상이 발생하고 샤드가 지나치게 많으면 검색 성능을 저하시키는 원인이 됩니다. 따라서 클러스터를 검색 엔진으로 구축할 경우 성능 테스트를 통해 적정한 수준의 샤드 수를 결정해야 합니다.

 

테스트는 다음과 같이 진행합니다. 먼저 데이터 노드 한대로 클러스터를 구성하고, 해당 노드에 데이터를 저장한 후 사용자의 검색 쿼리에 대한 응답을 100ms 이하로 줄 수 있는지를 테스트해야 합니다. 이때 클러스터 구성은 데이터 노드 한대, 레플리카 샤드 없이 프라이머리 샤드만 1개로 구성한다. 그리고 해당 샤드에 데이터를 계속 색인하면서 샤드의 크기가 커짐에 따라 검색 성능이 어떻게 변하는지를 측정합니다. 이렇게 구성해야 데이터 노드가 샤드 하나로 검색 요청을 처리할 때의 성능을 측정할 수 있습니다. 샤드 하나당 하나의 검색 스레드만 사용해야 검색 스레드 큐에 검색 쿼리가 너무 많이 쌓이지 않아서 하나의 샤드에서 측정된 검색 성능을 보장할 수 있기 때문입니다. 위의 요구사항에 맞추려면 took에 100ms 이하로 나오면 요구조건에 만족하는 것입니다.

 

단일 샤드로 구성되었기 때문에 이때 생성된 인덱스의 크기가 곧 단일 샤드의 크기가 됩니다. 이렇게 노드 한 대가 사용자의 요구인 100ms 속도로 검색 결과를 리턴해줄 수 있는 샤드의 적정 크기를 측정합니다. 일반적으로 샤드의 크기가 커짐에 따라 시간도 증가하는 양의 상관관계를 보입니다.

 

실제 서비스할 전체 데이터 크기를 테스트를 통해 산정한 인덱스의 크기로 나누면 그 값이 사용자가 원하는 응답 속도를 보여줄 수 있는 샤드의 개수가 됩니다. 예를 들어 테스트를 통해 100ms의 응답을 주는 인덱스의 크기가 25GB였을 때, 시나리오상 인덱스 크기 500GB를 25GB로 나누어 샤드 개수를 20개로 산정할 수 있습니다.

 

샤드 개수를 20개로 정했다면 역으로 데이터 노드 개수를 산정하기 위해서 n개로 나눠보면 데이터 노드 개수는 2, 4, 5, 10개 범위 내에서 선택할 수 있습니다. 데이터 노드 개수를 정했다면 필요한 용량만큼의 데이터 노드 서버 스팩을 선택할 수 있습니다.

 

  • 인덱스 패턴 search_index_v1
  • 프라이머리 샤드 기준 하루에 색인되는 인덱스 용량 : 500GB
  • 인덱스 보관 기간 : 재색인 요구가 있을 때까지 보관
  • 레플리카 샤드 개수 : 1개
  • 클러스터에 저장되는 전체 예상 용량 : 500GB * 2(레플리카) : 1TB
  • 클러스터의 전체 인덱스에 의해 생성되는 샤드 총 개수 : 20 * 2(레플리카) = 40개
  • 데이터 노드 개수 : 20 / n => 2, 4, 5, 10 개 중 선택 => 4 선택
  • 인덱스 하나를 기준으로 데이터 노드 한 대에 할당되는 샤드 개수 : 20 * 2 / 4 = 10개

 

100ms 응답을 주는 인덱스의 크기가 25GB였을 때를 기준으로 지정했지만 레플리카로 인해 샤드 개수가 늘어났고, 데이터 노드 개수를 늘렸으로 더 좋은 성능이 나오게 됩니다.

 

클러스터를 구성하는 비용 또한 중요한 문제이기 때문에 실제로는 적절하게 조절해야 합니다. 과하다 싶으면 샤드와 노드 개수를 줄이거나 급격한 데이터 증가가 예상된다면 레플리카 샤드 개수를 늘리거나 노드를 추가해서 성능을 확보할 수도 있습니다. 만약 색인이 공존하는 검색 엔진이 아니라면 forcemerge api나 read_only 설정을 적용해서 성능을 확보할 수 있습니다.

 

정리

 

매일 인덱스가 바뀌는 데이터 분석 엔진

  1. 인덱스의 총 보관 기간 동안 차지하게 될 용량 예측(replicas 개수와 함께 고려된 용량)
  2. 데이터 노드 한대에 저장 가능한 용량 측정
  3. 차지하게 될 총 용량을 1대의 노드가 장애가 난 상태에서도 80% 이하로 구축할 수 있는 데이터 노드 개수 산출
  4. 산출된 데이터 노드 개수 * n배(처음에는 2배)로 프라이머리 노드 개수 지정
  5. 하루 색인되는 용량 / 프라이머리 노드 개수 <= 20GB로 프라이머리 개수 구하는 n배수를 조정 (10GB 이내로 맞출 수 있다면 맞추기)
  6. 총 샤드 개수(레플리카 + 프라이머리) / 데이터 노드 <= 640 개로 만족하지 않으면 노드를 추가하거나 샤드 개수 n배수 조정

장기간 보관 데이터의 경우, hot, warm 아키텍처를 고려해볼 수 있습니다.

 

검색 엔진(데이터가 불려진다는 가정)

  1. 검색 쿼리에 대한 응답 ms 기준 측정(ex. 100ms 이하로 응답)
  2. 클러스터 구성(데이터 노드 1대, 레플리카 샤드 X, 프라이머리 샤드 1대)으로 쿼리 응답 ms 기준을 만족할 때까지 데이터를 계속 색인하여 적정한 하나의 샤드 크기를 결정 => 단일 샤드로 구성되었으므로 인덱스의 크기가 곧 단일 샤드의 크기
  3. 실제 서비스할 전체 데이터 크기 / 앞서 측정한 단일 샤드의 크기 = 사용자가 원하는 응답 속도를 보여줄 수 있는 샤드의 개수
  4. 샤드 개수 / n = 선택할 수 있는 데이터 노드 개수 범위

 

검색엔진은 예상되는 크기의 전체 디스크 사용량이 크게 중요하지 않기 때문에 성능을 보고 데이터 노드와 레플리카 샤드 개수를 조정해야 합니다.

 

인덱스 운영 전략

 

템플릿과 명시적 매핑 활용

 

실제 서비스에서는 동적으로 생성되는 것보다는 최대한 명시적 매핑을 지정하는 것이 좋습니다.

 

라우팅 활용

 

라우팅 지정은 성능을 유의미하게 상승시키므로 사전에 서비스 요건과 데이터 특성 등을 파악하고 어떤 값을 라투잉으로 지정해야 할지 설계해야 합니다. 라우팅을 지정하기로 했다면 해당 인덱스에 접근하는 클라이언트도 라우팅 정책 내용을 숙지하고 있어야 하며 이를 위해 인덱스 매핑에서 _routing을 true로 지정해 라우팅 지정을 필수로 제한하는 방법을 검토해 볼 수 있습니다.

 

시계열 인덱스 이름

 

시계열 데이터를 색인한다면 인덱스 이름에도 시간값을 넣는 것이 좋습니다. 이런 방법을 채택하면 오래된 데이터를 백업하고 삭제하는 것이 편리하며 데이터 노드를 티어로 구분해서 클러스터를 구성하는 데이터 티어 구조에도 좋습니다.

 

alias

 

alias는 이미 존재하는 인덱스를 다른 이름으로 가리키도록 하는 기능입니다. alias가 하나 이상의 인덱스를 가리키도록 지정할 수도 있습니다.

POST _aliases
{
  "actions": [
    {
      "add": {
        "index": "my_index",
        "alias": "my_alias_name"
      }
    }
  ]
}

 

만약 여러 인덱스를 가리키는 alias라면 단일 문서 조회 작업의 대상이 될 수 없습니다. 업데이트, 삭제 등 쓰기 작업의 경우 is_write_index를 true로 지정한 인덱스를 대상으로 작업되고 없으면 쓰기가 불가능합니다.

 

단순 로그성 데이터가 아니라 서비스에 직접 활용되는 데이터를 들고 있는 인덱스라면 모두 alias를 사전에 지정하는 것이 중요합니다. 사전에 정의해두면 나중에 매핑이나 설정 등에 변화가 필요할 때 시 인덱스를 미리 만들고 alias가 가리키는 인덱스만 변경하면 운영 중에 새 인덱스로 넘어갈 수 있기 때문입니다.

 

롤오버

 

하나의 alias 에 여러 인덱스를 묶고 한 인덱스에만 is_write_true를 지정하면 쓰기를 담당하는 인덱스 내 샤드 크기가 커지면 새로운 인덱스를 생성해 같은 alias 안에 묶고 is_write_true를 새로운 인덱스로 옮기는 방식으로 운영하게 됩니다. 이를 한 번에 묶어서 수행하주는 기능이 롤오버입니다.

 

POST [롤오버 대상]/_rollover

 

alias의 이름 또는 데이터 스트림이 롤오버 대상으로 들어갑니다. alias내의 is_write_true 대상 인덱스는 하이픈 숫자 패턴을 따라야 합니다. (ex. test-index-000001) provided_name을 지정하고 대상을 alias로 묶고 롤오버 하면 provided_name 을 이용해서 새로운 인덱스를 생성할 수도 있습니다.

 

데이터 스트림

 

데이터 스트림은 내부적으로 여러 개의 인덱스로 구성되어 있습니다. 검색을 수행할 때는 해당 데이터 스트림에 포함된 모든 인덱스를 대상으로 검색할 수 있고, 문서를 추가 색인할 때는 가장 최근에 생성된 단일 인덱스에 새 문서가 들어갑니다. 롤오버 시에는 최근 생성된 인덱스의 이름 끝 숫자를 하나 올린 인덱스가 생성됩니다. 즉, 여러 인덱스를 묶고 is_write_true 인덱스를 하나 둔 alias와 유사하게 동작합니다. 데이터 스트림은 인덱스 템플릿과 연계해서 시계열 데이터 사용 패턴에 맞게 정형화하고 간단하게 사용할 수 있도록 정제된 것이라고 보면 됩니다.

 

데이터 스트림을 구성하는 인덱스는 뒷바침 인덱스(backing indices)라고 부르며 모두 hidden 속성입니다. 인덱스 이름 패턴은 고정이고 롤오버 시 명시적인 새 인덱스 이름 지정이 불가능합니다. 패턴은 .ds-데이터 스트림 이름-yyy.MM.dd-세대 수 형태입니다. 세대수는 000001부터 시작해서 증가하고, 반드시 인덱스 템플릿과 연계되어야 합니다. 문서 추가는 가능하지만 업데이트는 불가능하고 @timestamp 필드가 포함된 문서만 취급합니다.

 

ILM(index lifecycle management, 인덱스 생명 주기 관리) 정책 연동이 필수는 아니지만 데이터 스트림 기능 자체가 ILM과 연계를 염두에 두고 개발된 기능입니다.

 

데이터 스트림은 자동으로 많은 것들을 관리해 주는 대신 제약도 크기 때문에 서비스를 운영하다 보면 장애가 발생해서 특정 시간대에 발생한 시계열 데이터를 원하는 대로 온전히 처리하지 못하는 상황이 발생합니다. 데이터 스트림에 데이터는 멱등하게 재처리하기 쉽지 않기 때문에 시스템의 모니터링용 지표 데이터를 수집하기 위한 용도 등 그냥 문제 시간대의 데이터를 버려도 큰 문제가 되지 않는 경우에 사용하기 좋습니다.

 

데이터 티어 구조

 

데이터 티어 구조는 데이터 노드를 용도 및 성능별로 hot-warm-cold-frozen 티어로 구분해서 클러스터를 구분하는 방법입니다. 노드의 역할로 data를 지정하지 않고 data_content, data_hot, data_warm, data_cold, data_frozen을 지정해 클러스터를 구성합니다. 성능 차이가 많이 나는 장비를 가지고 클러스터를 구성해야 하거나 최근 데이터 위주로 사용하는 방식의 시계열 데이터를 운영할 때 채택하기 좋습니다.

 

데이터 티어 구조를 위해서는 node.roles에 data가 아니라 다른 역할을 지정해야 합니다. node.roles: ["data_content", "data_hot"] 와 같이 여러 역할을 겸임해도 됩니다.

 

  • data_content : 시계열 데이터가 아닌 데이터를 담는 노드로 실시간 서비스용 데이터용을 위한 역할입니다. 데이터 티어 구조를 채택한 경우 필수로 필요한 역할입니다.
  • data_hot : 시계열 데이터 중 가장 최근 데이터를 담당하는 노드로 잦은 업데이트, 읽기의 데이터를 담당합니다. 데이터 티어 구조를 채택한 경우 필수로 필요한 역할입니다.
  • data_warm : hot에 배정된 인덱스보다 기간이 오래된 인덱스를 담당하는 노드로, 상대적으로 수행 능력이 덜 요구되는 인덱스를 배정받습니다.
  • data_cold : 더 이상 업데이트를 수행하지 않는 읽기 전용 인덱스를 담당하는 노드입니다.
  • data_frozen : 인덱스를 검색 가능한 스냅숏으로 변환 한 뒤 이를 배정받는 노드로 단일 역할만 가지도록 설정하는 것이 좋습니다.
PUT my_index/_settings
{
  "index.routing.allocation.include._tier_preference": "data_warm,data_hot" 
}

 

위와 같이 지정할 경우 data_warmn 노드에 인덱스를 할당하고 없는 경우 data_hot에 인덱스를 할당합니다. 명시적으로 null을 지정하면 데이터 티어를 고려하지 않으며 기본값은 data_content이고 데이터 스트림 내 인덱스 생성 시 기본값은 data_hot입니다.

 

인덱스 생명 주기 관리

 

ILM은 인덱스를 hot-warm-cold-frozen-delete 페이즈로 구분해서 지정한 기간이 지나면 인덱스를 다음 페이즈로 전환시키로 이때 지정한 작업을 수행하도록 하는 기능입니다.

 

  • hot : 현재 업데이트가 수행되고 있는 읽기 작업도 가장 많은 상태
  • warm : 인덱스에 더 이상 업데이트가 수행되지는 않지만 읽기 작업은 들어오는 상태
  • cold : 인덱스에 더이상 업데이트가 수행되지 않고 읽기 작업도 가끔씩 들어오는 상태, 검색은 되나 느려도 되는 상태
  • frozen : 인덱스에 더이상 업데이트가 수행되지 않고 읽기 작업도 거의 들어오지 않는 상태, 검색은 되나 느려도 되는 상태
  • delete : 인덱스가 삭제되어도 되는 상태

 

frozen 페이즈에서는 운영 중인 인덱스를 검색 가능한 스냅숏으로 변환하는 작업이 가능하지만 엔터프라이즈 등급의 구독에서만 사용 가능합니다. 각 페이즈의 설계 의도는 위와 같지만 정책을 적용할 때 꼭 부합하지 않아도 됩니다. warm 페이즈에도 업데이트를 허용하도록 해도 되고 hot페이즈 다음에 바로 delete로 넘어가도 됩니다.

 

예를 들면 다음과 같은 시나리오가 가능합니다. 인덱스가 처음 생성된 hot 페이즈에서는 매일 자동으로 롤오버를 수행하고 샤드 사이즈가 8GB 크키가 넘어가면 날짜가 넘어가지 않아도 롤오버를 수행하도록 합니다. 생성된 지 3일이 지난 인덱스는 warm 페이즈로 전환하고 해당 인덱스는 읽기 전용으로 바꾸고 단일 세그먼트로 강제 병합합니다. 생성된 지 7일이 지난 인덱스는 cold 페이즈로 전환하고 샤드 복구 우선순위를 낮춥니다. 생성된지 30일이 지난 인덱스는 delete페이즈로 이동시켜 스냅숏 생명 주기 정책에서 스냅샷으로 백업될 때까지 기다렸다가 백업이 완료되면 삭제합니다.

 

페이즈 액션 종류
각 페이즈마다 지정할 수 있는 페이즈 액션 종류는 다음과 같습니다.

  • 롤오버
    • 지정된 조건을 만족할 때 롤오버를 수행합니다.
    • hot 페이즈에서 수행 가능합니다.
  • 읽기전용으로 만들기
    • hot, warm, cold 페이즈에서 수행 가능합니다. hot에서 사용 시, 롤오버 액션과 함께 지정되어야 합니다.
  • 세그먼트 병합
    • 세그먼트를 지정한 개수로 강제 병합하고 읽기 전용으로 만듭니다.
    • hot, warm 페이즈에서 수행 가능합니다.
    • hot 페이즈에서 이 액션을 지정하려면 롤오버 액션과 함께 지정해야 합니다.
  • shrink
    • 인덱스를 읽기 전용으로 만들고 shrink를 수행해서 새 인덱스를 만듭니다.
    • 새 인덱스의 샤드 개수와 샤드 하나의 최대 크기를 지정해서 개수를 정할 수도 있습니다.
    • 샤드가 한 노드에 모여 있지 않다면 복사를 수행하고 작업을 진행합니다.
    • hot, warm에서 수행 가능합니다.
    • hot 페이즈에서 수행하려면 롤오버 액션과 함께 지정해야 합니다.
  • 인덱스 우선순위 변경
    • 인덱스의 복구 우선순위를 변경합니다.
    • hot, warm, cold에서 수행가능합니다.
    • 값이 클수록 우선적으로 복구되고 지정되지 않은 인덱스의 기본 우선순위는 1입니다.
  • 할당
    • 노드 속성 지정을 통한 인덱스 단위 샤드 할당 필터링 설정을 변경해 어떤 노드가 이 인덱스를 할당받을 수 있는지 변경합니다.
    • 인덱스 설정을 업데이트해 복제본 샤드의 개수도 변경 가능합니다.
    • warm, cold에서 수행 가능합니다.
  • migrate
    • index.routing.allocation.include._tier_preference 설정을 변경합니다.
    • warm, colde 페이즈에서 수행 가능합니다.
  • 스냅샷 대기
    • 인덱스를 삭제하기 전 지정한 SLM 정책을 통해 해당 인덱스에 대한 스냅샷 백업이 완료될 때까지 대기합니다.
    • delete 페이즈에서 수행 가능합니다.
  • 삭제
    • 인덱스를 제거합니다.
    • delete 페이즈에서 수행 가능합니다.

 

ILM 정책 생성과 적용
정책은 키바나의 stack management 메뉴에서 index lifecycle policies 하위에서 create policy 버튼으로 생성할 수 있고 api로도 등록할 수 있습니다.

PUT _ilm/policy/test-ilm-policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_primary_shard_size": "4gb",
            "max_age": "1d"
          }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "forcemerge": {
            "max_num_segments": 1
          },
          "readonly": {}
        }
      },
      "cold": {
        "min_age": "14d",
        "actions": {
          "migrate": {
            "enabled": false
          },
          "allocate": {
            "number_of_replicas": 1
          }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "wait_for_snapshot": {
            "policy": "my-dail-snapshot-policy"
          },
          "delete" : {}
        }
      }
    }
  }
}
PUT [인덱스 이름]/_settings
{
  "index.lifecycle.name": "test-ilm-policy"
}

 

이렇게 직접 등록할 수 있는데 인덱스 템플릿을 이용하는 것이 편리합니다.

 

슬로우 로그 설정

 

슬로우 로그는 장애 원인을 추적하는데 도움이 되는데 기본적으로 설정이 저장되어 있지 않습니다.

PUT _settings
{
  "index.search.slowlog": {
    "threshold": {
      "query.warn": "10s",
      "query.info": "5s",
      "query.debug": "2s",
      "query.trace": "500ms",
      "fetch.warn": "1s",
      "fetch.info": "800ms",
      "fetch.debug": "500ms",
      "fetch.trace": "200ms",
    }
  }
}

 

이 로그는 샤드 레벨에서 측정됩니다. 검색 요청 전체에 소요된 시간을 측정해 로깅하는 것이 아닙니다. 느린 검색 로그를 남기도록 설정하려면 로그 디렉터리에 별도로 [클러스터 이름]_index_search_slow.log 파일이 생깁니다. 어떤 인덱스, 어떤 샤드에서 어떤 쿼리가 얼마나 시간을 소요했는지 확인할 수 있습니다.

 

PUT _settings
{
  "index.indexing.slowlog": {
    "source": "1000",
    "threshold": {
      "index.warn": "10s",
      "index.info": "5s",
      "index.debug": "2s",
      "index.trace": "500ms"
    }
  }
}

PUT [인덱스 이름]/_settings

 

source 설정은 느린 색인 작업의 _source를 몇 글자까지 로깅할 것인지 지정합니다. treu로 지정하면 전체를 로깅하고 false나 0을 지정하면 로깅하지 않습니다. 느린 검색 로그와 마찬가지로 [클러스터 이름]_index_index_slowlog.log 파일이 생깁니다. 이는 인덱스 단위로도 지정할 수 있습니다.

 

서킷 브레이커

 

엘라스틱서치는 처음부터 과도한 요청이 오는 경우 거부하는 정책을 채택했습니다.

  • 필드 데이터 서킷 브레이커
    • fielddata가 메모리에 올라갈 때 얼마만큼 메모리를 사용할지 예상합니다.
    • 기본값은 힙의 40%
    • indices.breaker.fielddata.limit으로 설정합니다.
  • 요청 서킷 브레이커
    • 요청 하나의 데이터 구조가 메모리르 ㄹ과다하게 사용하는지 계산합니다.
    • 기본값은 힙의 60%
    • indices.breaker.request.limit으로 설정합니다.
  • 실행 중 요청(in-flight request) 서킷 브레이커
    • 노드에 transport나 http를 통해 들어오는 모든 요청의 길이를 기반으로 메모리 사용량을 계산합니다.
    • 텍스트 원본 길이뿐만 아니라 요청 객체를 생성할 때 필요로 하는 메모리도 따집니다.
    • 기본값은 힙의 100%
    • network.breaker.inflight_requests.limit으로 설정합니다.
  • 부모 서킷 브레이커
    • 전체 메모리의 실제 사용량을 기준으로 동작합니다.
    • 다른 자식 서킷 브레이커가 산정한 예상 메모리 사용량의 총합도 체크합니다.
    • indices.breaker.total.use_real_memory 설정을 false로 변경하면 메모리의 실제 사용량은 체크하지 않습니다. 해당 값이 true인 경우 서킷 브레이커의 동작 기본값은 힙의 95%이고 false이면 70%입니다.
    • indices.breaker.total.limit으로 설정합니다.
PUT _cluster/settings
{
  "transient": {
    "indices": {
      "breaker": {
        "fielddata.limit": "30%",
        "request.limit": "70%",
        "total.limit": "90%"
      }
    },
    "network": {
      "breaker": {
        "inflight_requests.limit": "95%"
      }
    }
  }
}

 

서킷 브레이커 설정은 클러스터 api를 이용해 동적으로 설정할 수 있습니다.

 

클러스터 구성 전략

 

마스터 후보 노드와 데이터 노드 분리

 

규모가 있는 서비스를 위해 클러스터를 구성하는 경우 마스터 후보 노드를 데이터 노드와 분리시켜야 합니다. 데이터 노드를 재시작할 필요가 없는데 재시작한다면 불필요한 샤드 복구 과정이 수행되고 생기는 부하가 장애 상황인 클러스터를 더 심각한 장애상황으로 만들 수 있습니다. 마스터 노드를 재시작할 필요가 없는데 재시작한다면 불필요한 마스터 재선출 과정이 발생할 수 있습니다. 따라서 서버를 매우 적게 써야 하는 상황이 아니라면 장애 상황을 대비해 마스터 후보 노드와 데이터 노드를 분리하는 것이 좋습니다.

 

마스터 후보 노드 투표 구성원

 

투표 구성원은 마스터 후보 노드의 부분 집합으로 일반적으로는 마스터 후보 노드와 동일한 집합입니다. 투표 구성원의 과반 이상의 결정으로 마스터 선출 등 중요한 의사가 결정됩니다. 마스터 후보 노드는 홀수대를 준비하는 것이 비용 대비 효용성이 좋습니다. 7 버전 미만의 경우 split brain 문제를 방지하기 위해 minimum_master_nodes 옵션을 과반으로 지정해야 했지만 7버전 이상부터는 해당 개념이 없고 split brain 문제가 원척적으로 일어나지 않는 구조입니다. 그러나 홀수대의 후보 노드를 준비하는 것이 더 비용 대비 효용성이 좋습니다. 엘라스틱서치가 투표 구성원을 홀수로 유지하기 위해 투표 구성원에서 마스터 후보 노드를 하나 빼두기 때문입니다. 이 한대는 투표 구성원에 참여하지 않고 투표 구성원 중 하나의 노드가 죽으면 빠져 있던 것이 대신 들어오는 구조인데 순차적으로 죽는 경우라면 1대의 실패를 더 견딜 수 있지만 동시에 죽으면 2K+1, 2k+2 구성이든 모두 k대의 동시 실패만 견디기 때문에 홀수대를 준비하는 것이 비용 대비 효용성이 좋습니다.

 

서버 자원 대비 구성

 

  • 최소 구성 3대
    • 모두 마스터 후보 & 마스터 겸임
  • 사양이 낮은 4~5대 구성
    • 3대는 마스터 후보와 데이터 역할 겸임
    • 2대는 데이터 노드 전용
  • 사양이 높은 4~5대
    • 사양이 낮은 3대와 사양이 높은 3~4대 구성으로 변경
    • 사양이 낮은 장비에는 마스터 후보, 높은 장비에는 데이터 노드
  • 6~7대
    • 마스터 후보 노드와 데이터 노드를 완전히 분리

 

서버 설정 옵션

 

힙크기

 

7.8 이상 7.11 미만 버전의 경우 config/jvm.options.d 디렉터리는 있지만 config/jvm.options 파일의 기본 Xms, Xmx 설정에 주석처리가 안 되어있습니다. 따라서 config/jvm.options 파일의 수치를 수정할지, config/jvm.options 파일의 Xmx와 Xmx 설정을 주석처리 해서 제거하고 config/jvm.option.d 디렉토리 밑에 새 파일을 생성해 설정할지 선택해야 합니다. 7.11 버전 이상의 경우 후자가 관리하기 편리합니다.

vim config/jvm.options.d/heap-size.options
-Xms32736m
-Xmx32736m

 

힙크기를 선택하는 데는 다음과 같은 원칙이 있습니다.

 

  • 최소한 시스템 메모리의 절반 이하로 지정한다.
    • 루씬이 커널 시스템 캐시를 많이 활용하기 때문에 시스템 메모리의 절반은 운영체제가 캐시로 쓰도록 놔두는 것이 좋습니다.
  • 힙 크기를 32GB 이상 지정하지 않습니다.
    • 32GB 이내의 힙 영역에만 접근한다면 Compressed OOPs 기능을 적용할 수 있기 때문인데 실제로 경곗값은 32GB보다 살짝 아래쪽입니다.

 

java -Xmx32736m -XX:+PrintFlagsFinal 2> /dev/null | grep UseCompressedOops

bool UseCompressedOops := true

java -Xmx32737m -XX:+PrintFlagsFinal 2> /dev/null | grep UseCompressedOops

bool UseCompressedOops := false 

 

useCompressedOops 가 true가 되는 경곗값으로 힙 크기를 지정해야 합니다.

 

Compressed OOPs로 인코딩 된 주소를 실제 주소로 디코딩하려면 3비트 시프트 연산 후에 힙 영역이 시작되는 기본 주소를 더하는 작업이 필요한데 기본 주소를 0으로 바꿔준다면 시프트 연산을 제외한 나머지 과정을 없앨 수 있습니다. 이런 기능을 Zero-based compressed OOPs라고 합니다. 이를 적용하기 위한 힙 크기 경곗값은 Compressed OOPs보다 작으며 이도 확인해봐야 합니다.

java -XX:+UnlockDiagnosticVMOptions -Xlog:gc+heap+coops=debug -Xmx30721m -version

compressedOops mode : Non-zero disjoint base ...

java -XX:+UnlockDiagnosticVMOptions -Xlog:gc+heap+coops=debug -Xmx30720m -version

compressedOops mode : zero based ...

 

compressed Oops mode가 zero based로 적용되는 경곗값을 찾아야 합니다.

 

스와핑

 

엘라스틱서치는 스와핑을 사용하지 않는 것을 권장합니다.

# 여기서 swap 부분 제거 
sudo vim /etc/fstab

 

vm.max_map_count

 

이 값은 프로세스가 최대 몇 개까지 메모리 맵 영역을 가질 수 있는지 지정합니다. 루씬은 mmap을 사용하므로 이 값을 높일 필요가 있습니다. 만약 262144보다 낮은 값이라면 높여야 합니다.

sudo vim /etc/sysctl.d/98-elasticsearch.conf

vm.max_map_count = 262144

 

파일 기술자

 

엘라스틱서치는 많은 file descriptor를 필요하여 최소 65535 이상으로 지정하도록 가이드합니다.

# 확인
ulimit -a | grep "open files"

# 높이기
sudo vim /etc/security/limits.conf

# es 가동하는 유저명
username - nofile 65535
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감/반응 부탁드립니다.