학습 목표

상황

전자상거래 서비스의 상품 상세 페이지가 Amazon RDS for MySQL 단일 인스턴스를 씁니다. Multi-AZ는 켜져 있지 않고, 읽기 전용 복제본도 없습니다. 애플리케이션 서버는 데이터베이스 엔드포인트 하나만 알고 있으며, 모든 쿼리가 그 엔드포인트로 갑니다.

지난 금요일 프로모션이 시작되자 다음 순서로 장애가 진행됐습니다.

  1. 상품 상세 페이지 조회가 평소의 여러 배로 늘었습니다.
  2. 데이터베이스 CPU 사용률이 100%에 붙고 쿼리 지연이 올라갔습니다.
  3. 애플리케이션의 커넥션 풀이 반환되지 않은 연결로 가득 찼습니다.
  4. 새 요청이 연결을 얻지 못해 쓰기 경로(주문 생성)까지 함께 실패했습니다.

쿼리 로그를 보면 읽기가 전체 요청의 대부분이고, 그중에서도 같은 상품 몇 백 건에 대한 동일한 조회가 반복됩니다. 쓰기는 주문과 재고 차감 정도이며 양이 많지 않습니다.

같은 읽기 부하에 세 처방을 적용했을 때 프라이머리에 남는 요청량 동일한 읽기 부하를 위에서 흘려 보내고, 세 가지 처방이 그 부하를 얼마나 가로채는지 세 열로 나란히 비교합니다. 왼쪽 처방은 읽기 전용 복제본입니다. 애플리케이션이 읽기 엔드포인트로 읽기를 보내므로 상당 부분이 복제본에서 처리되고 프라이머리에는 쓰기와 남은 읽기만 도달합니다. 아래 막대가 크게 짧아집니다. 가운데 처방은 ElastiCache 캐시 계층입니다. 캐시에 적중한 요청은 데이터베이스까지 내려가지 않으므로 미스만 프라이머리에 도달하고 막대가 가장 짧습니다. 오른쪽 처방은 Multi-AZ 배포입니다. 다른 가용 영역의 대기 인스턴스는 읽기 트래픽을 전혀 받지 않으므로 부하가 그대로 통과합니다. 그 지점을 가위표로 표시했고, 아래 막대는 처방을 적용하기 전과 같은 길이로 남습니다. 결론은 Multi-AZ 가 가용성을 사는 구성이지 읽기 부하를 더는 처방이 아니라는 것입니다. 같은 읽기 부하 — 처방마다 프라이머리에 도달하는 양이 다릅니다 처방 A — 읽기 전용 복제본 애플리케이션 읽기 부하 읽기 전용 복제본 읽기 엔드포인트가 흡수 프라이머리 쓰기와 남은 읽기 프라이머리에 도달하는 양 크게 줄어듭니다 처방 B — ElastiCache 애플리케이션 읽기 부하 ElastiCache 캐시 계층 적중분은 내려가지 않음 프라이머리 캐시 미스만 처리 프라이머리에 도달하는 양 가장 크게 줄어듭니다 처방 C — Multi-AZ 배포 애플리케이션 읽기 부하 대기 인스턴스 읽기를 받지 않습니다 프라이머리 읽기 부하가 그대로 프라이머리에 도달하는 양 전혀 줄지 않습니다 Multi-AZ 는 가용성을 사는 구성이지 읽기 부하를 더는 처방이 아닙니다 읽기 부하 문제에는 읽기 전용 복제본이나 캐시 계층으로 답합니다
동일한 읽기 부하에 읽기 전용 복제본, ElastiCache 캐시 계층, Multi-AZ 세 처방을 적용했을 때 프라이머리 인스턴스에 실제로 도달하는 요청량의 차이 — Multi-AZ의 대기 인스턴스는 읽기 트래픽을 받지 않으므로 부하가 그대로 남습니다

요구사항과 제약

요구사항과 제약, 그리고 각각이 후보를 어떻게 걸러 내는가
구분 내용 후보 필터
요구 1 읽기 처리량을 수 배 이상 늘린다 프라이머리에 도달하는 요청 자체를 줄이는 안만 통과
요구 2 읽기 폭주가 쓰기 경로를 무너뜨리지 않게 한다 읽기와 쓰기의 자원을 분리하는 안이 유리
요구 3 단일 인스턴스 구성의 가용 영역 장애 취약성도 해소한다 가용성 처방이 별도로 필요하다 — 읽기 처방과 섞지 않는다
제약 1 상품 정보는 수 초의 지연을 허용한다 복제 지연과 캐시 신선도를 감수할 수 있는 쿼리가 있다
제약 2 재고 수량과 주문 조회는 지연을 허용하지 않는다 이 쿼리들은 프라이머리로 고정해야 한다
제약 3 다음 프로모션까지 시간이 없다. 큰 리팩터링은 어렵다 코드 변경량이 판단에 들어간다

배경 개념은 10장 RDS와 관계형 데이터베이스와 11장 NoSQL · 캐시 · 분석 DB에 있습니다.

