읽기 전용 복제본은 주 DB(primary, writer)의 데이터를 복제해 읽기 요청만 받는 DB 인스턴스다. 쓰기는 writer 한 곳에서 처리하고, 읽기를 여러 복제본으로 나눠 부하를 분산한다.
flowchart LR
App["앱"] -- "쓰기" --> W[("writer")]
App -- "읽기" --> R1[("복제본 1")]
App -- "읽기" --> R2[("복제본 2")]
W -. "비동기 복제 (지연)" .-> R1
W -. "비동기 복제 (지연)" .-> R2
왜 나누나
- 부하 분산: 대부분의 서비스는 읽기가 쓰기보다 훨씬 많다. 읽기를 복제본으로 돌리면 writer는 쓰기에 집중한다
- 수평 확장(horizontal scaling): 복제본을 추가하는 것만으로 읽기 처리량이 늘어난다. 인스턴스를 키우는 수직 확장(vertical scaling)과 달리 재시작이 필요 없다(수직 확장과 수평 확장)
- 가용성: writer에 문제가 생기면 복제본을 승격(promotion)해 중단 시간을 줄인다. 다른 리전에 두면 가까운 사용자의 응답이 빨라진다
복제 지연(replication lag)
보통 비동기로 복제되므로 writer에 쓴 직후 복제본에서 읽으면 옛 값이 보일 수 있다(최종 일관성).
- "방금 저장한 내 글이 안 보인다" → 쓰기 직후의 읽기, 트랜잭션 안의 읽기는 writer로 보낸다
- 잠금(
SELECT ... FOR UPDATE)을 쓰는 흐름은 반드시 writer로
Django DB 라우터로 나누기
Django는 DATABASE_ROUTERS에 라우터 클래스를 등록해 읽기·쓰기 DB를 고른다.
class ReplicaRouter:
def db_for_write(self, model, **hints):
return "default"
def db_for_read(self, model, **hints):
if transaction.get_connection("default").in_atomic_block:
return "default" # 트랜잭션 안에서는 writer
return random.choice(["default", "replica"])- 읽기를 전부 복제본으로 보내면 writer의 여유 자원을 버리게 된다
- 복제본이 여러 개면 앱에서 고르기보다 DB 쪽의 묶음 엔드포인트(예: Aurora의 리더·커스텀 엔드포인트)에 분산을 맡기는 편이 낫다
- 멀티 DB를 고려하지 않은 서드파티 라이브러리가
default를 하드코딩해 문제를 일으킬 수 있다
관리형 DB가 이런 복제를 어디까지 대신해 주는지는 IaaS·PaaS·SaaS, 복제 방식 전반(동기·비동기, 리더 선출)은 데이터베이스 다중화, 쓰기까지 나누는 방법은 샤딩을 본다.
출처: PyCon KR 2019 「Django DB Router로 Database Read Replicas 100% 활용기 및 Troubleshooting 경험 공유」 한종원 · Django 문서: Database routers