2.4 synchronous replication 모델
pg_auto_failover는 기본적으로 synchronous replication을 씁니다. commit은 standby가 WAL을 받았다고 응답해야 완료됩니다. 따라서 failover가 발생해도 새 primary에 확정된 트랜잭션이 모두 있다고 보장할 수 있습니다. 문제는 standby가 사라졌을 때입니다. 동기 복제를 그대로 두면 모든 commit이 멈춥니다. number_sync_standbys가 0인 구성(2노드 기본값)에서는 pg_auto_failover가 primary를 wait_primary 상태로 바꿔 동기 복제를 끕니다. 대신 자동 failover를 막습니다. 이 장에서는 그 전환 규칙을 다룹니다.
primary와 wait_primary
두 상태 모두 쓰기를 받습니다. 다만 monitor가 primary를 자동으로 wait_primary로 전환해 동기 복제를 끄는 것은 number_sync_standbys가 0일 때입니다. 1 이상이면 quorum standby가 부족해도 primary를 유지하고 commit을 대기시킵니다. 두 상태는 synchronous_standby_names 값과 failover 가능 여부가 다릅니다. 문서는 primary를 "failover할 수 있다고 아는 유일한 상태"라고 표현합니다. wait_primary에서 기록한 트랜잭션은 standby에 없을 수 있습니다. 따라서 이 상태의 primary를 잃으면 monitor는 standby를 자동 승격하지 않습니다(7.5).
synchronous_standby_names 자동 관리
synchronous_standby_names는 monitor가 계산하고 keeper가 적용합니다. 사용자가 직접 바꾸더라도 다음 상태 전이 때 덮어씁니다. v2.2 node_active_protocol.c의 계산 규칙은 다음과 같습니다.
| 조건 | 값 |
|---|---|
| 노드 1대 | '' |
노드 2대, standby가 quorum 참여 중이고 목표 상태가 secondary | ANY 1 (pgautofailover_standby_<id>) |
| 노드 2대, 그 외 | '' |
노드 3대 이상, quorum 참여 standby 0대 또는 primary가 wait_primary | '' |
| 노드 3대 이상, 그 외 | ANY <n> (quorum 참여 standby 목록). n은 number_sync_standbys, 0이면 1 |
number_sync_standbys = 0이어도 ANY 1을 씁니다. 소스 주석은 ANY 0 ()은 의미가 없으므로 보내지 않는다고 설명합니다. v2.2 문서의 multi-standby 장에는 이 경우 '*'를 쓴다고 나와 있습니다. 하지만 v1.5.1 CHANGELOG에는 "Avoid using synchronous standby name wildcard"가 있고 실측값도 ANY 1이었습니다.
primary | node1 | synchronous_standby_names | 'ANY 1 (pgautofailover_standby_2)'
3노드에서는 quorum에 참여하는 모든 standby가 목록에 들어갑니다.
primary | node2 | synchronous_standby_names | 'ANY 1 (pgautofailover_standby_1, pgautofailover_standby_3)'
실측: standby 장애 후 20초
2노드 구성에서 secondary를 docker kill로 멈추고 0.2초마다 INSERT를 실행했습니다.
kill at 05:30:42.555 UTC
node2 reached wait_primary in 20.0s
writer: 86 ok, 0 FAIL, longest gap between ok starts 20.08s (05:30:42.864 -> 05:31:02.948), slowest ok 19877ms at 05:30:42.864
ssn=[]
kill 직후에 시작한 INSERT 하나가 19.9초 동안 commit 응답을 기다렸습니다. 이 시간은 monitor가 standby를 unhealthy로 판정하는 데 걸린 시간(2.3)과 같습니다. monitor가 primary에 wait_primary를 지정하자 keeper가 synchronous_standby_names를 비웠습니다. 대기하던 INSERT는 실패하지 않고 성공으로 끝났습니다. 실패한 쓰기는 0건이지만 애플리케이션은 20초 동안 멈춘 셈입니다.
standby 장애는 "쓰기 실패"가 아니라 "쓰기 지연"으로 나타납니다. 애플리케이션의 statement timeout이나 connection pool timeout이 20초보다 짧으면 commit 대기가 timeout 오류로 바뀝니다. PostgreSQL에서는 동기 복제 대기 중 취소된 트랜잭션도 로컬에는 이미 commit되어 있습니다. 이 점에 주의해야 합니다. 클라이언트는 실패를 받았지만 데이터는 남아 있는 상태가 됩니다.
동기 복제로 돌아가는 조건
standby가 돌아오면 monitor는 먼저 standby를 catchingup으로 둡니다. 조건이 맞으면 secondary로 전환합니다. 이어서 primary를 primary로 되돌려 동기 복제를 다시 켭니다. 관련 GUC는 두 개입니다.
| GUC | 기본값 | 쓰이는 곳 |
|---|---|---|
pgautofailover.enable_sync_wal_log_threshold | 16777216 (16MB) | standby WAL 차이가 이 안이어야 동기 복제를 다시 켬 |
pgautofailover.promote_wal_log_threshold | 16777216 (16MB) | 승격 후보의 WAL 차이가 이 안이어야 승격 허용 |
문서는 동기 복제를 다시 켜는 순간 쓰기가 잠시 막힐 수 있다고 설명합니다. standby가 따라잡을 때까지 지연이 급증하기도 합니다. 16MB 이내에서만 다시 켜는 이유입니다.
비동기로 쓰고 싶을 때
쓰기 지연이 부담이라면 synchronous_commit을 낮춥니다. 문서는 인스턴스 전체가 아니라 역할이나 세션 단위로 바꾸기를 권장합니다.
ALTER ROLE fast_and_loose SET synchronous_commit TO local;
이 역할의 트랜잭션은 standby 응답을 기다리지 않습니다. failover가 일어나면 이 트랜잭션들은 새 primary에 없을 가능성이 있습니다. 모든 노드의 replication quorum을 false로 설정해 비동기 복제로 운영할 수도 있습니다(5.2).
v2.2 architecture 문서에는 synchronous_commit = 'local'일 때 "secondary가 10 WAL segment 넘게 뒤처지면 failover하지 않는다"는 문장이 있습니다. 소스의 승격 조건은 promote_wal_log_threshold(기본 16MB, 1 segment)를 씁니다. 이 노트에서는 소스 기준 값을 따랐습니다. 비동기 설정에서의 승격 거부 조건은 실측하지 않았습니다.
정리
pg_auto_failover는 기본적으로 동기 복제를 씁니다. standby가 사라지면 약 20초 동안 commit이 멈춘 뒤 비동기로 전환됩니다. 비동기인 wait_primary 동안에는 자동 failover가 막힙니다. 서비스 연속성을 위해 데이터 보장 수준을 잠시 낮추는 설계입니다. 비동기 구간 없이 항상 두 벌을 유지하려면 standby를 둘 이상 두어야 합니다. 또한 number_sync_standbys를 1 이상으로 설정해야 합니다. 이러한 보장도 synchronous_commit이 on 이상인 트랜잭션에만 해당합니다(5.1).