왜 샤드 크기가 힙을 압박하는가

Elasticsearch에서 각 샤드는 독립적인 Lucene 인덱스이며, 열려 있는 동안 세그먼트 메타데이터, term dictionary, doc values 캐시, 각종 자료구조를 JVM 힙에 상주시킨다. 문제는 문서 수가 아니라 샤드 개수 × 세그먼트 수가 힙 상주 오브젝트를 결정한다는 점이다. 샤드를 잘게 쪼갤수록 오버헤드가 곱으로 늘어나고, GC가 회수할 수 없는 상주 객체(live set)가 커지면서 Old GC가 잦아지고 결국 노드가 멈춘다.

경험칙은 여전히 유효하다. 샤드 하나는 대략 10~50GB, 노드 힙 1GB당 샤드 20개 이하가 안전선이다. 30GB 힙이면 클러스터 전체에서 노드당 600개 샤드가 상한이라는 뜻이다.

먼저 현황을 측정한다

추정하지 말고 실제 샤드 분포와 힙 상태부터 본다. `_cat` API로 샤드 크기 편차를, `_nodes/stats`로 힙 사용률과 GC 빈도를 확인한다.

#샤드 크기·개수 상위 확인 (큰 순서)
GET _cat/shards?v&h=index,shard,prirep,store,docs&s=store:desc

#노드별 힙 사용률과 세그먼트 메모리
GET _cat/nodes?v&h=name,heap.percent,heap.max,segments.count,segments.memory

#GC 빈도 (old gc가 분당 수회면 위험)
GET _nodes/stats/jvm?filter_path=**.gc.collectors.old

heap.percent가 상시 75%를 넘고 Old GC 시간이 계속 증가하면 힙 압박 상태다. 여기에 샤드 수가 수천 개라면 원인은 거의 확정이다.

세그먼트와 필드데이터 원인 분리

힙을 먹는 주체가 세그먼트 메타인지, 텍스트 필드 정렬/집계로 인한 fielddata인지 구분해야 처방이 갈린다. fielddata는 text 필드에 정렬·집계를 걸 때 폭발적으로 커진다.

GET _nodes/stats/indices/fielddata,segments?filter_path=nodes.*.name,nodes.*.indices.fielddata.memory_size_in_bytes,nodes.*.indices.segments.memory_in_bytes

fielddata가 크다면 text 필드 집계를 keyword 필드로 옮기거나 `eager_global_ordinals`를 검토한다. 세그먼트 메모리가 크다면 샤드/세그먼트 개수 자체를 줄이는 방향이다.

과다 샤드 통합: shrink와 rollover

이미 잘게 나뉜 인덱스는 강제 병합 후 shrink로 샤드 수를 줄인다. 시계열 데이터라면 처음부터 ILM rollover로 크기 기준 분할을 강제한다.

PUT logs-2026-09/_settings
{ "index.blocks.write": true }

POST logs-2026-09/_forcemerge?max_num_segments=1

POST logs-2026-09/_shrink/logs-2026-09-shrunk
{ "settings": { "index.number_of_shards": 1, "index.number_of_replicas": 1 } }

ILM 정책은 샤드가 50GB에 도달하거나 7일이 지나면 새 인덱스로 넘겨, 한 샤드가 무한정 커지거나 반대로 너무 잘게 생기는 것을 막는다.

설정 비교

상황증상처방
샤드 수천 개, 각 수백MBOld GC 빈발, 노드 불안정shrink / rollover로 통합, 인덱스당 샤드 수 축소
샤드 하나가 200GB 초과복구·리밸런싱 지연reindex로 primary 수 늘려 재분할
text 필드 집계fielddata 힙 급증keyword 서브필드로 집계 전환

주의점

forcemerge는 I/O가 매우 무거우므로 쓰기 트래픽이 적은 시간대에만 수행하고, 이미 롤오버가 끝난(더 이상 쓰지 않는) 인덱스에만 적용한다. 활성 인덱스에 `max_num_segments=1`을 걸면 거대 세그먼트가 생겨 이후 삭제 문서 회수가 어려워진다.

또한 fielddata circuit breaker(`indices.breaker.fielddata.limit`)를 낮추는 것은 근본 해결이 아니라 방어선일 뿐이다. 샤드 설계와 매핑을 고치지 않으면 쿼리 거부만 늘어난다. 힙을 무작정 키우는 것도 답이 아니다. 힙 30GB를 넘기면 compressed oops가 비활성화되어 오히려 효율이 떨어지므로, 26~30GB 선에서 멈추고 샤드 수를 줄이는 쪽이 정석이다.