느린 Elasticsearch 쿼리 최적화: 성능 튜닝을 위한 모범 사례

Elasticsearch 쿼리의 최고 성능을 구현하세요. 이 가이드는 쿼리 구조 최적화, 강력한 캐싱 메커니즘(노드, 샤드, 파일 시스템) 활용부터 Profile API를 통한 병목 현상 정밀 식별까지 필수 기술을 다루며, 느린 검색 성능을 해결하기 위한 실행 가능한 전략을 제공합니다. 효율적인 쿼리를 작성하고, `_source` 필터링을 활용하며, `search_after` 페이지네이션을 구현하고, 프로파일 결과를 해석하여 프로덕션 환경에서 성능 문제를 진단하고 해결하는 방법을 배우세요. Elasticsearch 전문성을 향상시키고, 번개처럼 빠른 사용자 경험을 보장하세요.

38 조회수

느린 Elasticsearch 쿼리 최적화: 성능 튜닝을 위한 모범 사례

Elasticsearch는 방대한 양의 데이터를 처리할 수 있는 강력한 분산 검색 및 분석 엔진입니다. 강력한 아키텍처에도 불구하고 비효율적인 쿼리는 성능 저하를 유발하여 사용자 경험과 애플리케이션 응답성에 영향을 미칠 수 있습니다. 이러한 병목 현상을 식별하고 해결하는 것은 건강하고 고성능을 유지하는 Elasticsearch 클러스터에 매우 중요합니다.

이 문서는 느린 검색 성능을 개선하기 위한 실용적인 전략을 깊이 있게 다룹니다. 쿼리 구조를 최적화하고, 다양한 캐싱 메커니즘을 효과적으로 활용하며, Elasticsearch의 내장 기능인 Profile API를 사용하여 성능 문제의 정확한 원인을 찾아내는 방법을 살펴보겠습니다. 이러한 모범 사례를 적용하면 쿼리 지연 시간을 크게 줄이고 Elasticsearch 클러스터가 최대 효율로 작동하도록 보장할 수 있습니다.

쿼리 성능 병목 현상 이해하기

해결책을 살펴보기 전에 느린 Elasticsearch 쿼리의 일반적인 원인을 이해하는 것이 도움이 됩니다. 여기에는 종종 다음이 포함됩니다.

  • 복잡한 쿼리: 여러 개의 bool 절, 중첩된 쿼리, 또는 대규모 데이터셋에 대한 wildcardregexp와 같은 비용이 많이 드는 작업을 포함하는 쿼리.
  • 비효율적인 데이터 검색: 불필요하게 _source를 가져오거나 페이지 매김을 위해 대량의 문서를 검색하는 경우.
  • 리소스 제약: 데이터 노드의 CPU, 메모리 또는 디스크 I/O가 부족한 경우.
  • 최적이 아닌 매핑: 집계에 대해 잘못된 데이터 유형을 사용하거나 doc_values를 활용하지 않는 경우.
  • 샤드 불균형 또는 과부하: 샤드가 너무 많거나 너무 적거나, 샤드/데이터 분포가 고르지 않은 경우.
  • 캐싱 부족: Elasticsearch의 내장 캐싱 메커니즘이나 외부 애플리케이션 수준 캐시를 활용하지 않는 경우.

쿼리 구조 최적화

쿼리를 구성하는 방식은 성능에 지대한 영향을 미칩니다. 작은 변경만으로도 상당한 개선을 가져올 수 있습니다.

1. 필요한 필드만 검색(_source 필터링 및 stored_fields)

