들어가며
OpenSearch에서 인덱스를 만들 때 가장 먼저 정하는 설정이 두 개 있습니다.
PUT /products
{
"settings": {
"index.number_of_shards": 3,
"index.number_of_replicas": 1
}
}둘 다 “샤드 몇 개”처럼 보이지만, 운영하다 보면 성격이 정반대라는 걸 알게 됩니다. replica 수는 아무 때나 바꿀 수 있는데, primary shard 수는 바꾸려고 하면 거절당합니다.
PUT /products/_settings
{ "index.number_of_shards": 4 }"type" : "illegal_argument_exception",
"reason" : "Can't update non dynamic settings [[index.number_of_shards]] for open indices [[products/vemOXB6tR0uIRp40GD0ojw]]"인덱스를 닫고(close) 다시 시도해도 마찬가지입니다. 에러 메시지만 바뀝니다.
"type" : "settings_exception",
"reason" : "final products setting [index.number_of_shards], not updateable"왜 하나는 되고 하나는 안 될까요? 결론부터 말하면, primary shard 수는 문서의 “주소”를 정하고, replica 수는 주소와 상관없는 “복사본 개수”이기 때문입니다.
이번 글의 범위
- 다루는 것: 두 설정의 의미와 차이, 샤드 수를 바꿔야 할 때의 선택지, 노드 한 대에서 yellow가 뜨는 이유
- 다루지 않는 것: 세그먼트와 복제의 내부 동작(OpenSearch 내부 구조 시리즈), 적정 샤드 크기 산정(별도 글)
출력은 전부 OpenSearch 3.6.0 단일 노드에서 직접 확인한 결과입니다.
비유로 먼저: 사물함 번호 규칙
학교에 사물함이 3개 있고, 학생(문서)마다 쓸 사물함을 이렇게 정한다고 해봅시다.
학번을 숫자 하나로 바꾼 뒤(해시), 3으로 나눈 나머지 번호의 사물함을 쓴다.
이 규칙 덕분에 어떤 학생의 짐을 찾을 때 사물함 3개를 다 열어볼 필요가 없습니다. 학번만 알면 사물함 번호가 바로 나오니까요.
그런데 사물함을 4개로 늘리면 어떻게 될까요? 규칙이 “4로 나눈 나머지”로 바뀌면서 대부분 학생의 사물함 번호가 달라집니다. 기존 짐을 전부 새 번호로 옮기지 않으면 아무도 자기 짐을 못 찾습니다.
반면 사물함마다 똑같은 복제 사물함을 다른 건물에 하나씩 더 두는 것은 번호 규칙과 아무 상관이 없습니다. 복제본을 한 벌 두든 세 벌 두든 “몇 번 사물함인가”는 그대로예요.
| 사물함 | OpenSearch |
|---|---|
| 사물함 개수 | number_of_shards (primary shard 수) |
| “학번 해시 % 3” 규칙 | 라우팅 공식 |
| 다른 건물의 복제 사물함 | number_of_replicas |
primary shard 수는 문서의 주소를 정한다
OpenSearch는 문서를 색인할 때 어느 primary shard에 넣을지 이 공식으로 정합니다.
shard_num = hash(_routing) % num_primary_shards_routing의 기본값은 문서의 _id입니다. primary가 3개인 인덱스에서 _id별로 어느 샤드가 배정되는지 _search_shards API로 확인해봤습니다.
GET /products/_search_shards?routing=1_id=1 → shard 2
_id=2 → shard 1
_id=3 → shard 1
_id=4 → shard 1
_id=5 → shard 0
_id=6 → shard 2_id만 알면 샤드가 계산되니, GET /products/_doc/1 같은 조회는 샤드 3개를 다 뒤지지 않고 2번 샤드로 곧장 갑니다.
이제 primary shard 수를 3에서 4로 바꿀 수 있다고 상상해봅시다. 공식의 % 3이 % 4가 되는 순간, 이미 들어 있는 문서 대부분이 “엉뚱한” 샤드에 있는 셈이 됩니다. _doc/1을 조회하면 새 공식으로 계산한 샤드로 찾아가는데, 문서는 옛 공식으로 배정된 샤드에 있으니 못 찾습니다. 그래서 OpenSearch는 이 설정을 아예 바꿀 수 없게 막아둡니다.
Static과 Final은 다릅니다.
인덱스 설정은 언제 바꿀 수 있느냐로 나뉩니다. Dynamic은 언제든, Static은 인덱스를 닫은 상태에서만 바꿀 수 있습니다. 그리고 Static 중에서도 Final은 인덱스를 만들 때 한 번 정하면 닫아도 못 바꿉니다.
number_of_shards가 바로 Final입니다. 앞에서 열린 인덱스와 닫힌 인덱스의 에러 메시지가 달랐던 이유가 이것입니다.
그래도 바꿔야 한다면
샤드 수를 “바꾸는” 게 아니라, 다른 샤드 수를 가진 새 인덱스를 만드는 방법을 씁니다.
| 방법 | 가능한 샤드 수 | 조건 |
|---|---|---|
_split | 원래 수의 배수만 (3 → 6, 9, …) | 원본 인덱스 쓰기 차단 |
_shrink | 원래 수의 약수만 (8 → 4, 2, 1) | 쓰기 차단 + 모든 샤드의 사본이 한 노드에 모여 있어야 함 + green |
| reindex + alias 전환 | 아무 수나 | 새 인덱스에 처음부터 다시 색인. 가장 오래 걸림 |
split을 직접 해보면 조건이 그대로 드러납니다. 먼저 쓰기를 막지 않으면 거절됩니다.
POST /products/_split/products-6
{ "settings": { "index.number_of_shards": 6 } }"type" : "illegal_state_exception",
"reason" : "index products must block write operations to resize index. use \"index.blocks.write=true\""쓰기를 막아도 배수가 아니면 안 됩니다.
PUT /products/_settings
{ "index.blocks.write": true }POST /products/_split/products-4
{ "settings": { "index.number_of_shards": 4 } }
"type" : "illegal_argument_exception",
"reason" : "the number of source shards [3] must be a factor of [4]"3 → 6은 성공합니다. 그리고 문서가 어디로 갔는지 보면 왜 배수여야 하는지가 보입니다.
_id=1: products shard 2 → products-6 shard 4
_id=2: products shard 1 → products-6 shard 2
_id=3: products shard 1 → products-6 shard 3
_id=5: products shard 0 → products-6 shard 1
_id=6: products shard 2 → products-6 shard 4원래 n번 샤드에 있던 문서는 새 인덱스의 2n번이나 2n+1번 샤드로만 갑 니다. 샤드 하나를 정확히 둘로 쪼개는 방식이라, 문서를 전부 다시 계산해서 섞을 필요가 없습니다. 3 → 4처럼 배수가 아니면 이렇게 깔끔하게 나눠지지 않으니 막혀 있는 겁니다.
이게 가능한 건 인덱스를 만들 때 내부적으로 해시 공간을 미리 잘게 나눠두기 때문입니다(index.number_of_routing_shards). 따로 지정하지 않으면 최대 1,024개까지 두 배씩 늘릴 수 있는 값으로 잡힙니다.
reindex + alias 전환은 무중단 인덱스 교체 전략에서 다뤘습니다.
replica 수는 주소와 상관없는 복사본 개수다
number_of_replicas는 primary shard 하나당 복사본을 몇 개 둘지입니다. 라우팅 공식에는 등장하지 않습니다. 복사본이 몇 개든 _id=1의 주소는 여전히 2번 샤드예요. 그래서 언제든 바꿀 수 있습니다(Dynamic).
PUT /products/_settings
{ "index.number_of_replicas": 0 }{"acknowledged":true}개수 계산은 헷갈리기 쉬운데, **replica 수는 “인덱스 전체의 복사본 수”가 아니라 “primary 하나당 복사본 수”**입니다.
| 설정 | primary | replica | 전체 샤드 |
|---|---|---|---|
| shards 3 / replicas 1 | 3 | 3 | 6 |
| shards 3 / replicas 2 | 3 | 6 | 9 |
| shards 4 / replicas 3 | 4 | 12 | 16 |
replica가 해주는 일
- 장애 대비: primary가 있는 노드가 죽으면 그 샤드의 replica가 primary로 승격되어 서비스가 이어집니다.
- 읽기 분산: 검색은 primary와 replica 중 어느 사본에서든 처리할 수 있습니다. 사본이 많을수록 검색 요청을 나눠 받을 곳이 늘어납니다.
replica의 비용
- 쓰기: 기본 설정(document replication)에서는 replica도 같은 문서를 직접 색인합니다. replica가 1이면 같은 색인 작업을 두 번 하는 셈입니다.
- 디스크: 데이터가
(1 + replica 수)배로 저장됩니다. - 노드 수: replica는 자기 primary와 같은 노드에 놓일 수 없습니다. 같은 노드에 두면 그 노드가 죽을 때 둘 다 사라지니까요.
마지막 항목이 다음 절의 주인공입니다.
노드 한 대에서 yellow가 뜨는 이유
로컬에서 OpenSearch를 단일 노드로 띄우고 인덱 스를 만들면 거의 반드시 만나는 상태가 있습니다. replicas 1로 만든 products의 샤드를 보면 이렇습니다.
GET _cat/shards/products?v&h=index,shard,prirep,state,docs,node&s=shard,prirepindex shard prirep state docs node
products 0 p STARTED 0 467688454c3c
products 0 r UNASSIGNED
products 1 p STARTED 0 467688454c3c
products 1 r UNASSIGNED
products 2 p STARTED 0 467688454c3c
products 2 r UNASSIGNEDreplica(r) 3개가 전부 UNASSIGNED이고, 헬스는 yellow입니다.
GET _cluster/health/products{
"status" : "yellow",
"number_of_nodes" : 1,
"active_primary_shards" : 3,
"active_shards" : 3,
"unassigned_shards" : 3
}왜 할당되지 않는지는 allocation/explain이 알려줍니다.
GET _cluster/allocation/explain
{ "index": "products", "shard": 0, "primary": false }"can_allocate" : "no",
"decider" : "same_shard",
"decision" : "NO",
"explanation" : "a copy of this shard is already allocated to this node [[products][0], node[5KHC71p_RY6NqWdD-fsjPw], [P], s[STARTED], ...]"“이 샤드의 사본이 이미 이 노드에 있다.” 노드가 하나뿐이니 replica가 갈 곳이 없는 겁니다.
| 상태 | 의미 |
|---|---|
| 🟢 green | primary와 replica가 모두 할당됨 |
| 🟡 yellow | primary는 모두 할당됐지만 일부 replica가 미할당. 동작은 하지만 복사본이 부족함 |
| 🔴 red | 할당되지 않은 primary가 있음. 일부 데이터를 읽을 수 없음 |
yellow여도 쓰기는 된다
yellow는 장애가 아닙니다. 이 상태에서 문서를 넣어도 성공합니다. 다만 응답을 보면 복사본이 없다는 게 드러납니다.
PUT /orders/_doc/1
{ "item": "커피" }"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
}total 2(primary + replica) 중 successful 1, primary에만 기록됐다는 뜻입니다.
쓰기가 되는 이유는 wait_for_active_shards의 기본값이 1이기 때문입니다. 쓰기를 시작하기 전에 활성 사본이 primary 하나만 있으면 된다는 뜻이에요. 모든 사본을 요구하면 yellow 상태에서는 쓰기가 실패합니다.
PUT /orders/_doc/2?wait_for_active_shards=all&timeout=2s"type" : "unavailable_shards_exception",
"reason" : "[orders][0] Not enough active copies to meet shard count of [ALL] (have 1, needed 2). Timeout: [2s]"로컬·개발 환경이라면
노드 한 대짜리 환경에서는 replica를 둘 곳이 없으니 0으로 내리면 green이 됩니다.
PUT /products/_settings
{ "index.number_of_replicas": 0 }노드 수에 맞춰 자동으로 조절하게 할 수도 있습니다. auto_expand_replicas(Dynamic, 기본은 꺼짐)를 "0-1"로 두면 데이터 노드가 한 대일 땐 0, 두 대 이상이면 1로 맞춰집니다.
PUT /products/_settings
{ "index.auto_expand_replicas": "0-1" }처음엔 몇으로 시작할까
깊은 이야기는 별도 글로 미루고, 여기서는 출발점만 정리합니다.
기본값은 primary 1 / replica 1입니다. 설정 없이 인덱스를 만들어 확인해보면 이렇게 나옵니다.
GET /defaults-demo/_settings"number_of_shards" : "1",
"number_of_replicas" : "1"- primary는 작게 시작합니다. 샤드 하나하나가 독립 된 Lucene 인덱스라서 CPU와 메모리를 따로 씁니다. 작은 샤드를 잔뜩 만들면 그 자체가 부담입니다. 데이터가 커지면 split으로 배수만큼 늘릴 수 있습니다. 참고로 AWS OpenSearch Service 가이드는 샤드 하나의 크기를 검색 지연이 중요한 워크로드는 10
30GiB, 로그처럼 쓰기가 많은 워크로드는 3050GiB로 유지하라고 권장합니다. - replica는 운영 환경이라면 1 이상으로 둡니다(데이터 노드 2대 이상 전제). 읽기가 많으면 늘리고, 대량 초기 적재처럼 쓰기가 몰릴 때는 잠시 0으로 낮췄다가 끝나고 되돌리는 운용도 가능합니다. 그동안은 복사본이 없는 위험을 감수하는 선택입니다.
정리
- primary shard 수는 문서의 주소를 정한다.
hash(_id) % primary 수. 바꾸면 모든 문서의 주소가 바뀌므로 Final이다. 바꾸려면 split(배수)·shrink(약수)·reindex로 새 인덱스를 만든다. - replica 수는 주소와 상관없는 복사본 개수다. 그래서 언제든 바꿀 수 있다. 대신 쓰기·디스크 비용이
(1 + replica 수)배로 든다. - replica는 자기 primary와 같은 노드에 놓이지 않는다. 노드가 한 대면 replica는 미할당 → yellow. 장애가 아니라 “복사본이 없는 상태”다.