엔지니어링

고급 튜닝: 느린 Elasticsearch 쿼리를 찾아 문제 해결

Elasticsearch는 매우 유연하고 기능이 풍부한 애플리케이션으로, 데이터를 쿼리하는 다양한 방법을 제공합니다. 그러나 기대했던 것보다 느린 속도의 쿼리를 경험해 본 적이 있으신가요? Elasticsearch와 같은 분산 시스템에는 로드 밸런서 설정, 네트워크 지연 시간(대역폭, NIC 카드/드라이버) 등과 같은 외부 요인을 비롯하여 쿼리 성능에 영향을 미치는 다양한 요소가 있을 수 있습니다.

이 블로그에서는 쿼리 속도 저하의 원인과 Elasticsearch의 컨텍스트 내에서 이를 식별하는 방법에 대해 설명합니다. 또한 Elasticsearch 작동 방식에 상당히 익숙해야 사용할 수 있는 몇 가지 일반적인 문제 해결 방법을 다룹니다.

Elasticsearch 쿼리 속도 저하의 일반적인 원인

몇 가지 까다로운 사례를 살펴보기 전에 먼저 쿼리 속도 저하의 가장 일반적인 원인과 그 해결 방법부터 알아보겠습니다.

증상: 비활성 상태에서 높은 리소스 사용률

모든 샤드는 리소스(CPU/메모리)를 소비합니다. 색인/검색 요청이 없는 경우에도 샤드가 있으면 클러스터 오버헤드가 소비됩니다.

문제

모든 쿼리의 실행 속도가 느리게 느껴질 정도로 클러스터에 샤드가 너무 많습니다. 경험상으로 보면 노드당 비 고정 샤드 수는 구성된 GB당 20개 미만으로 유지하는 것이 좋습니다.

해결 방법

샤드 수를 줄이고, 인덱스를 고정하거나, 로드를 처리할 노드를 추가합니다. 샤드 수를 효율적으로 관리하기 위해 Elasticsearch의 롤오버/축소 기능과 함께 핫/웜 아키텍처(시간 기반 인덱스에 매우 적합)를 고려합니다. 배포할 때 먼저 적절한 용량 계획을 수립하면 각 검색 사용 사례에 맞는 최적의 샤드 수를 결정하는 데 도움이 됩니다.

증상: 스레드 풀이 거부된 횟수 증가

검색 스레드 풀이 ‘거부된’ 횟수가 계속 증가합니다. 이 횟수는 마지막 클러스터 재시작을 기준으로 누적됩니다.

GET /_cat/thread_pool/search?v&h=node_name,name,active,rejected,completed

응답은 다음과 같습니다.

node_name             name   active rejected completed
instance-0000000001   search      0       10         0
instance-0000000002   search      0       20         0
instance-0000000003   search      0       30         0

문제

쿼리가 클러스터의 코어 수를 초과하여 너무 많은 샤드를 대상으로 합니다. 그러면 검색 스레드 풀에 대기 중인 작업이 생성되어 검색 거부로 이어집니다. 또 다른 일반적인 원인은 느린 디스크 I/O로 인해 검색이 대기열에 오르게 되거나 경우에 따라 CPU가 완전히 포화 상태가 됩니다. 

해결 방법

인덱스를 생성할 때 기본 1 : 복제본 1 모델을 채택합니다. 인덱스 템플릿은 인덱스 시간에 이 설정을 롤아웃하는 좋은 방법입니다. (Elasticsearch 7.0 이상에서는 1P:1R이 기본값입니다). Elasticsearch 5.1 이상에서는 검색 작업 취소 기능을 지원합니다. 이 기능은 작업 관리 API에 느린 쿼리가 표시될 때 유용합니다. 디스크 I/O를 개선하려면 스토리지 권장 사항을 확인하고 최대 성능을 위해 권장 하드웨어를 사용해야 합니다.

증상: 높은 CPU 사용률 및 긴 색인 지연 시간

메트릭 상관관계는 클러스터에 과부하가 발생할 때 CPU 사용률이 높고 색인 지연 시간이 길다는 것을 보여줍니다.

문제

클러스터에 색인 작업이 많으면 검색 성능에 영향을 미칩니다.

해결 방법

