LLM WikiAccess-protected knowledge portal
← 스터디 홈
5편 · 약 18분

성능 튜닝과 운영 트러블슈팅

성능 문제를 보는 순서

Elasticsearch 성능 문제는 보통 한 가지 설정값으로 해결되지 않는다. 같은 "느리다"라는 증상이라도 원인은 검색 쿼리, 샤드 설계, JVM heap, 디스크, CPU, bulk ingest, replica, refresh, mapping 폭증, thread pool 포화 중 하나일 수 있다. 그래서 운영자는 먼저 병목이 어디인지 분리해야 한다.

가장 안전한 접근은 다음 순서다.

  1. 사용자 증상을 구분한다. 검색 지연인지, 색인 지연인지, 429 TOO_MANY_REQUESTS인지, write block인지 먼저 나눈다.
  2. 클러스터 상태와 노드별 편차를 본다. 전체 tier가 느린지, 특정 노드만 뜨거운지 확인한다.
  3. 샤드 수와 샤드 크기를 본다. 너무 작은 샤드가 많아도, 너무 큰 샤드가 있어도 문제가 된다.
  4. JVM memory pressure와 GC를 본다. heap이 높다는 사실보다 GC 후에도 내려오지 않는지가 중요하다.
  5. 쿼리·집계·bulk 요청의 크기와 동시성을 줄여 재현한다.
  6. 그 다음에 설정을 바꾼다. 설정 변경은 원인 분리 후에 해야 한다.
느린 Elasticsearch 증상부터 나누기 검색 지연 쿼리 비용 · 샤드 수 · cache 색인 지연 bulk · refresh · replica · I/O 거절·장애 429 · breaker · watermark slow log + profile 비싼 query/aggs 찾기 throughput test bulk 크기와 worker 조절 node stats CPU · heap · disk · thread pool
Elasticsearch 성능 문제의 1차 분류

샤드 설계가 성능의 바닥을 결정한다

Elasticsearch에서 샤드는 병렬 처리 단위이면서 비용 단위다. 샤드가 많으면 여러 노드로 분산하기 쉬워 보이지만, 검색은 각 샤드에서 실행되고 결과를 모아야 한다. 특히 작은 샤드 수천 개를 동시에 검색하면 실제 데이터량보다 조정 비용과 thread pool 사용량이 커진다.

Elastic의 일반 가이드는 샤드 하나를 10GB~50GB 범위로 맞추고, 샤드당 문서 수는 2억 개 미만으로 유지하는 것이다. 이 숫자는 절대 법칙이 아니라 시작점이다. 검색 패턴, segment 수, mapping field 수, 복구 시간 목표에 따라 조정해야 한다.

운영에서 자주 보는 실패 패턴은 다음과 같다.

패턴증상대응
너무 많은 작은 샤드검색 thread pool 포화, heap overhead 증가, cluster state 부담rollover 크기 조정, ILM 적용, 오래된 index shrink 또는 reindex
너무 큰 샤드장애 복구와 relocation이 오래 걸림, 특정 샤드 검색 지연primary shard 수 재설계, rollover 기준 낮추기
특정 노드만 hot spot일부 노드 CPU·disk만 높음shard allocation 확인, routing key 편향 확인
field 수 폭증heap과 mapping 관리 비용 증가dynamic mapping 제한, explicit mapping 사용

샤드 문제는 보통 나중에 고치기 어렵다. 이미 index가 만들어진 뒤 primary shard 수는 쉽게 바꿀 수 없기 때문이다. 시간 기반 로그나 이벤트 데이터라면 data stream과 ILM으로 rollover 기준을 정하고, 실제 데이터량을 보며 조정하는 방식이 안전하다.

검색 성능 튜닝

검색 성능은 "더 빠른 서버"만의 문제가 아니다. Elasticsearch는 Lucene segment와 filesystem cache에 크게 의존한다. JVM heap을 과도하게 키우면 Lucene 파일을 담을 OS page cache가 줄어 오히려 검색이 느려질 수 있다. 그래서 heap은 보통 전체 메모리의 절반 이하로 두고, 나머지를 filesystem cache에 남기는 원칙이 중요하다.

검색 쿼리를 튜닝할 때는 다음 질문부터 한다.

  • 불필요하게 많은 index나 shard를 동시에 검색하고 있지 않은가?
  • query_string, multi_match가 너무 많은 field를 훑고 있지 않은가?
  • 점수가 필요 없는 조건을 filter가 아니라 must에 넣고 있지 않은가?
  • terms, date_histogram, nested aggregation이 heap을 많이 쓰고 있지 않은가?
  • nested나 parent-child join을 검색 시점에 반복하고 있지 않은가?

