[CS300 #135] 복제 전략 — 단일 리더, 다중 리더, 리더 없는 복제
컴퓨터공학 300 주제 시리즈의 135번째 글이다. 전체 지도는 여기.
한 줄 요약
복제는 같은 데이터를 여러 기계에 두는 일이다. 누가 쓰기를 받느냐에 따라 단일 리더·다중 리더·리더 없는 복제로 나뉘고, 쓰기 확인을 언제 하느냐에 따라 동기·비동기로 나뉜다. 각 조합은 내구성, 지연, 가용성, 일관성 사이의 다른 절충이다.
왜 필요한가
데이터를 한 기계에만 두면 세 가지가 문제다.
- 내구성: 디스크가 죽으면 데이터가 사라진다.
- 가용성: 기계가 재부팅되는 동안 서비스가 멈춘다.
- 읽기 확장·지연: 모든 읽기가 한 기계로 몰리고, 멀리 있는 사용자는 느리다.
복제는 이 셋을 해결한다. 대신 새 문제가 생긴다. 복제본들이 언제, 어떻게 같아지는가. 앞의 CAP·일관성 모델·Raft 글이 다룬 질문이 구체적인 설계 선택으로 내려오는 곳이 여기다.
핵심 개념
1. 단일 리더 (leader-follower, primary-replica)
쓰기 ──► [리더] ──변경 로그──► [팔로워1]
읽기 ──► [리더] └──► [팔로워2] ◄── 읽기(선택)
- 쓰기는 리더 하나만 받는다. 리더는 변경을 로그(PostgreSQL 의 WAL, MySQL 의 binlog)로 팔로워에 보낸다.
- 팔로워는 같은 순서로 적용한다. 순서가 하나라 충돌이 없다.
- 가장 흔한 방식이다. PostgreSQL, MySQL, MongoDB 레플리카 셋, Kafka 파티션이 이 구조다.
약점은 리더 장애다. 새 리더를 뽑는 장애 조치(failover)가 필요하고, 앞 글들의 스플릿 브레인·데이터 유실 위험이 여기서 나온다.
동기와 비동기
| 방식 | 커밋 응답 시점 | 장점 | 단점 |
|---|---|---|---|
| 동기 | 팔로워가 받았다고 확인한 뒤 | 리더가 죽어도 확인된 쓰기는 보존 | 느린 팔로워 하나가 모든 쓰기를 느리게 함 |
| 비동기 | 리더 로컬 기록 직후 | 빠름, 팔로워 장애가 쓰기에 영향 없음 | 리더가 죽으면 아직 안 보낸 쓰기가 사라짐 |
| 반동기 | N 개 중 일부의 확인 뒤 | 절충 | 설정이 복잡 |
PostgreSQL 은 synchronous_standby_names 에 FIRST 1 (a, b) 나 ANY 1 (a, b) 처럼 써서 몇 개의 대기 서버 확인을 기다릴지 정하고, synchronous_commit 으로 “확인” 의 수준(원격 기록, 원격 flush, 원격 적용)을 고른다.
복제 지연
비동기 팔로워는 리더보다 뒤처진다. 평소에는 1초 미만이지만 부하나 네트워크 문제로 수 분까지 벌어질 수 있다. 팔로워에서 읽으면 앞 일관성 모델 글의 “내 쓰기 읽기” 위반이 그대로 나타난다.
복제 로그의 형태
- 물리(바이트 단위) 복제: WAL 을 그대로 보낸다. 완전히 같은 복사본, 같은 메이저 버전 필요. PostgreSQL 스트리밍 복제.
- 논리(행 단위) 복제: “이 행이 이렇게 바뀌었다” 를 보낸다. 버전이 달라도 되고 일부 테이블만 복제할 수 있다. PostgreSQL 논리 복제, 변경 데이터 캡처(CDC).
- 문장 기반 복제: SQL 문 자체를 보낸다.
NOW(),RAND()같은 비결정적 함수 때문에 복제본이 달라질 수 있어 요즘은 잘 쓰지 않는다.
2. 다중 리더 (multi-leader)
여러 데이터센터에 리더를 하나씩 두고, 각자 쓰기를 받은 뒤 서로 비동기로 교환한다. 지역마다 쓰기 지연이 짧고 데이터센터 하나가 끊겨도 쓰기를 계속 받는다.
대가는 쓰기 충돌이다. 서울과 프랑크푸르트에서 같은 행을 동시에 고치면 어느 것이 맞는가. 마지막 쓰기 승리(LWW), 사용자 정의 병합, CRDT 같은 충돌 해결 규칙이 필요하다. 오프라인에서 편집하고 나중에 동기화하는 앱(캘린더, 협업 문서)도 사실상 다중 리더다.
3. 리더 없는 복제 (leaderless, Dynamo 스타일)
아마존 Dynamo 논문이 대중화한 방식이다. Cassandra, Riak 등이 따른다. 클라이언트(또는 코디네이터)가 N 개 복제본 모두에 쓰기를 보내고 W 개의 확인을 받으면 성공으로 본다. 읽기도 R 개에서 받아 가장 새 버전을 고른다.
R + W > N 이면 읽는 집합과 쓴 집합이 반드시 겹쳐 최신 값을 하나 이상 보게 된다. 이것을 쿼럼 조건이라 한다. N=3 이면 보통 W=2, R=2 다.
뒤처진 복제본은 읽을 때 발견해 고치거나(read repair), 백그라운드에서 머클 트리로 비교해 맞춘다(anti-entropy). 노드가 잠시 죽어 있으면 다른 노드가 대신 받아 두었다 돌려주는 힌트 핸드오프(hinted handoff)도 쓴다. 다만 쿼럼 조건을 만족해도 동시 쓰기, 실패한 쓰기의 부분 반영, 힌트 핸드오프 등의 경계 상황에서 선형화 가능성까지 보장되지는 않는다.
한눈에
| 방식 | 쓰기 충돌 | 리더 장애 영향 | 대표 |
|---|---|---|---|
| 단일 리더 | 없음 | 장애 조치 필요 | PostgreSQL, MySQL, Kafka |
| 다중 리더 | 있음, 해결 규칙 필요 | 다른 리더가 계속 | 지역 분산 DB, 오프라인 동기화 앱 |
| 리더 없음 | 있음(버전 병합) | 쿼럼만 되면 계속 | Cassandra, Riak, Dynamo |
직접 해 보기
N=3 리더 없는 복제에서 쓰기 직후(비동기 전파가 아직 안 된 순간) 무작위 복제본을 읽을 때, W 와 R 에 따라 최신 값을 볼 확률을 잰다.
import random
random.seed(1)
N = 3
def trial(W, R):
"""N 개 복제본 중 W 개에 새 버전(2)을 쓰고, 무작위 R 개에서 읽어 최신 버전을 고른다."""
replicas = [1] * N # 모두 옛 버전 1
for i in random.sample(range(N), W):
replicas[i] = 2 # 쓰기 확인을 받은 W 개만 새 버전
read = max(replicas[i] for i in random.sample(range(N), R))
return read == 2
for W, R in ((1, 1), (2, 1), (1, 2), (2, 2), (3, 1), (1, 3)):
ok = sum(trial(W, R) for _ in range(10000))
tag = "R+W>N" if R + W > N else " "
print(f"W={W} R={R} {tag} 최신 값 읽음 {ok/100:5.1f}%")
W=1 R=1 최신 값 읽음 32.8%
W=2 R=1 최신 값 읽음 66.3%
W=1 R=2 최신 값 읽음 66.7%
W=2 R=2 R+W>N 최신 값 읽음 100.0%
W=3 R=1 R+W>N 최신 값 읽음 100.0%
W=1 R=3 R+W>N 최신 값 읽음 100.0%
R+W>N 인 조합만 항상 최신 값을 본다. W=3, R=1 은 읽기가 빠르지만 복제본 하나만 죽어도 쓰기가 실패한다. W=1, R=3 은 그 반대다. W=2, R=2 는 한 대 장애를 견디면서 읽기·쓰기 모두 쿼럼을 만족하는 균형점이다.
현업에서는
- PostgreSQL 운영. 같은 데이터센터에 동기 대기 서버 하나, 원격에 비동기 대기 서버 하나를 두는 구성이 흔하다. 동기 대기 서버가 죽었는데
synchronous_standby_names에 대안이 없으면 primary 의 커밋이 멈춘다는 점을 꼭 기억해야 한다. 복제 지연은pg_stat_replication뷰의replay_lag등으로 본다. - 쿠버네티스 스테이트풀셋. 데이터베이스를 스테이트풀셋으로 띄우면 파드마다 고유한 이름과 볼륨이 생긴다. 하지만 복제 자체는 쿠버네티스가 해 주지 않는다. 오퍼레이터나 데이터베이스 자체 기능이 복제와 장애 조치를 맡는다.
- Kafka. 파티션마다 리더가 있고,
acks=all과min.insync.replicas=2조합은 “동기 복제본 최소 2개가 받아야 성공” 이라는 반동기 설정이다. - 백업은 복제가 아니다. 실수로
DELETE를 하면 그 삭제도 모든 복제본에 충실히 복제된다. 시점 복구(PITR)가 가능한 백업이 따로 있어야 한다.
확인 문제
- 비동기 복제에서 리더가 갑자기 죽으면 어떤 데이터가 위험한가?
- N=5 인 리더 없는 복제에서 쿼럼 조건을 만족하는 W, R 조합 하나를 들고, 몇 대 장애까지 읽기·쓰기가 모두 가능한지 말하라.
- 다중 리더 복제가 단일 리더보다 유리한 상황과 그 대가는?
- 문장 기반 복제가 위험한 이유는?
- “복제가 있으니 백업은 필요 없다” 는 주장의 문제는?
풀이
- 리더에서 커밋 응답을 했지만 아직 팔로워로 전송되지 않은 쓰기가 사라진다.
- W=3, R=3 이면 R+W=6>5 이다. 2대 장애까지 읽기·쓰기 모두 가능하다.
- 여러 지역에서 쓰기 지연을 줄이고, 한 지역이 끊겨도 쓰기를 계속 받아야 할 때 유리하다. 대가는 동시 쓰기 충돌과 그 해결 규칙의 복잡성이다.
NOW(),RAND()같은 비결정적 함수나 실행 순서 차이로 복제본마다 결과가 달라질 수 있다.- 논리적 실수(잘못된 삭제·갱신)도 그대로 복제되므로, 과거 시점으로 되돌릴 수단이 없다.