Elasticsearch 아키텍처: 클러스터, 노드, 샤드 이해하기
클러스터가 Yellow 상태로 바뀌었을 때
새벽 2시, PagerDuty 알림이 울렸다.
⚠️ [WARNING] Elasticsearch Cluster Status: YELLOW
Unassigned shards: 5
Cluster: production-search
검색 서비스는 동작하고 있었지만, 한 노드가 다운된 상태였다. 이 상황에서 추가로 노드가 하나 더 죽으면? 데이터 유실 가능성이 있었다.
Elasticsearch 클러스터를 운영하려면 클러스터, 노드, 샤드의 관계를 정확히 이해해야 한다. 이 글에서는 Elasticsearch의 분산 아키텍처를 운영 관점에서 깊이 있게 다룬다.
클러스터(Cluster)란?
클러스터는 하나 이상의 노드(서버)가 모여 형성하는 논리적 그룹이다. 같은 클러스터 이름을 가진 노드들이 자동으로 연결된다.
클러스터 구성 예시
# elasticsearch.yml
cluster.name: production-search
node.name: node-1
클러스터 상태(Cluster Health)
curl -X GET "localhost:9200/_cluster/health?pretty"
{
"cluster_name": "production-search",
"status": "green",
"number_of_nodes": 3,
"number_of_data_nodes": 3,
"active_primary_shards": 10,
"active_shards": 20,
"unassigned_shards": 0
}
상태별 의미
| 상태 | 의미 | 조치 |
|---|---|---|
| 🟢 Green | 모든 Primary/Replica 샤드 정상 | 정상 상태 |
| 🟡 Yellow | Primary 정상, 일부 Replica 미할당 | 노드 확인, Replica 할당 |
| 🔴 Red | 일부 Primary 샤드 미할당 | 긴급 대응 필요, 데이터 유실 가능 |
Yellow 상태의 흔한 원인:
- 노드 1개로 운영 (Replica 할당 불가)
- 노드 장애로 Replica 재할당 대기 중
- 디스크 용량 부족 (워터마크 초과)
노드(Node)란?
노드는 클러스터를 구성하는 단일 서버 인스턴스다. 각 노드는 역할에 따라 다른 작업을 수행한다.
노드 역할(Node Roles)
# elasticsearch.yml - 노드 역할 설정
node.roles: [ master, data, ingest ]
| 역할 | 설명 | 리소스 요구사항 |
|---|---|---|
| master | 클러스터 상태 관리, 샤드 할당 결정 | 낮은 CPU/메모리, 안정성 중요 |
| data | 데이터 저장, 검색/집계 쿼리 실행 | 높은 디스크/메모리/CPU |
| data_content | 일반 콘텐츠 데이터 (Hot 데이터) | 높은 성능 SSD |
| data_hot | 최신 데이터, 빈번한 쿼리 | 고성능 SSD |
| data_warm | 덜 빈번한 쿼리, 오래된 데이터 | 대용량 HDD |
| data_cold | 거의 접근 안 함, 아카이브 | 저비용 스토리지 |
| ingest | 인덱싱 전 파이프라인 처리 | CPU 중심 |
| ml | 머신러닝 작업 | 높은 CPU/메모리 |
| transform | 데이터 변환 작업 | CPU/메모리 |
| remote_cluster_client | 크로스 클러스터 검색 | 네트워크 |
| coordinating | 요청 라우팅 (역할 없음) | CPU/네트워크 |
Master Node 심층 분석
Master 노드는 클러스터의 두뇌 역할을 한다:
Master Node 역할:
├── 클러스터 상태(Cluster State) 관리
│ ├── 인덱스 메타데이터
│ ├── 노드 정보
│ └── 샤드 할당 정보
├── 인덱스 생성/삭제
├── 노드 추가/제거 처리
└── 샤드 할당 결정
Master 선출 과정:
1. 클러스터 시작 시 모든 master-eligible 노드가 투표
2. 과반수(quorum)를 얻은 노드가 Master로 선출
3. Master 장애 시 나머지 노드들이 재선출
Split-Brain 방지:
# 최소 과반수 설정 (3노드 클러스터)
discovery.zen.minimum_master_nodes: 2
# Elasticsearch 7.x 이상에서는 자동 계산
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]
Data Node 심층 분석
Data 노드는 실제 데이터를 저장하고 쿼리를 처리한다:
Data Node 역할:
├── 샤드(Shard) 저장
│ ├── Primary Shards
│ └── Replica Shards
├── CRUD 작업 수행
├── 검색 쿼리 실행
└── 집계(Aggregation) 처리
Hot-Warm-Cold 아키텍처:
# Hot 노드 (신규 데이터, 빈번한 쿼리)
node.roles: [ data_hot ]
node.attr.data: hot
# Warm 노드 (오래된 데이터, 가끔 쿼리)
node.roles: [ data_warm ]
node.attr.data: warm
# Cold 노드 (아카이브, 드문 접근)
node.roles: [ data_cold ]
node.attr.data: cold
Coordinating Node
모든 노드는 기본적으로 Coordinating 역할을 수행한다. 전용 Coordinating 노드를 두면 대규모 쿼리 처리에 유리하다:
# 전용 Coordinating 노드 (역할 없음 = Coordinating 전용)
node.roles: [ ]
Coordinating 노드 동작:
1. 클라이언트 요청 수신
2. 관련 샤드가 있는 Data 노드로 요청 분배
3. 각 노드의 응답 수집 및 병합
4. 최종 결과 클라이언트에 반환
샤드(Shard)란?
인덱스는 하나 이상의 샤드로 분할된다. 샤드는 Lucene 인덱스 그 자체다.
Primary Shard와 Replica Shard
Index: products (3 Primary, 1 Replica)
┌─────────────────────────────────────────────────────────┐
│ Cluster │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ P0 │ │ │ │ P1 │ │ │ │ P2 │ │ │
│ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ R1 │ │ │ │ R2 │ │ │ │ R0 │ │ │
│ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
P0, P1, P2 = Primary Shards
R0, R1, R2 = Replica Shards (각 Primary의 복제본)
| 구분 | Primary Shard | Replica Shard |
|---|---|---|
| 역할 | 원본 데이터, 쓰기 처리 | 복제본, 읽기 분산 |
| 개수 | 인덱스 생성 시 결정 (변경 불가) | 동적 변경 가능 |
| 장애 대응 | 장애 시 Replica가 승격 | Primary 복제 |
| 쿼리 처리 | 읽기/쓰기 모두 | 읽기만 |
샤드 수 결정하기
인덱스 생성 시 샤드 수 지정:
curl -X PUT "localhost:9200/products" -H "Content-Type: application/json" -d'
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
}'
샤드 수 결정 가이드라인:
| 고려 사항 | 권장 |
|---|---|
| 샤드 당 크기 | 10GB ~ 50GB |
| 노드 당 샤드 수 | 힙 1GB당 20개 이하 |
| 총 샤드 수 | 1000개 이하 권장 |
나쁜 예 vs 좋은 예:
# ❌ 나쁜 예: 1GB 데이터에 100개 샤드 (오버샤딩)
{
"settings": {
"number_of_shards": 100,
"number_of_replicas": 1
}
}
# ✅ 좋은 예: 데이터 크기에 맞는 샤드 수
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
}
}
오버샤딩의 문제:
- Master 노드 부하 증가 (클러스터 상태 관리)
- 메모리 사용량 증가 (샤드당 오버헤드)
- 검색 성능 저하 (너무 많은 샤드 조회)
샤드 할당 전략
특정 노드에 샤드 할당:
curl -X PUT "localhost:9200/logs-2024" -H "Content-Type: application/json" -d'
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.routing.allocation.require.data": "hot"
}
}'
샤드 재배치 확인:
curl -X GET "localhost:9200/_cat/shards/products?v"
index shard prirep state docs store ip node
products 0 p STARTED 1000 5mb 10.0.0.1 node-1
products 0 r STARTED 1000 5mb 10.0.0.3 node-3
products 1 p STARTED 1200 6mb 10.0.0.2 node-2
products 1 r STARTED 1200 6mb 10.0.0.1 node-1
products 2 p STARTED 800 4mb 10.0.0.3 node-3
products 2 r STARTED 800 4mb 10.0.0.2 node-2
문서 라우팅
문서가 어떤 샤드에 저장될지는 라우팅 공식으로 결정된다:
shard_num = hash(_routing) % num_primary_shards
기본 라우팅 (문서 ID 사용):
# 문서 ID "123"을 기반으로 샤드 결정
curl -X PUT "localhost:9200/products/_doc/123" -H "Content-Type: application/json" -d'
{
"name": "블루투스 이어폰"
}'
커스텀 라우팅 (특정 필드 기준):
# user_id 기준으로 라우팅 (같은 사용자의 문서는 같은 샤드에)
curl -X PUT "localhost:9200/orders/_doc/1?routing=user_123" -H "Content-Type: application/json" -d'
{
"user_id": "user_123",
"product": "블루투스 이어폰",
"amount": 50000
}'
커스텀 라우팅의 장점:
- 관련 문서가 같은 샤드에 위치
- 특정 쿼리 시 단일 샤드만 조회 (성능 향상)
커스텀 라우팅의 주의점:
- Hot Spot 발생 가능 (특정 샤드에 데이터 집중)
- 검색 시 라우팅 값 필요
장애 복구 시나리오
시나리오 1: Data 노드 1개 장애
Before (Green):
Node 1: [P0] [R1]
Node 2: [P1] [R2]
Node 3: [P2] [R0]
Node 2 Down (Yellow):
Node 1: [P0] [R1] [P1*] ← R1이 P1로 승격
Node 3: [P2] [R0]
* 5개 샤드 중 R1, R2 미할당
자동 복구 과정:
- Master가 Node 2 장애 감지
- Node 2의 Replica(R1)를 Primary로 승격
- 새로운 Replica 생성 시도
- 노드 부족으로 일부 Replica 미할당 → Yellow
시나리오 2: Master 노드 장애
Before:
Node 1 (Master): [P0]
Node 2: [P1]
Node 3: [P2]
Node 1 Down:
Node 2 (New Master): [P1] ← 새로운 Master 선출
Node 3: [P2]
* P0 미할당 → Red
복구 절차:
- 나머지 Master-eligible 노드가 재선출
- 새 Master가 클러스터 상태 복구
- P0이 있던 노드의 데이터 복구 필요
시나리오 3: 네트워크 파티션 (Split-Brain)
정상 상태:
[Master] ─── [Node 2] ─── [Node 3]
네트워크 분리:
[Master] │ [Node 2] ─── [Node 3]
│ └── 새로운 Master 선출 시도?
Split-Brain 방지 메커니즘:
- 최소 과반수(quorum) 없이는 Master 선출 불가
- 3노드 클러스터: 최소 2노드 필요
- 5노드 클러스터: 최소 3노드 필요
프로덕션 클러스터 구성 예시
소규모 (개발/테스트)
# 단일 노드 (개발용)
node.roles: [ master, data ]
discovery.type: single-node
중규모 (일반 프로덕션)
3-Node Cluster:
┌─────────────────────────────────────────────┐
│ Node 1 Node 2 Node 3 │
│ [M, D] [M, D] [M, D] │
│ │
│ M = Master-eligible │
│ D = Data │
└─────────────────────────────────────────────┘
# 모든 노드에 동일 설정
node.roles: [ master, data ]
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]
discovery.seed_hosts: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
대규모 (엔터프라이즈)
10+ Node Cluster:
┌───────────────────────────────────────────────────────┐
│ Dedicated Masters (3) │
│ [M1] [M2] [M3] │
│ │
│ Coordinating Nodes (2) ← 로드밸런서 연결 │
│ [C1] [C2] │
│ │
│ Data Hot Nodes (3) ← 최신 데이터 │
│ [D-H1] [D-H2] [D-H3] │
│ │
│ Data Warm Nodes (2) ← 오래된 데이터 │
│ [D-W1] [D-W2] │
│ │
│ Ingest Nodes (2) ← 파이프라인 처리 │
│ [I1] [I2] │
└───────────────────────────────────────────────────────┘
클러스터 모니터링 필수 API
클러스터 상태 확인
# 전체 상태
curl -X GET "localhost:9200/_cluster/health?pretty"
# 상세 상태
curl -X GET "localhost:9200/_cluster/health?level=shards&pretty"
노드 정보 확인
# 노드 목록
curl -X GET "localhost:9200/_cat/nodes?v"
# 노드 상세 정보
curl -X GET "localhost:9200/_nodes/stats?pretty"
샤드 할당 확인
# 샤드 목록
curl -X GET "localhost:9200/_cat/shards?v&s=index"
# 미할당 샤드 원인
curl -X GET "localhost:9200/_cluster/allocation/explain?pretty"
디스크 사용량 확인
curl -X GET "localhost:9200/_cat/allocation?v"
shards disk.indices disk.used disk.avail disk.total disk.percent host node
10 50gb 60gb 40gb 100gb 60 10.0.0.1 node-1
10 45gb 55gb 45gb 100gb 55 10.0.0.2 node-2
10 48gb 58gb 42gb 100gb 58 10.0.0.3 node-3
OpenSearch 차이점
OpenSearch는 Elasticsearch 7.10 포크로 대부분의 아키텍처가 동일하다.
주요 차이점
| 구분 | Elasticsearch | OpenSearch |
|---|---|---|
| 노드 역할 설정 | node.roles | 동일 (1.x부터 지원) |
| 클러스터 설정 | cluster.initial_master_nodes | 동일 |
| 보안 기본값 | 8.x부터 기본 활성화 | 기본 활성화 (Security Plugin) |
OpenSearch 클러스터 설정 예시
# opensearch.yml
cluster.name: production-search
node.name: os-node-1
node.roles: [ cluster_manager, data ] # master → cluster_manager
discovery.seed_hosts: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
cluster.initial_cluster_manager_nodes: ["os-node-1", "os-node-2", "os-node-3"]
용어 변경:
master→cluster_managermaster_eligible→cluster_manager_eligible
마무리
이 글에서 다룬 핵심 내용:
- 클러스터: 노드의 논리적 그룹, Green/Yellow/Red 상태
- 노드 역할: Master, Data, Coordinating, Ingest 등
- 샤드: Primary/Replica, 데이터 분산의 기본 단위
- 문서 라우팅: 해시 기반 샤드 결정
- 장애 복구: 자동 Failover, Split-Brain 방지
다음 글에서는 Docker와 Kubernetes에서 Elasticsearch를 설치하고 클러스터를 구성하는 방법을 실습한다. 로컬 개발 환경부터 프로덕션 배포까지 단계별로 알아본다.