🔥 Read Replica: 읽기 부하 분산
강의 목차

처음 RDS for MySQL을 운영하면서 읽기 트래픽이 한 인스턴스에서 다 처리되지 않을 시점이 왔다. 콘솔에서 Actions 메뉴를 열어 보니 그 안에 Create read replica라는 항목이 있었다. 누르고 인스턴스 타입을 골랐고, 옆 AZ에 reader 한 대가 새로 떴고, 그 reader의 endpoint를 애플리케이션 ORM의 read connection 쪽에 적어 두었다. 같은 메뉴 같은 다이얼로그였는데 이 reader가 앞 편의 Multi-AZ standby와는 완전히 다른 물건이라는 걸 그때 처음 알게 됐다.
Multi-AZ와 장애 조치: 가용성의 기제에서 본 standby는 동기 복제로 데이터 손실 0초를 보장하는 hot standby였다. 클라이언트는 standby에 connection을 만들 수 없었다. 그런데 read replica는 그 반대다. RDS는 비동기로 데이터를 복제하고, 클라이언트가 직접 읽기 쿼리를 날리는 게 존재 이유다. 같은 RDS 콘솔, 같은 단어 '복제'인데 메커니즘과 목적이 다르다. 가용성이 아니라 읽기 부하를 위한 두 번째 복제 모델이 정확히 어떻게 동작하고 어디까지 도와주는지를 따라가자.
같은 단어 '복제'가 가리키는 두 모델
RDS는 복제를 한 단어로 부르지만 두 가지 다른 모델로 나눈다. 한쪽은 가용성을 위한 동기 복제고, 다른 한쪽은 읽기 부하 분산을 위한 비동기 복제다. 콘솔의 같은 인스턴스 메뉴에서 둘 다 만들 수 있어서 한참 혼란스러웠다.
Multi-AZ는 standby 한 대를 다른 AZ에 두고 동기 복제로 같은 redo log를 적는다. primary가 죽으면 RDS는 그 standby를 새 primary로 승격한다. RPO가 0초인 가용성 모델이다. 클라이언트가 standby에 connection을 만들 일은 없다.
Read replica는 reader 인스턴스 한 대 또는 여러 대를 만들고 비동기 복제로 변경분을 따라간다. 클라이언트가 reader에 직접 읽기 쿼리를 보낸다. 가용성 보장이 목적이 아니라 읽기 처리량을 늘리는 게 목적이다. 두 모델은 같이 켤 수 있다. Multi-AZ 옵션을 켠 인스턴스에 read replica를 추가로 붙여 둔 토폴로지가 운영 환경에서는 흔하다.
수치로 비교하자. RDS for MySQL·MariaDB·PostgreSQL 기준, 한 source DB instance에 read replica는 최대 15개까지 만들 수 있다. 2022년 10월 발표 이후의 한도다. Multi-AZ는 옵션이 켜져 있다 꺼져 있다의 0/1 차원이지만, read replica는 0개부터 15개 사이 어디든 둘 수 있는 가산형 차원이다. AWS는 인스턴스 시간을 그만큼 더 청구한다. reader 한 대를 추가할 때마다 같은 인스턴스 단가가 한 줄 더 청구서에 들어온다.