검색 시점에 계산하는 일을 줄이면 효과가 크다. 예를 들어 자주 쓰는 가격 구간, 상태 조합, 정규화된 검색 필드는 index 시점에 미리 만들어 둘 수 있다. 여러 field를 매번 multi_match로 검색해야 한다면 copy_to로 검색 전용 field를 만들어 검색 대상 field 수를 줄이는 방법도 있다.

필터 조건은 가능한 bool.filter에 둔다. filter context는 scoring을 하지 않고 캐시될 수 있어 날짜 범위, 상태, tenant, 카테고리 같은 조건에 적합하다. 반대로 relevance가 중요한 전문 검색 조건만 query context에 남긴다.

색인 성능 튜닝

색인 성능은 bulk 크기, 동시성, refresh, replica, 디스크 I/O의 균형이다. 문서를 하나씩 넣는 방식은 대량 적재에 맞지 않는다. Bulk API를 사용하되, bulk 요청을 무작정 크게 만드는 것도 위험하다. 요청이 너무 크면 coordinating node와 data node의 heap pressure가 올라가고, 실패 시 재시도 비용도 커진다.

Elastic은 단일 노드·단일 샤드 기준으로 bulk 크기를 100, 200, 400개처럼 늘려가며 처리량이 plateau에 도달하는 지점을 찾으라고 권한다. 비슷한 성능이면 더 작은 bulk 크기를 선택하는 편이 운영상 안전하다. 요청 하나가 수십 MB를 넘지 않도록 조심해야 한다.

동시 worker도 같은 방식으로 올린다. 처음에는 worker 수를 늘릴수록 처리량이 오른다. 어느 순간 CPU, disk, thread pool, indexing pressure 중 하나가 한계가 되고 429가 늘어난다. 이 지점부터는 더 밀어 넣는 것이 아니라 backoff가 필요하다.

대량 초기 적재에서는 다음 선택지가 있다.

  1. 검색 가시성 지연을 허용할 수 있으면 index.refresh_interval30s처럼 늘린다.
  2. 완전한 초기 적재라면 일시적으로 refresh를 -1로 꺼둘 수 있다.
  3. 원본 데이터가 안전하게 남아 있고 재적재 가능하면 replica를 일시적으로 0으로 줄일 수 있다.
  4. 적재 후 refresh와 replica를 원래대로 되돌리고, 필요한 경우에만 force merge를 검토한다.

주의할 점은 replica를 0으로 줄이면 노드 장애 시 데이터 손실 가능성이 생긴다는 것이다. 운영 데이터의 일반적인 실시간 ingest에서는 이 방법을 쉽게 쓰면 안 된다.

JVM heap과 GC를 읽는 법

Elasticsearch heap은 정상 상태에서도 오르내린다. 중요한 것은 순간 heap 사용률이 아니라 GC 후 회복되는지다. 건강한 패턴은 heap이 올라갔다가 GC 후 다시 내려오는 톱니 모양이다. Elastic은 일반적으로 heap이 약 30%~70% 사이에서 오르내리는 패턴을 정상으로 설명한다.

문제가 되는 패턴은 다음과 같다.

  • GC 후에도 heap이 30% 근처로 내려오지 않는다.
  • old GC가 잦아진다.
  • GC pause가 길어져 요청 latency가 튄다.
  • JVM memory pressure가 지속적으로 85%를 넘는다.
  • circuit breaker 오류가 늘어난다.

수동으로 heap을 지정해야 한다면 -Xms-Xmx를 같은 값으로 두고, 전체 메모리의 50% 이하로 설정하는 것이 일반 원칙이다. 약 31GB를 넘기지 않는 것도 오래된 실무 원칙이다. 최신 Elasticsearch는 노드 role과 메모리를 보고 heap을 자동 설정하므로, 특별한 이유가 없다면 기본값을 먼저 신뢰하는 편이 낫다.

Heap pressure가 높을 때는 JVM 옵션부터 바꾸기보다 원인을 찾아야 한다. oversharding, 큰 aggregation bucket, 너무 큰 bulk 요청, fielddata: true, mapping 폭증, 비싼 query가 더 흔한 원인이다.

429 TOO_MANY_REQUESTS는 실패가 아니라 압력 신호다

