03 9월 2014 엔지니어링

Elasticsearch 인덱싱에 대한 성능 고려 사항

By Michael McCandless

2015년 11월 2일 추가 : 만일 Elasticsearch 2.0 이후 버전을 사용중이라면, Elasticsearch 2.0 에서의 인덱싱에 대한 성능 고려 사항 블로그 포스트를 확인하세요.

Elasticsearch 사용자들은 작은 로그 몇 줄 부터 웹 스케일 규모의 대용량 도큐먼트 컬렉션 등 매우 다양한 인덱싱 사례를 경험하고 있으며, 인덱싱 처리 속도의 극대화는 공통적으로 중요한 목표입니다. 우리는 "전형적인" 애플리케이션에 대한 우수한 기준을 기본 설정으로 하기 위해 노력하고 있으며, 여기에 기술된 몇 가지 간단한 모범 사례만 따라도 인덱싱 효율을 빠르게 개선할 수 있습니다.

우선, 제어할 자신이 없다면 너무 큰 Java heap은 사용하지 마십시오. Elasticsearch 가 작업을 할 수 있는 최대한의 크기 만큼 설정을 유지하되, 시스템 RAM의 절반 이상을 넘지 않도록 해야 합니다. 그래야만 OS가 IO 캐싱을 관리할 수 있는 적당한 크기의 RAM이 남습니다. Java 프로세스를 스왑 하지 않도록 주의 하십시오.

최신 Elasticsearch 버전 (1.3.2 기준)으로 업그레이드 하십시오. 최근 버전에서는 인덱싱과 관련된 수많은 문제들이 해결되었습니다.

자세한 내용을 살펴보기 전 주의 사항: 여기의 모든 정보는 현재 버전(1.3.2)을 기준으로 하지만, Elasticsearch는 빠르게 발전하고 있으므로, 이 정보는 미래의 구글러인 여러분이 보았을 때 더는 정확하지 않을 수 있습니다. 확실하지 않다면 사용자 커뮤니티에서 확인하십시오.

Marvel 은 인덱싱 성능을 위해 클러스터를 튜닝할 때 특히 유용합니다. 여기에 설명된 설정법을 따라 각각의 변경 사항이 클러스터의 동작에 미치는 영향을 쉽게 시각화할 수 있습니다.

클라이언트 설정

항상 벌크 api를 사용하십시오. 벌크 api는 하나의 요청으로 여러 개의 도큐먼트들을 인덱싱하고 적절한 개수로 조정하여 각 벌크 요청과 함께 전송합니다. 최적의 크기는 여러 요인에 따라 결정되지만, 지나치게 많은 도큐먼트 개수 보다는 지나치게 적다 싶은 개수 부터 시작해서 방향을 잡아 시행착오를 거쳐야 합니다. 클라이언트 쪽 스레드가 포함된 동시 벌크 요청이나 독립적인 비동기 요청을 사용하십시오.

인덱싱이 너무 느리다고 결론을 내리기 전에 클러스터의 하드웨어가 실제로 전부 활용되고 있는지 확인하십시오. iostat, top, ps와 같은 도구를 사용하여 CPU 또는 모든 노드의 IO가 포화 상태인지 확인하십시오. 포화 상태가 아니라면 더 많은 동시 요청을 보내도록 합니다. 하지만 자바 클라이언트에서 EsRejectedExecutionException 가 발생하였거나, REST 요청 응답으로 TOO_MANY_REQUESTS (429) 을 받았다면 현재 너무 많은 동시 요청을 보내고 있는 것입니다. Marvel을 사용 중인 경우라면 대시보드의 Node Statistics화면의 THREAD POOLS - BULK 섹션 아래에서 거부(rejection)된 요청 횟수를 확인할 수 있습니다. 대개 벌크 스레드 풀 크기(코어 개수로 기본 설정되어 있음)를 늘리는 것은 총 인덱싱 처리량이 감소될 수 있으므로 바람직하지 않습니다. 클라이언트 쪽의 동시 실행 수를 줄이거나 노드를 추가하는 것이 더 좋은 방법입니다.

