본문으로 건너뛰기

2.5 split brain 방지

monitor가 primary를 unhealthy로 판정해도 primary가 실제로 중단되었다고 보장할 수는 없습니다. monitor와의 네트워크만 끊겼을 뿐, primary는 여전히 클라이언트의 쓰기 요청을 받고 있을 수 있습니다. 이때 standby를 바로 승격하면 두 노드가 동시에 쓰기를 받는 split brain이 발생합니다. pg_auto_failover에는 외부 fencing 장치(STONITH)가 없습니다. 대신 monitor는 일정 시간 기다리고, 옛 primary는 스스로 멈춥니다. 이 장에서는 두 방식이 작동하는 조건과 시간을 정리합니다.

이름이 비슷한 세 가지​

이름종류위치기본값
demote_timeout상태 머신의 상태monitor가 옛 primary에 지정하는 목표 상태해당 없음
pgautofailover.primary_demote_timeoutmonitor GUCmonitor가 새 primary에 쓰기를 열어 주기 전 대기 시간30000 ms
timeout.network_partition_timeoutkeeper 설정고립된 primary keeper가 PostgreSQL을 내리기까지의 시간20 s

세 가지는 서로 독립적입니다. demote_timeout 상태는 "이 노드는 강등되어야 하는데 확인을 못 받았다"는 표시입니다. primary_demote_timeout은 이 상태를 지정한 뒤 monitor가 기다리는 시간입니다. network_partition_timeout은 monitor와 무관하게 primary keeper가 독립적으로 측정하는 시간입니다.

monitor 쪽: 기다린 뒤에 연다​

failover 과정에서 standby는 먼저 stop_replication 상태로 승격됩니다. v2.2 keeper의 fsm_stop_replication()은 승격 직전에 default_transaction_read_only를 on으로 바꿉니다. 로그에도 이 의도가 그대로 기록됩니다.

05:08:02 10 INFO FSM transition from "prepare_promotion" to "stop_replication": Prevent against split-brain situations.
05:08:02 10 INFO Prevent writes to the promoted standby while the primary is not demoted yet, by making the service incompatible with target_session_attrs = read-write

승격은 끝났지만 target_session_attrs=read-write로 접속하는 클라이언트는 이 노드를 primary로 선택하지 않습니다. 이는 강제 fencing이 아니라 새 트랜잭션에 적용되는 기본값입니다. 세션이 값을 명시적으로 바꾸면 쓰기가 가능합니다. monitor는 동시에 옛 primary에 demote_timeout을 지정합니다. 다음 두 조건 중 하나가 먼저 충족되면 새 primary를 wait_primary로 전환합니다. 그런 다음 읽기 전용 설정을 해제합니다(group_state_machine.c).

  1. 옛 primary keeper가 demote_timeout에 도달했다고 보고
  2. demote_timeout을 지정한 시각부터 primary_demote_timeout(30초)이 지남

switchover처럼 옛 primary가 살아 있으면 1번 조건이 곧바로 충족됩니다. 따라서 switchover는 수 초 안에 끝납니다. primary 서버가 사라진 경우에는 보고가 오지 않으므로 30초를 모두 기다립니다.

노트

v2.2 문서의 fault-tolerance 장에는 "monitor는 새 primary를 승격하기 전에 90초를 기다린다"는 문장이 있습니다. 그러나 v2.2 소스와 실측 결과는 30초(primary_demote_timeout 기본값)였습니다. 여기서는 90초라는 설명을 오래된 서술로 간주합니다.

keeper 쪽: 혼자 남으면 멈춘다​

primary keeper는 monitor에 연결하지 못할 때마다 is_network_healthy()를 실행합니다(service_keeper.c).

판정 기준은 pg_stat_replication에 standby 연결이 보이는지 여부입니다. standby 쪽 네트워크가 끊겨도 walsender는 wal_sender_timeout(pg_auto_failover가 30초로 설정)이 지나야 사라집니다. 따라서 primary가 스스로 멈추기까지 걸리는 시간에는 walsender가 사라지는 시간과 20초가 포함됩니다.

실측: primary 전체 분리​

3노드 구성에서 primary(node2)를 Docker 네트워크에서 분리했습니다.

disconnect at 05:36:14.284 UTC
05:36:15 11 WARN We lost the monitor, but still have a standby: we're not in a network partition, continuing.
...
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