이 차이를 이해하지 못한 채 read replica를 가용성 백업처럼 취급하면 운영에서 사고가 난다. read replica는 비동기 복제라 primary가 죽는 순간 일부 변경분이 reader에 아직 도착하지 않았을 수 있다. 그 reader를 promote해서 새 primary로 띄우면 그만큼의 데이터 손실이 발생한다. 이 부분의 차이를 보려면 비동기 복제의 메커니즘을 더 깊게 짚어야 한다.
비동기 복제의 메커니즘
RDS for MySQL과 RDS for PostgreSQL은 비동기 복제 메커니즘이 서로 다르다. 두 엔진 모두 native replication을 그대로 쓴다는 점은 같다. AWS가 별도 replication 계층을 만들어 끼워 넣지 않았다. 그래서 엔진별로 알고 있는 운영 지식이 RDS 위에서도 거의 그대로 통한다.
MySQL은 binary log 기반이다. primary가 트랜잭션을 commit하면 MySQL은 binary log에 한 줄을 추가한다. reader는 자기 IO thread로 그 binary log를 가져오고 자기 SQL thread가 그걸 다시 적용한다. binary log 좌표 방식과 GTID(Global Transaction Identifier) 방식 두 가지가 있다. RDS for MySQL 5.7.44 이상의 5.7 버전과 8.0.28 이상의 8.0 버전에서는 GTID 기반 복제가 가능하다.
PostgreSQL은 WAL(Write-Ahead Log) streaming 기반이다. primary가 트랜잭션을 commit하면 PostgreSQL은 WAL에 record 한 줄을 더한다. reader가 streaming replication 프로토콜로 그 WAL을 받아 자기 디스크에 적용한다. RDS의 PostgreSQL read replica는 hot standby로 동작해서 클라이언트의 읽기 쿼리를 받을 수 있다.
두 엔진 모두 비동기다. 정확히 무슨 의미인가. primary가 트랜잭션을 commit할 때 reader에 변경분이 도착하기를 기다리지 않는다는 뜻이다. primary는 자기 redo log와 binary log/WAL을 자기 디스크에 sync한 시점에 commit을 끝낸다. 그 이후의 reader 적용은 reader 쪽 사정이다. 이 점이 Multi-AZ instance deployment의 동기 복제와 가장 큰 차이다.
이 비동기성의 결과로 reader는 primary보다 항상 약간 늦게 따라온다. 그 시간 차이를 replica lag이라고 부른다.

Replica lag, 0이 아닌 게 정상이다
replica lag을 0초로 만들 방법은 없다. 비동기 복제의 정의 자체가 reader가 primary를 일정 시간 뒤따라간다는 것이다. 그래서 운영자는 lag을 0으로 만드는 게 아니라 "허용 가능한 lag이 얼마인가"를 정해 놓고 그걸 모니터링하는 일을 한다.
CloudWatch에 그 lag을 측정하는 지표가 있다. RDS for MySQL은 ReplicaLag라는 지표를 노출한다. 단위는 초다. 이 지표는 reader 인스턴스에서 SHOW REPLICA STATUS 명령을 실행했을 때 나오는 Seconds_Behind_Master 필드를 그대로 갖다 쓴다. AWS 공식 문서가 표현한 그대로 옮기면, ReplicaLag 지표는 SHOW REPLICA STATUS 명령의 Seconds_Behind_Master 필드 값을 보고한다는 것(원문: The ReplicaLag metric reports the value of the Seconds_Behind_Master field of the SHOW REPLICA STATUS command). 같은 문서에 한 가지 함정이 더 적혀 있다. ReplicaLag이 -1이면 그것은 Seconds_Behind_Master가 NULL이라는 뜻이다. 즉 IO thread가 끊겼거나 SQL thread가 멈춰 있을 가능성이 있는 상태다. 이 -1을 정상으로 오해하면 사고가 난다.