여기에서 논의중인 설정은 단일 샤드에 대한 인덱싱 처리량을 극대화하는 데 집중되어 있으므로, 단일 루씬 인덱스가 수용 가능한 도큐먼트의 양을 측정하기 위해 먼저 하나의 노드를 복제본(replica)이 없는 하나의 샤드 만으로 테스트를 하고, 다시 반복적인 튜닝을 거쳐 전체 클러스터로 확장시켜 나가는 것이 가장 좋습니다. 이 방법을 통해 인덱싱 처리량의 요구 사항을 충족하기 위해 전체 클러스터에 필요한 대략적인 노드 개수의 추산치에 대한 기준을 얻을 수 있습니다.

올바르게 작동하는 단일 샤드가 확보되고 나면, 샤드 및 복제본의 개수를 늘려 클러스터에서 Elasticsearch의 다중 노드를 이용한 확장성을 완벽하게 활용할 수 있습니다.

결론을 도출하기 전에 전체 클러스터의 성능을 꽤 오랫동안(60분) 측정해야 대규모 병합, GC 주기, 샤드 이동, OS의 IO 캐시 초과, 예기치 않은 스와핑 등의 이벤트를 포함한 전체 수명 주기(lifecycle)에 대해 테스트를 수행할 수 있습니다.

저장 장치

인덱스를 보관하는 저장 장치는 당연하게도 인덱싱 성능에 막대한 영향을 미칩니다.

  • 최신 SSD를 사용하세요. 최고 속도의 스핀 디스크보다 훨씬 빠릅니다. 최신 SSD는 랜덤 액세스와 높은 순차 IO에 대한 지연 시간을 단축시킬 뿐만 아니라, 동시 인덱싱, 병합 및 검색에 요구되는 고차원의 동시 IO에서도 더 우수한 성능을 제공합니다.
  • 원격 마운트 파일 시스템(예: NFS 또는 SMB/CIFS)에 인덱스를 저장하지 마십시오. 대신 시스템의 로컬 저장 장치를 사용하십시오.
  • Amazon의 Elastic Block Storage와 같은 가상화 스토리지에 주의하십시오. 가상화 스토리지는 Elasticsearch와 매우 잘 작동하며 빠르고 설정이 간단하다는 이점이 있지만 안타깝게도 지속적인 관점에서 보았을 때 전용 로컬 스토리지에 비해 본질적으로 느리다는 단점이 있습니다. 최근의 한 비공식 테스트에서는 가장 우수한 성능으로 프로비저닝된 IOPS SSD 지원 EBS 옵션 조차도 로컬 인스턴스 연결 SSD보다 여전히 상당히 느렸습니다. 또한 로컬 인스턴스에 연결된 SSD 조차도 해당 물리 시스템에 있는 모든 가상 시스템 간에 공유되므로 해당 물리 시스템의 다른 가상 시스템이 갑자기 IO 사용량이 많아진 경우 막대한 속도 저하를 경험하게 될 것입니다.
  • 데이터를 여러 개의 path.data 디렉터리를 설정하여 저장 하거나, RAID 0 어레이를 구성해서 여러 개의 SSD에 인덱스를 스트라이핑하십시오. 이 두 방법은 파일 블록 수준에서 스트라이핑된다는 점을 제외하면 개별 인덱스 파일 수준의 Elasticsearch "스트라이핑"과 유사합니다. 두 접근법 모두 한 SSD에 오류가 발생하면 인덱스가 손상되기 때문에 단일 샤드의 오류 위험(더 빠른 IO 성능으로 상쇄됨)이 증가한다는 점만 기억하십시오. 이는 일반적으로 어쩔 수 없는 부분입니다. 최대 성능을 위해 단일 샤드를 최적화한 다음, 서로 다른 노드에 복제본을 추가하면 노드 오류에 대해 중복성이 보장됩니다. 나중에 사용할 수 있도록 스냅샷 및 복원 을 사용하여 인덱스를 백업할 수도 있습니다.

