서론
이전 글에서는 Primary-Secondary Replication의 기본 구조와 읽기 요청 분산 방법을 살펴보았습니다.
Primary는 쓰기를 처리하고, Secondary는 Primary의 변경을 전달받아 자신의 데이터에 적용합니다.
지연을 허용할 수 있는 읽기 요청을 Secondary로 보내면 Primary의 읽기 부하를 줄일 수 있습니다.
하지만 Primary에서 쓰기가 성공한 시점과 Secondary에서 해당 변경을 조회할 수 있는 시점이 반드시 같지는 않습니다.
Primary에서 Commit
↓
Replication Log 전송
↓
Secondary가 로그 수신
↓
Secondary가 로그 저장
↓
Secondary가 로그 적용
↓
Secondary의 조회 결과에 변경 반영
여기서 중요한 질문은 다음과 같습니다.
Primary가 쓰기 성공을 반환했을 때 Secondary는 해당 변경을 어디까지 처리한 상태인가?
이 질문에 답하려면 동기 Replication과 비동기 Replication의 차이를 이해해야 합니다.
이번 글에서는 두 방식에서 쓰기 성공이 의미하는 범위를 비교하고, Primary 장애 시 성공한 쓰기가 사라질 수 있는 이유와 RPO, RTO, 멱등성까지 살펴보겠습니다.
1. 쓰기 성공은 무엇을 의미하는가
애플리케이션이 데이터베이스로부터 쓰기 성공 응답을 받으면 일반적으로 해당 변경이 안전하게 저장되었다고 생각합니다.
하지만 Primary-Secondary 구조에서는 쓰기 성공이 어느 범위까지 완료된 상태를 의미하는지 구분해야 합니다.
쓰기 요청은 다음과 같은 단계를 거칠 수 있습니다.
1. Primary가 트랜잭션을 실행한다.
2. Primary가 변경을 자신의 로그에 기록한다.
3. Secondary가 해당 로그를 수신한다.
4. Secondary가 로그를 저장한다.
5. Secondary가 로그를 데이터에 적용한다.
6. Secondary의 새로운 조회에 변경이 반영된다.
Primary가 어느 단계에서 클라이언트에 성공을 반환하는지는 Replication 방식과 설정에 따라 달라집니다.
쓰기 성공은 다음 중 하나를 의미할 수 있습니다.
- Primary에만 변경이 기록됨
- Secondary 변경 로그 수신
- Secondary 로그 저장
- Secondary에서 로그를 실제 데이터에 적용
따라서 단순히 “Replication을 사용하고 있다”는 사실만으로 쓰기 성공의 보장 범위를 판단할 수는 없습니다.
다음과 같은 질문에 답할 수 있어야 합니다.
쓰기 성공을 반환하기 전에 어떤 Secondary가 어떤 처리 단계까지 완료해야 하는가?
2. 비동기 Replication에서는 Secondary를 기다리지 않는다
비동기 Replication에서는 일반적으로 Primary가 Secondary의 처리 완료를 기다리지 않고 클라이언트에 성공을 반환할 수 있습니다.
Client Primary Secondary
│ │ │
│── write ────▶│ │
│ │── local commit │
│◀── success ──│ │
│ │── Replication Log ───▶│
│ │ │── apply
개념적인 처리 흐름은 다음과 같습니다.
1. Client가 쓰기 요청을 보낸다.
2. Primary가 트랜잭션을 Commit한다.
3. Primary가 Client에 성공을 반환한다.
4. Replication Log가 Secondary로 전송된다.
5. Secondary가 로그를 저장하고 적용한다.
Primary의 Commit과 Secondary의 변경 반영이 서로 분리되어 있기 때문에 클라이언트가 성공 응답을 받은 직후 Secondary를 조회하면 이전 데이터가 반환될 수 있습니다.
예를 들어 사용자가 배송지를 A에서 B로 변경했다고 가정하겠습니다.
1. Primary에서 배송지가 A에서 B로 변경된다.
2. Primary가 Client에 성공을 반환한다.
3. 애플리케이션이 Secondary에서 배송지를 조회한다.
4. Secondary에는 아직 변경 로그가 적용되지 않았다.
5. Secondary가 배송지 A를 반환한다.
쓰기 요청은 정상적으로 성공했지만, Secondary의 데이터 반영 수준이 Primary를 따라오지 못한 상태입니다.
비동기 Replication에서는 이러한 시간차가 주로 Stale Read로 나타납니다.
Primary
배송지: B
Secondary
배송지: A
이처럼 비동기 Replication의 장점은 Primary가 Secondary를 기다리지 않기 때문에 쓰기 응답 시간을 비교적 짧게 유지할 수 있다는 점입니다.
반면 다음과 같은 비용이 있습니다.
- Secondary에서 이전 데이터가 반환될 수 있습니다.
- Primary 장애 시 Secondary에 전달되지 않은 변경이 유실될 수 있습니다.
- Secondary별 데이터 반영 수준이 서로 다를 수 있습니다.
- 쓰기 성공만으로 Secondary의 상태를 판단할 수 없습니다.
3. 동기 Replication은 Secondary의 확인을 기다린다
동기 Replication에서는 Primary가 클라이언트에 성공을 반환하기 전에 지정된 Secondary의 확인을 기다립니다.
Client Primary Sync Secondary
│ │ │
│── write ────▶│ │
│ │── Replication Log ───▶│
│ │ │── process
│ │◀── confirmation ──────│
│◀── success ──│ │
다만 Secondary가 무엇을 완료했을 때 확인을 보내는지에 따라 보장 범위가 달라집니다.
Secondary의 처리 단계는 다음과 같이 나눌 수 있습니다.
1. 로그를 네트워크로 수신한다.
2. 로그를 운영체제 파일 시스템에 기록한다.
3. 로그를 영구 저장 장치에 기록한다.
4. 로그를 실제 데이터에 적용한다.
5. 새로운 조회에서 변경을 확인할 수 있다.
동기 Replication이라고 해서 항상 5번 단계까지 완료한 뒤 성공을 반환하는 것은 아닙니다.
설정에 따라 로그 수신만 확인할 수도 있고, 저장까지 기다릴 수도 있으며, 데이터 적용까지 기다릴 수도 있습니다.
따라서 다음과 같이 표현하는 것은 충분하지 않습니다.
동기 Replication을 사용하므로 Secondary에도 변경이 반영되어 있다.
대신 다음 질문에 구체적으로 답할 수 있어야 합니다.
Primary가 성공을 반환하는 시점에 동기 Secondary는 Replication Log를 어디까지 처리했는가?
4. PostgreSQL의 synchronous_commit
PostgreSQL에서는 synchronous_commit 설정을 통해 Commit이 어느 단계까지 기다릴지 조정할 수 있습니다.이를 통해 Primary가 Commit 성공을 반환하는 시점에 Secondary가 WAL을 어디까지 처리했는지 파악할 수 있습니다.
remote_write
remote_write에서는 Primary가 동기 Standby로부터 WAL이 운영체제의 파일 시스템에 기록되었다는 확인을 기다립니다.
Primary
│
│ WAL 전송
▼
Sync Standby
│
│ 운영체제 파일 시스템에 기록
▼
확인 응답
이 시점에는 다음 작업이 완료된 상태입니다.
- Standby가 WAL을 수신했습니다.
- Standby의 운영체제 파일 시스템에 WAL이 기록되었습니다.
하지만 다음 작업까지 완료되었다고 볼 수는 없습니다.
- WAL이 영구 저장 장치에 완전히 반영되었습니다.
- WAL이 실제 데이터에 적용되었습니다.
- 새로운 조회에서 변경을 확인할 수 있습니다.
따라서 remote_write는 Secondary가 변경 로그를 전달받았다는 보장 범위를 강화하지만, 데이터 조회에 변경이 반영되었다는 의미는 아닙니다.
on
synchronous_commit을 on으로 설정하면 Primary는 동기 Standby가 WAL을 영구 저장 장치에 기록했다는 확인을 기다립니다.
Primary
│
│ WAL 전송
▼
Sync Standby
│
│ WAL 영구 저장
▼
확인 응답
이 설정에서는 Primary 장애와 Standby의 PostgreSQL 프로세스 중단이 함께 발생하더라도 Standby의 저장 장치가 정상이라면 저장된 WAL을 이용해 변경을 복구할 가능성이 높아집니다.
하지만 WAL이 이미 데이터에 적용되었다는 의미는 아닙니다.
WAL 저장 완료
↓
Primary 성공 반환
↓
WAL Replay는 아직 진행 중일 수 있음
따라서 Primary가 성공을 반환한 직후 해당 Standby를 조회하면 이전 데이터가 반환될 수 있습니다.
즉, on은 변경 로그의 내구성을 강화하지만 Standby의 데이터 반영 완료까지 보장하는 설정은 아닙니다.
remote_apply
remote_apply에서는 Primary가 동기 Standby의 WAL Replay까지 기다립니다.
Client Primary Sync Standby
│ │ │
│── write ────▶│ │
│ │── WAL ───────────────▶│
│ │ │── WAL 저장
│ │ │── WAL Replay
│ │◀── apply 확인 ────────│
│◀── success ──│ │
이 설정에서는 다음 단계가 완료된 뒤 Primary가 성공을 반환합니다.
1. Standby가 WAL을 수신한다.
2. Standby가 WAL을 저장한다.
3. Standby가 WAL을 데이터에 적용한다.
4. Standby가 적용 완료를 Primary에 알린다.
5. Primary가 Client에 성공을 반환한다.
따라서 Primary가 성공을 반환한 직후 해당 동기 Standby를 조회하면 해당 변경을 확인할 수 있도록 구성할 수 있습니다.
remote_apply는 remote_write나 on보다 더 높은 데이터 반영 수준을 요구합니다.
대신 WAL Replay가 완료될 때까지 Primary의 Commit이 대기해야 하므로 쓰기 응답 시간이 길어질 수 있습니다.
5. 동기 Replication도 Replication Lag을 없애지는 않는다
동기 Replication을 사용하더라도 네트워크 전송과 WAL 저장, WAL Replay에 필요한 시간이 사라지는 것은 아닙니다.
차이는 해당 시간을 누가 기다리는가에 있습니다.
비동기 Replication에서는 Primary가 먼저 성공을 반환하고 Secondary가 이후에 변경을 따라갑니다.
Primary Commit
↓
Client 성공 응답
↓
Secondary WAL 수신·저장·적용
동기 Replication에서는 설정된 Secondary 처리 단계가 완료될 때까지 클라이언트가 성공 응답을 기다립니다.
Primary Commit
↓
Secondary WAL 수신·저장·적용
↓
Client 성공 응답
비동기 Replication에서는 Replication 처리 시간이 쓰기 성공 이후의 Stale Read로 나타날 수 있습니다.
동기 Replication에서는 해당 시간이 클라이언트의 쓰기 대기 시간에 포함됩니다.
따라서 동기 Replication의 장점을 쓰기 속도가 빨라지는 것으로 볼 수는 없습니다.
동기 Replication의 장점은 쓰기 성공의 의미를 더 강하게 설정할 수 있다는 점입니다.
Primary가 성공을 반환하기 전에 Secondary의 로그 수신, 영구 저장 또는 데이터 적용을 확인함으로써 성공한 변경이 Primary에만 존재하는 상황을 줄일 수 있습니다.
6. 동기 Replication의 비용
동기 Replication을 사용하면 다음과 같은 비용이 발생할 수 있습니다.
네트워크 왕복 시간이 쓰기 지연에 포함된다
Primary가 원격 Secondary의 응답을 기다려야 하므로 네트워크 지연이 쓰기 응답 시간에 직접 반영됩니다.
Primary와 Secondary가 멀리 떨어져 있을수록 영향이 커질 수 있습니다.
동기 Secondary가 느리면 Primary도 느려질 수 있다
동기 Secondary의 디스크 I/O나 WAL Replay가 느리면 Primary의 Commit도 함께 지연될 수 있습니다.
Secondary 처리 지연
↓
확인 응답 지연
↓
Primary Commit 대기
↓
Client 응답 지연
동기 Secondary가 응답하지 않으면 쓰기가 대기할 수 있다
설정에서 요구하는 동기 Secondary의 확인을 받을 수 없으면 Primary가 Commit을 완료하지 못하고 대기할 수 있습니다.
환경과 설정에 따라 다음과 같은 동작이 발생할 수 있습니다.
- 쓰기가 계속 대기합니다.
- Timeout 이후 오류를 반환합니다.
- 다른 동기 후보가 선택됩니다.
- 운영자가 설정을 변경할 때까지 쓰기가 중단됩니다.
정확한 동작은 데이터베이스 제품과 운영 설정을 기준으로 확인해야 합니다.
트랜잭션이 잠금을 더 오래 보유할 수 있다
Commit이 Secondary의 확인을 기다리는 동안 트랜잭션이 보유한 잠금이 더 오래 유지될 수 있습니다.
이로 인해 다른 트랜잭션의 대기 시간이 증가할 수 있습니다.
데이터 보호와 쓰기 가용성 사이에서 선택해야 한다
네트워크가 분리되어 Primary와 동기 Secondary가 통신할 수 없다고 가정하겠습니다.
데이터 보호를 우선하면 Secondary의 확인을 받을 수 없으므로 쓰기를 중단해야 할 수 있습니다.
반대로 쓰기 가용성을 우선하여 Secondary의 확인 없이 계속 쓰기를 허용하면 장애 발생 시 일부 변경이 다른 노드에 남지 않을 수 있습니다.
즉, 동기 Replication 설정은 다음 두 요구사항 사이의 선택과 관련됩니다.
데이터 보호 수준 강화
↕
쓰기 가용성 유지
7. 모든 Secondary가 같은 방식으로 동작하는 것은 아니다
하나의 Primary에 여러 Secondary가 연결되어 있더라도 모든 Secondary가 동기 방식으로 동작하는 것은 아닙니다.
일부만 동기 Standby이고 나머지는 비동기 Standby일 수 있습니다.
┌─ Sync Secondary A
Primary ─────────┼─ Sync Secondary B
└─ Async Secondary C
Primary가 Sync Secondary A의 확인을 기다렸다고 해서 Async Secondary C에도 동일한 변경이 반영되었다는 의미는 아닙니다.
각 Secondary는 다음 상태가 서로 다를 수 있습니다.
- 동기 Standby인지 비동기 Standby인지
- 현재 실제 동기 Standby로 선택된 노드인지
- WAL을 어디까지 수신했는지
- WAL을 어디까지 저장했는지
- WAL을 어디까지 적용했는지
- 현재 처리 오류가 발생했는지
따라서 읽기 요청을 Secondary로 보낼 때는 단순히 Secondary인지 여부만 확인해서는 안 됩니다.
다음 두 가지를 함께 고려해야 합니다.
해당 읽기에 필요한 데이터 반영 수준과 각 Secondary의 Replication 상태
예를 들어 Primary가 Sync Secondary A의 WAL Replay 완료를 기다린 뒤 성공을 반환했다고 가정하겠습니다.
이 경우 A에서는 해당 변경을 조회할 수 있을 가능성이 높습니다.
하지만 비동기 방식으로 따라오는 Secondary C에서는 아직 이전 데이터가 반환될 수 있습니다.
Primary
W1, W2, W3, W4
Sync Secondary A
W1, W2, W3, W4
Async Secondary C
W1, W2, W3
결국 개발자는 “동기인가, 비동기인가?”에서 멈추지 말고 다음 질문에 답해야 합니다.
Primary가 성공을 반환하는 시점에 어떤 Secondary가 Replication Log를 어디까지 처리했는가?
8. 비동기 Replication에서는 성공한 쓰기가 사라질 수 있다
비동기 Replication에서는 클라이언트가 성공 응답을 받은 쓰기가 Failover 과정에서 사라질 수 있습니다.
다음 상황을 가정하겠습니다.
Primary
W1 적용
W2 적용
Secondary
W1 적용
Primary는 W2를 Commit한 뒤 클라이언트에 성공을 반환했습니다.
하지만 W2가 Secondary로 전달되기 전에 Primary가 복구할 수 없는 장애를 일으켰습니다.
Client Primary Secondary
│ │ │
│── W2 ───────▶│ │
│ │── W2 Commit │
│◀── success ──│ │
│ │ │
│ X 장애 │
이 상태에서 Secondary를 새로운 Primary로 Promotion하면 새로운 Primary에는 W1만 존재합니다.
New Primary
W1
Client
W2가 성공했다고 인식
클라이언트는 W2가 성공했다고 기억하지만, Failover 이후의 데이터베이스에는 W2가 존재하지 않습니다.
즉, Primary에서는 성공한 쓰기였지만 Promotion 대상 Secondary의 데이터 반영 수준이 W2까지 도달하지 못했기 때문에 변경이 유실된 것입니다.
9. RPO: 어느 시점까지의 데이터 손실을 허용할 것인가
RPO는 Recovery Point Objective의 약자로, 장애 발생 시 어느 시점까지의 데이터 손실을 허용할 것인지를 나타냅니다.
비동기 Replication에서는 장애 시점에 Secondary로 전달되지 않은 변경이 데이터 손실 범위에 포함될 수 있습니다.
예를 들어 Primary와 Secondary 사이에 최대 5초의 Replication Lag이 발생할 수 있다면 장애 상황에서 최근 5초 동안의 변경이 유실될 가능성을 고려해야 합니다.
현재 시점
│
├─ 5초 이전 변경: Secondary에 반영되었을 가능성이 높음
└─ 최근 5초 변경: 유실 가능성 존재
RPO를 0에 가깝게 유지해야 하는 데이터라면 다음과 같은 방식을 검토할 수 있습니다.
- 동기 또는 준동기 Replication
- 여러 노드의 확인을 요구하는 구조
- 별도의 내구성 있는 이벤트 로그
- 안전하게 재처리할 수 있는 멱등성 구조
- 외부 시스템과의 상태 대조 절차
- 업무 데이터 Recovery 절차
다만 동기 Replication을 사용한다고 해서 모든 장애 상황에서 데이터 손실이 절대 발생하지 않는다고 단정할 수는 없습니다.
예를 들어 다음과 같은 장애는 별도로 고려해야 합니다.
- 저장 장치 손상
- 운영자의 잘못된 데이터 삭제
- 애플리케이션 버그
- 데이터센터 전체 장애
- 잘못된 Failover 작업
- 여러 노드의 동시 손상
따라서 RPO를 정할 때, 장애 범위까지 견뎌야 하는지를 정의한 뒤 목표에 맞는 저장 및 Recovery 구조를 선택해야 합니다.
10. RTO: 얼마 만에 서비스를 복구할 것인가
RTO는 Recovery Time Objective의 약자로, 장애 발생 이후 서비스를 다시 제공하기까지 허용되는 시간을 의미합니다.
Secondary를 자동으로 Promotion하면 수동으로 장애를 처리하는 것보다 서비스 복구 시간을 줄일 수 있습니다.
하지만 빠르게 Promotion하는 것만으로 안전한 Failover가 완성되지는 않습니다.
빠른 Promotion만을 우선하면 다음과 같은 문제가 발생할 수 있습니다.
- 일시적인 네트워크 문제를 Primary 장애로 오판합니다.
- 데이터 반영 수준이 낮은 Secondary를 Promotion합니다.
- 여러 Secondary가 동시에 Promotion됩니다.
- 기존 Primary의 Fencing에 실패합니다.
- 애플리케이션 연결이 기존 Primary에 남아 있습니다.
11. RPO와 RTO를 함께 고려해야 한다
RTO를 줄이려면 장애 감지부터 Promotion, 애플리케이션 연결 전환까지의 시간을 단축해야 합니다.
하지만 서비스 복구 속도만을 우선해서는 안 됩니다.
Promotion 전에 기존 Primary가 실제로 쓰기를 처리할 수 없는 상태인지 확인해야 하며, 여러 Secondary 중 Primary의 변경을 가장 많이 반영한 노드를 선택해야 합니다.
이러한 확인 절차를 생략하면 서비스는 빠르게 재개될 수 있지만, 다음과 같은 문제가 발생할 수 있습니다.
- 기존 Primary와 New Primary가 동시에 쓰기를 처리합니다.
- Primary의 변경을 충분히 반영하지 못한 Secondary가 Promotion됩니다.
- 장애 직전에 성공한 쓰기가 New Primary에 존재하지 않을 수 있습니다.
- 애플리케이션의 일부 연결이 기존 Primary에 남아 있을 수 있습니다.
따라서 Failover에서는 서비스 복구 시간뿐 아니라 데이터 손실 범위도 함께 고려해야 합니다.
RPO는 장애 발생 시 어느 시점까지의 데이터 손실을 허용할 것인지를 나타내고, RTO는 장애 이후 얼마 안에 서비스를 다시 제공해야 하는지를 나타냅니다.
RPO
→ 장애 발생 시 허용할 수 있는 데이터 손실 범위
RTO
→ 장애 이후 서비스를 복구해야 하는 시간
예를 들어 데이터 반영 수준을 충분히 확인하지 않고 Secondary를 즉시 Promotion하면 경우, RTO는 짧아질 수 있지만 Primary의 변경이 적게 반영된 Secondary가 선택될 수 있습니다.
반대로 Secondary별 WAL 수신 및 적용 위치, 기존 Primary의 상태와 Fencing 여부를 확인하면 더 안전한 Promotion 대상을 선택할 수 있지만, 서비스 재개까지 시간이 더 필요할 수 있습니다.
즉, Failover 정책에서는 다음 두 가지 기준을 함께 정의해야 합니다.
- 장애 발생 시 어느 범위까지의 데이터 손실을 허용할 것인가?
- 안전한 Promotion에 필요한 확인 절차를 포함해 얼마 안에 서비스를 재개할 것인가?
RPO와 RTO는 어느 하나만 선택하는 목표가 아닙니다.
서비스가 요구하는 데이터 보존 수준과 복구 시간을 기준으로 장애 감지, Fencing, Promotion 대상 선택 및 연결 전환 절차를 설계해야 합니다.
12. 클라이언트가 쓰기 성공 여부를 알 수 없는 경우
장애 상황에서는 성공한 쓰기가 사라지는 문제뿐 아니라, 클라이언트가 요청의 성공 여부를 판단하지 못하는 문제도 발생할 수 있습니다.
Primary가 트랜잭션을 Commit한 직후 성공 응답을 보내기 전에 연결이 끊어졌다고 가정하겠습니다.
Client Primary
│ │
│── request ──▶│
│ │── Commit 완료
│ X │
│ 응답 유실 │
Primary에서는 트랜잭션이 정상적으로 Commit되었습니다.
하지만 클라이언트는 성공 응답을 받지 못했습니다.
이 때, 클라이언트 입장에서는 다음 두 상황을 구분할 수 없습니다.
상황 1
요청이 Commit되기 전에 실패했다.
상황 2
요청은 Commit되었지만 응답만 유실되었다.
클라이언트가 요청이 실패했다고 판단하고 같은 요청을 다시 보내면 첫 번째 요청이 이미 처리된 경우 중복 작업이 발생할 수 있습니다.
예를 들어 주문 생성 요청이라면 동일한 주문이 두 번 생성될 수 있습니다.
결제 요청이라면 동일한 결제가 중복으로 처리될 수 있습니다.
이러한 문제를 해결하려면 동일한 요청을 안전하게 다시 보낼 수 있도록 멱등성을 보장해야 합니다.
13. 멱등성 키를 이용한 안전한 재시도
클라이언트가 요청마다 고유한 requestId를 전달하도록 구성할 수 있습니다.
requestId = "order-request-7f82"
서버는 해당 requestId를 업무 데이터와 함께 저장합니다.
데이터베이스에는 requestId에 Unique Constraint를 설정합니다.
첫 번째 요청은 다음과 같이 처리됩니다.
1. requestId 확인
2. 동일한 requestId가 존재하지 않음
3. 주문 생성
4. requestId 저장
5. 처리 결과 반환
클라이언트가 동일한 requestId로 요청을 다시 보내면 새로운 주문을 생성하지 않습니다.
1. requestId 확인
2. 동일한 requestId가 이미 존재함
3. 새로운 주문을 생성하지 않음
4. 기존 처리 결과 반환
전체 흐름은 다음과 같습니다.
첫 번째 요청
→ 주문 생성
→ requestId 저장
→ 응답 유실
같은 requestId로 재시도
→ 기존 요청 확인
→ 중복 주문 생성 방지
→ 기존 결과 반환
이를 통해 클라이언트가 성공 여부를 알 수 없는 요청을 재시도하더라도 동일한 업무가 중복 처리되는 것을 막을 수 있습니다.
다만 단순히 requestId를 저장하는 것만으로 충분하지 않을 수 있습니다.
다음 사항도 함께 정의해야 합니다.
- requestId의 유효 범위
- 동일한 requestId에 다른 요청 내용이 전달된 경우의 처리
- 기존 처리 결과의 보관 기간
- 처리 중인 요청이 다시 들어온 경우의 처리
- 실패한 요청의 재시도 가능 여부
- 외부 결제 시스템에도 동일한 멱등성 키를 전달할지 여부
14. Replication과 멱등성은 서로 다른 문제를 해결한다
Replication과 멱등성은 장애 상황에서 함께 언급되지만 서로 다른 문제를 해결합니다.
Replication은 Primary의 변경을 Secondary로 전달하여 데이터 사본을 유지하고, Primary 장애 시 다른 노드가 서비스를 이어받을 수 있도록 합니다.
Replication
→ 장애 이후 데이터를 얼마나 보존할 것인가
→ 어떤 Secondary가 Primary를 대신할 것인가
멱등성은 클라이언트가 요청 결과를 알 수 없을 때 같은 요청을 안전하게 다시 실행할 수 있도록 합니다.
멱등성
→ 결과를 알 수 없는 요청을 어떻게 재시도할 것인가
→ 중복 업무 처리를 어떻게 방지할 것인가
Replication만으로는 중복 요청을 막을 수 없습니다.
Primary에서 요청이 한 번 처리되었더라도 클라이언트가 응답을 받지 못하면 동일한 요청을 다시 보낼 수 있기 때문입니다.
반대로 멱등성만으로 Primary 장애에 따른 데이터 유실을 방지할 수도 없습니다.
멱등성 키가 저장된 변경 자체가 Secondary에 전달되지 않은 상태에서 Primary가 손실되면 New Primary에 해당 기록이 존재하지 않을 수 있습니다.
따라서 장애 상황에서 데이터 누락과 중복을 줄이려면 Replication과 멱등성을 함께 설계해야 합니다.
15. 어떤 Replication 방식을 선택해야 하는가
동기 Replication과 비동기 Replication 중 어느 하나가 항상 더 좋은 방식은 아닙니다.
서비스가 요구하는 쓰기 응답 시간, 데이터 보존 수준, 장애 대응 방식에 따라 선택해야 합니다.
비동기 Replication은 다음과 같은 경우에 적합할 수 있습니다.
- 쓰기 응답 시간이 중요합니다.
- 짧은 데이터 반영 지연을 허용할 수 있습니다.
- 장애 발생 시 일부 데이터 유실 가능성을 업무적으로 처리할 수 있습니다.
- 이벤트 재처리 또는 외부 시스템과의 대조가 가능합니다.
동기 Replication은 다음과 같은 경우에 적합할 수 있습니다.
- Primary에만 변경이 존재하는 상황을 줄여야 합니다.
- 쓰기 응답 시간 증가를 감수할 수 있습니다.
- 원격 Secondary의 상태를 안정적으로 유지할 수 있습니다.
- 데이터 종류에 따라 더 높은 데이터 반영 수준이 필요합니다.
실제 시스템에서는 모든 데이터에 같은 방식을 적용하지 않을 수도 있습니다.
예를 들어 일반 로그나 통계성 데이터는 비동기 Replication으로 처리하고, 결제나 정산처럼 데이터 보존 요구가 높은 업무에는 더 강한 Commit 조건을 적용할 수 있습니다.
그렇기 때문에 중요한 것은 “동기인가, 비동기인가?”만 결정하는 것이 아닌, 다음 항목을 구체적으로 정의해야 합니다.
- 몇 개의 Secondary로부터 확인을 받을 것인가?
- 로그 수신, 저장, 적용 중 어느 단계까지 기다릴 것인가?
- Secondary의 확인을 받을 수 없을 때 쓰기를 중단할 것인가?
- 쓰기 응답 시간 증가를 어느 정도까지 허용할 것인가?
- 장애 시 어느 범위의 데이터 손실을 허용할 것인가?
- 결과가 불확실한 요청을 어떻게 재시도할 것인가?
마무리
비동기 Replication에서는 Primary가 Secondary의 처리를 기다리지 않고 클라이언트에 성공을 반환할 수 있습니다.
이로 인해 쓰기 응답 시간을 줄일 수 있지만, Secondary에서 이전 데이터가 반환되거나 Primary 장애 시 아직 전달되지 않은 변경이 유실될 수 있습니다.
동기 Replication에서는 Primary가 지정된 Secondary의 확인을 기다립니다.
하지만 동기 Replication이라는 이름만으로 데이터 반영 수준을 판단할 수는 없습니다.
Secondary가 로그를 수신한 시점까지 기다리는지, 영구 저장까지 기다리는지, 데이터 적용까지 기다리는지를 구분해야 합니다.
PostgreSQL의 synchronous_commit 설정을 기준으로 정리하면 다음과 같습니다.
remote_write
→ Standby의 운영체제 파일 시스템 기록까지 대기
on
→ Standby의 WAL 영구 저장까지 대기
remote_apply
→ Standby의 WAL Replay까지 대기
비동기 Replication에서는 Primary가 Secondary의 처리를 기다리지 않고 클라이언트에 성공을 반환할 수 있습니다.
따라서 쓰기 응답 시간을 비교적 짧게 유지할 수 있지만, Secondary의 데이터 반영이 완료되기 전에 조회하면 Stale Read가 발생할 수 있습니다.
또한 Primary의 변경이 Secondary에 전달되기 전에 장애가 발생하면 클라이언트가 성공 응답을 받은 쓰기가 Failover 이후 사라질 수도 있습니다.
반면 동기 Replication에서는 Primary가 설정된 Secondary 처리 단계가 완료될 때까지 성공 응답을 반환하지 않습니다.
이 경우 Replication에 필요한 시간이 사라지는 것이 아니라 클라이언트의 쓰기 대기 시간에 포함됩니다.
그 대신 Primary가 성공을 반환하기 전에 Secondary의 WAL 수신, 영구 저장 또는 데이터 적용을 확인하여 쓰기 성공의 보장 범위를 강화할 수 있습니다.
다만 동기 Replication을 사용하더라도 클라이언트가 요청 결과를 항상 확실하게 알 수 있는 것은 아닙니다.
Primary가 Commit을 완료한 직후 성공 응답을 보내기 전에 연결이 끊어지면, 클라이언트는 요청이 처리되었는지 판단할 수 없습니다.
이 상태에서 같은 요청을 다시 보내면 주문이나 결제가 중복으로 처리될 수 있습니다.
따라서 안정적인 쓰기 처리를 위해서는 다음 두 문제를 함께 해결해야 합니다.
Replication
→ 성공한 변경을 장애 이후 얼마나 보존할 것인가
멱등성
→ 성공 여부를 알 수 없는 요청을 어떻게 안전하게 재시도할 것인가
이번 글에서는 동기·비동기 Replication의 차이와 쓰기 성공의 의미, RPO와 RTO, 멱등성을 살펴보았습니다.
하지만 Primary가 실제로 장애를 일으키면 데이터 보존 여부를 판단하는 것만으로는 부족합니다.
여러 Secondary 중 하나를 새로운 Primary로 선택하고, 기존 Primary가 다시 쓰기를 처리하지 못하도록 차단해야 합니다.
다음 글에서는 Primary 장애 시 Secondary Promotion이 어떻게 수행되는지, 그리고 Split-Brain을 방지하기 위해 Fencing이 왜 필요한지 살펴보겠습니다.