Elasticsearch 입문 31편 Performance Tuning. Heap·Thread Pool·Cache·Fielddata·Circuit Breaker·Tune indexing·search.
이 글은 Elasticsearch 입문에서 운영까지 시리즈 38편 중 31편이에요. 26~30편이 "클러스터를 죽지 않게 운영하나" 였다면, 이번 편은 한 단계 더 들어가서 "같은 하드웨어로 응답 시간을 절반으로 줄이고 처리량을 두 배로 늘리려면 무엇을 만지나" 자리예요. 운영 ES 의 성능을 결정하는 다섯 군데가 JVM Heap · Thread Pool · Cache · Fielddata · Circuit Breaker 인데, 이 다섯 자리를 모르고 만지면 튜닝이 아니라 OOM 으로 가는 지름길 이 됩니다.
이 글은 Elasticsearch 8.x 공식 docs 의 Tune for indexing speed · Tune for search speed · Circuit breaker settings · Thread pools 챕터를 한국어 학습 노트로 풀어쓴 자료예요.
튜닝은 *측정 → 가설 → 한 변수 변경 → 재측정* 사이클이 핵심이에요. 한 번에 다섯 군데 만지면 어디가 효과인지 영영 모릅니다.
운영 ES 의 응답 시간·처리량을 결정하는 다섯 자리
운영 ES 에서 응답이 느려지거나 처리량이 안 나올 때 원인 분포가 거의 항상 다음 다섯 자리예요. JVM Heap 이 부족하거나 너무 커서 GC pause 가 길고, Thread Pool 이 막혀 요청이 대기열에 쌓이고, Query/Request Cache 가 동작 안 해 매번 풀스캔이 돌고, Fielddata 가 메모리를 폭주시키고, Circuit Breaker 가 트리거돼 503 이 떨어져요. 이 다섯 자리만 잡으면 운영 ES 의 90% 성능 사고가 사라져요.
튜닝의 첫 단계는 추측하지 않기 예요. ES 는 _nodes/stats · _cluster/stats · _cat/thread_pool · _nodes/hot_threads · profile API 같은 운영 진단 도구를 풍부하게 줘서, 어디가 병목인지 를 거의 항상 눈으로 확인 할 수 있어요. 30편(Monitoring) 의 도구로 병목 자리를 먼저 찾고, 그 자리에 맞는 설정만 만지는 게 순서예요.
이 글은 다섯 자리를 위에서부터 차례로 깊이 들어가요. 마지막에 Index 튜닝(쓰기 쪽) 과 Search 튜닝(읽기 쪽) 의 표준 체크리스트로 마무리.
JVM Heap — 50% 룰과 31GB 천장
ES 의 첫 번째 성능 자리는 JVM Heap 이에요. 너무 작으면 GC 가 폭주하고, 너무 크면 GC pause 가 길어지면서 오히려 느려져요. 두 자리 사이에 50% 룰 과 31GB 천장 이라는 두 개의 절대 규칙이 있어요.
50% 룰
서버 물리 RAM 의 50% 를 JVM Heap 에 할당 하고, 나머지 50% 는 OS Page Cache 로 남겨 둡니다. ES 는 Lucene 의 세그먼트 파일 을 mmap 으로 매핑해 읽기 때문에, OS Page Cache 가 풍부할수록 디스크 I/O 가 줄어들어요. Heap 을 80~90% 까지 욕심내면 Page Cache 가 모자라서 Heap 은 남아도는데 디스크 I/O 가 폭주 하는 역설이 생겨요.
64GB 서버라면 Heap = 32GB 처럼 보이지만, 다음 천장이 또 있어요.
31GB 천장 — Compressed Ordinary Object Pointers
JVM 은 Heap 32GB 미만 일 때 Compressed Oops (Compressed Ordinary Object Pointers) 라는 최적화를 켜요. 객체 포인터를 64bit 대신 35bit 로 압축해서 메모리 사용량과 CPU 캐시 효율 을 30% 가량 개선하는 기법이에요. Heap 을 32GB 이상으로 잡으면 이 최적화가 꺼져서 — Heap 을 늘렸는데 오히려 가용 메모리가 줄어드는 역설이 발생해요.
그래서 ES 권장 Heap 상한은 31GB 예요. 64GB 서버에서도 Heap = 32GB 가 아니라 Heap = 30~31GB 를 잡고 나머지를 Page Cache 로 두는 게 표준이에요. 더 큰 메모리가 필요하면 Heap 을 늘리지 말고 노드 수를 늘리는 게 답입니다.
설정 방법
8.x 의 Heap 설정은 config/jvm.options.d/heap.options 또는 환경 변수로 박아요.
# config/jvm.options.d/heap.options
-Xms30g
-Xmx30g
Xms = Xmx 로 최소 = 최대 를 같게 박는 게 운영 표준이에요. Heap 동적 증가 는 GC pause 를 길게 만들고 메모리 단편화 도 일으켜요. 기동 시점에 최대 메모리를 미리 잡아 두는 게 안전.
자동 계산을 원하면 ES_JAVA_OPTS 를 비우고 ES 가 RAM 의 50% (최대 31GB) 를 자동으로 잡도록 두는 옵션도 있어요. Docker 환경에서는 컨테이너 메모리 제한이 인식돼서 그 50% 를 잡습니다.
GC 선택 — G1GC vs ZGC
ES 8.x 의 기본 GC 는 G1GC (Garbage-First Garbage Collector) 예요. 대용량 Heap·짧은 pause 목적의 제너레이셔널 GC 고, 대부분의 운영 환경이 이걸로 충분해요. Heap 4GB 미만은 자동으로 Parallel GC 로 떨어집니다.
Java 17+ 환경에서 서브밀리초 pause 가 필요하면 ZGC (Z Garbage Collector) 도 선택지예요. 다만 처리량은 G1GC 보다 5~10% 떨어진다 는 트레이드오프가 있어서, 쓰기 처리량 중심 노드에는 G1GC 그대로 두는 게 일반이에요. ZGC 전환은 jvm.options.d/zgc.options 에 -XX:+UseZGC 를 박아 실험.
OOM 방지 — -XX:HeapDumpOnOutOfMemoryError
기본 jvm.options 에 -XX:+HeapDumpOnOutOfMemoryError 가 켜져 있어서, OOM 발생 시점에 heap dump (.hprof) 가 자동 생성돼요. 30편(Monitoring) 에서 짚었듯 dump 디렉터리는 충분한 디스크 가 있는 자리로 박아 두세요. dump 한 번에 30GB 가 나옵니다.
Thread Pool — 종류·queue·rejection
ES 의 두 번째 자리는 Thread Pool 이에요. ES 는 요청 종류별로 별도 스레드 풀 을 운영해서, 검색 부하가 색인을 막거나 그 반대 사고를 구조적으로 차단합니다. 풀 종류와 queue 동작을 모르면 503 Rejection 의 원인을 영영 못 찾아요.
주요 Thread Pool 일곱 가지
GET _cat/thread_pool?v 로 현재 상태를 확인할 수 있어요. 자주 만지는 풀은 다음.
| 풀 | 크기 (default) | queue | 자리 |
|---|---|---|---|
| search | int((CPU * 3) / 2) + 1 |
1,000 | 검색 요청 처리 |
| search_throttled | 1 | 100 | searchable snapshot 등 throttled 검색 |
| write | CPU 수 (최대 32) |
10,000 | 색인·bulk·update·delete |
| get | CPU 수 |
1,000 | GET by id |
| analyze | 1 | 16 | _analyze API |
| management | 5 | (스레드 한도) | 클러스터 관리 |
| snapshot | min(CPU/2, 5) |
(한도) | 스냅샷 작업 |
기본값이 대부분 충분 해서 만질 일이 거의 없어요. 만지더라도 크기보다 queue 를 우선 확인하는 게 운영 감각.
Queue 와 Rejection
각 풀은 고정 크기 스레드 와 대기열(queue) 으로 구성돼요. 모든 스레드가 바쁘면 요청이 queue 에 쌓이고, queue 까지 가득 차면 EsRejectedExecutionException (HTTP 429 또는 503) 으로 떨어집니다. 이 rejection 카운트 가 운영 중 가장 먼저 봐야 할 신호예요.
GET _cat/thread_pool/write?v&h=node_name,name,active,queue,rejected
rejected 열에 숫자가 쌓이면 쓰기 부하가 처리 용량을 초과 한다는 뜻이에요. 대응 순서는 (1) bulk 사이즈를 줄여 한 번에 던지는 양을 낮추고, (2) refresh_interval 을 늘려 색인 비용을 줄이고, (3) Data 노드를 추가해 부하를 분산하는 거예요. queue 크기를 늘리는 건 마지막 수단 — queue 만 늘리면 latency 가 폭증하면서 OOM 으로 가는 경로가 됩니다.
Queue 늘리기 — 정말 필요한 경우만
elasticsearch.yml 에서 queue 크기를 조정할 수 있어요.
thread_pool.write.queue_size: 20000
운영 권고는 queue 를 늘리기 전에 부하 자체를 줄이는 쪽 이에요. queue 를 늘리면 처리되지 못한 요청이 메모리에 쌓여 heap 사용량이 동반 증가하고, 끝내 Circuit Breaker 가 트리거되거나 OOM 으로 갑니다.
Query Cache — node level
ES 의 세 번째 자리는 Query Cache 예요. 노드 단위 캐시고, term·range 같은 비-scoring 쿼리 를 캐싱해서 같은 필터가 반복될 때 응답 시간을 수십 ms → 1ms 로 줄여요.
캐싱 대상
Query Cache 는 filter context 에서 실행되는 쿼리만 캐싱해요. score 계산이 필요한 쿼리(match 같은 풀텍스트) 는 캐싱 X. 12편(Search API) 에서 짚었듯 — bool.filter 자리에 둔 쿼리 가 캐싱 대상이고, bool.must 자리 는 score 계산 때문에 캐싱 X 라는 차이가 여기서 드러나요.
GET products/_search
{
"query": {
"bool": {
"must": [ { "match": { "title": "노트북" } } ],
"filter": [ { "term": { "brand": "samsung" } },
{ "range": { "price": { "gte": 500000 } } } ]
}
}
}
위 쿼리에서 filter 의 term/range 두 줄이 캐싱되고, 다음번에 다른 사용자가 같은 brand·price 조건으로 검색하면 캐시 히트 가 됩니다.
설정 — indices.queries.cache.size
기본값은 Heap 의 10% 예요. 캐시 히트율이 낮으면 늘리고, 메모리 압박이 심하면 줄여요. _nodes/stats/indices/query_cache 로 hit_count / miss_count 비율을 확인.
indices.queries.cache.size: 15%
인덱스별 비활성화
특정 인덱스에서 Query Cache 가 오히려 메모리만 먹는다 면 인덱스 settings 로 끌 수 있어요.
PUT logs-2026-05/_settings
{ "index.queries.cache.enabled": false }
시계열 로그 처럼 같은 필터가 거의 반복되지 않는 인덱스는 캐시 효율이 낮아서, 끄는 게 오히려 빠른 경우가 있어요.
Request Cache — shard level
네 번째 자리는 Request Cache 예요. 샤드 단위 캐시고, size=0 으로 집계 결과 만 받는 검색 요청을 캐싱해서 대시보드 응답 을 폭발적으로 빠르게 만들어 줘요.
캐싱 대상
size=0 인 검색 요청(=문서는 안 받고 집계 결과만 받음) 이 자동으로 Request Cache 에 들어가요. Kibana 대시보드 가 이 자리에 정확히 매칭돼서, ELK 운영 환경에서 가장 효과가 큰 캐시 예요.
GET orders/_search
{
"size": 0,
"aggs": {
"by_status": { "terms": { "field": "status" } }
}
}
이 요청은 각 샤드별로 집계 결과 가 캐시되고, 인덱스가 변경되지 않는 한 같은 요청은 수 ms 로 응답이 떨어져요. 시간 기반 인덱스(logs-2026-05-18) 처럼 어제 인덱스가 더 이상 안 바뀌는 자리에서 Request Cache 효율이 90% 이상 잡힙니다.
설정 — indices.requests.cache.size
기본값은 Heap 의 1% 로 작아요. Kibana 대시보드 부하가 큰 환경은 2~5% 로 늘리는 게 일반.
indices.requests.cache.size: 2%
?request_cache=true 또는 ?request_cache=false 쿼리스트링으로 개별 요청 강제도 가능해요. 최신 데이터를 매번 받아야 하는 실시간 대시보드는 false 로 막아 둡니다.
Fielddata — text 필드 정렬·집계의 함정
다섯 번째 자리, 그리고 운영 ES OOM 사고의 1순위 원인 이 Fielddata 예요.
왜 위험한가
ES 의 text 필드 는 정렬·집계가 기본적으로 막혀 있어요. 시도하면 다음 에러가 떨어집니다.
Fielddata is disabled on text fields by default.
Set fielddata=true on [field_name] in order to load fielddata in memory by uninverting the inverted index.
이걸 해결한답시고 "fielddata": true 를 켜면, 색인 시점에 만들어 둔 역색인(inverted index) 을 런타임에 반대로 뒤집어서(uninvert) 메모리에 통째로 올려요. 100만 건 인덱스에 1KB 짜리 text 필드가 있으면 Fielddata 가 1GB 를 그 자리에서 먹어요. 데이터가 늘어날수록 비례해서 Heap 을 갉아먹고, 결국 OOM 이거나 Circuit Breaker 가 트리거됩니다.
표준 답 — keyword + doc_values
운영 표준은 text 필드를 절대 정렬·집계에 쓰지 않는 것 이에요. 집계·정렬이 필요한 필드는 keyword 로 매핑 하고, ES 가 기본으로 켜 두는 doc_values (Lucene 의 컬럼 지향 저장소) 를 사용해요.
PUT products
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"raw": { "type": "keyword" }
}
}
}
}
}
이렇게 multi-field 로 박아 두면 풀텍스트 검색은 title 로, 정렬·집계는 title.raw 로 쓸 수 있어요. 8편(Mapping Deep) 의 표준 패턴이 여기서 다시 등장.
doc_values vs fielddata 차이
| doc_values | fielddata | |
|---|---|---|
| 저장 위치 | 디스크 (mmap, OS Page Cache) | JVM Heap |
| 생성 시점 | 색인 시점 | 검색 시점 (uninvert) |
| 지원 타입 | keyword·number·date·geo 등 (text 제외) | text |
| 운영 부담 | 거의 없음 | 매우 큼 |
doc_values 는 디스크에 컬럼 지향으로 미리 저장 돼 있어서, OS Page Cache 가 알아서 캐싱해 줘요. Heap 부담이 거의 0 이고, ES 의 대부분 집계·정렬 이 이 자리에서 돌아갑니다.
Circuit Breaker — 메모리 보호 마지막 방어선
여섯 번째 자리는 Circuit Breaker 예요. ES 가 메모리 폭주로 죽기 직전 에 요청을 거절해서 프로세스 자체를 살리는 마지막 방어선이에요.
종류 다섯 가지
GET _nodes/stats/breaker 로 현재 상태를 볼 수 있어요.
| Breaker | 한도 (default) | 자리 |
|---|---|---|
| parent | Heap 95% (indices.breaker.total.limit) |
모든 breaker 의 상위 한도 |
| fielddata | Heap 40% | Fielddata 메모리 |
| request | Heap 60% | 단일 요청이 사용하는 메모리 |
| in_flight_requests | Heap 100% | 전송 중 요청 (HTTP body 포함) |
| accounting | Heap 100% | 색인이 들고 있는 메모리 (segment 등) |
parent 가 최상위 한도예요. 모든 하위 breaker 의 합이 Heap 의 95% 를 넘으면 Circuit Breaker Trigger 가 발생하고, ES 가 다음 에러를 떨어뜨립니다.
[parent] Data too large, data for [<http_request>] would be [12345678/12.3mb],
which is larger than the limit of [11796480/11.2mb]
대응 순서
circuit_breaking_exception 이 보이면 원인 breaker 를 먼저 봅니다. fielddata 가 원인이면 → Fielddata 끄기·매핑 수정. request 가 원인이면 → 대형 집계(terms.size: 100000) 가 의심. in_flight_requests 가 원인이면 → bulk 사이즈 축소. 한도를 늘리는 건 마지막 선택 이고, 한도를 늘릴수록 OOM 위험 이 같이 커진다는 트레이드오프가 항상 같이 옵니다.
한도 동적 변경
런타임에 cluster setting 으로 한도 변경이 가능해요.
PUT _cluster/settings
{
"persistent": {
"indices.breaker.total.limit": "85%",
"indices.breaker.fielddata.limit": "30%"
}
}
기본 95% 가 너무 빡빡해서 정상 운영에서도 자주 트리거되는 환경은 85% 로 낮춰서 더 일찍 거절하게 만드는 패턴도 자주 써요. 늦게 거절하면 OOM, 빨리 거절하면 503 — 둘 사이의 균형을 잡아 가는 자리가 Circuit Breaker 튜닝이에요.
Index 튜닝 — 쓰기 처리량 두 배
여기까지가 공통 다섯 자리 였고, 이제 쓰기·읽기 따로 의 표준 체크리스트로 들어갑니다.
refresh_interval 30s+
ES 의 기본 refresh_interval 은 1초 예요. 1초마다 메모리 버퍼 가 Lucene 세그먼트 로 flush 되고 검색 가능 상태가 됩니다. 이게 near real-time 의 정체.
대량 색인 자리에서는 이 1초가 세그먼트 폭증 → merge 폭주 의 주범이에요. 대시보드용 로그 인덱스 같이 실시간성이 필요 없는 인덱스는 30s 또는 60s 로 늘리면 쓰기 처리량이 2~3 배 로 뜁니다.
PUT logs-2026-05/_settings
{ "index.refresh_interval": "30s" }
재색인이나 일회성 bulk 작업이라면 아예 끄고 작업 후 다시 켜는 패턴도 표준.
PUT bulk-target/_settings
{ "index.refresh_interval": -1 }
Replica = 0 (재색인 시)
초기 bulk 색인 이나 재색인(reindex) 자리에서는 Replica = 0 으로 시작해서 색인 끝난 후에 Replica 를 복구하는 패턴이 표준이에요. Replica = 1 이면 모든 쓰기가 두 번 일어나서 처리량이 절반으로 떨어져요.
PUT bulk-target/_settings
{ "index.number_of_replicas": 0 }
// bulk 작업 진행
PUT bulk-target/_settings
{ "index.number_of_replicas": 1 }
Force Merge after bulk
대용량 bulk 가 끝나면 세그먼트가 수백~수천 개 로 잘게 쪼개진 상태가 돼서 검색 성능이 떨어져요. 인덱스가 더 이상 안 바뀌는 상태 라면 force_merge 로 세그먼트를 1 개로 합칩니다.
POST logs-2026-05/_forcemerge?max_num_segments=1
주의 — force_merge 는 비용이 매우 큰 작업 이고, I/O 폭주 와 CPU 폭주 를 동시에 일으켜요. 운영 중인 인덱스에는 절대 X. 시간 기반 인덱스의 어제 인덱스 처럼 더 이상 쓰지 않는 자리에서만 돌립니다.
Bulk 사이즈 — 5~15MB
Bulk API 의 한 번에 던지는 사이즈 는 5~15MB 가 권장 범위예요. 너무 작으면 네트워크 왕복이 늘고, 너무 크면 circuit_breaker 가 트리거됩니다. 23편(Bulk API) 에서 깊이 들어간 자리.
Search 튜닝 — 읽기 응답 시간 절반
track_total_hits=false
기본 응답의 hits.total.value 는 정확한 전체 매칭 수 를 계산해 줘요. 이게 수백만 건 인덱스 에서는 불필요한 비용 이 됩니다. 페이지네이션에 정확한 총합이 필요 없는 자리는 track_total_hits: false 로 끄면 응답 시간이 30~50% 빨라집니다.
GET products/_search
{
"track_total_hits": false,
"query": { "match_all": {} }
}
또는 대략 10,000 까지만 정확히 알고 싶다면 track_total_hits: 10000 처럼 임계값 만 잡아 줘도 됩니다.
_source 최소화
_source 는 원본 문서 전체 를 응답에 담아 줘요. 큰 문서(>100KB) 가 자주 검색되는 환경은 필요한 필드만 골라 받는 게 빠릅니다.
GET products/_search
{
"_source": ["id", "title", "price"],
"query": { "match": { "title": "노트북" } }
}
_source: false 로 완전히 끄거나, fields API 로 runtime 가공된 값만 받는 패턴도 자리.
Search Profile API
특정 쿼리가 왜 느린지 가 안 보이면 Profile API 를 켭니다. ?profile=true 또는 본문에 "profile": true 를 박으면 쿼리의 각 단계별 시간 이 응답에 같이 나와요.
GET products/_search
{
"profile": true,
"query": { "match": { "title": "노트북" } }
}
응답의 profile.shards[].searches[].query 에 Lucene 의 weight·scorer 호출별 시간(ns) 이 박혀서, 어느 sub-query 가 90% 의 시간을 먹는지 눈으로 확인 할 수 있어요.
preference 로 캐시 적중률 끌어올리기
같은 사용자의 연속 요청을 항상 같은 샤드 복제본 으로 보내면 Request Cache 적중률 이 폭증해요. ?preference=_local 이나 사용자 id 를 ?preference=user-12345 처럼 박아 두면 해시 기반 으로 같은 replica 로 라우팅됩니다.
자주 만나는 사고
사고 1 — Heap 32GB 초과 후 Compressed Oops 망함
원인 — Heap 이 클수록 좋다 는 직관으로 64GB 서버에 Heap = 48GB 를 박았더니, Compressed Oops 가 꺼져서 가용 메모리가 오히려 줄어들고 응답 시간이 30% 느려짐.
해결 — Heap 을 30~31GB 로 다시 낮춥니다. 더 많은 메모리가 필요하면 노드 수를 늘리는 게 정답이지, 한 노드의 Heap 을 키우는 게 답이 아닙니다.
사고 2 — Thread Pool queue 폭주 503
원인 — 야간 일괄 색인 작업이 bulk 100MB × 100 동시 로 들어가면서 write thread pool 의 queue 10,000 이 가득 차고, EsRejectedExecutionException 이 폭주.
해결 — 단계 (1) bulk 사이즈를 100MB → 10MB 로, (2) 동시성을 100 → 10 으로, (3) refresh_interval 을 1s → 30s 로 늘리는 순서. queue 크기를 늘리는 건 마지막. 늘려도 latency 폭증 + OOM 으로 가는 게 일반.
사고 3 — Query Cache TTL 없어 stale 데이터 응답
원인 — Query Cache 는 인덱스가 변경되지 않는 한 유지되는 near-LRU 캐시예요. 자주 갱신되는 카테고리 인덱스에서 오래된 카운트 가 자꾸 나오는 사고.
해결 — 운영 표준은 인덱스 갱신 시 Lucene 세그먼트 변경 → 캐시 자동 무효화 라 대부분 자동 해결. 그래도 안 풀리면 POST <index>/_cache/clear 로 수동 무효화. 정말 stale 이 답이 아닌 자리는 그 인덱스의 Query Cache 자체를 끄는 것 이 답.
사고 4 — Fielddata Circuit Breaker triggered
원인 — Kibana 대시보드에서 text 필드 message 로 terms aggregation 을 돌렸더니, ES 가 Fielddata 를 메모리에 올리려다 Circuit Breaker 가 트리거.
해결 — 진짜 답은 매핑 수정 이에요. message 필드에 multi-field .keyword 를 추가하고, Kibana 에서 message.keyword 로 집계를 다시 설정. 임시 회피로 fielddata: true 를 켜는 건 내일 OOM 이 예약돼 있으니 X.
사고 5 — Force Merge 운영 중 실행
원인 — 응답이 느려져서 force_merge 를 운영 시간에 돌렸더니, I/O · CPU 폭주 로 검색 응답이 수십 초 단위 로 폭증.
해결 — force_merge 는 반드시 비운영 시간 에, 읽기 트래픽이 적은 인덱스 에만. 시간 기반 어제 인덱스 처럼 더 이상 쓰지 않는 자리에서만 돌려요.
사고 6 — Deep Pagination + track_total_hits=true
원인 — from: 100000, size: 100 + track_total_hits: true 의 검색이 각 샤드에서 100,100 개를 모두 정렬 해야 해서 수십 초 단위로 응답이 떨어짐.
해결 — 19편(Search Features) 의 search_after 또는 PIT(Point in Time) 으로 전환. 동시에 track_total_hits 를 false 또는 10000 임계값 으로 박아 둡니다.
사고 7 — Bulk 사이즈 100MB
원인 — bulk 한 번에 100MB 로 던져서 in_flight_requests circuit breaker 가 트리거.
해결 — bulk 사이즈를 5~15MB 로 분할하고 동시 요청 수 를 CPU 수의 1~2 배 로 맞춥니다. 클라이언트 라이브러리(예: Spring Data ES, elasticsearch-java) 의 BulkProcessor 가 이 자리를 자동으로 잡아 줍니다.
운영 권장 패턴
운영 ES 의 튜닝은 다음 4가지 패턴이 거의 모든 사고를 막아 줘요.
Heap 은 30~31GB 고정, Xms = Xmx. 그 이상의 메모리가 필요하면 노드 수를 늘리는 게 답. 한 노드의 Heap 을 키우는 게 답이 아닙니다.
Thread Pool 은 거의 기본값 그대로, queue 를 늘리지 말고 부하 자체 를 줄여요. bulk 사이즈 축소·refresh 늘림·동시성 축소 순서가 표준 대응 사다리.
Fielddata 는 운영에서 영원히 끄고, 집계·정렬이 필요한 필드는 keyword + doc_values 로. multi-field 패턴 이 표준.
대량 색인 시점 패턴 — Replica = 0 + refresh_interval = -1 + bulk 5~15MB → 끝난 후 Replica·refresh 복구 → force_merge. 이 사이클을 잡아 두면 색인 처리량이 기본 대비 3~5 배 로 뜁니다.
대시보드에 Heap usage · GC pause · Thread pool rejected · Circuit breaker tripped · Query cache hit ratio · Request cache hit ratio 여섯 지표를 박아 두고, 지표가 임계값을 넘는 순간 알람 으로 잡는 게 30편(Monitoring) 과 이어지는 자리예요.
시험 직전 한 번 더 — 압축 노트
- 운영 ES 성능 자리 다섯 = Heap · Thread Pool · Cache · Fielddata · Circuit Breaker.
- Heap 50% 룰 — 서버 RAM 의 50% 를 Heap 에, 나머지는 OS Page Cache.
- 31GB 천장 — Compressed Oops 가 Heap < 32GB 에서만 동작. Heap = 30~31GB 권장.
- Xms = Xmx 로 최소 = 최대 동일. Heap 동적 증가는 GC pause 증가.
- 기본 GC 는 G1GC. 서브밀리초 pause 필요시 ZGC.
- Thread Pool 주요 = search · write · get · analyze.
- Rejection 카운트 가 가장 먼저 봐야 할 신호. queue 늘리기 전에 부하 축소.
- Query Cache = 노드 레벨, filter context 만 캐싱. default Heap 10%.
- Request Cache = 샤드 레벨, size=0 집계 만 캐싱. default Heap 1%.
- Fielddata = text 필드 정렬·집계 시 메모리 폭주 → 운영에서 영원히 OFF.
- 답은 keyword + doc_values + multi-field 패턴.
- Circuit Breaker 종류 = parent · fielddata · request · in_flight_requests · accounting.
- parent default = Heap 95%, fielddata default = Heap 40%.
- 색인 튜닝 — refresh_interval 30s+ · Replica=0 (재색인) · force_merge after bulk · bulk 5~15MB.
- 검색 튜닝 — track_total_hits=false · _source 최소화 · profile API · preference 라우팅.
- force_merge 는 운영 중 X, 비운영 시간 + 어제 인덱스 에만.
- 자주 만나는 사고 7가지 = Heap 32GB 초과·queue 폭주·Query Cache stale·Fielddata Breaker·force_merge 운영 중·Deep Pagination·bulk 100MB.
시리즈 다른 편
- 26편 Cluster Operations — master·voting·rolling restart
- 27편 Shard Allocation — awareness·exclude·delay
- 28편 Snapshot & Restore — S3·SLM·복구 시나리오
- 29편 Security — TLS·RBAC·API Key
- 30편 Monitoring — cluster health·stats·slow log
- 다음 글 = 32편 Spring Data Elasticsearch — Repository·Template·POJO
- 33편 Kibana & ELK Stack — Discover·Visualize·Dashboard
- 38편 시리즈 마무리 — 결정 트리·체크리스트·자격증
한 줄 정리 — Performance Tuning = Heap 30~31GB · Thread Pool 기본 + 부하 축소 · Query/Request Cache 활용 · Fielddata 영구 OFF · Circuit Breaker 한도 관리 · refresh·replica·force_merge 색인 사이클 · track_total_hits·_source·profile 검색 절약 의 8자리 체크리스트. 측정 → 한 변수 변경 → 재측정 사이클이 절대 규칙.