monitor 쪽 이벤트는 다음과 같습니다.

05:36:34.546|node2|primary|draining|Setting goal state of node 2 "node2" (node2:5432) to draining after it became unhealthy.
05:36:38.534|node2|primary|demote_timeout|Setting goal state of node 2 "node2" (node2:5432) to demote_timeout and node 1 "node1" (node1:5432)
05:37:08.890|node1|stop_replication|wait_primary|Setting goal state of node 1 "node1" (node1:5432) to wait_primary and node 2 "node2" (node2:5432) to

옛 primary는 분리된 지 50초 만에 스스로 멈췄습니다. 새 primary는 54.6초 만에 쓰기를 허용했습니다. 두 사건 사이의 여유는 4.6초였습니다. 50초는 walsender가 사라지는 약 30초와 network_partition_timeout 20초의 합으로 설명할 수 있습니다. 54.6초는 unhealthy 판정 약 20초, 선출 단계 약 4초, primary_demote_timeout 30초의 합입니다.

여유가 사라지는 조건​

기본값에서는 "옛 primary가 멈추는 시각"이 "새 primary가 열리는 시각"보다 빠릅니다. 이 순서는 설정값의 조합에 따라 달라지며, 합의 절차로 보장되지 않습니다. monitor 쪽 두 GUC를 줄이면 새 primary가 먼저 열립니다. 실습에서 node_considered_unhealthy_timeout = 10s, primary_demote_timeout = 15s로 비대칭 분리를 만들었습니다. 그 결과 두 노드가 11.4초 동안 동시에 쓰기를 받았습니다. 옛 primary가 그사이에 받은 93건은 재합류할 때 pg_rewind로 사라졌습니다(7.4).

경고

monitor의 판정 시간(unhealthy timeout과 demote timeout의 합)은 keeper의 자기 강등 시간(walsender 소멸 시간과 network_partition_timeout의 합)보다 충분히 길어야 합니다. failover 시간을 단축하려고 monitor GUC만 줄이면 split brain 구간이 생깁니다. 조정 방법은 7.6에서 다룹니다.

동기 복제는 두 번째 방어 수단입니다. 고립된 primary는 standby 응답을 받을 수 없습니다. 따라서 기본 synchronous_commit = on 세션의 commit은 완료되지 않습니다. 위 실험에서는 synchronous_commit = local 세션으로 쓰기를 시도했습니다. 기본 설정 세션이 고립된 동안 어떻게 응답하는지는 따로 측정하지 않았습니다.

main 브랜치의 보완​

v2.2 이후 main에는 3개 데이터센터 구성에서 발생하는 문제가 수정되어 있습니다. 이 문제는 primary와 standby 사이의 연결만 끊기고, 두 노드는 모두 monitor와 통신할 때 발생합니다. 이 경우 primary는 standby의 부재를 감지하지 못합니다. monitor도 primary를 정상으로 판단하므로 commit이 무한히 대기합니다. main의 pgautofailover.replication_stall_timeout(기본 10초)은 이때 wait_primary를 지정합니다(#1149). v2.2에는 이 설정이 없습니다.

정리​

pg_auto_failover의 split brain 방지는 두 개의 시계에 의존합니다. monitor는 옛 primary의 응답을 최대 30초 동안 기다립니다. 옛 primary는 monitor와 standby를 모두 잃은 뒤 20초가 지나면 스스로 멈춥니다. 기본값을 사용한 이 실습에서는 두 시계가 몇 초 차이로 올바른 순서를 지켰습니다. 그러나 이 순서는 보장되지 않습니다. 자기 fencing은 keeper가 살아 있을 때만 작동합니다. 현재 상태가 정확히 primary여야 하며, monitor와 모든 replica 연결도 끊겨야 합니다. replica 하나라도 pg_stat_replication에 남아 있으면 접촉 시각이 계속 갱신됩니다. 반면 monitor는 옛 primary의 종료를 확인하지 않습니다. primary_demote_timeout이 만료되면 새 primary의 쓰기를 허용합니다. 다중 standby의 비대칭 분리나 keeper 정지 상황에서는 설정값의 크기 관계만으로 split brain을 막을 수 없습니다. 필요하면 외부 fencing을 추가해야 합니다. Patroni는 이 문제를 DCS leader key의 TTL과 watchdog으로 처리합니다.