PostgreSQL 쪽은 측정 방식이 약간 다르다. RDS for PostgreSQL은 replica lag을 currentTime - lastCommittedTransactionTimestamp로 계산한다. 이 정의에서 한 가지 운영적 함정이 따라온다. primary에 사용자 트랜잭션이 거의 들어오지 않는 idle 시간대에는 reader가 보고하는 lag이 자연스럽게 커진다. 마지막 commit 트랜잭션의 타임스탬프와 현재 시각의 차이가 늘어나기 때문이다. AWS 문서는 idle 상태일 때 RDS for PostgreSQL read replica가 최대 5분까지 replication lag을 보고한다고 적고 있다. 그 5분이라는 숫자는 PostgreSQL의 WAL segment switch 주기에서 온다. RDS는 기본 5분마다 WAL segment를 switch하는데, segment가 switch될 때 transaction record가 한 줄 reader에 도달하면서 lag 보고가 줄어든다.
이게 운영적으로 무슨 뜻인지 한 예로 짚자. 운영 환경에서 새벽 3시부터 5시 사이에 사용자 트래픽이 적은 워크로드를 가정한다. 이 시간대에 CloudWatch alarm을 ReplicaLag > 60s로 걸어 두면 매일 한두 차례 알람이 울리는 사고가 난다. 트래픽이 없어서 commit이 안 들어오는 정상 상태를 lag으로 오해한 것이다. PostgreSQL이라면 idle 시간대 alarm threshold를 5분 이상으로 두거나, 트래픽이 도는 시간대만 alarm을 활성화하는 식으로 운영을 맞춰야 한다.
비동기 복제의 또 다른 함정은 read-after-write 일관성이다. 사용자가 게시글을 작성해서 primary에 INSERT하고, 화면을 새로고침하면 reader에서 SELECT를 한다. 그 사이에 lag이 100ms라면 새로고침한 화면에 방금 쓴 글이 안 보일 수 있다. 이 문제를 푸는 방법은 정해진 패턴이 있다. 같은 사용자의 작성 직후 N초 동안은 read도 primary로 보내거나, 트랜잭션 ID를 클라이언트에 들려 보내고 reader가 그 ID 이상까지 적용한 게 확인될 때만 reader에서 읽는 식이다. 어느 쪽이든 비동기 복제 위에서 strong consistency가 필요한 경로는 애플리케이션이 책임을 지는 영역이다.
Cross-region read replica: DR과 비용이 한 묶음
read replica는 같은 region 안에서만 만드는 게 아니다. 다른 region에도 만들 수 있다. AWS 공식 문서가 정리한 한 문장으로 표현하면, source DB instance는 여러 AWS region에 cross-region read replica를 가질 수 있고 단 source VPC의 ACL 항목 한도 때문에 RDS는 cross-region read replica를 source 한 곳당 5개까지만 보장한다(원문: A source DB instance can have cross-Region read replicas in multiple AWS Regions. Because of the limit on the number of access control list (ACL) entries for the source VPC, RDS can't guarantee more than five cross-Region read replica DB instances). same-region까지 합한 한도는 15지만, cross-region 부분만 따로 떼면 5라는 점이 다르다.
지원 엔진은 거의 전부다. RDS for Db2, MariaDB, SQL Server, MySQL, Oracle, PostgreSQL이 모두 cross-region read replica를 만들 수 있다. 단 한 가지 제약이 있다. RDS for Db2, RDS for SQL Server, RDS for Oracle, RDS for PostgreSQL 14.1 미만은 cross-region read replica를 만들 때 source가 다른 read replica가 아닌 primary여야 한다. 즉 replica chain이 안 된다. RDS for MariaDB와 RDS for MySQL은 chain이 가능해서, 한 region에 첫 reader를 만들고 그 reader 아래에 추가 reader를 또 매다는 토폴로지를 만들 수 있다. RDS for PostgreSQL 14.1 이상도 chain이 풀려 있다.
chain이 가능한 엔진에서 이 차이가 비용으로 직접 닿는다. AWS 문서에 직접 적힌 예제를 짚자. source-instance-1이 ap-northeast-2(서울)에 있고, us-east-1(버지니아)에 reader를 세 대 두고 싶다고 하자. 한 가지 방법은 us-east-1에 read-replica-1, read-replica-2, read-replica-3 세 대를 모두 source-instance-1에서 직접 만드는 것이다. 이렇게 하면 RDS는 source-instance-1의 변경분을 region을 가로질러 세 번 전송한다. 다른 방법은 us-east-1에 read-replica-1 한 대만 source-instance-1에서 만들고, read-replica-2와 read-replica-3은 read-replica-1을 source로 잡아 같은 us-east-1 region 안에서 만드는 것이다. 이 경우 source-instance-1의 변경분이 region을 가로지르는 것은 한 번뿐이다. AWS 문서는 이 시나리오에서 AWS가 source-instance-1에서 read-replica-1로 전송한 데이터에 대해서만 청구한다고 적었다. 같은 16개 변경분이라도 첫 번째 토폴로지는 cross-region 전송 비용이 3배다.
여기서 운영적으로 worked example 하나를 짚자. 하루 100GB의 변경분이 발생하는 워크로드를 가정한다. 첫 번째 토폴로지로 ap-northeast-2 → us-east-1 cross-region 전송이 GB당 $0.02이라고 하면 AWS는 reader 세 대에 각각 100GB씩 보낸다. 일 6달러, 월 180달러를 cross-region DTO 청구로 따로 받는다. 두 번째 토폴로지로는 첫 번째 cross-region hop만 100GB이고 나머지 두 reader는 같은 region 안에서 무료로 받는다. 일 2달러, 월 60달러다. 차이가 월 120달러다. 같은 가용성과 같은 읽기 처리량을 얻으면서 토폴로지만 바꿔서 비용을 1/3로 떨군다.

같은 region 안의 read replica 트래픽은 데이터 전송 비용이 무료다. 그 대신 cross-region replica는 AWS가 두 가지를 따로 청구한다. 첫째, replica를 처음 만들 때 source의 snapshot을 다른 region으로 옮기는 트래픽. 둘째, 그 이후 source의 데이터 변경분이 destination region으로 이동하는 streaming 트래픽. 두 가지 모두 AWS는 outbound DTO 단가로 청구한다. ap-northeast-2 발신 cross-region DTO 단가는 destination region에 따라 다르고 GB당 $0.02 가량이 일반적이지만, 정확한 단가는 발신 시점의 RDS pricing 페이지에서 확인하는 편이 안전하다.

Read Replica를 끄는 게 답인 곳
관리형 도구라고 늘 답은 아니다. read replica가 답이 아닌 경우도 셋 있다.
첫째는 write-heavy 워크로드다. read replica는 읽기 처리량을 늘리는 도구지 쓰기 처리량을 늘리는 도구가 아니다. write가 병목인 워크로드에 reader를 열다섯 대 깔아도 primary의 write commit latency는 그대로다. 오히려 reader마다 binary log/WAL을 받아 적용해야 해서 primary의 binary log/WAL 발행 부담은 reader 수만큼 영향을 받을 수 있다. write가 병목이면 sharding이나 더 큰 인스턴스 클래스가 답이지 read replica가 답이 아니다.
둘째는 strong consistency 요구가 강한 워크로드다. 결제, 잔액 조회, 재고 차감처럼 방금 쓴 값을 즉시 같은 값으로 읽어야 하는 경로는 비동기 replica가 답일 수 없다. 100ms든 1초든 lag이 0이 아니라는 사실 자체가 그 경로의 정합성을 깬다. 그런 경로는 primary로 직접 읽거나, 엔진 선택: MySQL·PostgreSQL·Aurora에서 본 Aurora의 storage 공유 모델을 쓰는 편이 맞다. Aurora의 read replica는 storage 자체가 6 복제로 분산돼 있고 reader가 같은 segment를 mount하는 구조라서 lag이 보통 ms 단위로 짧다.
셋째는 connection pool과 인스턴스 사이즈 조정으로 충분한 워크로드다. read replica를 추가하기 전에 primary 인스턴스의 자원이 정말로 포화돼 있는지 먼저 확인한다. CPU 80% 이상이 지속되거나, IOPS가 provisioned 한도에 닿거나, connection 수가 max_connections에 가까운 상태가 아니라면, 인스턴스 클래스를 한 단계 키우거나 connection pool 설정을 다시 잡는 쪽이 더 단순하고 더 싸다. 인스턴스를 두 대로 늘리는 결정은 같은 단가가 두 줄 청구서로 들어온다는 사실을 한 번 확인하고 내리는 게 좋다.
처음 콘솔의 Create read replica 메뉴 앞에서 별 생각 없이 reader 한 대를 띄웠던 곳으로 돌아가 보자. 이제 그 한 번의 클릭이 비동기 binary log/WAL streaming의 시작이고, 0이 아닌 replica lag을 받아들이는 결정이고, reader 수만큼 인스턴스 시간 청구가 곱해지는 토글이고, cross-region으로 가면 데이터 전송 비용이 따로 붙는 토폴로지의 첫 한 칸임을 또렷이 안다. 가용성을 위한 동기 복제와 부하 분산을 위한 비동기 복제가 같은 단어 '복제' 아래 들어가지만 메커니즘과 운영 구조가 어떻게 다른지가 여기서의 답이다. 다음으로 짚을 주제는 reader로 부하를 나눠도 데이터 자체가 망가지면 어디로 돌아갈 것인가다. RDS의 자동 백업과 PITR이 닿는 시점의 범위가 그 질문의 답을 만든다.