기본적으로 Elasticsearch는 일치하는 각 문서에 대해 전체 _source 필드를 반환합니다. 애플리케이션이 소수의 필드만 필요로 하는 경우 전체 _source를 가져오는 것은 네트워크 대역폭과 파싱 시간 측면에서 낭비입니다.

  • _source 필터링: _source 매개변수를 사용하여 포함하거나 제외할 필드 배열을 지정합니다.

    json GET /my-index/_search { "_source": ["title", "author", "publish_date"], "query": { "match": { "content": "Elasticsearch performance" } } }

  • stored_fields: 매핑에서 특정 필드를 명시적으로 저장한 경우(예: "store": true), stored_fields를 사용하여 직접 검색할 수 있습니다. 이는 _source가 클 때 _source 파싱을 건너뛰므로 더 빠를 수 있습니다.

    json GET /my-index/_search { "stored_fields": ["title", "author"], "query": { "match": { "content": "Elasticsearch performance" } } }

2. 효율적인 쿼리 유형 선호

일부 쿼리 유형은 본질적으로 다른 유형보다 리소스를 더 많이 사용합니다.

  • 선행 와일드카드 및 정규식 피하기: wildcard, regexp, prefix 쿼리는 계산 비용이 많이 드는데, 특히 선행 와일드카드(예: *test)와 함께 사용될 때 더욱 그렇습니다. 이는 일치하는 용어를 찾기 위해 전체 용어 사전을 스캔해야 하기 때문입니다. 가능하다면 이러한 사용을 피하도록 애플리케이션을 재설계하거나 접두사 일치에는 completion suggester를 사용하십시오.

    ```json

    비효율적 - 선행 와일드카드 사용 금지

    {
    "query": {
    "wildcard": {
    "name.keyword": {
    "value": "*search"
    }
    }
    }
    }

    더 나은 방법 - 접두사를 알고 있는 경우

    {
    "query": {
    "prefix": {
    "name.keyword": {
    "value": "Elastic"
    }
    }
    }
    }
    ```

  • 구문에 대해 여러 match 절 대신 match_phrase 사용: 정확한 구문 일치의 경우 match_phrasebool 쿼리 내에 여러 match 쿼리를 결합하는 것보다 더 효율적입니다.

  • 필터링을 위한 constant_score: 문서가 얼마나 잘 일치하는지가 아니라 문서가 필터와 일치하는지 여부만 중요할 경우, 쿼리를 constant_score 쿼리로 래핑합니다. 이렇게 하면 점수 계산이 생략되어 CPU 주기를 절약할 수 있습니다.

    json GET /my-index/_search { "query": { "constant_score": { "filter": { "term": { "status": "active" } } } } }

3. 불리언 쿼리 최적화

  • 절의 순서: 가장 제한적인 절(가장 많은 문서를 필터링하는 절)을 bool 쿼리의 시작 부분에 배치합니다. Elasticsearch는 쿼리를 왼쪽에서 오른쪽으로 처리하며, 조기 가지치기는 후속 절에서 처리해야 하는 문서 수를 크게 줄일 수 있습니다.
  • minimum_should_match: bool 쿼리에서 minimum_should_match를 사용하여 일치해야 하는 should 절의 최소 횟수를 지정합니다. 이는 결과를 조기에 가지치기하는 데 도움이 될 수 있습니다.

4. 효율적인 페이지 매김(search_afterscroll)

전통적인 from/size 페이지 매김은 깊은 페이지(예: from: 10000, size: 10)에서 매우 비효율적입니다. Elasticsearch는 각 샤드에서 from + size까지의 모든 문서를 검색하고 정렬한 다음 from 문서들을 폐기해야 합니다.

  • search_after: 실시간 심층 페이지 매김의 경우 search_after가 권장됩니다. 이는 이전 페이지 마지막 문서의 정렬 순서를 사용하여 다음 결과 집합을 찾는 방식으로, 기존 데이터베이스의 커서와 유사합니다. 상태 비저장 방식이며 확장성이 더 좋습니다.

    ```json

    첫 번째 요청

    GET /my-index/_search
    {
    "size": 10,
    "query": {"match_all": {}},
    "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }

    첫 번째 요청의 마지막 문서 정렬 값을 사용하는 후속 요청

    GET /my-index/_search
    {
    "size": 10,
    "query": {"match_all": {}},
    "search_after": [1678886400000, "doc_id_XYZ"],
    "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    ```

  • scroll API: 대규모 데이터셋의 일괄 검색(예: 인덱스 재작성 또는 데이터 마이그레이션)의 경우 scroll API가 이상적입니다. 이는 인덱스의 스냅샷을 찍고 스크롤 ID를 반환하며, 이 ID는 후속 배치를 검색하는 데 사용됩니다. 실시간 사용자 대상 페이지 매김에는 적합하지 않습니다.