세그먼트 및 병합

새로 인덱싱된 문서는 우선 Lucene의 IndexWriter에 의해 RAM에 보관됩니다. 주기적으로 RAM 버퍼가 가득 차거나 Elasticsearch이 flush 또는 refresh를 트리거하면 이 문서들은 디스크의 새 세그먼트에 쓰여집니다. 결국 세그먼트가 너무 많아지게 되면 병합 정책 및 스케줄러에 따라 병합이 수행됩니다. 이 프로세스는 단계식입니다. 병합된 세그먼트는 큰 세그먼트를 생성하고 작은 병합이 충분히 수행된 후에는 이 큰 세그먼트들도 병합됩니다. 이 내용을 시각적으로 보여주는 자료 를 참조하십시오.

특히 큰 병합을 실행하는 경우에는 매우 긴 시간이 소요될 수 있습니다. 이는 대개 정상이며 이런 병합은 물론 드물기 때문에 상각 비용은 낮게 유지됩니다. 그러나 병합이 인덱싱을 따라잡지 못하는 경우, 인덱스에 너무 많은 세그먼트가 있을 때 심각한 문제가 생기는 것을 방지하기 위해 Elasticsearch는 수신 인덱싱 요청을 단일 스레드로 제한 합니다(1.2 기준).

Marvel 에서 현재 인덱싱이 제한되고 있다는 INFO 수준 로그 메시지가 표시되거나 세그먼트 개수가 계속 증가하는 것이 확인되는 경우는 병합이 뒤처지고 있는 것입니다. Marvel은 대시보드의 Index Statistics 화면MANAGEMENT EXTENDED 섹션 아래에 세그먼트 개수를 표시하며, 매우 느리게 로그(logarithmic) 지수의 비율로 증가해야 합니다. 큰 병합이 완료되면 톱니 모양이 표시됩니다.

병합이 뒤처지는 이유는 무엇일까요? 디폴트로 Elasticsearch는 병합에서 사용되는 총 바이트의 허용치를 겨우 20 MB/초로 제한합니다. 회전식 디스크의 경우 병합으로 인해 일반적인 드라이브의 IO 용량이 포화되지 않도록 하며, 여전히 원활한 성능으로 동시 검색이 가능합니다. 그러나 인덱싱 중 동시에 검색을 하지 않거나, 검색 성능이 인덱싱 처리 성능에 비해 덜 중요하거나, 또는 인덱스가 SSD에 보관되는 경우에는 index.store.throttle.type을 none로 설정하여 병합 제한을 완전히 비활성화해야 합니다. 이에 대한 자세한 내용은 저장 을 참조하십시오. 1.2 버전 이전에는 병합 IO 제한이 요청했던 것보다 훨씬 제한적인 심각한 버그가 존재했었습니다. 업그레이드하십시오!

아직 동시 IO를 SSD만큼 처리할 수 없는 회전식 디스크를 사용하고 있는 경우에는 index.merge.scheduler.max_thread_count 를 1로 설정해야 합니다. 그렇지 않고 기본값(SSD에 유리함)을 사용하면 한 번에 실행하기에는 너무 많은 병합이 허용됩니다.

아직 업데이트가 진행 중인 인덱스에 최적화(optimize) 를 실행하지 마십시오. 그 이유는 모든 세그먼트를 병합하는 일은 비용이 매우 큰 작업이기 때문입니다. 그러나 주어진 인덱스에 문서 추가를 완료한 경우에는 검색 중에 필요한 리소스가 줄어들기 때문에 해당 시점에서 최적화를 실행하는 것은 좋은 방법입니다. 예를 들어 시간 기준으로 인덱싱을 하여 하루 단위의 로그 분량이 인덱싱이 되는 경우 해당 날짜가 지난 후에 인덱스를 최적화하는 것이 좋으며, 노드가 며칠 분량의 인덱스를 보유하게 되는 경우에 특히 이러한 인덱스 최적화가 중요합니다.

