MIT 6.824 분산 시스템 입문: 개발자를 위한 핵심 개념 정리
메타데이터
- 작성일: 2024-11-15
- 카테고리: 분산시스템 MIT6824 컴퓨터과학 아키텍처
- 태그: MapReduce Raft Concurrency 시스템설계
- 참고: MIT 6.824 Distributed Systems 강의
- https://youtu.be/gA4YXUJX7t8?si=6uhGq2oyNgaRGBeA
📌 서론: 왜 분산 시스템을 배워야 하는가?
현대 소프트웨어 개발자에게 분산 시스템은 선택이 아닌 필수입니다. Netflix, YouTube, Amazon 같은 서비스는 모두 수천 대의 서버가 협력하는 분산 시스템입니다. 하지만 분산 시스템은 단순히 “서버를 많이 쓰는 것”이 아닙니다. 그 안에는 일관성, 내결함성, 확장성이라는 근본적인 문제들과 그 해결책이 숨어 있습니다.
🎯 Part 1: 분산 시스템의 본질
1.1 분산 시스템 정의
분산 시스템: 네트워크로 연결되어 자원을 공유하고, 사용자에게는 마치 하나의 시스템처럼 보이지만, 실제로는 여러 독립적인 컴퓨터로 구성된 시스템
일상적인 예시
- 단일 시스템: 개인 노트북의 웹 서버, 1대의 데이터베이스 서버
- 분산 시스템: Google 검색, Netflix 스트리밍, Kubernetes 클러스터, 온라인 뱅킹
핵심 특징
- 복제(Replication): 동일한 데이터나 서비스가 여러 머신에 복사
- 투명성(Transparency): 사용자는 분산 구조를 알 필요 없음
- 독립적 실패: 한 부분 고장 시에도 전체 시스템 계속 동작
1.2 분산 시스템을 만드는 이유
목표 1: 확장 가능한 성능 (Scalable Performance)
수평 확장의 위력:
1대 서버: 10,000 req/sec
10대 서버: 100,000 req/sec (10배)
100대 서버: 1,000,000 req/sec (100배)
핵심: 머신을 추가하면 성능이 선형적으로 증가
목표 2: 내결함성 (Fault Tolerance)
Murphy's Law의 현실:
- 서버 1대 연간 장애율: 1%
- 1000대 시스템: 항상 10대 정도는 고장
- "정상 상태 = 일부 고장" 가정 필요
가용성 개선:
단일 서버 (99%): 연간 3.65일 다운
3대 복제 (각 99%): 연간 32초 다운
목표 3: 물리적 제약 극복
지리적 분산 예시:
서울 유저 → 미국 서버: 200ms 지연
서울 유저 → 서울 서버: 5ms 지연
해결책: 각 대륙마다 서버 배치 (CDN, Edge Computing)
1.3 분산 시스템의 어려움
함정 1: 네트워크는 신뢰할 수 없다
# 개발자의 착각
def send_message(data):
network.send(data)
# 당연히 도착했겠지? ← 위험한 가정!
# 실제 발생하는 문제들
- 패킷 손실 (Packet Loss)
- 패킷 순서 바뀜 (Reordering)
- 네트워크 분할 (Network Partition)
- 지연 급증 (Latency Spike)
- 중복 전송 (Duplicate Delivery)함정 2: 지연 시간의 현실
지연 시간 비교:
- 같은 데이터센터: 0.5ms
- 다른 가용 영역: 5ms
- 대륙 간 (한국-미국): 150ms
- 위성 통신: 500ms
함정: 코드상으로는 동일해 보이지만...
local_result = get_user(user_id) # 0.001ms
remote_result = get_user_rpc(user_id) # 150ms ← 150,000배 느림!
함정 3: 부분 실패 (Partial Failure)
단일 시스템: 프로세스 죽음 → 전체 중단 (명확)
분산 시스템:
- 서버 A: 정상
- 서버 B: 네트워크 끊김 (죽은지 느린지 불명)
- 서버 C: 디스크 풀
- 서버 D: CPU 100% (느림)
→ 부분적 실패 상태를 어떻게 다룰 것인가?
🛠️ Part 2: MapReduce - 분산 시스템의 첫 번째 교훈
2.1 MapReduce 탄생 배경
Google의 2004년 문제:
- 작업: 전체 웹 페이지 인덱싱
- 데이터: 수 테라바이트
- 요구사항: 매일 밤 처리 완료
- 단일 서버로는 수개월 소요
해결책: 수천 대 서버로 병렬 처리 새 문제: 매번 분산 처리 코드와 장애 처리 로직 구현?
2.2 MapReduce의 핵심: 추상화의 힘
# 프로그래머는 두 함수만 구현
class WordCount:
def map(self, document):
"""각 단어를 (단어, 1) 형태로 출력"""
for word in document.split():
emit(word, 1)
def reduce(self, word, counts):
"""같은 단어의 카운트를 합산"""
emit(word, sum(counts))
# 프레임워크가 자동 처리하는 것들:
# - 데이터 분할 및 분산
# - 네트워크 통신
# - 장애 처리 및 재시도
# - 결과 수집2.3 MapReduce 실행 과정
1. [입력 분할]
1TB 데이터 → 10,000개 chunk (각 100MB) → 10,000개 Worker
2. [Map Phase]
Worker 1: "hello" → (hello, 1), "world" → (world, 1)
Worker 2: "hello" → (hello, 1), ...
3. [Shuffle Phase] ← 자동 처리
같은 키 → 같은 Reducer로 전송
"hello" → [1,1,1,...] → Reducer 1
"world" → [1,1,1,...] → Reducer 2
4. [Reduce Phase]
Reducer 1: "hello" → 1,523,456
Reducer 2: "world" → 987,234
2.4 MapReduce의 내결함성
장애 처리 원칙:
- Master가 모든 Worker 상태 추적 (heartbeat)
- 장애 감지 시 미완료 작업 재할당
- 중요: Map 작업은 완료되어도 재실행 필요
- 이유: 중간 결과가 해당 Worker 로컬 디스크에 있음
- Reducer가 접근 불가능하므로 다른 Worker에서 재실행
핵심 요구사항: Map/Reduce 함수는 멱등성(Idempotent) 필수
2.5 MapReduce의 트레이드오프
장점:
- ✅ 프로그래밍 단순함 (두 함수만 작성)
- ✅ 자동 장애 처리
- ✅ 선형 확장성 (머신 2배 → 속도 2배)
- ✅ 로컬리티 최적화 (데이터 있는 곳에서 계산)
단점:
- ❌ 일괄 처리만 가능 (실시간 처리 불가)
- ❌ 반복 작업 비효율 (매번 디스크 I/O)
- ❌ 높은 지연 시간 (모든 Map 완료까지 Reduce 대기)
- ❌ 데이터 신선도 희생 (몇 시간 지연)
실무 적용:
적합: 로그 분석, 웹 크롤링 인덱싱, 일일 리포트
부적합: 실시간 추천, 주식 거래, 채팅 앱
→ 이후 Spark, Flink 같은 실시간 프레임워크 등장
🏗️ Part 3: 소프트웨어 아키텍처 원칙
3.1 아키텍처 정의
아키텍처: “나중에 변경하기 어려운 결정들의 집합”
실무 예시:
변경 쉬운 것 (非아키텍처):
- 세율 변경: 1분
변경 어려운 것 (아키텍처):
- 모놀리스 vs 마이크로서비스: 6개월
- SQL vs NoSQL: 3개월
- 동기 vs 비동기 통신: 2개월
- 단일 리전 vs 멀티 리전: 1년
3.2 품질 속성 (Quality Attributes)
성능 (Performance)
# 나쁜 예: 막연한 요구사항
"빨라야 한다"
# 좋은 예: 측정 가능한 요구사항
performance_requirements = {
"평균 응답 시간": "< 100ms",
"95 percentile": "< 200ms",
"99 percentile": "< 500ms",
"처리량": "> 10,000 req/sec"
}가용성 (Availability)
99%: 연간 3.65일 다운 (스타트업 초기)
99.9%: 연간 8.77시간 다운 (일반 서비스)
99.99%: 연간 52.6분 다운 (중요 서비스)
99.999%: 연간 5.26분 다운 (금융, 의료)
주의: 각 레벨 달성 비용은 기하급수적 증가!
확장성 (Scalability)
수직 확장 (Vertical): 더 강한 서버로 교체
- 장점: 구현 간단
- 단점: 물리적 한계, 비용 급증
수평 확장 (Horizontal): 서버 대수 증가
- 장점: 이론상 무한 확장
- 단점: 복잡도 증가 (분산 시스템 필요)
3.3 트레이드오프 사례
일관성 vs 가용성 (CAP 정리)
# 강한 일관성 (Strong Consistency)
class BankTransfer:
def transfer(self, from_account, to_account, amount):
# 모든 노드 동의할 때까지 대기
# 장점: 잔액 항상 정확
# 단점: 네트워크 장애 시 서비스 중단
with distributed_transaction():
from_account.balance -= amount
to_account.balance += amount
# 최종 일관성 (Eventual Consistency)
class SocialMediaLike:
def like_post(self, post_id, user_id):
# 일단 빠르게 응답, 나중에 동기화
# 장점: 항상 빠름, 항상 가용
# 단점: 일시적으로 좋아요 수 부정확 가능
local_db.increment_likes(post_id)
async_sync_to_other_nodes(post_id)마이크로서비스 vs 모놀리스
모놀리스:
✅ 배포 간단, 트랜잭션 쉬움, 디버깅 용이
❌ 확장 어려움, 배포 위험 (하나 버그 = 전체 중단)
마이크로서비스:
✅ 독립 배포, 선택적 확장, 기술 스택 자유
❌ 분산 트랜잭션 복잡, 네트워크 오버헤드, 디버깅 어려움
정답: 회사 규모와 팀 구조에 따라 다름
🧵 Part 4: 병행성 (Concurrency) 관리
4.1 스레드 필요성
문제 상황:
# 단일 스레드 서버 (끔찍한 성능)
while True:
client = server.accept() # 연결 대기
data = client.recv(1MB) # 1초
result = process_data(data) # 10초
db.save(result) # 2초
client.send(result) # 1초
# 다음 클라이언트는 14초 대기!
# 처리량: 약 4 req/min해결책:
# 멀티스레드 서버
def handle_client(client):
data = client.recv(1MB)
result = process_data(data)
db.save(result)
client.send(result)
while True:
client = server.accept()
threading.Thread(target=handle_client, args=(client,)).start()
# 처리량: 수천 req/min (동시 처리)4.2 레이스 컨디션 (Race Condition)
버그 숨은 코드:
balance = 1000
def withdraw(amount):
global balance
# 이 코드는 원자적(atomic)이지 않음!
temp = balance # 1. 읽기
temp = temp - amount # 2. 계산
balance = temp # 3. 쓰기
# 두 스레드 동시 실행
thread1 = Thread(target=withdraw, args=(100,))
thread2 = Thread(target=withdraw, args=(100,))
# 기대값: 800, 실제값: 900 (100원 증발!)발생 과정:
시간 Thread 1 Thread 2 balance
t0 1000
t1 temp1 = 1000
t2 temp2 = 1000
t3 temp1 = 900
t4 temp2 = 900
t5 balance = 900
t6 balance = 900 ← 덮어쓰기!
4.3 동기화 해결책
Mutex (상호 배제)
import threading
balance = 1000
lock = threading.Lock()
def withdraw(amount):
global balance
# 임계 영역 보호
with lock: # 한 번에 하나 스레드만 진입
temp = balance
temp = temp - amount
balance = temp
# 이제 안전: 결과 항상 800채널 (Go 스타일)
func worker(jobs <-chan int, results chan<- int) {
for job := range jobs {
result := process(job)
results <- result // 결과 전송
}
}
// 공유 메모리 없이 메시지 전달로 동기화
// "Don't communicate by sharing memory; share memory by communicating"📚 추천 도서 및 자료
필수 도서
-
“Designing Data-Intensive Applications” by Martin Kleppmann
- 분산 시스템의 바이블
- 실무 관점의 깊이 있는 분석
-
“Distributed Systems” by Maarten van Steen
- 이론적 기초가 탄탄
- 알고리즘 증명과 수학적 배경
온라인 강의
- MIT 6.824 Distributed Systems: https://pdos.csail.mit.edu/6.824/
- Martin Kleppmann’s Course: https://www.youtube.com/playlist?list=PLeKd45zvjcDFUEv_ohr_HdUFe97RItdiB
핵심 논문
- MapReduce (2004): https://pdos.csail.mit.edu/6.824/papers/mapreduce.pdf
- Raft (2014): https://raft.github.io/raft.pdf
- Bigtable (2006): Google의 NoSQL 설계
💡 핵심 요약
분산 시스템 3대 원칙
- 확장성: 머신 추가 → 성능 선형 증가
- 내결함성: 일부 장애 항상 발생 가정하고 설계
- 일관성: 모든 노드 동일 데이터 (또는 의도적 포기)
트레이드오프의 현실
- 완벽한 시스템은 없다
- 모든 선택에는 대가가 따른다
- 중요한 것은 “왜” 이 선택을 했는지 설명 가능한가
관련 노트
- MIT_6824_주말_학습_가이드 - 주말 집중 학습 계획 및 로드맵
- 쿠버네티스 서비스 실습 결과 보고서 - 분산 시스템 실무 적용 사례
- Gateway API vs Ingress - 현대적 분산 시스템 네트워킹
- 대규모 이벤트 로그 처리 아키텍처 - 실전 분산 데이터 처리