후보 설계안 비교

후보 설계안 4종 비교 — 읽기 부하 경감 효과를 기준으로 판정
항목 A · 인스턴스 클래스 확대 B · Multi-AZ 활성화 C · 읽기 전용 복제본 D · ElastiCache 캐시 계층
복제 방식 해당 없음 동기(synchronous) 비동기(asynchronous) 복제가 아니라 별도 저장소
읽기 트래픽을 받는가 같은 인스턴스가 다 받는다 대기 인스턴스는 받지 않는다 받는다 — 별도 읽기 엔드포인트 받는다 — 데이터베이스에 도달하기 전에 응답
읽기 부하 경감 상한이 인스턴스 크기에 묶인다 없음 복제본 수만큼 분산 반복 조회가 많을수록 가장 큼
가용성 기여 없음 가용 영역 장애 시 자동 장애 조치 수동 승격이 필요하다 데이터베이스 가용성과 무관
데이터 신선도 항상 최신 항상 최신 복제 지연만큼 뒤처질 수 있다 TTL과 무효화 전략에 달렸다
코드 변경 없음 없음 — 엔드포인트가 그대로 읽기 쿼리를 읽기 엔드포인트로 보내는 분기 캐시 조회·적재 로직 추가
판정 임시방편 — 상한이 곧 다시 온다 채택하되 요구 3(가용성)에 대해서만 채택 — 요구 1·2의 1차 처방 채택 — 반복 조회에 대한 근본 처방

선택과 근거

B + C + D를 서로 다른 요구에 각각 배정합니다. 하나로 다 해결하려는 시도가 이 케이스에서 가장 흔한 오답입니다.

요구별 처방 배정과 적용 순서
순서 처방 해결하는 요구 왜 이 순서인가
1 Multi-AZ 활성화 요구 3 (가용 영역 장애) 코드 변경이 없고 엔드포인트가 유지되므로 가장 먼저 넣어도 위험이 없다
2 읽기 전용 복제본 추가 + 지연 허용 읽기를 읽기 엔드포인트로 요구 1·2 애플리케이션 변경이 "엔드포인트 분기" 한 곳으로 끝난다. 제약 3에 맞는다
3 반복 조회를 ElastiCache로 흡수 요구 1의 근본 처방 효과가 가장 크지만 캐시 무효화 설계가 필요하므로 프로모션 이후에 넣는다
4 연결이 다시 병목이면 RDS Proxy 검토 요구 2의 잔여 위험 CPU를 먼저 해결한 뒤에도 연결 수가 문제인지 확인하고 판단한다

쿼리를 두 갈래로 나눈다

제약 1과 제약 2가 이 설계의 핵심입니다. 모든 읽기를 복제본으로 보내면 안 됩니다. 읽기 전용 복제본은 비동기로 복제되므로 프라이머리보다 뒤처진 데이터를 반환할 수 있습니다.

쿼리 유형별 경로 배정
쿼리 지연 허용 경로
상품 상세 · 카테고리 목록 · 리뷰 허용 캐시 → 없으면 읽기 엔드포인트
재고 수량 확인 불가 프라이머리(기본 엔드포인트) 고정
방금 생성한 주문 조회 불가 프라이머리 고정 — 쓰기 직후 읽기는 복제 지연에 그대로 노출된다
관리자 리포트 · 집계 허용 읽기 엔드포인트 — 무거운 쿼리를 프라이머리에서 떼어 낸다

1단계 · Multi-AZ 활성화

enable-multi-az.sh — 기존 인스턴스에 Multi-AZ 적용 (식별자는 예시입니다)
aws rds modify-db-instance \
  --db-instance-identifier shop-prod \
  --multi-az \
  --apply-immediately

# 적용 상태와 대기 가용 영역을 확인합니다.
aws rds describe-db-instances \
  --db-instance-identifier shop-prod \
  --query 'DBInstances[0].[MultiAZ,AvailabilityZone,SecondaryAvailabilityZone,Endpoint.Address]' \
  --output table

엔드포인트 주소는 바뀌지 않습니다. 장애 조치가 일어나면 같은 이름이 대기 인스턴스를 가리키도록 DNS가 전환됩니다. 그래서 애플리케이션 설정을 고칠 필요가 없습니다.

2단계 · 읽기 전용 복제본

create-read-replica.sh — 복제본 생성과 읽기 엔드포인트 확인 (식별자는 예시입니다)
aws rds create-db-instance-read-replica \
  --db-instance-identifier shop-prod-read-1 \
  --source-db-instance-identifier shop-prod

# 복제본은 자기만의 엔드포인트를 가집니다. 이 주소로 읽기 쿼리를 보냅니다.
aws rds describe-db-instances \
  --db-instance-identifier shop-prod-read-1 \
  --query 'DBInstances[0].[DBInstanceStatus,Endpoint.Address]' \
  --output table

