[분산 시스템] Day 5: 장애 다루기 - 시계, 부분 실패, split-brain, 그리고 운영
이 글은 AI(Claude)의 도움을 받아 작성하고, 작성자가 검토·편집했습니다.
서론: 이론과 현실 사이에서 터지는 것들
Day 1~4에서 합의·복제·파티셔닝·일관성을 배웠다. 하지만 프로덕션 분산 시스템을 무너뜨리는 것은 대개 교과서에 없는 현실의 디테일이다. 믿을 수 없는 시계, 애매한 부분 실패, split-brain, 재시도 폭풍. 마지막 편은 이 함정들과 그것을 다루는 운영 패턴을 정리한다.
1. 시계를 믿지 마라
분산 시스템에서 가장 흔한 함정은 “시간”이다. 각 노드의 벽시계(wall clock)는 NTP로 맞춰도 어긋나고, 거꾸로 가기도 한다.
1
2
3
4
5
6
7
8
위험한 코드: 타임스탬프로 "최신" 판단 (Day 2의 LWW)
노드 A 시계가 100ms 빠르면 → A의 오래된 쓰기가 B의 최신 쓰기를 덮어씀
→ 조용한 데이터 유실
해법:
- 단조 시계(monotonic clock)로 경과 시간 측정 (벽시계 아님)
- 논리 시계: Lamport 타임스탬프, 버전 벡터로 인과관계 추적
- 물리 시계가 꼭 필요하면 신뢰구간 인지 (구글 TrueTime)
원칙: 순서가 중요하면 물리 시계가 아니라 논리 시계를 쓴다. 타임스탬프 비교로 인과관계를 판단하지 않는다.
2. 부분 실패와 타임아웃의 딜레마
분산 시스템의 본질적 어려움: 요청을 보냈는데 응답이 없을 때, 상대가 죽은 건지 그냥 느린 건지 구분할 수 없다.
1
2
3
4
5
6
7
8
9
10
요청 → ??? (응답 없음)
가능한 진실:
- 노드가 죽음 (재시도 안전)
- 요청은 처리됐는데 응답만 유실 (재시도하면 중복 실행!)
- 노드가 그냥 느림 (재시도하면 부하 가중)
타임아웃 딜레마:
너무 짧으면: 멀쩡한 노드를 죽었다고 오판 → 불필요한 장애 조치
너무 길면: 진짜 장애 감지가 늦어 사용자 지연 증가
완벽한 타임아웃 값은 없다. 적응형 타임아웃(과거 응답 시간 분포 기반)과 멱등성(아래)으로 대응한다.
3. 멱등성: 재시도의 안전망
부분 실패를 구분할 수 없으므로, 재시도해도 한 번만 실행된 효과를 보장해야 한다.
1
2
3
4
비멱등: "잔액에서 100원 차감" → 재시도 시 200원 차감
멱등: "잔액을 900원으로 설정" → 몇 번 재시도해도 결과 동일
부수효과가 있는 연산은 멱등성 키로:
1
2
3
4
5
6
7
def transfer(idempotency_key, amount):
# 같은 키의 처리 기록이 있으면 재실행 없이 그 결과 반환
if record := store.get(idempotency_key):
return record.result
result = do_transfer(amount)
store.put(idempotency_key, result) # 키와 결과를 원자적으로 기록
return result
gRPC 시리즈 Day 4에서 본 멱등성 키가 여기서도 핵심이다. 분산 시스템에서 “정확히 한 번(exactly-once)”은 사실상 “멱등 + 최소 한 번(at-least-once)”으로 구현된다.
4. split-brain: 두 개의 리더
네트워크 분할로 양쪽이 각자 리더를 뽑으면, 둘 다 쓰기를 받아 데이터가 갈린다. 분산 시스템 최악의 시나리오다.
1
2
3
4
5
6
7
8
9
10
분할 전: [Leader] - Follower - Follower
분할 후: [Leader] | [새 Leader] - Follower ← 양쪽 다 리더!
(구 리더, 자기가 여전히 리더인 줄 앎)
방어:
1. 정족수(quorum): 과반을 못 가진 쪽은 리더가 될 수 없음 (Day 1)
→ 소수파는 스스로 물러남
2. 펜싱 토큰(fencing token): 단조 증가 번호를 붙여,
낡은 리더의 쓰기를 스토리지가 거부
3. STONITH: 의심 노드를 강제 종료
1
2
3
4
펜싱 토큰 동작:
락 획득 시 토큰 33 발급 → 쓰기에 "33" 첨부
새 리더가 토큰 34 발급
스토리지: 34를 본 뒤엔 33(낡은 리더)의 쓰기를 거부
5. 장애 전파를 막는 패턴
한 컴포넌트의 장애가 시스템 전체로 번지는 것을 막아야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
백프레셔(backpressure):
처리 능력을 넘는 요청은 받지 않고 거절/대기
→ 큐 무한 적재로 인한 메모리 붕괴 방지
벌크헤드(bulkhead):
자원 풀을 격리 (서비스 A용 스레드풀과 B용을 분리)
→ A가 느려도 B는 영향 없음 (배의 격벽처럼)
서킷 브레이커(gRPC Day 4):
죽은 의존 서비스 호출을 즉시 차단
로드 셰딩(load shedding):
과부하 시 우선순위 낮은 요청부터 버림
핵심은 “장애를 막을 수 없으니 장애를 가두는” 발상이다. 한 부분의 실패가 전체 실패가 되지 않도록 경계를 친다.
6. 운영: 관측과 카오스 엔지니어링
분산 시스템은 복잡해서, 장애가 나기 전에 약점을 찾아야 한다.
1
2
3
4
5
6
7
8
9
관측성 (분산 추적):
한 요청이 수십 개 서비스를 거치므로, 추적 ID를 전파해
"어느 서비스의 어느 단계가 느렸나"를 추적 (gRPC Day 5의 OpenTelemetry)
카오스 엔지니어링:
프로덕션에서 의도적으로 장애 주입 (노드 종료, 지연 추가, 패킷 손실)
→ 복원력을 가정이 아니라 실험으로 검증 (Netflix Chaos Monkey)
핵심 지표: 가용성(SLA), p99 지연, 에러율, 복제 지연
7. 시리즈 종합 체크리스트
- 합의(Raft)로 노드들이 하나의 진실에 동의하는 법을 이해했다. (Day 1)
- 복제 전략과 동기/비동기의 일관성·가용성 트레이드오프를 파악했다. (Day 2)
- 파티셔닝과 일관성 해싱으로 데이터를 수평 확장했다. (Day 3)
- CAP·PACELC와 일관성 스펙트럼으로 “무엇을 포기할지” 설계했다. (Day 4)
- 시계·부분실패·split-brain·멱등성·장애격리 패턴으로 현실 장애에 대비했다. (Day 5)
시리즈 마무리
분산 시스템 설계의 핵심 교훈은 하나로 모인다. 부품은 반드시 실패하고, 네트워크는 반드시 분할되고, 시계는 반드시 어긋난다. 좋은 설계는 이것을 없애려 하지 않고 전제로 삼는다. 멱등성으로 재시도를 안전하게, 정족수로 split-brain을 막고, 벌크헤드로 장애를 가두고, 데이터마다 적절한 일관성을 택한다.
합의→복제→파티셔닝→일관성→장애대응 다섯 단계를 거치면, “왜 이 시스템은 이렇게 설계됐는가”를 트레이드오프의 언어로 읽고, 직접 설계 결정을 내릴 수 있는 토대를 갖추게 된다.