- Published on
이중화·삼중화 완전 가이드 — 99.999% 가용성의 비밀
- Authors
- Name

들어가며
"서버 하나가 죽으면 서비스도 죽는다" — 이게 단일 장애점(SPOF)입니다.
이중화, 삼중화는 이 SPOF를 제거하는 기술입니다. 은행, 증권, 항공 관제 시스템은 삼중화까지 하고, 우리가 매일 쓰는 클라우드 서비스도 이중화 이상으로 운영됩니다.
가용성(Availability) 숫자의 의미
가용성 = 정상 운영 시간 / 전체 시간 × 100%
| 가용성 | 등급 | 연간 다운타임 | 월간 다운타임 |
|---|---|---|---|
| 99% | Two 9s | 3.65일 | 7.3시간 |
| 99.9% | Three 9s | 8.77시간 | 43.8분 |
| 99.99% | Four 9s | 52.6분 | 4.38분 |
| 99.999% | Five 9s | 5.26분 | 26.3초 |
| 99.9999% | Six 9s | 31.5초 | 2.6초 |
핵심: 99.9% → 99.99%로 올리는 건 8시간 → 52분. 비용은 10배 이상 증가!
이중화 (Redundancy) 패턴
1. Active-Standby (액티브-스탠바이)
정상 상태:
[Active 서버] ←── 모든 트래픽
[Standby 서버] (대기, 동기화만)
장애 발생:
[Active 서버] ✗ 장애 감지!
[Standby 서버] ←── 트래픽 전환 (Failover)
↑
새 Active로 승격
# Kubernetes에서 Active-Standby: StatefulSet + Leader Election
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-ha
spec:
replicas: 2 # 1 Active + 1 Standby
selector:
matchLabels:
app: redis
template:
spec:
containers:
- name: redis
image: redis:7
ports:
- containerPort: 6379
- name: sentinel # 장애 감지 + 자동 Failover
image: redis:7
command: ['redis-sentinel', '/etc/sentinel.conf']
장점: 리소스 효율적 (Standby는 최소 자원) 단점: Failover 시간 (수 초~수십 초 다운타임)
2. Active-Active (액티브-액티브)
정상 상태:
[서버 A] ←── 트래픽 50%
[서버 B] ←── 트래픽 50%
↑
Load Balancer가 분산
장애 발생:
[서버 A] ✗ 장애!
[서버 B] ←── 트래픽 100% (자동)
# Kubernetes Service = 자동 Active-Active
apiVersion: v1
kind: Service
metadata:
name: my-api
spec:
selector:
app: my-api
ports:
- port: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api
spec:
replicas: 2 # 둘 다 Active!
template:
spec:
containers:
- name: api
image: my-api:latest
readinessProbe: # 장애 감지
httpGet:
path: /health
port: 8080
periodSeconds: 5
failureThreshold: 3 # 3번 실패 → 트래픽 제외
장점: 다운타임 거의 0 (이미 분산 중이므로) 단점: 데이터 동기화 복잡 (충돌 해결 필요)
3. N+1 / N+2 이중화
N+1: N대가 필요한데 1대 더 (최소 여유분)
[서버1] [서버2] [서버3] [+예비1]
→ 1대 죽어도 OK
N+2: N대가 필요한데 2대 더
[서버1] [서버2] [서버3] [+예비1] [+예비2]
→ 2대 동시에 죽어도 OK + 1대 점검 가능
2N: 전체를 2배로 (가장 비쌈)
[서버1] [서버2] [서버3] [서버4] [서버5] [서버6]
→ 반이 죽어도 OK (미션 크리티컬)
삼중화 (Triple Redundancy)
왜 삼중화?
이중화의 치명적 약점: 스플릿 브레인 (Split Brain)
이중화에서 네트워크 단절 시:
[서버 A] ──✗── [서버 B]
"B가 죽었나? 내가 Active!" "A가 죽었나? 내가 Active!"
→ 둘 다 Active → 데이터 불일치 → 재앙!
삼중화 + 쿼럼으로 해결:
삼중화 (3노드 클러스터):
[노드 A]──[노드 B]──[노드 C]
네트워크 분할 시:
[노드 A] [노드 B]──[노드 C]
1표 (소수) 2표 (과반수 = 쿼럼!)
→ A는 자진 사퇴
→ B,C가 계속 서비스
→ 스플릿 브레인 방지!
쿼럼 공식: 과반수 = (N/2) + 1
3노드: 2표 필요 (1노드 장애 허용)
5노드: 3표 필요 (2노드 장애 허용)
7노드: 4표 필요 (3노드 장애 허용)
etcd (Kubernetes의 뇌) — 삼중화 필수!
# etcd 3노드 클러스터 (Raft 합의 알고리즘)
# 1노드 죽어도 쿼럼(2/3) 유지 → 서비스 계속
# 2노드 죽으면 쿼럼 상실 → 읽기만 가능
# kubeadm 기본 설정
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
etcd:
local:
extraArgs:
initial-cluster: >-
etcd-0=https://10.0.0.1:2380,
etcd-1=https://10.0.0.2:2380,
etcd-2=https://10.0.0.3:2380
# Raft 합의 알고리즘 (의사코드)
class RaftNode:
def __init__(self, node_id, peers):
self.state = "follower" # follower → candidate → leader
self.term = 0
self.voted_for = None
self.log = []
def request_vote(self):
"""리더 선출 — 과반수 투표 필요"""
self.state = "candidate"
self.term += 1
votes = 1 # 자기 자신
for peer in self.peers:
if peer.grant_vote(self.term, self.log):
votes += 1
if votes > len(self.peers) // 2: # 쿼럼!
self.state = "leader"
return True
return False
def replicate(self, entry):
"""데이터 복제 — 과반수 확인 후 커밋"""
self.log.append(entry)
acks = 1
for peer in self.peers:
if peer.append_entry(entry):
acks += 1
if acks > len(self.peers) // 2: # 쿼럼!
self.commit(entry) # 과반수 확인 → 안전하게 커밋
계층별 이중화 전략
전체 아키텍처
[사용자]
│
▼
[DNS] ← Route 53 헬스체크 (다중 리전)
│
▼
[CDN] ← CloudFront (글로벌 엣지)
│
▼
[L4 LB] ← NLB x2 (Active-Active, AZ 분산)
│
▼
[L7 LB] ← ALB x2 (Active-Active)
│
▼
[웹 서버] ← Deployment replicas: 3+ (Active-Active)
│
▼
[앱 서버] ← Deployment replicas: 3+ (Active-Active)
│
├──▶ [DB Primary] ──동기복제──▶ [DB Standby] (Active-Standby)
│ └── 비동기복제 ──▶ [DB Read Replica x2]
│
├──▶ [Redis Primary] ── [Redis Replica x2] (Sentinel)
│
└──▶ [MQ] ← RabbitMQ 3노드 (쿼럼 큐)
데이터베이스 이중화
[동기 복제] (Strong Consistency)
Primary ──COMMIT──▶ Standby
"양쪽 다 쓸 때까지 기다림"
장점: 데이터 손실 0
단점: 느림 (네트워크 RTT 추가)
[비동기 복제] (Eventual Consistency)
Primary ──COMMIT──▶ (나중에) Standby
"일단 Primary에만 쓰고 응답"
장점: 빠름
단점: 장애 시 최근 데이터 유실 가능 (RPO > 0)
[반동기 복제] (Semi-Sync)
Primary ──COMMIT──▶ Standby (1개만 확인)
MySQL 기본 + 금융권 선호
-- PostgreSQL Streaming Replication 설정
-- Primary (postgresql.conf)
-- wal_level = replica
-- max_wal_senders = 3
-- synchronous_standby_names = 'standby1'
-- Standby 확인
SELECT client_addr, state, sync_state
FROM pg_stat_replication;
-- client_addr | state | sync_state
-- 10.0.0.2 | streaming | sync
-- 10.0.0.3 | streaming | async
장애 복구 지표
RPO (Recovery Point Objective):
"얼마나 많은 데이터를 잃어도 되나?"
동기 복제: RPO = 0 (데이터 무손실)
비동기 복제: RPO = 수 초~수 분
RTO (Recovery Time Objective):
"얼마나 빨리 복구해야 하나?"
Active-Active: RTO ≈ 0
Active-Standby: RTO = 수 초~수 분
백업 복원: RTO = 수 시간
MTBF (Mean Time Between Failures):
장애 간 평균 시간 (길수록 좋음)
MTTR (Mean Time To Repair):
복구까지 평균 시간 (짧을수록 좋음)
가용성 = MTBF / (MTBF + MTTR)
비용 대비 가용성
가용성 │ 구성 │ 상대 비용
───────────┼───────────────────┼──────────
99% │ 단일 서버 │ 1x
99.9% │ 이중화 (1 리전) │ 2~3x
99.99% │ 삼중화 + 자동복구 │ 5~10x
99.999% │ 다중 리전 Active │ 10~50x
99.9999% │ 다중 리전 + DR │ 50~100x
📝 퀴즈 — 이중화/삼중화 (클릭해서 확인!)
Q1. Active-Standby와 Active-Active의 가장 큰 차이는? ||Active-Standby: 평시에 1대만 트래픽 처리, Failover 시 전환 지연 있음. Active-Active: 평시에 모두 트래픽 분산, 한 대 죽어도 나머지가 즉시 흡수 (거의 무중단)||
Q2. 스플릿 브레인(Split Brain)이란 무엇이고 어떻게 방지하나? ||네트워크 단절로 양쪽 모두 자신이 Active라고 판단하는 상태. 데이터 불일치 발생. 삼중화 + 쿼럼(과반수 투표)으로 방지 — 과반수를 얻지 못한 쪽이 자진 사퇴||
Q3. etcd가 3노드인 이유는? 4노드가 아닌 이유는? ||3노드: 1노드 장애 허용 (쿼럼 2/3). 4노드: 역시 1노드만 장애 허용 (쿼럼 3/4)으로 3노드와 같은데 비용만 증가. 홀수가 효율적||
Q4. RPO와 RTO의 차이는? ||RPO: 허용 가능한 데이터 손실량 (동기복제 = 0, 비동기 = 수초). RTO: 장애 발생부터 서비스 복구까지 허용 시간. 둘 다 짧을수록 좋지만 비용 증가||
Q5. 99.99%와 99.999% 가용성의 연간 다운타임 차이는? ||99.99% = 52.6분/년, 99.999% = 5.26분/년. 약 10배 차이이며 비용은 5~10배 이상 증가||
Q6. N+1과 2N 이중화의 차이는? ||N+1: 필요 대수(N) + 여유 1대 — 비용 효율적, 1대 장애 허용. 2N: 전체를 2배 — 비용 높지만 반이 죽어도 서비스 유지, 미션 크리티컬 시스템용||
Q7. 동기 복제와 비동기 복제를 각각 어떤 상황에서 쓰나? ||동기: 금융 거래, 결제 등 데이터 무손실 필수 (RPO=0). 비동기: 읽기 부하 분산, 분석용 복제 등 약간의 지연 허용 가능한 경우 (성능 우선)||
Q8. Raft 합의에서 리더 선출 과정을 설명하라. ||Follower가 리더 heartbeat를 못 받으면 Candidate로 전환 → term 증가 → 다른 노드에 투표 요청 → 과반수 득표 시 Leader 승격 → 클라이언트 요청 처리 시작||