Elasticsearch가 429를 반환하는 것은 요청을 버그로 판단했다는 뜻이 아니다. 현재 node나 thread pool, circuit breaker, indexing pressure가 더 받아들이기 어렵다는 뜻이다. 클라이언트는 즉시 같은 속도로 재시도하면 안 된다. randomized exponential backoff를 적용해야 한다.

운영자는 어떤 종류의 거절인지 분리해야 한다.

확인 대상API 예시해석
thread pool rejectionGET _cat/thread_pool?v=true&h=node_name,name,active,queue,rejected,completedsearch, write queue가 포화되는지 확인
circuit breakerGET _nodes/stats/breakermemory 보호를 위해 거절되는지 확인
indexing pressureGET _nodes/stats/indexing_pressurebulk ingest가 heap 한계를 넘는지 확인
CPU hot threadGET _nodes/hot_threads어떤 Java thread가 CPU를 쓰는지 snapshot 확인

여기서 중요한 함정은 많은 카운터가 node 시작 이후 누적값이라는 점이다. 한 번 숫자가 높다고 현재도 장애라는 뜻은 아니다. 시간 간격을 두고 증가율을 봐야 한다.

디스크 watermark와 write block

Elasticsearch는 data node의 disk가 꽉 차는 것을 막기 위해 watermark를 사용한다. 기본 동작은 단계적으로 강해진다.

disk 사용률watermark동작
85%low해당 노드에 replica와 대부분의 shard 할당을 피함
90%highshard를 다른 노드로 relocation하려고 함
95%flood-stage해당 노드에 shard가 있는 index를 read-only로 설정

Flood-stage에 도달하면 write API가 실패하고 read-only-allow-delete block이 보일 수 있다. 이때 단순히 block만 해제하면 안 된다. 디스크 여유를 먼저 확보해야 한다. 원인이 남아 있으면 Elasticsearch가 다시 write block을 걸 수 있다.

점검 순서는 다음과 같다.

  1. GET _cat/allocation?v와 monitoring으로 노드별 disk 사용률을 본다.
  2. GET _cluster/health에서 relocating shard가 움직이는지 본다.
  3. GET _cat/recovery?v=true&active_only=true로 relocation 진행률을 본다.
  4. shard가 빠지지 않으면 GET _cluster/allocation/explain으로 allocation을 막는 조건을 확인한다.
  5. 오래된 index 삭제, replica 조정, ILM, 저장 field 축소, shard 재설계를 검토한다.

디스크 문제는 capacity planning 문제인 경우가 많다. 특히 replica는 primary와 같은 크기의 디스크를 사용한다. 보존 기간을 늘리거나 ingest가 급증하면 replica 수만큼 디스크 증가가 확대된다.

운영용 빠른 점검표

장애 시간에는 모든 가능성을 길게 토론할 여유가 없다. 아래 순서로 증거를 모으면 원인 분리가 빠르다.

  1. 전체 상태: GET _cluster/health, GET _cat/nodes?v=true&s=cpu:desc&h=name,role,master,cpu,load*,heap.percent,ram.percent,disk.used_percent,node.role
  2. 샤드 상태: GET _cat/shards?v=true&s=store:desc, GET _cluster/allocation/explain
  3. 검색 지연: index slow log, slow query sample, _profile은 필요한 쿼리에만 제한적으로 사용
  4. 색인 지연: bulk 크기, worker 수, refresh interval, replica, GET _nodes/stats/indexing_pressure
  5. heap 문제: GET _nodes/stats/jvm, GC log, breaker stats
  6. CPU 문제: GET _nodes/hot_threads, node별 load와 allocated processors 비교
  7. disk 문제: watermark log, allocation, recovery 진행률

튜닝의 목표는 단일 지표를 0으로 만드는 것이 아니다. 운영 목표는 검색 latency, ingest throughput, 장애 복구 시간, 비용 사이의 균형이다. 예를 들어 refresh interval을 늘리면 ingest는 빨라질 수 있지만 near-real-time 검색성은 낮아진다. replica를 줄이면 쓰기는 빨라질 수 있지만 가용성과 읽기 분산이 약해진다. force merge는 검색을 빠르게 만들 수 있지만 실행 중 I/O를 크게 사용한다.

따라서 Elasticsearch 운영에서 좋은 튜닝은 "가장 빠른 설정"이 아니라 장애 시에도 예측 가능한 설정이다.

References