데이터중심 어플리케이션 5장: 복제
복제가 필요한 이유
복제(Replication)는 여러 장비에 동일한 데이터의 복사본을 유지하는 것을 의미하며, 다음과 같은 이점이 있습니다.
- 지연 시간 감소: 지리적으로 사용자와 가까운 위치에 데이터를 배치하여 네트워크 지연 시간을 줄일 수 있음
- 가용성 향상: 시스템 일부에 장애가 발생해도 다른 복제 서버를 통해 서비스를 지속적으로 제공 가능
- 읽기 처리량 증가: 읽기 질의를 처리하는 서버의 수를 늘려 전체 시스템의 처리량을 향상시킬 수 있음
복제의 어려움
복제에서 가장 어려운 문제는 복제된 데이터의 변경을 어떻게 처리할 것인가입니다. 이를 해결하기 위한 세 가지 주요 알고리즘이 있습니다.
- 단일 리더(Single Leader) 방식
- 다중 리더(Multi-Leader) 방식
- 리더 없는(Leaderless) 방식
리더 기반 복제
기본 구조
리더 기반 복제에서는 데이터베이스의 복사본을 저장하는 각 노드를 복제 서버라고 부르며, 두 가지 역할로 구분됩니다.
리더(Leader, Master, Primary)
- 클라이언트의 모든 쓰기 요청을 받는 서버
- 쓰기 작업은 오직 리더에만 허용됨
- 로컬 저장소에 데이터를 기록한 후, 변경 사항을 복제 로그(replication log) 또는 변경 스트림(change stream)을 통해 팔로워에게 전송
팔로워(Follower, Read Replica, Slave, Secondary)
- 리더로부터 전송받은 데이터 변경사항을 자신의 로컬 저장소에 적용
- 클라이언트의 읽기 요청을 처리할 수 있음
- 쓰기는 불가능하며, 리더의 데이터를 복제하는 역할만 수행
동기식 vs 비동기식 복제
동기식 복제(Synchronous Replication)
- 리더가 쓰기를 확정하기 전에 팔로워가 쓰기를 성공적으로 받았다는 확인을 기다림
- 장점: 팔로워와 리더가 항상 일관성 있게 최신 데이터 복사본을 가지는 것을 보장
- 단점: 팔로워가 응답하지 않으면(서버 장애, 네트워크 문제 등) 쓰기 작업이 중단됨
- 모든 팔로워를 동기식으로 운영하는 것은 비현실적
반동기식 복제(Semi-Synchronous Replication)
- 팔로워 중 하나는 동기식으로, 나머지는 비동기식으로 운영
- 동기식 팔로워에 문제가 생기면 다른 비동기식 팔로워를 동기식으로 승격
비동기식 복제(Asynchronous Replication)
- 리더가 팔로워의 확인을 기다리지 않고 쓰기를 확정
- 장점: 팔로워에 문제가 생겨도 쓰기 처리를 계속할 수 있음
- 단점: 리더에 장애가 발생하면 팔로워에 아직 복제되지 않은 쓰기가 모두 유실될 수 있음
새로운 팔로워 설정
장애가 발생한 노드를 대체하거나 복제 서버를 추가할 때, 새로운 팔로워가 리더의 정확한 복제본을 가지도록 하는 과정:
- 스냅숏 생성: 특정 시점에 리더의 데이터베이스 스냅숏을 생성
- 스냅숏 복사: 생성된 스냅숏을 새로운 팔로워 노드에 복사
- 변경사항 동기화: 팔로워가 리더에 연결하여 스냅숏 이후 발생한 모든 데이터 변경을 요청
- 스냅숏은 리더의 복제 로그의 정확한 위치와 연결되어야 함
- PostgreSQL: 로그 일련번호(log sequence number)
- MySQL: 이진로그 좌표(binlog coordinate)
- 따라잡기 완료: 팔로워가 스냅숏 이후의 모든 데이터 변경 backlog를 처리하면 동기화 완료
노드 중단 처리 (고가용성 확보)
팔로워 장애 (복구가 비교적 쉬움)
팔로워는 리더로부터 수신한 데이터 변경 로그를 로컬 디스크에 보관하기 때문에:
- 보관된 로그에서 장애 발생 직전의 마지막 트랜잭션을 확인
- 해당 시점 이후 발생한 데이터 변경사항을 리더에게 요청하여 복구
리더 장애 (복구가 어려움)
리더에 장애가 발생하면 장애 복구(Failover) 과정이 필요합니다.
장애 복구 단계
- 리더 장애 판단: 타임아웃 등을 사용하여 리더가 장애 상태인지 확인
- 새로운 리더 선출: 팔로워 중 하나를 새로운 리더로 승격
- 일반적으로 가장 최신 데이터를 가진 팔로워를 선택
- 시스템 재설정:
- 클라이언트가 새로운 리더로 쓰기 요청을 보내도록 재설정
- 다른 팔로워들이 새로운 리더로부터 데이터 변경을 받도록 설정
- 이전 리더가 복구되면 팔로워로 전환되도록 처리
복제 로그의 구현 방식
1. 구문 기반 복제(Statement-Based Replication)
모든 쓰기 요청(INSERT, UPDATE, DELETE 등의 SQL 구문)을 기록하고, 구문 로그를 팔로워에게 전송하여 실행하는 방식입니다.
문제점
RAND(), NOW() 같은 비결정적 함수는 서버마다 다른 값을 생성
UPDATE ... WHERE 등 데이터베이스의 기존 데이터에 의존하는 구문은 실행 순서에 따라 결과가 달라질 수 있음
- 자동 증가 컬럼이나 트리거가 있는 경우 문제 발생 가능
해결 방법
- 리더가 비결정적 함수 호출을 고정된 값으로 대체하여 로그에 기록
2. Write-Ahead Log (WAL) Shipping
데이터베이스가 모든 쓰기를 로그에 먼저 기록한 후 실제 데이터를 변경하는 방식을 활용합니다.
- 리더가 WAL을 팔로워에게 전송
- 팔로워가 이 로그를 처리하면 리더와 정확히 일치하는 데이터 구조의 복제본 생성
문제점
- 로그가 저수준의 데이터를 기술하므로 저장소 엔진과 밀접하게 연결됨
- 데이터베이스 엔진 버전이 변경되면 호환성 문제가 발생할 수 있음
- 리더와 팔로워가 다른 버전의 소프트웨어를 실행하기 어려움
3. 로우 기반 복제(Row-Based Replication, Logical Log)
복제 로그를 저장소 엔진 내부와 분리하기 위한 방식으로, 물리적 데이터 표현과 구분되는 논리적 로그를 사용합니다.
- 삽입된 로우의 모든 컬럼 값을 기록
- 삭제된 로우를 고유하게 식별하는 정보 기록
- 수정된 로우를 식별하는 정보와 새로운 컬럼 값 기록
장점
- 저장소 엔진 내부와 독립적이어서 버전 호환성 문제가 적음
- 외부 애플리케이션이 로그를 파싱하기 쉬움 (데이터 웨어하우스 등)
4. 트리거 기반 복제(Trigger-Based Replication)
애플리케이션 레벨에서 복제를 처리하는 방식입니다.
- 데이터베이스의 트리거(Trigger)나 스토어드 프로시저(Stored Procedure)를 사용
- 데이터 변경이 발생하면 트리거가 실행되어 변경 사항을 별도의 테이블에 기록
- 외부 프로세스가 이 테이블을 읽어 다른 시스템에 복제
특징
- 더 유연하지만 오버헤드가 크고 버그 발생 가능성이 높음
- 특정 데이터만 선택적으로 복제하거나 복잡한 충돌 해결이 필요한 경우에 유용
복제 지연 문제
비동기식 복제를 사용하면 복제 지연(replication lag)이 발생할 수 있으며, 이로 인해 일관성 문제가 생깁니다.
1. 쓰기 후 읽기 일관성(Read-After-Write Consistency)
문제 상황
- 사용자가 쓰기를 수행한 직후, 복제가 완료되지 않은 팔로워에서 데이터를 읽으면 방금 쓴 내용이 보이지 않음
해결 방법
- 사용자가 수정한 내용을 읽을 때는 리더에서 읽기
- 마지막 쓰기 후 일정 시간 동안은 리더에서 읽기
- 타임스탬프 기반으로 복제 서버 선택
- 클라이언트가 가장 최근 쓰기의 타임스탬프를 기억
- 해당 타임스탬프까지 업데이트가 적용된 복제 서버에서만 읽기
2. 단조 읽기(Monotonic Reads)
문제 상황
- 시간이 거꾸로 흐르는 현상 발생
- 사용자가 먼저 최신 데이터를 가진 복제 서버에서 읽고, 그 다음 복제 지연이 있는 다른 복제 서버에서 읽으면 이전에 봤던 데이터가 사라진 것처럼 보임
해결 방법
- 각 사용자의 읽기가 항상 동일한 복제 서버에서 수행되도록 보장
- 예: 사용자 ID의 해시를 기반으로 복제 서버를 선택하여 동일 사용자는 항상 같은 서버에 연결
3. 일관된 순서로 읽기(Consistent Prefix Reads)
문제 상황
- 여러 파티션에 분산된 데이터를 읽을 때, 인과관계가 있는 쓰기들이 순서가 뒤바뀌어 보일 수 있음
- 예: 질문과 답변이 역순으로 표시되는 경우
해결 방법
- 인과관계가 있는 쓰기는 동일한 파티션에 기록
- 복제 지연을 추적하여 인과적으로 관련된 쓰기가 모두 적용된 후에 읽기
리더 없는 복제(Leaderless Replication)
리더가 쓰기를 담당하는 것이 아닌, 모든 복제 서버가 클라이언트로부터 쓰기를 직접 받을 수 있게 허용하는 방식입니다. Dynamo, Riak, Cassandra 등에서 사용됩니다.
읽기 복구(Read Repair)
클라이언트가 여러 노드에서 병렬로 읽기를 수행할 때:
- 오래된 값을 감지하면 새로운 값을 해당 복제 서버에 다시 쓰는 방식
- 자주 읽히는 데이터에 효과적
안티 엔트로피(Anti-Entropy)
백그라운드 프로세스가 복제 서버 간의 데이터 차이를 지속적으로 찾아서 복사하는 방식:
- 읽기 요청이 없어도 데이터 일관성 유지
- 복제에 상당한 지연이 있을 수 있음
읽기와 쓰기를 위한 정족수(Quorum)
n개의 복제 서버가 있을 때:
- 쓰기: w개의 노드에서 쓰기가 성공해야 확정
- 읽기: r개의 노드에서 읽기를 수행
- 정족수 조건: w + r > n이면 최신 값을 읽을 수 있음을 보장
일반적인 설정
- n = 3, w = 2, r = 2
- 한 개의 노드 장애를 견딜 수 있으면서도 최신 데이터 읽기 보장
특징
- w, r 값을 조정하여 읽기/쓰기 성능과 내구성의 균형을 맞출 수 있음
- w = n, r = 1: 읽기에 최적화 (모든 노드에 쓰기 완료 필요)
- w = 1, r = n: 쓰기에 최적화 (한 노드에만 쓰기, 모든 노드에서 읽기)