5. 집계 최적화

집계는 특히 고유성(cardinality)이 높은 필드에서 리소스를 많이 사용할 수 있습니다.

  • 집계 사전 계산: 복잡하고 실시간이 아닌 집계는 인덱싱 중에 또는 예약된 시간에 실행하여 결과를 사전 계산하고 별도의 인덱스에 저장하는 것을 고려하십시오.
  • doc_values: 집계에 사용되는 필드에 doc_values가 활성화되어 있는지 확인합니다(대부분의 비-텍스트 필드에서는 기본값). 이를 통해 Elasticsearch는 _source를 로드하지 않고도 집계를 위한 데이터를 효율적으로 로드할 수 있습니다.
  • eager_global_ordinals: terms 집계에 자주 사용되는 keyword 필드의 경우 매핑에서 eager_global_ordinals: true를 설정하면 전역 오디널(ordinals)을 미리 구축하여 쿼리 시간 집계 속도를 높일 수 있습니다. 이는 인덱스 새로 고침 시 비용이 발생하지만 쿼리 시간 집계 속도를 높입니다.

캐싱 기술 활용

Elasticsearch는 반복적인 쿼리를 크게 가속화할 수 있는 여러 계층의 캐싱을 제공합니다.

1. 노드 쿼리 캐시

  • 메커니즘: 자주 사용되는 bool 쿼리 내 필터 절의 결과를 캐시합니다. 노드 수준의 인메모리 캐시입니다.
  • 효과: 여러 쿼리에 걸쳐 상수이고 비교적 적은 수의 문서(10,000개 미만)와 일치하는 필터에 가장 효과적입니다.
  • 구성: 기본적으로 활성화되어 있습니다. indices.queries.cache.size(기본값은 힙의 10%)로 크기를 제어할 수 있습니다.

2. 샤드 요청 캐시

  • 메커니즘: 검색 요청의 전체 응답(히트, 집계, 제안 포함)을 샤드별로 캐시합니다. size=0이고 점수 계산이 없는 필터 절만 사용하는 요청(스코어링이 없는 요청)에 대해서만 작동합니다.
  • 효과: 동일한 요청(집계 포함)이 동일한 매개변수로 반복 실행되는 대시보드 쿼리 또는 분석 애플리케이션에 탁월합니다.
  • 사용 방법: "request_cache": true를 사용하여 쿼리에서 명시적으로 활성화합니다.

    json GET /my-index/_search?request_cache=true { "size": 0, "query": { "bool": { "filter": [ {"term": {"status.keyword": "active"}}, {"range": {"timestamp": {"gte": "now-1h"}}} ] } }, "aggs": { "messages_per_minute": { "date_histogram": { "field": "timestamp", "fixed_interval": "1m" } } } }

  • 주의 사항: 샤드가 새로 고쳐질 때마다(새 문서가 인덱싱되거나 기존 문서가 업데이트될 때) 캐시가 무효화됩니다. 동일한 결과를 자주 반환하는 쿼리에만 유용합니다.