index.refresh_interval(문서가 색인되는 시점과 문서가 표시되는 시점 사이의 시간)을 예를 들어 30초로 늘리면 일반적으로 색인 성능이 향상됩니다. 실제 적합한 간격은 상황에 따라 다를 수 있으므로 테스트가 중요합니다. 이렇게 하면 1초마다(기본값) 새 세그먼트를 생성하여 샤드가 작업을 과도하게 수행하지 않아도 됩니다.

색인 작업이 많은 사용 사례의 경우 인덱스 튜닝 권장 사항을 확인하여 인덱스와 검색 성능을 모두 최적화합니다.

증상: 더 많은 복제본 샤드로 지연 시간 증가

복제본 샤드 수를 늘리면(예: 1개에서 2개로) 쿼리 지연 시간이 발생할 수 있습니다. 더 많은 데이터가 있는 경우 캐시된 데이터가 더 빨리 제거되어 운영 체제 페이지 오류가 증가합니다.

문제

파일 시스템 캐시에는 인덱스 중 자주 쿼리되는 부분을 캐시하기에 충분한 메모리가 없습니다. Elasticsearch의 쿼리 캐시는 LRU 제거 정책을 구현합니다. 즉, 캐시가 가득 차면 가장 최근에 사용되지 않은 데이터가 제거되어 새 데이터를 저장할 수 있게 됩니다.

해결 방법

파일 시스템 캐시용으로 물리적 RAM의 50% 이상을 남겨둡니다. 메모리가 많을수록 더 많은 부분을 캐시할 수 있으며 특히 클러스터에 I/O 문제가 발생할 경우 유용합니다. 힙 크기가 적절하게 구성되었다고 가정할 때 파일 시스템 캐시에 사용할 수 있는 나머지 물리적 RAM은 검색 성능을 향상하는 데 큰 도움이 됩니다.

예를 들어, 128GB RAM 서버에서 힙 크기로 30GB를 설정하고 나머지 메모리를 파일 시스템 캐시(OS 캐시라고도 함)용으로 설정합니다. 이 방법은 운영 체제가 최근에 액세스한 4KB 블록의 데이터를 캐시하는 방식입니다. 따라서 동일한 파일을 반복해서 읽으면 대부분의 경우 디스크로 이동할 필요 없이 메모리에서 직접 읽기 요청을 처리합니다.

Elasticsearch는 파일 시스템 캐시 외에도 쿼리 캐시와 요청 캐시를 사용하여 검색 속도를 높입니다. 이러한 캐시는 모두 특정 검색 요청을 서로 다른 복사본에 번갈아 보내는 대신 매번 동일한 샤드 집합으로 라우팅하도록 검색 요청 기본 설정을 사용하여 최적화할 수 있습니다. 이렇게 하면 요청 캐시, 노드 쿼리 캐시 및 파일 시스템 캐시를 더 잘 활용할 수 있습니다.

증상: 리소스 공유 시 사용률 증가

운영 체제에 일관되게 높은 CPU 및 디스크 I/O 사용량이 표시됩니다. 타사 애플리케이션을 중지하면 성능이 향상되는 것을 확인할 수 있습니다.

문제

다른 프로세스(예: Logstash)와 Elasticsearch 자체 간에 리소스(CPU 및/또는 디스크 I/O) 경합이 있습니다.

해결 방법

공유 하드웨어에서 Elasticsearch를 리소스 집약적인 다른 애플리케이션과 함께 실행하지 않도록 합니다.

증상: 매우 고유한 필드를 집계할 때 힙 사용량 증가

매우 고유한 값(예: ID, 사용자 이름, 이메일 주소 등)이 포함된 집계된 필드를 쿼리할 때 성능이 저하됩니다. 힙 덤프 분석 중에 "search", "buckets", "aggregation" 등의 용어가 포함된 Java 객체가 힙을 많이 사용하는 것을 볼 수 있습니다.

문제

카디널리티가 높은 필드, 즉 많은 버킷을 가져오기 위해 많은 리소스가 필요한 필드에서 집계 작업이 실행되고 있습니다. 중첩 필드 및/또는 조인 필드를 포함하는 중첩 집계가 있을 수도 있습니다.

해결 방법

