본문으로 건너뛰기

7.2 secondary 장애

pg_auto_failover는 기본적으로 synchronous replication을 사용합니다. 2노드에서 secondary가 중단되면 primary의 커밋도 멈춥니다. standby의 확인을 기다리기 때문입니다. monitor가 이 대기를 해제하는 데 걸리는 시간을 측정했습니다. 그동안 애플리케이션에 나타나는 현상도 확인했습니다.

2노드: 타임라인​

run2에서 node2가 primary이고 node1이 secondary일 때 node1을 kill했습니다.

항목측정값
kill → node2 wait_primary 도달 (wait-state.sh)20.0초
05:30:42.864에 시작한 INSERT 소요19,877ms
실패한 INSERT0건
쓰기 공백 (05:30:42.864 → 05:31:02.948)20.08초

run1도 같은 결과였습니다. kill 05:11:29.907 이후 wait_primary까지 20.2초가 걸렸습니다. 05:11:30.275에 시작한 INSERT는 20,370ms 만에 성공했습니다.

INSERT는 실패하지 않았습니다. 대기하다가 20초 뒤에 성공했습니다. primary에서는 트랜잭션이 이미 로컬에 커밋된 상태였습니다. PostgreSQL은 synchronous_standby_names에 맞는 standby의 확인을 기다립니다. keeper가 이 값을 비우자 대기 중이던 커밋이 재개됐습니다.

05:31:02 11 INFO Monitor assigned new state "wait_primary"
05:31:02 11 INFO FSM transition from "primary" to "wait_primary": Secondary became unhealthy
05:31:02 11 INFO Disabling synchronous replication
05:31:02 11 INFO Transition complete: current state is now "wait_primary"

전환 직후 primary의 설정을 확인하면 빈 문자열이 표시됩니다.

docker compose exec -T node2 psql -d appdb -Atc \
"select 'ssn=['||current_setting('synchronous_standby_names')||']'"
ssn=[]

20초는 어디서 오는가​

primary 장애와 같은 판정 규칙을 적용합니다. monitor는 health check 실패를 확인합니다. 또한 keeper의 마지막 보고가 node_considered_unhealthy_timeout(20초)보다 오래되기를 기다립니다. run1의 event에서 unhealthy 표시는 kill 후 7.8초(05:11:37.749)에 기록됐습니다. 그러나 catchingup과 wait_primary 지정은 20.7초(05:11:50.616)에 나왔습니다.

05:11:37.749 | 0/2 | secondary | secondary | Node node 2 "node2" (node2:5432) is marked as unhealthy by the monitor
05:11:50.616 | 0/2 | secondary | catchingup | Setting goal state of node 2 "node2" (node2:5432) to catchingup after it became unhealthy.
05:11:50.616 | 0/1 | primary | wait_primary | Setting goal state of node 1 "node1" (node1:5432) to wait_primary because none of the standby nodes in the quorum are healthy at the moment.

secondary에는 primary_demote_timeout과 같은 추가 대기가 없습니다. 따라서 primary 장애보다 절반 이하의 시간에 끝납니다.

경고

20초 동안 쓰기 요청은 실패하지 않고 대기합니다. 애플리케이션의 statement timeout이나 커넥션 풀 timeout이 20초보다 짧으면 이 구간에서 오류가 발생합니다. 반대로 timeout이 없으면 요청 스레드가 20초씩 쌓입니다. 쓰기 경로의 timeout은 이 대기 시간을 고려해 정해야 합니다.

wait_primary가 의미하는 것​

wait_primary 상태의 primary는 standby 확인 없이 커밋합니다. 데이터는 primary 한 곳에만 존재합니다. monitor는 이 상태의 secondary를 catchingup으로 둡니다. 따라서 secondary는 승격 후보에서 제외됩니다. 이때 primary까지 잃으면 자동 failover가 일어나지 않습니다. 이 동작은 7.5에서 직접 확인했습니다.

복귀​

node1을 다시 시작하자 2.0초 만에 secondary로 돌아왔습니다. node2는 곧 primary로 바뀌고 동기 복제를 다시 활성화했습니다.

node1 reached secondary in 2.0s
node2 reached primary in 0.2s
ssn=[ANY 1 (pgautofailover_standby_1)]

문서에 따르면 standby가 primary WAL과 일정 범위 안에 있어야 합니다. 그 범위는 pgautofailover.enable_sync_wal_log_threshold(기본 16 MB)입니다. 이 조건을 충족하면 동기 복제를 다시 활성화합니다. 문서에서는 이 순간 쓰기 지연이 잠시 늘어날 수 있다고 설명합니다. 이 실습에서는 데이터가 작아 차이가 드러나지 않았습니다.

3노드에서는​

3노드(standby 2대, number_sync_standbys = 1)에서 synchronous_standby_names는 'ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3)' 형태입니다. v2.2 문서(architecture-multi-standby.rst)에서는 다음과 같이 설명합니다.

  • standby 1대 손실: 남은 standby가 확인을 보내므로 쓰기 지속 및 두 벌의 데이터 유지
  • standby 2대 모두 손실: quorum을 충족하지 못해 쓰기 차단. 서비스가 사실상 읽기 전용으로 전환
  • number_sync_standbys를 0으로 직접 낮춘 경우: 2대 모두 손실 시 wait_primary로 전환해 쓰기 허용

이 노트에서는 3노드의 secondary kill을 측정하지 않았습니다. 대신 3노드에서 secondary 하나를 maintenance로 전환한 실험이 있습니다. 이때 primary는 primary 상태를 유지했습니다. synchronous_standby_names도 바뀌지 않았습니다(6.3).

노트

2노드와 3노드의 차이는 "standby 하나를 잃었을 때 동기 복제를 포기하는가"입니다. 2노드는 동기 복제를 포기하고 쓰기를 허용합니다. 3노드 기본 구성은 남은 standby로 동기 복제를 유지합니다. 장애 조합별 보장은 5.4에 표로 정리했습니다.

정리​

2노드에서 secondary를 잃으면 진행 중인 커밋이 약 20초 동안 멈춥니다. 이후 커밋은 성공합니다. 이 20초는 monitor가 standby의 보고 중단을 확인하는 시간입니다. 그 뒤 primary는 wait_primary로 바뀌어 비동기 방식으로 씁니다. secondary는 승격 후보에서 제외됩니다. 쓰기는 계속할 수 있지만 데이터는 한 벌만 남습니다.