7.4 네트워크 분리
프로세스가 죽는 장애와 달리 네트워크 분리에서는 primary가 살아 있습니다. monitor에서 failover가 진행되는 동안 고립된 primary가 쓰기를 계속 받으면 두 노드가 동시에 primary가 됩니다. pg_auto_failover는 두 가지 장치로 이를 막습니다. 하나는 고립된 primary의 keeper가 스스로 멈추는 장치입니다. 다른 하나는 monitor가 새 primary를 열기 전에 기다리는 장치입니다. 두 가지 방식으로 분리한 뒤 이 순서를 측정했습니다.
실험 A: primary 완전 고립
3노드(node2 primary)에서 docker network disconnect로 node2를 네트워크에서 분리했습니다. monitor, standby, 클라이언트 모두 node2에 접속할 수 없는 상태입니다.
| 구간 | 시각 차 | 측정값 |
|---|---|---|
| 분리 → 고립 primary의 demote_timeout 전환 | 05:36:14.284 → 05:37:04 | 약 50초 |
| 분리 → monitor의 wait_primary 지정 | 05:36:14.284 → 05:37:08.890 | 54.6초 |
| 쓰기 공백 (writer.sh) | 05:36:14.223 → 05:37:09.137 | 54.9초 |
고립된 node2의 keeper 로그는 두 구간으로 나뉩니다. 처음 약 28초(05:36:15 ~ 05:36:42) 동안은 standby가 아직 연결되어 있다고 판단해 서비스를 계속합니다.
05:36:15 11 WARN Checking for network partitions...
05:36:15 11 WARN We lost the monitor, but still have a standby: we're not in a network partition, continuing.
그 뒤에는 이 줄이 사라지고, 20초 후 스스로 내려옵니다.
05:37:04 11 INFO Network in not healthy, switching to state demote_timeout
05:37:04 11 INFO FSM transition from "primary" to "demote_timeout": A failover occurred, no longer primary
keeper는 pg_stat_replication에 복제 사용자의 연결이 보이면 standby가 있다고 판단합니다. 네트워크가 끊겨도 walsender는 즉시 사라지지 않습니다. PostgreSQL의 wal_sender_timeout(pg_auto_failover 설정값 30s)이 지나야 연결이 끊어집니다. 처음 28초는 이 대기 시간에 해당하는 것으로 보입니다. 로그만으로 walsender 종료 시각을 직접 확인하지는 않았습니다. 따라서 이 부분은 추론입니다. 이후 keeper의 network_partition_timeout(기본 20초)이 지나 05:37:04에 전환했습니다. 결과적으로 고립 primary는 약 50초 후 내려왔습니다. monitor는 54.6초 후 새 primary의 쓰기를 허용했습니다.
분리 후 40초 동안 node2 내부에서 로컬 소켓으로 pg_is_in_recovery()를 확인한 결과는 매번 f였습니다. 그동안 node2가 여전히 primary였다는 뜻입니다.
재연결하자 node2는 6.1초 만에 secondary로 돌아왔습니다. 분리 중 생긴 WAL 차이 때문에 pg_rewind가 실제로 되감았습니다.
05:37:58 11 INFO pg_rewind: servers diverged at WAL location 0/9026A30 on timeline 6
05:37:59 11 INFO pg_rewind: need to copy 180 MB (total source directory size is 206 MB)
05:38:01 11 INFO pg_rewind: Done!
실험 B: 비대칭 분리, 클라이언트는 옛 primary에 접속 가능
실제 장애에서는 primary가 monitor와 standby에만 연결되지 않고 애플리케이션과는 통신할 수 있는 경우가 흔합니다. 2노드 구성(node2 primary, node1 secondary)에서 partition.sh로 primary 내부에 blackhole route를 추가했습니다. monitor와 standby IP만 차단했습니다. app 컨테이너는 계속 옛 primary에 접속할 수 있습니다.
./scripts/partition.sh on node2 172.28.0.10 172.28.0.11 # monitor, node1 차단
docker compose exec -T app bash /lab/scripts/dualcheck.sh 85 > dual.log
dualcheck.sh는 0.5초마다 두 노드에 직접 접속해 synchronous_commit = local INSERT를 시도합니다. 성공하면 W를 기록합니다.
| 항목 | 시각 | 분리 기준 |
|---|---|---|
| 분리 | 06:07:17.200 | 0초 |
| monitor의 node2 unhealthy 판정 | 06:07:40.520 | +23.3초 |
| monitor의 wait_primary 지정 | 06:08:11.086 | +53.9초 |
| 옛 primary node2의 마지막 W | 06:08:08.757 | +51.6초 |
| 새 primary node1의 첫 W | 06:08:11.453 | +54.3초 |
| 두 노드가 같은 표본에서 모두 W | 없음 | 0건 |
옛 primary는 51.6초까지 쓰기를 받았고, 새 primary는 54.3초부터 쓰기를 받았습니다. 두 구간은 겹치지 않았지만 여유는 2.7초였습니다. 옛 primary의 keeper는 이번에도 "50 seconds" 후 내려왔습니다.
06:08:08 11 INFO Failed to contact the monitor or standby in 50 seconds, at 20 seconds we shut down PostgreSQL to prevent split brain issues
06:08:08 11 INFO Network in not healthy, switching to state demote_timeout
실험 A보다 monitor의 unhealthy 판정이 늦었습니다(+10.5초 대 +23.3초). 컨테이너를 분리한 경우와 달리 blackhole은 연결 요청에 응답하지 않습니다. 따라서 health check가 health_check_timeout(5초)까지 기다렸습니다. 재시도까지 하면서 BAD 판정이 늦어진 것으로 보입니다.
옛 primary가 분리 후 받은 비동기 쓰기 95건(dualcheck.sh의 W 표본)은 재합류할 때 pg_rewind로 사라졌습니다. 이 실험은 synchronous_commit = local로만 측정했습니다. 기본값이 on이었다면 해당 커밋들은 standby 확인을 기다리며 멈췄을 것입니다. 클라이언트는 성공 응답을 받지 못했을 것입니다. 이는 동기 복제 설계에 따른 설명입니다. 이 노트에서 별도로 측정하지는 않았습니다. synchronous_commit을 세션이나 role 단위로 낮춰 사용하는 애플리케이션에서는 분리 구간의 쓰기가 성공 응답 후에도 사라집니다.
무효로 처리한 시도
실험 B 전에 두 번 실패했습니다. 첫 시도에서는 dualcheck 스크립트를 tmpfs인 컨테이너 /tmp에 docker cp로 넣었습니다. 그러나 파일이 없어 측정하지 못했습니다. 이때 사용한 별도 클라이언트 네트워크 방식에서는 node1이 unhealthy로 표시된 지 10초 만에 다시 healthy가 되었습니다. 따라서 failover가 일어나지 않았습니다. 네트워크 간 도달 경로가 불분명해 방식을 blackhole route로 바꿨습니다. 두 번째 시도에서는 partition.sh에 실행 권한이 없어 분리가 적용되지 않았습니다. 두 결과는 수치에서 제외했습니다.
순서가 안전을 만든다
옛 primary의 강등(D)이 새 primary의 쓰기 허용(G)보다 먼저 일어나야 동시 쓰기가 발생하지 않습니다. 기본값에서는 두 실험 모두 이 순서를 지켰고 여유는 2.7~4.9초였습니다. v2.2 fault-tolerance.rst는 monitor가 새 primary 승격 전에 90초를 기다린다고 설명합니다. 그러나 실제 대기는 primary_demote_timeout 기본값인 30초였습니다. 이 순서를 깨는 설정은 7.6에서 확인합니다.
정리
고립된 primary는 standby 연결이 사라진 뒤 network_partition_timeout 20초가 지나면 스스로 내려옵니다. 이 실습에서는 분리 후 약 50초가 걸렸습니다. monitor는 그보다 늦은 54초 무렵에 새 primary의 쓰기를 허용했습니다. 기본값에서 확인한 여유는 몇 초에 불과했습니다. 그사이 비동기로 커밋된 쓰기는 옛 primary와 함께 사라졌습니다. 이 순서는 두 실험에서 관찰된 결과일 뿐 v2.2가 보장하는 불변식은 아닙니다. monitor의 primary_demote_timeout과 keeper의 timeout 사이 관계를 검사하는 코드는 없습니다.