카디널리티가 높은 용어 집계의 성능을 향상하려면 컨설팅 팀 동료가 작성한 이 블로그 게시물(https://www.elastic.co/kr/blog/improving-the-performance-of-high-cardinality-terms-aggregations-in-elasticsearch)을 읽어보시기 바랍니다.

추가적인 튜닝 작업을 하려면 중첩 필드조인 필드에 대한 권장 사항을 확인하여 집계 성능을 개선하시기 바랍니다.

가끔 느린 쿼리

일반적으로 말해서 가끔 또는 간헐적으로 느린 쿼리에는 인덱스 튜닝/검색 튜닝 권장 사항 중 일부를 활용할 수 있습니다. 가끔 느린 쿼리는 다음 모니터링 메트릭 중 하나 이상과 밀접하게 연관되어 있습니다.

Elasticsearch에는 조율 노드가 데이터 노드의 로드를 인식하고 검색을 실행할 수 있는 최상의 샤드 복사본을 선택하도록 하여 검색 처리량과 지연 시간을 개선하는 또 다른 유용한 기능인 ARS(Adaptive Replica Selection)가 있습니다. ARS는 쿼리 시간 동안 로드를 더욱 고르게 분산시킴으로써 가끔 발생하는 속도 저하를 개선하는 데 큰 도움이 될 수 있습니다. Elasticsearch 7.0 이상에서는 ARS가 기본적으로 켜져 있습니다.

일관되게 느린 쿼리

일관되게 느린 쿼리의 경우 쿼리에서 기능을 하나씩 제거하면서 쿼리가 여전히 느린지 여부를 확인하면 됩니다. 성능 문제를 재현하는 가장 간단한 쿼리를 찾으면 문제를 분리하고 식별하는 데 도움이 됩니다.

  • 강조 표시가 없어도 여전히 느린가요?
  • 집계가 없어도 여전히 느린가요?
  • 크기가 0으로 설정되어 있어도 여전히 느린가요? (크기가 0으로 설정되면 Elasticsearch에서 검색 요청 결과를 캐시하여 검색 속도를 높임)

‘검색 튜닝’ 권장 사항 몇 가지를 적용하면 도움이 될까요?

문제를 해결할 때는 다음과 같은 방법이 유용할 때가 많습니다.

  • 프로필이 켜진 상태에서 쿼리 응답을 가져옵니다.
  • While(true) 루프에서 실행 중인 쿼리로 노드 핫 스레드 출력을 수집합니다. 이는 CPU 시간이 어디에 사용되는지 이해하는 데 도움이 됩니다.
  • 프로필 API의 이 사용자 친화적인 버전을 사용하여 쿼리를 프로파일링합니다.

쿼리가 Kibana 시각화에서 수신된 경우 Visualization Spy Panel(Kibana 버전 6.3 이하) 또는 Dashboard Inspect Panel(Kibana 버전 6.4 이상)을 사용하여 실제 쿼리 요청을 보고 내보낸 후 추가 분석을 위해 프로파일 API로 가져오십시오.

느리거나 비싼 쿼리 포착

Elasticsearch와 같은 분산 애플리케이션에서는 다양한 요청/스레드가 동시에 처리되므로 느리거나 비싼 쿼리를 포착하기가 어려울 수 있습니다. 클러스터 성능을 저하하거나(예: 긴 가비지 수집(GC) 주기) 더 안 좋게는 OOM(메모리 부족) 상황을 유발하는 비싼 쿼리를 실행하는 사용자를 제어할 수 없을 경우 상황은 더욱 복잡해집니다.

Elasticsearch 버전 7.0에는 메모리가 예약될 때 실제 힙 메모리 사용량을 측정하는 새로운 회로 차단 전략이 도입되었습니다. 이 새로운 전략은 클러스터 과부하를 유발하는 비싼 쿼리에 대한 노드 복원력을 개선하고, 기본적으로 켜져 있으며, 새로운 클러스터 설정인 indices.breaker.total.use_real_memory로 제어할 수 있습니다.

그러나 이러한 방법은 최선의 노력임을 유념해야 합니다. 여기에서 다루지 않은 시나리오의 경우 OOM 크래시 후 힙 덤프 또는 실행 중인 JVM에서 힙 덤프를 수집하여 근본 원인을 보다 잘 이해하는 것이 좋습니다.

Elasticsearch에는 클러스터를 OOM으로부터 보호하기 위한 또 다른 보호 설정(최대 버킷 소프트 한도)이 있습니다. 이 최대 버킷 집계 설정은 예를 들어 여러 집계 계층을 실행하느라 버킷 수(버전 7.0에서 기본값은 10,000개)가 초과되면 실행을 중지하고 검색 요청을 실패로 처리합니다.

잠재적인 고가의 쿼리를 추가로 식별하기 위해 낮은 임계값에서 시작하여 쿼리를 분리하고 임계값을 점차 높여 특정 쿼리로 범위를 좁혀갈 수 있도록 회로 차단 설정(indices.breaker.request.limit)을 구성할 수 있습니다.

느린 로그

Elasticsearch에서 느린 로그를 켜서 실행 속도가 느린 쿼리를 식별할 수도 있습니다. 느린 로그는 구체적으로 샤드 수준에서만 작동하며, 이는 데이터 노드만 적용됨을 의미합니다. 조율 전용/클라이언트 노드는 데이터(인덱스/샤드)를 보유하지 않으므로 제외됩니다.

느린 로그는 다음과 같은 질문에 대답하는 데 도움이 됩니다.

  • 쿼리 작업에 시간이 얼마나 걸렸나요?
  • 쿼리 요청 본문의 내용은 무엇인가요?

느린 로그 출력 샘플:

[2019-02-11T16:47:39,882][TRACE][index.search.slowlog.query] [2g1yKIZ] [logstash-20190211][4] took[10.4s], took_millis[10459], total_hits[16160], types[], stats[], 
search_type[QUERY_THEN_FETCH], total_shards[10], source[{"size":0,"query":{"bool":{"must":[{"range":{"timestamp":{"from":1549266459837,"to":1549871259837,"include_lower":true,
"include_upper":true,"format":"epoch_millis","boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"_source":{"includes":[],"excludes":[]},"stored_fields":"*","docvalue_fields":
[{"field":"timestamp","format":"date_time"},{"field":"utc_time","format":"date_time"}],"script_fields":{"hour_of_day":{"script":{"source":"doc['timestamp'].value.getHourOfDay()",
"lang":"painless"},"ignore_failure":false}},"aggregations":{"maxAgg":{"max":{"field":"bytes"}},"minAgg":{"min":{"field":"bytes"}}}}], id[]],

느린 로그 메시지 세부 항목:

항목설명
[2019-02-11T16:47:39,882]쿼리 날짜
[TRACE]로그 수준
[index.search.slowlog.query]검색 느린 로그의 쿼리 단계
[2g1yKIZ]노드 이름
[logstash-20190211]인덱스 이름
[4]실행된 쿼리의 샤드 번호
took[10.4s]샤드 처리에 걸린 시간[4]. 참고: 느린 로그를 살펴볼 때, 각 샤드가 병렬로 실행될 수 있기 때문에 서로 다른 샤드의 시간을 모두 추가하지 않는 것이 좋습니다.
took_millis[10459]소요 시간(밀리초)
total_hits[16160]적중 결과 합계
search_type[QUERY_THEN_FETCH]검색 유형
total_shards[10]인덱스의 샤드 합계
source[]실행된 쿼리 본문

감사 로그

Elastic 보안 기능이 포함된 골드 또는 플래티넘 구독 고객은 감사 로그를 켜서 쿼리에 대한 자세한 내용을 캡처할 수 있습니다. (사용자는 30일 체험판을 통해 Elastic 보안 기능을 테스트할 수 있습니다.) 감사 로깅은 다음과 같은 질문에 대답하는 데 도움이 됩니다.

  • 쿼리는 언제 발생했나요?
  • 누가 쿼리를 실행했나요?
  • 쿼리 내용은 무엇인가요?

기본 설정은 부족한 부분이 많으므로 이를 튜닝해야 합니다.

  1. 보안 감사 로그 활성화: elasticsearch.yml에 xpack.security.audit.enabled: true 설정.
  2. 보안 감사 출력에서 로그 또는 인덱스 활성화: elasticsearch.yml에 xpack.security.audit.outputs:[logfile, index] 설정.

    참고:
    • xpack.security.audit.outputs 설정은 버전 6.0, 6.2 및 5.x에만 적용됩니다. xpack.security.audit.enabled가 true로 설정되면 버전 7.0에서는 이 설정을 허용하지 않으며 json 출력을 기본값(<clustername>_audit.json)으로 사용합니다.
    • 감사 로깅의 세부 정보로 인해 보안 인덱스가 의도한 크기보다 커질 경우 클러스터 성능에 부담을 줄 수 있으므로 문제 해결을 위해서는 인덱스보다 로그 파일을 선택하는 것이 좋습니다.
    • 감사 모드에서는 매우 자세한 정보가 표시될 수 있으므로 문제가 해결되면 끄십시오.
  3. 이벤트 목록에 authentication_success 액세스를 포함하십시오. elasticsearch.yml에 xpack.security.audit.logfile.events.include: authentication_success 설정.

    참고:
    • 이 설정은 기본 이벤트에 포함되지 않습니다. 이를 설정하면 기본 설정을 덮어씁니다.
    • 하나 이상의 이벤트를 추가해야 하는 경우(대체가 아니라), 기존 기본 이벤트 목록을 먼저 작성한 후 마지막 항목 뒤에 위 설정을 추가하십시오.
    • 감사 이벤트의 요청 본문 출력: elasticsearch.yml에 xpack.security.audit.logfile.events.emit_request_body: true 설정.

이 설정을 사용하면 다음과 같은 사용자 쿼리를 모니터링할 수 있습니다.

  • 사용자: louisong
  • 쿼리 작업 시간: 2019-05-01T19:26:53,554 (UTC)
  • 쿼리 엔드포인트: _msearch(이는 일반적으로 Kibana 시각화/대시보드에서 발행된 쿼리를 의미함)
  • 쿼리 본문: 다음 로그에서 "request.body":로 시작되는 부분:

    {"@timestamp":"2019-05-01T19:26:53,554", "node.id":"Z1z_64sIRcy4dW2eqyqzMw", "event.type":"rest", "event.action":"authentication_success", "user.name":"louisong", "origin.type":"rest", "origin.address":"127.0.0.1:51426", "realm":"default_native", "url.path":"/_msearch", "url.query":"rest_total_hits_as_int=true&ignore_throttled=true", "request.method":"POST", "request.body":"{\"index\":\"*\",\"ignore_unavailable\":true,\"preference\":1556709820160}\n{\"aggs\":{\"2\":{\"terms\":{\"field\":\"actions\",\"size\":5,\"order\":{\"_count\":\"desc\"},\"missing\":\"__missing__\"}}},\"size\":0,\"_source\":{\"excludes\":[]},\"stored_fields\":[\"*\"],\"script_fields\":{},\"docvalue_fields\":[{\"field\":\"access_token.user_token.expiration_time\",\"format\":\"date_time\"},{\"field\":\"canvas-workpad.@created\",\"format\":\"date_time\"},{\"field\":\"canvas-workpad.@timestamp\",\"format\":\"date_time\"},{\"field\":\"creation_time\",\"format\":\"date_time\"},{\"field\":\"expiration_time\",\"format\":\"date_time\"},{\"field\":\"maps-telemetry.timeCaptured\",\"format\":\"date_time\"},{\"field\":\"task.runAt\",\"format\":\"date_time\"},{\"field\":\"task.scheduledAt\",\"format\":\"date_time\"},{\"field\":\"updated_at\",\"format\":\"date_time\"},{\"field\":\"url.accessDate\",\"format\":\"date_time\"},{\"field\":\"url.createDate\",\"format\":\"date_time\"}],\"query\":{\"bool\":{\"must\":[{\"range\":{\"canvas-workpad.@timestamp\":{\"format\":\"strict_date_optional_time\",\"gte\":\"2019-05-01T11:11:53.498Z\",\"lte\":\"2019-05-01T11:26:53.498Z\"}}}],\"filter\":[{\"match_all\":{}},{\"match_all\":{}}],\"should\":[],\"must_not\":[]}},\"timeout\":\"30000ms\"}\n", "request.id":"qrktsPxyST2nVh29GG7tog"}
        

    결론

    이 블로그에서는 쿼리 속도 저하의 일반적인 원인과 이를 해결하는 방법에 대해 설명했습니다. 또한 일관되게 느린 쿼리와 가끔 느린 쿼리를 식별하기 위한 다양한 방법에 대해서도 논의했습니다. 일반적으로 쿼리 속도 저하는 해결해야 하는 광범위한 클러스터 성능 문제의 증상 중 하나입니다.

    Elasticsearch에서는 쿼리 시간을 개선하기 위해 끊임없이 노력하고 있으며 향후 릴리즈 버전에서 더 빠른 쿼리 성능을 제공하기 위한 작업을 진행 중입니다. 느린 쿼리를 처리할 때 이 게시물이 도움이 되길 바랍니다. 추가로 논의하고자 하는 내용이 있다면 Elasticsearch 토론 포럼에 문의를 남겨주십시오. 즐거운 검색 되세요!