3. 파일 시스템 캐시(OS 수준)

  • 메커니즘: 운영 체제의 파일 시스템 캐시가 중요한 역할을 합니다. Elasticsearch는 자주 액세스되는 인덱스 세그먼트를 캐시하기 위해 이 캐시에 크게 의존합니다.
  • 효과: 쿼리 성능에 매우 중요합니다. 인덱스 세그먼트가 RAM에 있으면 디스크 I/O가 완전히 우회되어 쿼리 실행이 훨씬 빨라집니다.
  • 모범 사례: 서버 RAM의 최소 절반을 파일 시스템 캐시에 할당하고 나머지 절반을 Elasticsearch JVM 힙에 할당합니다. 예를 들어, 64GB RAM이 있는 경우 Elasticsearch 힙에 32GB를 할당하고 OS 파일 시스템 캐시에 32GB를 남겨둡니다.

4. 애플리케이션 수준 캐싱

  • 메커니즘: 자주 요청되는 검색 결과를 위해 애플리케이션 계층(예: Redis, Memcached 또는 인메모리 캐시 사용)에서 캐시를 구현합니다.
  • 효과: 반복적인 요청에 대해 Elasticsearch를 완전히 우회하여 가장 빠른 응답 시간을 제공할 수 있습니다. 정적 또는 느리게 변경되는 검색 결과에 가장 적합합니다.
  • 고려 사항: 캐시 무효화 전략이 핵심입니다. 데이터 일관성을 보장하기 위해 신중한 설계가 필요합니다.

병목 현상 식별을 위한 Profile API 사용

Profile API는 Elasticsearch가 쿼리를 실행하는 방식과 시간이 어디에 소비되는지 정확히 이해하는 데 매우 유용한 도구입니다. 쿼리 및 집계의 각 구성 요소에 대한 실행 시간을 분석하여 보여줍니다.

Profile API 사용 방법

검색 요청 본문에 "profile": true를 추가하기만 하면 됩니다.

GET /my-index/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "Elasticsearch"}},
        {"term": {"status.keyword": "published"}}
      ],
      "filter": [
        {"range": {"publish_date": {"gte": "2023-01-01"}}}
      ]
    }
  },
  "aggs": {
    "top_authors": {
      "terms": {
        "field": "author.keyword",
        "size": 10
      }
    }
  }
}

Profile API 결과 해석

응답에는 각 샤드에서 쿼리 및 집계 실행을 자세히 설명하는 profile 섹션이 포함됩니다. 확인해야 할 주요 측정항목은 다음과 같습니다.

  • description: 특정 쿼리 또는 집계 구성 요소.
  • time_in_nanos: 이 구성 요소 실행에 소요된 시간.
  • breakdown: 쿼리의 경우 build_scorer_time, collect_time, set_weight_time 및 집계의 경우 reduce_time과 같은 세부 하위 측정항목.
  • children: 복잡한 쿼리 내에서 시간이 분배되는 방식을 보여주는 중첩된 구성 요소.

해석 예시:

WildcardQuery에 대해 높은 time_in_nanos가 표시되면 해당 부분이 쿼리에서 비용이 많이 드는 부분임을 확인하는 것입니다. collect_time이 높으면 일치 후 문서 검색 및 처리가 병목 현상임을 시사하며, 이는 _source 파싱이나 심층 페이지 매김 때문일 수 있습니다. 집계에서 높은 reduce_time은 최종 병합 단계에서 부하가 컸음을 나타낼 수 있습니다.

이러한 측정항목을 검토하여 가장 많은 리소스를 소비하는 특정 쿼리 절 또는 집계 필드를 찾아내고, 이전에 논의된 최적화 기술을 적용할 수 있습니다.

성능을 위한 일반적인 모범 사례

쿼리별 최적화 외에도 클러스터 전체 및 인덱스 수준의 몇 가지 모범 사례가 전반적인 검색 성능에 기여합니다.

