본문으로 건너뛰기
1.2 PostgreSQL 고가용성이라는 문제

1.2 PostgreSQL 고가용성이라는 문제

streaming replication은 데이터를 여러 노드에 실시간으로 복제해 준다. 그러나 고가용성은 데이터 사본이 있다는 것만으로 완성되지 않는다. primary가 죽었을 때 서비스가 이어지려면 누군가 장애를 감지하고, 승격할 replica를 고르고, 승격을 실행하고, 나머지 replica와 애플리케이션 연결을 새 primary로 돌려야 한다. PostgreSQL은 이 중 어느 것도 스스로 하지 않는다.

replication이 해 주는 것, 해 주지 않는 것

streaming replication에서 replica는 primary가 보내는 WAL을 받아 실시간으로 적용한다. 승격 수단도 준비되어 있다. pg_ctl promote를 실행하거나 pg_promote() 함수를 호출하면 replica는 그 자리에서 쓰기가 가능한 primary가 된다.

없는 것은 판단이다. 언제 승격할지, 여러 replica 중 어느 것을 승격할지, 승격 뒤 남은 노드들을 어떻게 새 primary에 붙일지 결정하는 주체가 PostgreSQL 안에는 없다. 복제는 메커니즘이고 failover는 정책인데, PostgreSQL은 메커니즘까지만 제공한다.

수동 failover의 함정

판단을 사람이 맡는 구성을 생각해 보자. 모니터링이 primary 이상을 알리면 DBA가 접속해 상황을 확인하고 승격을 실행한다.

첫 번째 함정은 지연이다. 알림이 도착하고, 사람이 알림을 보고, 서버에 들어가 상황을 파악하고, 승격을 결정하는 각 단계의 시간이 전부 다운타임에 더해진다. 자동화된 감지가 초 단위로 끝낼 일을 사람은 분에서 시간 단위로 처리한다.

두 번째 함정은 오판이다. primary에 접속이 안 된다는 사실이 primary가 죽었다는 뜻은 아니다. 모니터링 서버와 primary 사이 네트워크만 끊겼을 뿐, primary는 애플리케이션의 쓰기를 정상으로 받는 중일지도 모른다. 이 상태에서 replica를 승격하면 primary가 둘이 된다.

split brain

쓰기를 받는 primary가 클러스터에 둘 존재하는 상황을 split brain이라고 부른다. 만들어지는 경로는 단순하다.

    flowchart TD
  A["감시자와 primary 사이<br/>네트워크 단절"] --> B["감시자가 primary<br/>장애로 오판"]
  B --> C["replica를 승격"]
  A --> D["기존 primary는<br/>정상 동작 중"]
  C --> E["새 primary가<br/>쓰기 수신"]
  D --> F["기존 primary도<br/>쓰기 수신"]
  E --> G["데이터가 두 갈래로<br/>분기"]
  F --> G
  

두 primary가 각자 다른 트랜잭션을 커밋하는 순간부터 데이터는 두 갈래로 갈라진다. 갈라진 갈래를 자동으로 병합하는 방법은 없다. 한쪽 갈래의 커밋을 버리거나, 사람이 애플리케이션 수준에서 두 데이터를 대조해 맞춰야 한다. 다운타임은 복구하면 끝나지만 split brain은 데이터 정합성 자체를 훼손한다. 그래서 HA 도구들은 빠른 failover보다 잘못된 failover를 막는 데에 공을 들인다.

split brain은 극적인 장애가 아니라 평범한 네트워크 단절과 성급한 승격의 조합에서 나온다. failover를 스크립트로만 자동화하고 합의 장치 없이 운영하는 구성이 특히 위험하다. 승격 스크립트가 실행되는 순간에도 기존 primary가 살아 있을지 모른다는 가정을 항상 유지해야 한다.

합의를 바깥에 두는 접근

split brain의 뿌리는 각 노드가 자기 관점만으로 전체 상태를 판단하는 데 있다. 노드 A가 노드 B에 닿지 못할 때, 그것이 B의 장애인지 A 자신의 고립인지 A 혼자서는 구분할 수 없다. 그래서 Patroni는 “누가 leader인가"라는 질문의 답을 개별 노드가 아닌 외부 합의 저장소, DCS(Distributed Configuration Store)에 둔다.

  • leader key는 DCS 안에 하나만 존재하고, primary의 Patroni가 TTL 주기로 갱신한다
  • replica의 Patroni는 leader key가 유효한지 감시한다
  • primary가 갱신을 멈춰 key가 만료되면 replica들이 leader race를 벌이고, 이긴 하나만 승격한다
  • primary 쪽 Patroni가 DCS에 닿지 못해 leader key가 만료되면 스스로 demote해서, 고립된 옛 primary가 쓰기를 계속 받는 상황을 차단한다 (failsafe mode를 켜면 이 동작이 달라진다)

DCS 자체가 etcd, ZooKeeper 같은 quorum 기반 분산 저장소이므로, leader에 대한 판단이 노드 과반의 합의 위에서 내려진다. HA 루프의 주기, leader race 규칙, watchdog을 통한 fencing까지 포함한 전체 방어선은 Part II 아키텍처, 그중에서도 split brain 방지에서 따로 다룬다.

정리

PostgreSQL은 복제와 승격 수단까지만 제공하고, 감지와 판단과 실행은 바깥의 몫으로 남긴다. 그 판단을 사람이나 개별 노드가 맡으면 지연과 오판, 최악의 경우 split brain으로 이어진다. Patroni는 이 판단을 DCS라는 합의 저장소에 맡겨, leader가 언제나 과반 합의 위에서 결정되도록 만든 도구다.