다음은 추가로 조정되어야 할 몇 가지 설정들입니다.

  • 가령 _all 필드의 비활성화와 같이, 실제로 필요하지 않은 필드가 꺼지도록 매핑을 조정하십시오. 보관하려는 필드의 경우에도 인덱싱 또는 저장 여부와 그 방법을 조정할 수 있습니다.
  • 때로 _source 필드를 비활성화하는 것을 고려하게 되지만, 이 필드는 인덱싱 비용이 비교적 낮고(인덱싱하지 않고 저장만 함), 향후 업데이트나 기존 인덱스를 완전히 다시 작성하는 경우에는 큰 가치가 있으므로 디스크 사용량이 중요하지 않는 이상 비활성화는 그다지 가치가 없으며 디스크는 비교적으로 저렴한 자원이기 때문에 비활성화가 필요할 경우는 거의 없습니다.
  • 최근에 인덱싱한 도큐먼트 검색의 지연을 허용할 수 있다면 index.refresh_interval 을 30초로 늘리거나 -1로 설정해서 완전히 비활성화하십시오. 그러면 큰 세그먼트가 플러시되고 향후 병합에 대한 부담이 줄어듭니다.
  • flush를 가끔 수행할 때 과도한 RAM 사용이 유발될 수 있는 문제수정Elasticsearch 1.3.2 를 사용하는 경우에 한해 index.translog.flush_threshold_size 를 기본값(현재 200 mb)에서 1 gb로 늘리면 인덱스 파일에서 fsync를 호출하는 빈도를 줄일 수 있습니다.
    Marvel은 대시보드의 Index Statistics 화면 MANAGEMENT 섹션 아래에 flush 속도를 표시합니다.
  • 복제본을 사용하지 않고 큰 초기 인덱스를 구축한 다음 나중에 복제본을 활성화시키고 따라잡게 할 수 있습니다. 복제본이 0개일 때는 중복성이 없기 때문에 노드 오류가 발생하면 데이터가 소실되는(클러스터가 빨간색이 됨) 점을 유의해야 합니다. 추가할 문서가 더는 없어서 최적화(optimize) 를 실행할 계획이라면 복제본이 최적화된 세그먼트를 복사할 수 있도록 인덱싱을 완료한 후 복제본 개수를 늘리기 전에 수행하는 것이 좋습니다. 자세한 내용은 인덱스 설정 업데이트 를 참조하십시오.

인덱스 버퍼 크기

노드가 대용량 인덱싱만 수행하고 있는 경우, indices.memory.index_buffer_size 는 활성 샤드당 최대 512 MB 인덱싱 버퍼(대개 이 값을 넘어서도 인덱싱 성능은 개선되지 않음)에서 제공할 만큼 충분히 큰지 확인하십시오. Elasticsearch는 이 설정(Java heap 비율 또는 절대 바이트 크기)을 채택하여 min_index_buffer_size와 max_index_buffer_size 값을 노드의 현재 활성 샤드에 걸쳐 균등하게 나누어 적용합니다. 메모리 용량을 크게 설정하면 Lucene이 최초 세그먼트로 큰 용량을 쓸 수 있고, 이것은 나중에 병합에 대한 부담이 적어지는 것을 의미합니다.

기본값은 10%이며 대개 이정도면 충분합니다. 예를 들어, 노드에 5개의 활성 샤드가 존재하고 힙이 25 GB이면 각 샤드는 25 GB의 10% 중 1/5에 해당하는 512 MB(이미 최대치)를 가집니다. 대용량 인덱싱 후에 이 설정을 대용량 인덱싱 전용 설정에서 기본값(현재 10%)으로 낮추면 검색 작업에 사용할 RAM은 충분합니다. 아직은 이것이 동적 설정이 아니라는 점에 유의하십시오. 수정해야 할 미해결 문제가 아직 남아 있습니다.

