Designing Data-Intensive Applications(데이터 중심 애플리케이션 설계)
- Designing Data-Intensive Applications(데이터 중심 애플리케이션 설계)
- Ch01 Reliability, Scalability, Maintainability
- Ch02 Data Models and Query Languages
- Ch03 Storage and Retrieval
- Ch04 Encoding and Evolution
- Ch05 Replication
- Ch06 Partitioning
- Ch07 Transaction
- Ch08 The Trouble with Distributed Systems
- Ch09 Consistency and Consensus
- Ch10 Batch Processing
- Ch11 Stream Processing
- Ch12 The Future of Data Systems
Designing Data-Intensive Applications(데이터 중심 애플리케이션 설계)
Ch01 Reliability, Scalability, Maintainability
TL;DR
- 품질은 신뢰성, 확장성, 유지보수성의 균형으로 본다.
- 평균 대신 백분위(p95, p99)와 오류율로 측정한다.
- 신뢰성은 결함이 발생해도 시스템이 올바르게 동작하고, 장애 전파를 막는 설계다.
- 확장성은 캐시, 큐 그리고 수평확장 등 부하가 증가해도 좋은 성능을 유지하기 위한 전략이다.
- 유지보수성은 레거시를 줄이고, 운용성, 단순성 그리고 발전성을 높이는 일이다.
Key Ideas
- 결함: 잘못될 수 있는 일이다. 결함을 예측하고 대처할 수 있는 시스템을 내결함성 또는 탄력성을 지녔다고 말한다.
- 부하 증가: 성능 저하를 유발하는 가장 흔한 이유이다. 그리고 부하 매개변수를 정의할 수 있다.
- 응답 시간: 온라인 시스템에서의 중요한 관심사다. 평균보다는 백분위(p95, p99, p999)를 사용하는 편이 좋다.
- 단순성: 우발적 복잡도를 줄이고 좋은 추상화로 유지할 수 있다.
Trade-offs
선택 | 장점 | 단점 | 언제 |
---|---|---|---|
수평 확장 | 탄력성과 확장성이 증가한다. | 키/리밸런싱이 복잡하다. | 고정적인 고부하 또는 대규모 데이터 발생 시 유용하다. |
수직 확장 | 장점: 도입이 쉽고, 단일 노드 성능이 증가한다. | 단점: 상향의 한계가 있고, 비용이 급증한다. | 언제 적용하는가: 일시적 스파이크가 발생하거나 간단한 워크로드에 유용하다. |
동기 처리 | 장점: 단순하고, 결과가 일관됐다. | 단점: 지연 시간이 증가하고, 장애가 전파된다. | 언제 적용하는가: 강한 즉시성이 필요할 때 유용하다. |
비동기 + 큐 | 장점: 스파이크를 흡수하거나 격리한다. | 단점: 순서, 지연을 관리해야 한다. | 언제 적용하는가: 쓰기 요청이 폭주하거나 후처리에 유용하다. |
Apply our Domain
- 우리 서비스는 백분위 기준이 존재하지 않음
- p95 기준 정의하기 ex)
GET /orders?nearbyBaseOrderId -> p95: 500ms
- 응답 시간 줄이기 ex)
GET /orders?nearbyBaseOrderId
SLI, SLO
- SLI
orders.read.p95_ms
:GET /orders
p95 응답시간orders.read.error_rate
: 5xx + 타임아웃/전체 요청cache.hit_rate.orders
: 주문 조회 캐시 히트율
- SLO (30 days)
orders.read.p95_ms < 200
orders.read.error_rate < 0.1%
cache.hit_rate.orders >= 80%
Load Parameters
- TOP-N
- Cache Hit by Key
- Egress/Ingress
Open Questions
- 부하 매개변수를 증가시키고 시스템 자원은 변경하지 않고 유지하면 시스템 성능은 어떻게 영향을 받을까?
- 부하 매개변수를 증가시켰을 때 성능이 변하지 않고 유지되길 원한다면 자원을 얼마나 많이 늘려야 할까?
- 캐시 TTL 1m/5m/10m에서 p95, 히트율, 신선도 불만율의 상관관계는?
Ch02 Data Models and Query Languages
TL;DR
- 스키마는 접근 패턴이 정한다. (무엇을 얼마나 자주/어떻게 읽고 쓰는가)
- 색인(인덱스) 는 읽기를 빠르게 하지만 쓰기/공간 비용이 든다. (복수 인덱스 유지=쓰기 증폭)
- 문서 모델은 읽기 스키마(schema-on-read) 에 가깝고 지역성/집계 읽기에 유리, 관계형은 조인/트랜잭션에 유리.
- 정규화 vs 비정규화는 쓰기 일관성 vs 읽기 성능의 교환.
Key Ideas
- Access-Pattern First: “최근 7일 유저별 주문 20개”, “주문 상세 빠르게” 같은 핵심 질의를 먼저 적고 스키마/인덱스 설계.
- 인덱스 기본기: 복합 인덱스(예: (user_id, created_at DESC)), 커버링 인덱스(SELECT 컬럼이 전부 인덱스에 있을 때), 선택도(cardinality) 체크.
- 정규화/비정규화 믹스: 핵심 엔터티는 정규화, API 응답 최적화용 읽기 모델은 비정규화/스냅샷 필드(ex: shipping_address_snapshot).
- 참조 vs 내장(embedding): 변경 잦고 공유되는 것은 ID 참조, 항상 같이 읽는 작은 하위 구조는 내장.
- Row-store vs Column-store: 온라인 트랜잭션(OLTP)은 보통 행 저장, 분석/리포트(OLAP)는 열 저장(+압축) 채택.
- Schema-on-write vs read: RDB는 쓰기 시 강한 스키마, 문서/데이터 레이크는 읽기 시 해석(유연하지만 쿼리 복잡도↑).
- 무중단 스키마 변경 패턴: Expand → Migrate → Contract(새 컬럼/인덱스 추가 → 백필/듀얼리드 → 구식 제거).
Trade-offs
| 선택 | 장점 | 단점 | 언제 | |———————|—————–|—————|——————| | 문자열 내장 저장 | 한 번에 읽기, 조인 없음 | 중복/부분 업데이트 비용 | 응답에 항상 필요한 작은 필드 | | ID 참조 | 중복 없음, 변경 전파 쉬움 | 조인/추가 조회 필요 | 재사용/변경 잦은 공통 엔터티 | | 정규화 | 쓰기 일관성·중복 최소 | 읽기 시 조인 비용 | 강한 정합·중복 방지 우선 | | 비정규화(스냅샷) | 읽기/캐시 지역성↑ | 쓰기 시 동기화 부담 | 응답 최적화·과거 상태 보존 | | 보조 인덱스 추가 | p95 읽기↓, 스캔↓ | 쓰기/공간↑, 유지비↑ | 조회가 훨씬 많을 때 | | Row-store | 단건 읽기/쓰기 빠름 | 대규모 집계 비효율 | OLTP 경로 | | Column-store | 대용량 집계 효율↑ | 단건 트랜잭션 느림 | 리포트/분석 경로 | | Schema-on-write | 검증·일관성↑ | 스키마 진화 마찰 | 핵심 트랜잭션 데이터 | | Schema-on-read | 유연성↑ | 쿼리 복잡/품질 편차 | 레이크/로그/실험 데이터 |
Apply our Domain
도메인: 주문(Order) 조회/상세, “nearbyBaseOrderId” 탐색
모델 제안
주문 RDB(정규화) + 읽기 모델(비정규화) 병행
RDB 테이블: orders(id, user_id, status, created_at, total_amount, …), order_items(order_id, product_id, qty, price, …)
읽기 최적화 필드(스냅샷): shipping_address_snapshot, user_name_snapshot
문서형 대안(선택): order 문서에 items 내장(최근 주문 타임라인 API 등 한 번에 내려줄 때)
인덱스/쿼리
- 최근 주문 목록: INDEX (user_id, created_at DESC) → WHERE user_id=? ORDER BY created_at DESC LIMIT 20
- 상태 필터: INDEX (status, created_at DESC)
- 유니크: UNIQUE(order_no)
- nearbyBaseOrderId: 키셋 페이징(offset 대신 created_at < :cursor_created_at OR (created_at = :cursor AND id < :cursor_id)), 복합 인덱스 (created_at DESC, id DESC)
마이그레이션(무중단) 예시
- Expand: 새 컬럼 shipping_address_snapshot nullable 추가, 새 인덱스 생성
- Migrate: 백필 배치 + API 듀얼 리드(없으면 조인, 있으면 스냅샷 사용)
- Contract: 충분한 기간 후 구식 조인 경로 제거, NOT NULL 전환/제약 강화
SLI, SLO (초안)
SLI
db.orders.query.p95_ms
: 주문 목록/상세 쿼리 p95db.orders.index_hit_rate
: 해당 쿼리 인덱스 히트율db.orders.rows_scanned
: 쿼리당 스캔 로우 수 p95api.orders.stale_read_rate
: 신선도 임계(예: 5분) 초과 응답 비율
SLO (rolling 30d)
db.orders.query.p95_ms
< 150db.orders.index_hit_rate
≥ 95%db.orders.rows_scanned.p95
≤ 5kapi.orders.stale_read_rate
≤ 0.5%
Open Questions
- nearbyBaseOrderId 탐색에서 키셋 페이징으로 완전히 전환 가능한가? (정렬 기준/커서 설계 확정)
- 스냅샷 필드의 신선도 임계(ex. 5분)를 넘는 경우, 언제/어떻게 갱신? (비동기 리프레시/쓰기 시 동기화)
- 문서형 대안을 도입한다면, 어떤 엔드포인트에서 조인 제거→응답 시간/비용 이득이 가장 큰가?
- 보조 인덱스 추가로 쓰기 QPS가 얼마나 감소하는가? (배치 쓰기/인덱스 리빌드 윈도우 필요?)
메모(마이그레이션 팁)
- 컬럼 추가 시 nullable + 기본값으로 시작 → 애플리케이션이 null 허용하도록 먼저 배포(네가 적어둔 포인트 아주 좋음!)
- 인덱스는 온라인 생성/백그라운드 옵션 사용, 리빌드 시간 동안 배포 금지 윈도우 설정
- 이중 쓰기가 필요하면 가능한 Outbox/이벤트로 일관성 보강, 아니면 듀얼 리드로 점진 전환
Ch03 Storage and Retrieval
TL;DR
Key Ideas
Trade-offs
Apply our Domain
SLI, SLO
SLI
SLO
Open Questions
Ch04 Encoding and Evolution
TL;DR
Key Ideas
Trade-offs
Apply our Domain
SLI, SLO
SLI
SLO
Open Questions
Ch05 Replication
TL;DR
Key Ideas
Trade-offs
Apply our Domain
SLI, SLO
SLI
SLO
Open Questions
Ch06 Partitioning
TL;DR
Key Ideas
Trade-offs
Apply our Domain
SLI, SLO
SLI
SLO
Open Questions
Ch07 Transaction
TL;DR (3문장)
- 트랜잭션은 동시성에서 불변식(invariant) 을 지키기 위한 도구다. 격리 수준 선택이 허용/차단할 이상(anomaly)을 결정한다.
- Snapshot Isolation(RR) 은 dirty/non-repeatable/read skew는 막지만 write skew/phantom은 막지 못한다. Serializable이 가장 안전하나 비용↑.
- 서비스 간에는 2PC보다 SAGA + Outbox + 멱등성으로 일관성을 설계하고, 재시도/백오프/서킷을 전제한다.
Key Ideas
- ACID 재정의(실무 관점): A(원자성)·I(격리)·D(내구성)는 스토리지+CC로 보장, C(일관성)은 애플리케이션 불변식으로 정의해야 함.
- 격리 수준과 이상
- RC(Read Committed): dirty read x, 그 외 대부분 허용
- RR/SI(Repeatable Read / Snapshot Isolation): dirty/non-repeat/read skew x, write skew o, phantom (DB 의존)
- Serializable: 주요 이상 전부 x (가장 안전)
-
주요 이상
- Dirty Read: 커밋 전 데이터 읽음
- Non-repeatable Read: 같은 행 재조회 값 변동
- Read Skew: 서로 다른 행/테이블 사이 시간 왜곡
- Write Skew: 두 트랜잭션이 각각 검사-쓰기로 불변식 위반
- Lost Update: 동시 갱신 중 덮어쓰기
- Phantom: 조건에 맞는 행 집합이 동시성으로 바뀜
-
동시성 제어(요약)
- 2PL(락 기반): 행/범위(갭/프레디킷) 락으로 직렬화 보장, 교착/대기↑
- MVCC + SI: 스냅샷 읽기(락 적음), 대신 write-write 충돌/쓰기 스큐 가능
- SSI(Serializable SI): SI 위에 충돌 검사로 직렬화 보장(충돌 시 abort)
- 명시적 원자 연산/제약:
UPSERT/UNIQUE/FOREIGN KEY
,SELECT ... FOR UPDATE
-
분산 트랜잭션
- 2PC는 블로킹/파티션 장애에 취약. 실무에선 SAGA(보상 트랜잭션) + Outbox/CDC + 소비자 멱등성을 조합
-
패턴
- 검사-후-쓰기(Check-then-write)는 SI에서 위험 → 원자적 카운터/제약/락/큐로 대체
- 장시간 트랜잭션은 핫락/재시도 폭증 유발 → 작게 쪼개기/오프로드
Trade-offs
선택 | 장점 | 단점 | 언제 |
---|---|---|---|
RC | 지연·경합↓, 처리량↑ | 읽기 왜곡/스큐 허용 | 단순 조회/보고용 |
RR/SI | 읽기 안정성↑, 락 경합↓ | Write Skew/Phantom 위험 | 대부분의 OLTP 기본값 |
Serializable(2PL/SSI) | 불변식 강보장 | 지연/Abort/교착↑ | 금전·재고 등 강한 제약 |
SELECT … FOR UPDATE | 잃어버린 업데이트 방지 | 락 경합/대기↑ | 소수 행 갱신 충돌 방지 |
UNIQUE/제약/원자연산 | 애플리케이션 버그 차단 | 스키마 의존, 유연성↓ | 불변식은 제약으로 표현 |
2PC | 전역 원자성 | 복잡/장애취약/락홀드 | 내부 단일 스토리지 계열 |
SAGA+Outbox | 탄력/장애내성, 독립 배포 | 보상 로직 필요(복잡) | 서비스 간 업무 흐름 |
Apply to Our Domain (주문/배차)
-
불변식 1: "동시 배차 ≤ 10건/지역"
- 옵션A: Serializable + 프레디킷/갭 락(성능 비용↑)
- 옵션B: 카운터 테이블에
atomic increment
후 임계 초과 시 롤백(권장) - 옵션C: 큐(단일 워커)로 직렬화
- 불변식 2: 주문번호 유일 → DB UNIQUE + Idempotency-Key (재시도 안전)
- Lost Update 방지: 버전 칼럼으로 CAS(낙관적 잠금) 또는
FOR UPDATE
- 서비스 간 일관성: OrderCreated → Outbox/CDC로 발행, 소비자는 멱등 처리
- 읽기-당장-쓰기(Read-your-writes): 사용자 세션은 리더 라우팅/세션 스티키
Metrics & SLO (권장)
db.tx.abort_rate
(전체/사유별: deadlock, serialization, lock timeout)db.lock_wait_ms.p95
/db.row_version_conflicts
api.idempotency.dedup_hits
(중복 차단 성공률)- SLO 예시(30일):
deadlock_rate < 0.1%
,serialization_fail_rate < 0.5%
,lock_wait_p95 < 50ms
Open Questions
- 어떤 경로에 Serializable을 적용/예외로 둘 것인가? (화이트리스트)
- 카운터 테이블 방식과 큐 직렬화의 성능/가용성 비교 실험 계획은?
- 멱등키 보존 TTL과 저장소(캐시/RDB) 선택은?
- 다중 리전에선 세션 일관성을 어떻게 보장할 것인가?(리더 고정/CRDT/지연 허용)
Ch08 The Trouble with Distributed Systems
TL;DR (3문장)
- 네트워크는 손실,지연,중복,재정렬이 모두 가능하다. 응답이 없다고 해서 미전달이 아님 → 타임아웃,재시도,멱등성이 기본 전제.
- 시계는 신뢰하기 어렵다(스큐,점프). 지연/타임아웃은 monotonic clock으로, 순서 판단은 버전/시퀀스로.
- 프로세스는 GC,스톨로 멈출 수 있고, FD(결함 감지기)는 오탐/지연 트레이드오프를 가진다. 정족수/리스/펜싱으로 리더십을 안전하게.
Key Ideas
- 네트워크 현실: 손실,중복,재정렬 + 큐잉 지연. → 멱등키/시퀀스/중복제거 설계.
- 타임아웃 설계: 홉별 예산분배 +
timeout ≈ 2d + r
(왕복/처리 시간 추정) + 모노토닉 기준. - 재시도 제어: 지수 백오프 + 지터 + Retry Budget(예: ≤5%)로 증폭 방지. 멱등 작업만 재시도.
- Failure Detector(FD): 헬스체크/φ-accrual로 의심 점수화. 공격적(빠른 차단) ↔ 보수적(오탐↓) 조절.
- 헤지드 요청: p95/p99에서 조건부 세컨드샷로 tail 레이턴시 절단(중복 안전 필수).
- 정족수(Quorum): N, R, W 선택으로 최신성↔가용성 조절. W+R>N이면 최신 쓰기와 교집합 보장.
- 리스/펜싱 토큰: 리더/락 소유권을 시간,토큰으로 방어(클럭 스큐 고려).
- Exactly-once 환상 깨기: 현실은 at-least-once/at-most-once → 소비자 멱등,재처리로 보정.
- 프로세스 스톨: GC/Stop-the-world → 타임아웃/풀,큐 상한으로 전파 억제, 관측성으로 감지.
Trade-offs
| 선택 | 장점 | 단점 | 언제 | |—————-|——————|—————-|————–| | 짧은 타임아웃 | Fail-fast, 빠른 전환 | 오탐↑, 재시도 폭증 | 내부 저지연 링크 | | 긴 타임아웃 | 오탐↓ | 복구 느림, 자원 홀드 | 외부/불안정 네트워크 | | 공격적 FD | 장애 격리 빠름 | False-suspect↑ | 핵심 경로 SLO 엄격 | | 보수적 FD | 오탐↓ | 장애 인지 늦음 | 배치/비핵심 | | 헤지 ON(조건부) | tail 절단 | 부하↑, 중복처리 필요 | p99 문제가 클 때만 | | W,R 크게 | 최신성/안전↑ | 지연/가용성↓ | 강한 정합 필요 | | 단일 리더 + 리스 | 단순/일관성↑ | 리더 장애시 전환 비용 | 쓰기 주도 경로 | | 멀티 리더 | 가용성↑ | 충돌/병합 복잡 | 지리 분산/쓰기 분산 |
Apply to Our Domain (Orders/Dispatch)
- 홉별 타임아웃/재시도 예산
- GW 100ms (retry 0) → OrderSvc 350ms (내부 호출 2×100ms) → DB 250ms (retry 0)
- Retry Budget ≤ 5%, 지수 백오프(100→200→400ms)+지터
- 멱등/중복제거
POST /orders
:Idempotency-Key
저장(24h TTL) → 중복 200/동일 바디- 이벤트 소비자: 키-데두프 스토어+시퀀스 번호(재정렬 허용 창)
- FD & 리더십
- φ-accrual FD, 임계 φ=8에서 격리; 리더는 리스(> clock_skew_p99 + RTT) + 펜싱 토큰
- 배차 카운터 저장소: N=3, W=2, R=2 (Raft/Quorum)
- Tail 컷(선택적)
/orders/{id}
p95>300ms이고 에러버짓 여유>50%일 때 헤지 1회(중복 안전 경로만)
Metrics & SLO (30일 롤링 권장)
dep.timeout_rate < 0.2%
(의존성별)dep.retry_rate ≤ 5%
(budget)hedged_share ≤ 2%
(헤지 발사 비율)fd.false_suspect_rate < 0.1%/h
clock_skew_p99 < 200ms
,gc_pause_p99 < 50ms
quorum_write_p95_ms < 120ms
api.idempotency.dedup_hits
추이(↑일수록 재시도 흡수)
Open Questions (실험 계획으로 전환)
- 현재 RTT/큐잉 분해 기준으로 홉별 타임아웃은 적정한가?
- 헤지 조건을 어느 엔드포인트에, 어떤 임계(p95, 에러버짓)로 둘까?
- 리더 리스 기간을 얼마로? (
lease >> clock_skew_p99 + RTT
) - Retry Budget 소진 시 정책(스로틀/셰딩/에러)과 알람 기준은?
- 이벤트 스트림의 재정렬 허용 창(seconds)과 데두프 TTL은?