목차15개 항목
출처와 참고
ByteByteGo의 System Design 101 가이드 「How to Design for High Availability」, 「System Design Cheat Sheet」, 「A Cheat Sheet for Designing Fault-Tolerant Systems」, 「How to Detect Node Failures in Distributed Systems」를 바탕으로 재구성한 노트다. 페일오버와 스플릿 브레인은 「fru1tworld/cs-basic 모의 면접 질문 목록」이 묻는 개념을 채워 넣었다. Educative의 「The complete guide to the System Design Interview」의 하트비트 설명으로 중앙 감시자가 없는 방식을 더했다. karanpratapsingh의 「System Design」 강의 저장소를 바탕으로 나인 표의 월·주 단위, 가용성과 신뢰성·결함 허용의 구분, 클러스터링을 보강했다.
가용성이란
가용성(availability)은 장애가 나지 않은 노드가 합리적인 시간 안에 오류 없이 응답하는 비율이다. 응답이 온다는 것만 보장할 뿐, 그 데이터가 최신이라는 것은 보장하지 않는다. 최신성은 일관성의 문제다.
목표는 흔히 「9가 몇 개인가(nines)」로 말한다.
가용성 = 가동 시간 ÷ (가동 시간 + 중단 시간)
| 목표 | 연간 허용 중단 | 월(30일) | 주 | 하루 |
|---|---|---|---|---|
| 90% (one nine) | 약 36.5일 | 72시간 | 16.8시간 | 2.4시간 |
| 99% (two nines) | 약 3.65일 | 7.2시간 | 1.68시간 | 약 14.4분 |
| 99.9% (three nines) | 약 8.76시간 | 약 43분 | 약 10분 | 약 1.44분 |
| 99.99% (four nines) | 약 52.6분 | 약 4.3분 | 약 1분 | 약 8.64초 |
| 99.999% (five nines) | 약 5.26분 | 약 26초 | 약 6초 | 약 0.86초 |
| 99.9999% (six nines) | 약 31.5초 | 약 2.6초 | 약 0.6초 | 약 0.09초 |
9가 하나 늘 때마다 허용 중단은 10분의 1로 준다. 일곱 개 이상은 연간 몇 초 이하라 사람이 개입해 복구하는 것 자체가 불가능한 수준이다.
구성 방식에 따른 가용성 계산
직렬(모두 살아 있어야 동작): A = A1 × A2 × …
병렬(하나만 살아 있으면 동작): A = 1 − (1 − A1)(1 − A2)…
예) 가용성 90% 노드 하나 → 90%
같은 노드 둘을 병렬로 → 1 − 0.1 × 0.1 = 99%
99.9% 서비스 셋을 직렬로 → 약 99.7%이중화는 가용성을 곱으로 끌어올리고, 의존성은 곱으로 끌어내린다. 요청 경로에 동기 의존성이 많을수록 전체 가용성은 가장 약한 고리보다도 낮아진다. 병렬 계산은 노드 장애가 서로 독립이라는 가정이 있어야 맞으므로, 같은 랙·같은 AZ에 둔 복제본은 계산만큼 안전하지 않다.
가용성, 신뢰성, 결함 허용
| 구분 | 뜻 | 관계 |
|---|---|---|
| 가용성(availability) | 필요할 때 응답하는 시간의 비율 | 장애가 잦아도 빨리 복구하면 높게 나온다 |
| 신뢰성(reliability) | 장애 없이 올바르게 동작하는 능력(MTBF 등) | 신뢰할 수 있는 시스템은 가용하지만, 가용한 시스템이 늘 신뢰할 수 있는 것은 아니다 |
| 결함 허용(fault tolerance) | 구성 요소가 고장 나도 중단 없이 계속 도는 성질 | 고가용성은 짧은 중단(페일오버 시간)을 허용하고, 결함 허용은 중단 자체가 없다. 대신 전체 하드웨어를 이중으로 두고 동시에 돌려야 해 훨씬 비싸다 |
이중화 구조
노드 두 개로 이중화하기
| 구조 | 동작 | 장점 | 주의 |
|---|---|---|---|
| 주-백업(primary-backup) | 주 노드만 일하고, 데이터는 백업으로 복제한다. 주 노드가 죽으면 백업으로 전환한다 | 단순하다 | 백업 자원이 평소에 논다. 전환이 수동이면 그동안 중단된다 |
| 주-보조(primary-secondary) | 쓰기는 주 노드, 읽기는 보조 노드도 받는다 | 읽기 부하를 나눈다 | 복제 지연 때문에 보조 노드에서 옛 값을 읽을 수 있다 |
| 주-주(primary-primary) | 두 노드 모두 읽기·쓰기를 받고 서로 복제한다 | 처리량이 늘어난다 | 같은 데이터를 양쪽에서 고치면 최종 상태를 예측하기 어렵다. 충돌 해결 규칙이 있어야 한다 |
스트림 처리에서의 이중화
| 구조 | 동작 | 주의 |
|---|---|---|
| 핫-핫(hot-hot) | 두 인스턴스가 같은 입력을 받아 둘 다 하류로 출력한다. 한쪽이 죽어도 끊김이 없다 | 하류가 중복을 걸러야(dedupe) 한다 |
| 핫-웜(hot-warm) | 두 인스턴스가 같은 입력을 받지만 핫 쪽만 출력한다. 핫이 죽으면 웜이 출력을 시작한다 | 전환 순간의 유실·중복을 처리해야 한다 |
클러스터 이중화
| 구조 | 동작 | 핵심 |
|---|---|---|
| 단일 리더 클러스터 | 리더 하나가 쓰기를 받아 복제본에 퍼뜨린다 | 리더가 죽으면 리더 선출(leader election)로 새 리더를 뽑는다 |
| 리더 없는 클러스터 | 아무 노드나 쓰기를 받고 다른 노드로 복제한다 | 정족수(quorum): 노드 수 N, 쓰기 확인 수 W, 읽기 응답 수 R이 W + R > N이면 읽기 집합과 쓰기 집합이 최소 한 노드에서 겹쳐 최신 값을 읽는다 |
데이터센터·리전 단위의 이중화(백업·파일럿 라이트·웜 스탠바이·액티브-액티브)와 RTO·RPO는 고가용성과 재해 복구 설계에 있다.
클러스터링
클러스터(cluster)는 네트워크로 이은 컴퓨터(노드) 둘 이상이 하나의 시스템처럼 함께 일하는 구성이다. 노드들이 공유 저장소나 각자의 로컬 저장소를 쓰고, 보통 리더 노드 하나가 진입점이 되어 일을 나눠 주고 결과를 모은다. 사용자는 상대가 클러스터인지 한 대인지 몰라야 하고, 노드 간 통신이 병목이 되지 않게 설계한다.
| 종류 | 목적 |
|---|---|
| 고가용성(페일오버) 클러스터 | 한 노드가 죽으면 다른 노드가 이어받는다 |
| 부하 분산 클러스터 | 여러 노드가 같은 일을 나눠 처리한다 |
| 고성능 컴퓨팅(HPC) 클러스터 | 잘게 나눈 병렬 작업을 여러 노드의 CPU·메모리로 함께 계산한다 |
| 구성 | 동작 | 특징 |
|---|---|---|
| 액티브-액티브 | 둘 이상의 노드가 같은 서비스를 동시에 돌리고, 앞의 로드 밸런서가 일을 나눈다 | 처리량과 응답 시간이 좋아진다. 한 노드가 죽으면 남은 노드가 전체 부하를 감당할 여유가 있어야 한다 |
| 액티브-패시브 | 한 노드만 일하고 나머지는 대기(standby)한다 | 단순하지만 대기 노드 자원이 논다. 전환 시간만큼 중단된다 |
클러스터 모드는 쿠버네티스·Amazon ECS 같은 컨테이너 플랫폼, Cassandra·MongoDB 같은 DB, Redis 같은 캐시에 흔히 들어 있다.
| 어려움 | 내용 |
|---|---|
| 설치·운영 | OS, 앱, 의존성을 모든 노드에 설치·갱신해야 한다. 노드가 이기종이면 더 어렵다 |
| 관측 | 노드마다 자원 사용률을 보고 로그를 한곳으로 모아야 한다 |
| 저장소 | 공유 저장소는 노드끼리 서로 덮어쓰지 않게 막아야 하고, 분산 저장소는 동기화를 유지해야 한다 |
로드 밸런싱과의 차이(서버가 서로를 아는가)는 로드 밸런서에 있다.
결함 허용 설계의 원칙
결함 허용(fault tolerance)은 일부가 고장 나도 시스템 전체는 계속 동작하는 성질이다.
| 원칙 | 하는 일 |
|---|---|
| 복제(replication) | 데이터·서비스의 사본을 여러 노드·위치에 둔다 |
| 중복(redundancy) | 고장 난 구성 요소를 대신할 여분을 미리 둔다 |
| 부하 분산(load balancing) | 트래픽을 나눠 한 서버가 단일 장애점이 되지 않게 한다. 죽은 서버는 헬스 체크로 빼낸다 |
| 페일오버(failover) | 주 구성 요소가 죽으면 대기 구성 요소로 자동 전환한다 |
| 우아한 성능 저하(graceful degradation) | 일부가 죽으면 전체를 멈추지 않고 기능을 줄여 계속 돈다. 추천 영역을 숨기고 핵심 구매 흐름만 살리는 식이다 |
| 모니터링과 알림 | 상태와 성능을 계속 재고, 이상이 생기면 알린다. 감지하지 못한 장애는 복구할 수도 없다 |
노드 장애 감지: 하트비트
분산 시스템은 상대 노드가 죽었는지 직접 알 수 없다. 그래서 하트비트(heartbeat), 즉 주기적인 생존 신호를 주고받고 일정 시간 신호가 없으면 죽었다고 추정한다.
| 방식 | 동작 | 장점 | 단점 |
|---|---|---|---|
| 푸시(push) | 노드가 주기적으로 감시자에게 신호를 보낸다 | 구현이 가장 단순하다 | 네트워크 혼잡으로 신호가 늦으면 살아 있는 노드를 죽었다고 오판한다 |
| 풀(pull) | 감시자가 주기적으로 노드에 상태를 묻는다 | 신호 흐름을 감시자가 통제한다 | 감지가 늦을 수 있다 |
| 헬스 체크 포함 | 신호에 CPU·메모리·앱 지표를 담는다 | 「살아 있지만 아픈」 노드를 가려낸다 | 메시지가 커지고 판단 로직이 복잡해진다 |
| 타임스탬프 포함 | 신호에 보낸 시각을 담는다 | 생존 여부와 함께 네트워크 지연도 본다 | 노드 간 시계가 어긋나면 해석이 틀어진다 |
| 확인 응답(ack) | 받는 쪽이 응답을 돌려보낸다 | 보낸 노드뿐 아니라 양방향 경로가 살아 있음을 확인한다 | 메시지가 두 배가 된다 |
| 중앙 감시자 없음(무작위 피어) | 각 서버가 몇 초마다 무작위로 고른 서버 몇 대에 신호를 보낸다(가십 방식) | 감시자가 단일 장애 지점이 되지 않는다 | 장애가 퍼져 알려지기까지 시간이 걸리고 오판을 걸러야 한다 |
| 정족수(quorum) | 과반 노드와 하트비트가 닿는지로 클러스터 소속과 리더를 유지한다(Raft·Paxos) | 네트워크가 갈라져도 과반 쪽만 결정을 내려 스플릿 브레인을 막는다 | 노드 가입·탈퇴 때 정족수 변경을 다루기 복잡하다 |
타임아웃을 정하는 감각
- 짧으면 일시적 지연에도 장애로 판정해 불필요한 페일오버가 일어난다. 페일오버 자체가 부하를 만들어 상황을 악화시킬 수 있다.
- 길면 진짜 장애를 늦게 알아 그만큼 중단이 길어진다.
- 그래서 「N번 연속 놓치면 장애」처럼 여러 번 관측해 판정하고, 판정 뒤에도 바로 지우기보다 의심(suspect) 상태를 거치게 한다.
페일오버와 스플릿 브레인
단일 리더 구조에서 리더가 죽으면 쓰기가 멈춘다. 복제본에서 읽기는 계속될 수 있지만, 새 리더가 서기 전까지 쓰기 요청은 실패하고 진행 중이던 트랜잭션은 결과를 알 수 없게 된다. 그래서 리더를 바꾸는 페일오버(failover) 를 자동화한다.
1. 감지 하트비트·헬스 체크가 정해진 횟수만큼 실패하면 리더가 죽었다고 판정한다
2. 선출 복제본 중 가장 최신 데이터를 가진 노드를 새 리더로 고른다
(합의로 뽑거나, 과반 감시자의 동의를 받는 조정자가 고른다)
3. 재구성 클라이언트·프록시가 새 리더로 쓰기를 보내게 바꾼다 (DNS, 가상 IP, 프록시, 서비스 디스커버리)
4. 강등 옛 리더가 돌아오면 새 리더의 팔로워로 들어오게 한다| 도구 | 대상 | 조정 방식 |
|---|---|---|
| Patroni | PostgreSQL | etcd·Consul·ZooKeeper의 리더 키(리스)를 쥔 노드가 주 DB가 된다 |
| Orchestrator, MySQL InnoDB Cluster | MySQL | 토폴로지를 감시해 승격, 그룹 복제의 과반 합의 |
| Redis Sentinel | Redis | 감시자 여럿이 정족수로 장애를 판정하고 승격한다(캐시 클러스터 운영과 장애 대응) |
| Raft 내장 | etcd, Kafka KRaft, CockroachDB | 프로토콜 자체의 리더 선출 |
비동기 복제와 데이터 유실
비동기 복제에서 리더는 복제본이 받기 전에 클라이언트에게 성공을 돌려준다. 리더가 그 직후 죽으면, 새 리더로 승격된 복제본에는 확인까지 받은 쓰기가 없다. 옛 리더가 돌아와도 그 쓰기는 새 리더의 기록과 충돌하므로 대개 버려진다. 즉 비동기 복제의 페일오버는 RPO가 0이 아니다.
- 버려진 쓰기를 이미 다른 시스템이 봤다면 더 위험하다. 새 리더가 자동 증가 ID를 다시 발급해 캐시나 외부 시스템에 남은 옛 ID와 다른 행이 같은 ID를 갖는 식으로 어긋난다.
- 줄이는 방법: 복제본 최소 하나가 받은 뒤 응답하는 반동기·동기 복제, 승격 후보를 가장 앞선 복제본으로 고르기, 복제 지연이 큰 복제본은 후보에서 빼기. 복제 방식의 비교는 데이터베이스 확장과 성능 최적화에 있다.
스플릿 브레인
스플릿 브레인(split-brain) 은 두 노드가 동시에 자기가 리더라고 믿고 쓰기를 받는 상태다. 네트워크 분할, 긴 GC 멈춤, 너무 짧은 타임아웃 때문에 살아 있는 리더를 죽었다고 오판하면 생긴다. 두 쪽의 쓰기가 갈라져 나중에 합칠 수 없게 된다.
| 방지책 | 원리 |
|---|---|
| 정족수(quorum) | 과반의 동의를 얻은 쪽만 리더가 된다. 과반은 하나뿐이므로 두 리더가 동시에 설 수 없다. 노드는 홀수(3, 5)로 두고, 두 곳뿐이면 제3의 위치에 감시자(witness)를 둔다 |
| 임기·에포크 번호 | 리더가 바뀔 때마다 번호를 올리고, 모든 노드와 저장소가 낮은 번호의 명령을 거부한다(Raft의 term, Kafka의 leader epoch) |
| 펜싱(fencing) | 옛 리더가 정말 아무것도 못 쓰게 막는다. 전원을 끄거나(STONITH, 「다른 노드의 머리를 쏴라」), 공유 스토리지 접근을 끊거나, 저장소가 오래된 펜싱 토큰의 쓰기를 거부하게 한다(분산 시스템의 통신·데이터 흐름 패턴의 분산 락) |
| 리스(lease) | 리더 권한을 시간 제한으로 받고 과반으로 갱신한다. 갱신하지 못한 리더는 리스가 끝나면 스스로 쓰기를 멈추고, 새 리더는 옛 리스가 확실히 끝난 뒤에 선다. 시계가 크게 어긋나지 않는다는 가정이 붙는다 |
정족수만으로는 「자기가 아직 리더인 줄 아는」 옛 리더의 쓰기를 막지 못한다. 그래서 정족수로 새 리더를 뽑고, 에포크·펜싱으로 옛 리더의 쓰기를 거부하는 두 겹을 함께 쓴다. 합의 알고리즘이 이를 어떻게 지키는지는 분산 시스템의 시간과 장애의 Raft에 있다.
정리
- 가용성은 나인으로 재고, 이중화는 곱으로 올리고 직렬 의존성은 곱으로 내린다.
- 주-백업은 단순하지만 놀고, 주-보조는 읽기를 나누지만 옛 값을 줄 수 있고, 주-주는 처리량을 늘리지만 쓰기 충돌을 감수한다.
- 리더 없는 복제는
W + R > N정족수로 최신 값을 읽는다. - 장애 감지는 추정이다. 오판과 늦은 감지 사이에서 타임아웃과 판정 횟수를 고른다.
- 페일오버는 감지 → 선출 → 재구성 → 강등이다. 비동기 복제면 확인된 쓰기를 잃을 수 있다.
- 스플릿 브레인은 정족수로 새 리더를 뽑고 에포크·펜싱으로 옛 리더의 쓰기를 거부해 막는다.
관련 노트: 고가용성과 재해 복구 설계, 시스템 설계의 품질 속성과 트레이드오프, 장애 확산을 막는 복원력 패턴, 캐시 클러스터 운영과 장애 대응, 분산 시스템의 통신·데이터 흐름 패턴
원본 기록7
- 원문 (새 창) bytebytego.com/guides/how-do-we-design-for-high-availability
- 원문 (새 창) bytebytego.com/guides/system-design-cheat-sheet
- 원문 (새 창) bytebytego.com/guides/a-cheat-sheet-for-designing-fault-tolerant-systems
- 원문 (새 창) bytebytego.com/guides/how-do-we-detect-node-failures-in-distributed-systems
- GitHub 원문 (새 창) github.com/fru1tworld/cs-basic/blob/main/interview/etc/system_design.md
- 원문 (새 창) grokkingtechinterview.com/the-complete-guide-to-the-system-design-interview-in-2022-e2d30fe22448
- GitHub 원문 (새 창) github.com/karanpratapsingh/system-design