1. 최적의 인덱스 매핑

  • textkeyword: 전문 검색에는 text를, 정확한 값 일치, 정렬 및 집계에는 keyword를 사용합니다. 잘못 일치하는 유형은 비효율적인 쿼리로 이어질 수 있습니다.
  • doc_values: 정렬하거나 집계하려는 필드에 doc_values가 활성화되어 있는지 확인합니다. keyword 및 숫자 유형에는 기본적으로 활성화되어 있지만, 나중에 해당 필드에서 집계를 수행해야 하는 경우 text 필드에 대해 명시적으로 비활성화하면 디스크 공간을 절약하는 대신 집계 성능이 저하될 수 있습니다.
  • norms: 문서 길이 정규화가 필요하지 않은 필드(예: ID 필드)의 경우 norms를 비활성화합니다("norms": false). 이렇게 하면 디스크 공간이 절약되고 인덱싱 속도가 향상되며, 점수 계산이 없는 쿼리 성능에는 미치는 영향이 거의 없습니다.
  • index_options: text 필드의 경우 용어가 문서에 존재하는지 여부만 알고 싶으면 index_options: docs를 사용하고, 구문 쿼리 및 근접 검색이 필요한 경우 index_options: positions(기본값)를 사용합니다.

2. 클러스터 상태 및 리소스 모니터링

  • 녹색 클러스터 상태: 클러스터가 항상 녹색 상태인지 확인합니다. 노란색 또는 빨간색 상태는 할당되지 않았거나 누락된 샤드를 나타내며, 이는 쿼리 안정성과 성능을 심각하게 저해할 수 있습니다.
  • 리소스 모니터링: 데이터 노드의 CPU, RAM, 디스크 I/O 및 네트워크 사용량을 정기적으로 모니터링합니다. 이러한 메트릭의 급증은 종종 느린 쿼리와 상관 관계가 있습니다.
  • JVM 힙: JVM 힙 사용량을 주시하십시오. 높은 사용량은 잦은 가비지 수집 일시 중지를 유발하여 쿼리를 느리게 만들 수 있습니다. 힙 압력을 줄이도록 쿼리를 최적화하십시오.

3. 적절한 샤드 할당

  • 샤드가 너무 많은 경우: 각 샤드는 리소스(CPU, RAM, 파일 핸들)를 소비합니다. 노드에 너무 많은 작은 샤드가 있으면 오버헤드가 발생할 수 있습니다. 대부분의 사용 사례에서 합리적인 크기(예: 10GB-50GB)의 샤드를 목표로 하십시오.
  • 샤드가 너무 적은 경우: 병렬 처리를 제한합니다. 샤드가 너무 적은 인덱스에 대한 쿼리는 사용 가능한 모든 데이터 노드를 효율적으로 활용할 수 없습니다.

4. 인덱싱 전략

  • refresh_interval: 더 낮은 refresh_interval(기본값 1초)은 데이터를 더 빨리 가시화하지만 인덱싱 오버헤드를 증가시킵니다. 검색 집약적인 워크로드의 경우 새로 고침 압력을 줄이기 위해 이 값을 약간 늘리는 것(예: 5-10초)을 고려하십시오.

결론

느린 Elasticsearch 쿼리 최적화는 데이터, 액세스 패턴 및 Elasticsearch의 내부 작동 방식을 이해하는 것을 포함하는 지속적인 프로세스입니다. 신중한 쿼리 구성 적용, Elasticsearch 캐싱 메커니즘의 효과적인 활용, 그리고 Profile API와 같은 강력한 진단 도구 활용을 통해 검색 애플리케이션의 성능과 응답성을 크게 향상시킬 수 있습니다.

Profile API를 사용한 구체적인 느린 쿼리에 대한 심층 분석과 결합된 정기적인 모니터링은 Elasticsearch 설정을 지속적으로 개선하여 사용자에게 빠르고 효율적인 검색 경험을 보장할 수 있는 힘을 제공합니다. 잘 구성된 인덱스와 건강한 클러스터가 모든 쿼리 최적화가 구축되는 기반이라는 점을 기억하십시오.