primary shard는 왜 못 바꾸고, replica는 왜 언제든 바꿀 수 있을까?

2026년 10월 27일

opensearch

# OpenSearch# 샤드# Replica# 라우팅# 인덱스설정
목차

들어가며

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번 샤드로 곧장 갑니다.

GET /products/_doc/1hash(_id = 1)% 3(primary shard 수)shard 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 하나당 복사본 수”**입니다.

설정primaryreplica전체 샤드
shards 3 / replicas 1336
shards 3 / replicas 2369
shards 4 / replicas 341216

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,prirep
index    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      UNASSIGNED

replica(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가 갈 곳이 없는 겁니다.

상태의미
🟢 greenprimary와 replica가 모두 할당됨
🟡 yellowprimary는 모두 할당됐지만 일부 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 가이드는 샤드 하나의 크기를 검색 지연이 중요한 워크로드는 1030GiB, 로그처럼 쓰기가 많은 워크로드는 3050GiB로 유지하라고 권장합니다.
  • replica는 운영 환경이라면 1 이상으로 둡니다(데이터 노드 2대 이상 전제). 읽기가 많으면 늘리고, 대량 초기 적재처럼 쓰기가 몰릴 때는 잠시 0으로 낮췄다가 끝나고 되돌리는 운용도 가능합니다. 그동안은 복사본이 없는 위험을 감수하는 선택입니다.

정리

  1. primary shard 수는 문서의 주소를 정한다. hash(_id) % primary 수. 바꾸면 모든 문서의 주소가 바뀌므로 Final이다. 바꾸려면 split(배수)·shrink(약수)·reindex로 새 인덱스를 만든다.
  2. replica 수는 주소와 상관없는 복사본 개수다. 그래서 언제든 바꿀 수 있다. 대신 쓰기·디스크 비용이 (1 + replica 수)배로 든다.
  3. replica는 자기 primary와 같은 노드에 놓이지 않는다. 노드가 한 대면 replica는 미할당 → yellow. 장애가 아니라 “복사본이 없는 상태”다.
🤔primary shard 3개, replica 1로 만든 인덱스를 데이터 노드 2대짜리 클러스터에 올렸습니다. 이 인덱스의 샤드는 모두 몇 개이고, 헬스 상태는 어떻게 될까요?

참고자료

© 2026, 미나리와 함께 만들었음