1.3.0에서는 현재 인덱스 버퍼에 의해 사용되고 있는 바이트 수를 표시하는 기능이 인덱스 통계 API 에 추가되었습니다. 이 내용은 indices.segments.index_writer_memory 값에서 확인할 수 있습니다. Marvel에는 아직 표시되지 않으며 이후 버전에 추가될 예정이지만, 지금도 직접 차트를 추가할 수는 있습니다. (Marvel은 아직 데이터를 수집하고 있음)

향후 출시될 1.4.0에서 인덱스 통계 API 는 각 활성 샤드에 얼마나 많은 RMA 버퍼가 할당되었는지를 indices.segments.index_writer_max_memory로 정확하게 보여줍니다. 주어진 인덱스에 대해 이러한 값을 단위 샤드 기준으로 확인하려면 http://host:9200/<indexName>/_stats?level=shards; 를 사용하십시오. 이 요청은 샤드당 통계와 모든 샤드의 총 개수를 표시합니다.

자동 ID 사용 또는 좋은 ID 고르기

도큐먼트가 어떤 ID를 가지는지가 중요하지 않은 경우에는 Elasticsearch가 자동으로 ID를 할당 하게 할 수 있습니다. 이 경우는 도큐먼트별로 ID 및 버전을 저장하도록 최적화되며 (1.2 기준) Elasticsearch의 야간 인덱싱 벤치마크 에서 성능 차이를 확인할 수 있습니다(Fast 및 FastUpdate 행 비교).

자체 ID가 있는 경우에는 관리가 가능하여 Lucene에서 빠른 것을 고르고 ID 조회에 대해 더 최적화되어 있는 1.3.2 이상으로 업그레이드하십시오. Java의 UUID.randomUUID() 는 세그먼트에 ID를 할당하는 방법을 예측할 수 없거나 패턴이 없기 때문에 세그먼트에 따른 검색 을 유발하는 최악의 선택입니다.

Flake ID 를 사용하는 경우에는 Marvel화면에서인덱싱 속도의 차이를 확인할 수 있습니다.

완전 랜덤 UUID 사용 시와 비교:

곧 출시될 1.4.0에서는 랜덤 UUID에서 Flake ID로 Elasticsearch의 ID 자동 생성 기능이 전환되었습니다.

인덱스에 대한 Lucene의 low-level 작업이 궁금하신 분은 enablinglucene.iw TRACE 로깅(1.2 버전부터 가능) 을 이용해 보십시오. 이는 매우 장황한 출력을 생성하지만 Lucene의 IndexWriter 수준에서 일어나는 일을 이해하는 데 유용합니다. 최하단의 low-level 에 대한 내용이 출력됩니다. Marvel 은 인덱스에서 일어나는 일에 대해 더욱 개선된 실시간 시각화 기능을 제공합니다.

확대

여기에서 단일 샤드(Lucene 인덱스)에 관한 성능 조정을 중심적으로 다루었지만 Elasticsearch의 가장 뛰어난 기능은 인덱싱 및 검색 설정을 시스템 전체 클러스터로 편리하게 확장할 수 있다는 점입니다. 따라서 샤드 개수를 다시 늘리면(현재 기본값 5) 시스템에 동시성이 부여되고 최대 인덱스 크기가 늘어나고 검색할 때 지연 시간이 단축됩니다. 하드웨어 오류에 대비한 중복 저장을 확보하기 위해서는 복제본을 1 이상으로 늘려야 하는 점도 유의하십시오.

끝으로, 아직도 어려움을 겪고 있다면 Elasticsearch 사용자 포럼을 통해 문의하십시오. 아마도 수정이 필요한 흥미로운 버그가 있을지도 모릅니다(패치는 언제나 환영입니다!).