4장. 인덱스와 샤드 — 데이터는 어디에 놓이는가
Elasticsearch 운영 사고의 최다 원인은 쿼리가 아니라 샤드 설계 실수입니다. 라우팅, 사이징, 장애 대응까지 — 후회 없는 샤드 설계의 전부를 다룹니다.
① 인덱스=논리, 샤드=물리 — primary/replica가 노드에 분배되는 규칙 ② 문서가 샤드를 찾아가는 라우팅 공식 ③ 노드 장애 시 replica 승격과 클러스터 색상(GREEN/YELLOW/RED) ④ 샤드 사이징 가이드와 oversharding의 파국
4.1 인덱스는 논리, 샤드는 물리
이 구분이 모든 것의 출발점입니다. 인덱스(index)는 사용자가 보는 논리적 이름(products, logs-2026.07.11)이고, 실제 데이터는 인덱스를 쪼갠 샤드(shard)에 저장됩니다. 각 샤드는 3장에서 본 하나의 완전한 Lucene 인덱스이며, 클러스터의 어느 노드에든 배치될 수 있습니다.
# 인덱스 생성 — 샤드 수는 이 순간 결정된다
PUT /logs-2026.07.11
{
"settings": {
"number_of_shards": 3, # primary 수 — 생성 후 변경 불가!
"number_of_replicas": 1 # replica 수 — 언제든 동적 변경 가능
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"message": { "type": "text" },
"level": { "type": "keyword" }
}
}
}
3 primary + replica 1이면 총 6개의 샤드가 생깁니다. 이들이 노드에 어떻게 놓이고, 문서가 어떻게 자기 샤드를 찾아가는지 봅시다.
number_of_shards=3, replicas=1로 인덱스를 생성했습니다. 인덱스는 이름일 뿐 — 실제 데이터가 살 곳은 지금부터 정해집니다.
마스터가 3개의 primary 샤드(P0, P1, P2)를 세 노드에 고르게 분배합니다. 각각이 독립적인 Lucene 인덱스입니다.
이어서 replica(R0, R1, R2)가 배치됩니다. 핵심 규칙: primary와 그 복제본은 절대 같은 노드에 두지 않습니다. 같이 있으면 노드 장애 때 둘 다 잃으니까요.
문서가 들어오면 hash(_id) % primary 수 공식으로 목적지가 정해집니다. "abc123"은 P1(Node B)로 갑니다. 어느 노드가 받았든 결과는 같습니다.
P1이 색인을 마치면 자신의 replica R1(Node C)로 동기 복제합니다. 모든 in-sync 사본의 ack를 받아야 클라이언트에 응답 — 색인 성공 = 모든 활성 사본에 도달입니다.
라우팅 공식이 % num_primary_shards에 묶여 있어서, primary 수를 바꾸면 기존 문서의 위치 계산이 전부 틀어집니다. 그래서 생성 후 변경 불가이며, 바꾸려면 reindex 또는 split/shrink(4.6)가 필요합니다. 반면 replica 수는 라우팅과 무관하므로 언제든 동적 변경 가능합니다.
4.2 Primary vs Replica — 두 개의 얼굴
| Primary | Replica | |
|---|---|---|
| 색인(쓰기) | 모든 색인은 primary 먼저 | primary로부터 동기 복제 수신 |
| 검색(읽기) | 가능 | 가능 — 부하 분산 대상 |
| 장애 시 | 죽으면 replica가 승격 | primary 장애 시 승격 후보 |
| 수 변경 | 생성 후 변경 불가 | 동적 변경 가능 |
replica는 고가용성과 읽기 처리량이라는 두 역할을 동시에 수행합니다. replica를 늘리면 검색 QPS는 선형으로 늘지만, 모든 사본에 복제해야 하므로 색인 처리량은 떨어집니다. 이 트레이드오프를 활용한 대표 패턴이 있습니다.
# 대량 초기 적재 패턴: replica 0으로 색인 → 끝나면 복원
PUT /logs-init/_settings
{ "index": { "number_of_replicas": 0 } }
# ... bulk 색인 (복제 비용 없이 최대 처리량) ...
PUT /logs-init/_settings
{ "index": { "number_of_replicas": 1 } } # 완료된 세그먼트를 통째로 복사
4.3 라우팅 공식 — 문서의 주소 계산
정확한 공식은 split을 대비해 한 단계가 더 있습니다.
# 공식 문서에 명시된 라우팅 공식
routing_factor = num_routing_shards / num_primary_shards
shard_num = (hash(_routing) % num_routing_shards) / routing_factor
# _routing 기본값 = 문서의 _id
# num_routing_shards는 미래의 split을 위한 여유분 (예: 4 primary에 128)
# → 4 → 8 → 16 ... 128까지 split해도 hash 결과가 유지됨
같은 사용자의 문서를 한 샤드에 모으고 싶다면 ?routing=user_42처럼 custom routing을 쓸 수 있습니다. 검색이 전체 샤드 broadcast 대신 1개 샤드만 조회해 빨라지지만, 특정 키에 데이터가 몰리는 skew(쏠림) 위험을 항상 평가해야 합니다.
4.4 노드 장애 — replica 승격의 순간
replica가 있는 이유를 가장 극적으로 보여주는 시나리오입니다. 노드 하나가 갑자기 사라지면 무슨 일이 벌어질까요?
3노드 클러스터, 2 primary + replica 1 구성입니다. 모든 샤드가 할당되어 GREEN 상태입니다.
Node B가 다운됩니다. P1(primary)과 R0(replica)이 함께 사라졌습니다. P1의 데이터는 이제 Node C의 R1 사본에만 있습니다. 클러스터는 YELLOW로 전환됩니다.
마스터가 즉시 Node C의 R1을 P1으로 승격(promote)합니다. 색인과 검색이 중단 없이 계속됩니다 — primary와 replica를 같은 노드에 두지 않은 규칙이 빛나는 순간입니다.
이어서 부족해진 replica들을 살아있는 노드에 재복제합니다. 세그먼트 파일을 통째로 복사하므로 데이터량에 비례한 시간과 네트워크 비용이 듭니다.
모든 샤드가 다시 할당되면 GREEN 복귀. 데이터 유실은 없었습니다. 만약 replicas=0이었다면? P1의 데이터는 그대로 소멸했을 것입니다.
GREEN = 모든 primary·replica 할당됨. YELLOW = primary는 다 있지만 일부 replica 미할당(읽기·쓰기는 정상, 여유가 사라진 상태). RED = 일부 primary가 미할당 — 해당 데이터에 접근 불가, 즉시 대응 필요.
4.5 샤드 사이징 — 10~50GB의 과학
공식 문서의 가이드는 명확합니다: "샤드 크기는 10GB~50GB, 샤드당 문서 2억 개 이하". 그리고 노드 관점에서는 "JVM heap 1GB당 샤드 20개 이하". 이 범위를 벗어나면 양쪽 어디로든 사고가 납니다.
| 지표 | 위험 임계 | 의미 |
|---|---|---|
| 노드당 샤드 수 | heap GB × 20 초과 | 메타데이터만으로 heap 압박 |
| 클러스터 총 샤드 | 50,000 초과 | cluster state 비대화 |
| 평균 샤드 크기 | 1GB 미만 | oversharding — 오버헤드가 데이터보다 큼 |
| 평균 샤드 크기 | 100GB 초과 | undersharding — recovery·검색 느려짐 |
| cluster state publish | 수 초 이상 | 변경마다 클러스터 일시정지 |
샤드가 너무 많으면 어떤 파국이 오는지, 2장의 cluster state 지식과 연결해서 봅시다.
heap 30GB 노드 3대에 600개 샤드 — 노드당 200개, 샤드당 ~30GB. cluster state는 ~30MB, publish는 100ms 미만. 건강한 상태입니다.
같은 3대에 샤드 60,000개가 있다면? "일별 인덱스 × 서비스별 인덱스 × 기본 샤드 수"가 방치되면 실제로 일어나는 일입니다. 샤드당 데이터는 수십 MB뿐인데, 샤드 하나마다 Lucene 인덱스 전체의 오버헤드가 붙습니다.
cluster state가 기가바이트 단위로 비대해집니다. 인덱스 생성 같은 모든 변경이 전 노드 publish 수십 초를 유발 — 클러스터가 변경 때마다 멈칫거립니다.
메타데이터가 heap을 압박해 GC pause가 잦아지고, 마스터가 타임아웃으로 재선출을 반복하다 결국 회복 불능이 됩니다. 대응은 shrink/reindex로 샤드 통합, 그리고 미래 인덱스는 크기 기반 rollover로 예방하는 것입니다.
시계열 데이터라면 매일 새 인덱스 대신 rollover가 정석입니다. 크기 조건으로 전환하면 샤드가 늘 적정 크기를 유지합니다.
# ILM 정책 — 샤드가 50GB에 도달하면 새 인덱스로 자동 전환
PUT /_ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_primary_shard_size": "50gb", # 공식 권장 패턴
"max_age": "1d"
}
}
}
}
}
}
4.6 사후 도구함 — Shrink · Split · Clone · 그리고 watermark
이미 만들어진 인덱스의 샤드 수를 조정해야 한다면:
- Shrink (샤드 ↓): read-only 전환 + 모든 샤드 사본을 한 노드에 모은 뒤, hard link로 복사 없이 새 인덱스 생성. 새 primary 수는 기존의 약수(8→4, 2, 1).
- Split (샤드 ↑):
num_routing_shards여유분 안에서 배수로 증가(1→2→4→8...). - Clone: 같은 설정의 사본. 백업·실험용.
- Reindex: 매핑·설정까지 바꿀 수 있는 만능 도구지만 모든 데이터를 다시 색인하므로 가장 느립니다.
# shrink 예: 8 shards → 1 shard
PUT /my-index/_settings
{
"settings": {
"index.routing.allocation.require._name": "node-1", # 한 노드로 모으기
"index.blocks.write": true # read-only
}
}
POST /my-index/_shrink/my-index-shrunk
{ "settings": { "index.number_of_shards": 1 } }
디스크 사용률 85%(low)부터 새 샤드 할당 거부, 90%(high)면 샤드를 다른 노드로 이동, 95%(flood_stage)에 도달하면 인덱스가 자동으로 read-only가 됩니다. 디스크를 확보한 뒤 PUT /_all/_settings {"index.blocks.read_only_allow_delete": null}로 해제해야 합니다.
✍️ 이해도 체크
이 장의 원문 문서: chapters/ch04_index_and_shards.md — 샤드 수 산정 공식, allocation awareness(랙/AZ 분산), oversharding 진단 체크리스트가 담겨 있습니다.