복제본 개수에는 상한이 있습니다. MySQL · MariaDB · PostgreSQL은 인스턴스당 최대 15개, Oracle과 SQL Server는 5개, Db2는 3개입니다. 복제본은 다른 가용 영역이나 다른 리전에도 만들 수 있어 재해 복구 구성의 재료로도 쓰입니다 (케이스 8 · 리전 장애에 대비한 DR을 설계하라).

3단계 · 캐시 계층

반복 조회가 지배적이라는 관측이 캐시를 근본 처방으로 만듭니다. 같은 상품 한 건을 1초에 수백 번 조회한다면, 복제본을 몇 개 붙이든 같은 결과를 매번 다시 계산하고 있는 셈입니다.

create-cache.sh — 복제와 자동 장애 조치를 갖춘 캐시 구성 (식별자는 예시입니다)
aws elasticache create-replication-group \
  --replication-group-id shop-cache \
  --replication-group-description "product detail cache" \
  --cache-node-type cache.t3.micro \
  --num-cache-clusters 2 \
  --automatic-failover-enabled \
  --multi-az-enabled \
  --cache-subnet-group-name shop-cache-subnets \
  --security-group-ids sg-0123456789abcdef0

엔진 선택 기준은 단순합니다. 복제 · 자동 장애 조치 · 스냅샷이 필요하면 복제 그룹을 만들 수 있는 엔진(Valkey 또는 Redis OSS)을 고릅니다. Memcached는 다중 스레드로 단순한 키-값 캐시를 처리하지만 복제 그룹과 자동 장애 조치를 제공하지 않습니다. 시험에서 "캐시가 죽어도 데이터가 남아야 한다"거나 "정렬된 자료 구조가 필요하다"는 신호가 있으면 Memcached는 소거 대상입니다.

캐시 채우기 전략과 무효화

지연 로딩과 라이트 스루의 차이
전략 동작 장점 단점
지연 로딩(lazy loading) 캐시에 없으면 데이터베이스에서 읽어 캐시에 넣는다 실제로 요청된 데이터만 캐시에 남는다 첫 요청은 항상 느리고, 캐시가 비면 부하가 한꺼번에 몰린다
라이트 스루(write-through) 쓰기 시점에 데이터베이스와 캐시를 함께 갱신한다 캐시가 항상 최신이다 읽히지 않는 데이터까지 캐시에 쌓이고, 쓰기 경로가 느려진다
TTL 병행 두 전략 어느 쪽이든 만료 시간을 함께 둔다 무효화를 놓쳐도 언젠가 정정된다 TTL 동안은 낡은 값이 보인다

이 케이스에서는 상품 정보에 지연 로딩과 짧은 TTL을 함께 쓰고, 가격 변경처럼 즉시 반영이 필요한 쓰기에서만 해당 키를 명시적으로 삭제합니다. 재고 수량은 캐시에 넣지 않습니다 — 제약 2 때문입니다.

비용과 운영에 미치는 영향

각 처방이 추가하는 비용 항목과 운영 항목 — 금액이 아니라 항목의 성격입니다
처방 추가되는 비용 항목 추가되는 운영 항목 얻는 것
인스턴스 클래스 확대 더 큰 인스턴스 요금으로 대체 변경 시 재시작 계획 일시적 여유. 상한은 그대로
Multi-AZ 대기 인스턴스와 가용 영역 간 복제 트래픽 장애 조치 시나리오 점검 가용성. 읽기 처리량은 그대로
읽기 전용 복제본 복제본 인스턴스 수만큼. 교차 리전이면 리전 간 전송 복제 지연 감시, 승격 절차 문서화 읽기 처리량 분산
ElastiCache 캐시 노드 무효화 설계, 캐시 적중률 감시, 콜드 스타트 대비 가장 큰 부하 경감과 가장 낮은 응답 지연
RDS Proxy 프록시 사용 항목 연결 고정(pinning) 여부 확인 연결 수 병목 완화, 장애 조치 시 연결 전환 단축

설계 체크리스트

진단할 때

설계할 때

관련 시험 포인트

문제문의 신호와 정답 방향
문제문의 신호 고르는 것 소거하는 것
읽기가 많아 느리다 / 리포팅 쿼리가 무겁다 읽기 전용 복제본 Multi-AZ
가용 영역 장애를 견뎌야 한다 / 계획된 유지 관리 중단을 줄여야 한다 Multi-AZ 읽기 전용 복제본
같은 데이터를 반복 조회한다 / 밀리초 단위 응답이 필요하다 ElastiCache 인스턴스 클래스 확대
without changing application code Multi-AZ, RDS Proxy, 인스턴스 확대 캐시 계층 도입(코드 변경이 필요하다)
Lambda 동시 실행이 늘어 연결이 고갈된다 Amazon RDS Proxy 복제본 추가(연결 문제를 풀지 못한다)
DynamoDB 앞에 캐시가 필요하다 DynamoDB Accelerator(DAX) ElastiCache(가능하지만 코드 변경이 더 크다)
다른 리전에서도 읽어야 한다 교차 리전 읽기 전용 복제본, Aurora 글로벌 데이터베이스 Multi-AZ(같은 리전 안의 구성이다)

이어서 볼 곳

공식 문서 출처