TT Lab
블로그 시작하기
배우기 러닝패스 코스

PostgreSQL 고가용성: 서버 세 대로 하는 active-standby

분할, 두 주 서버, 펜싱

TT Lab 에서 이어서 보기

목표

두 서버 사이만 끊긴 상황에서 승격해 두 주 서버(split-brain)를 직접 만들고, 펜싱과 꺼내기·되감기·다시 넣기로 정리합니다.

왜 중요한가

'주 서버에 닿지 않으면 승격한다' 는 규칙은 주 서버가 정말 죽었을 때만 맞습니다. 선만 끊긴 것이라면 두 서버가 각자 쓰기를 받고, 그 둘은 합쳐지지 않습니다. 이 장면을 한 번 만들어 보면 HA 도구가 왜 다수결과 펜싱에 그렇게 집착하는지 이해하게 됩니다. 그리고 사고가 난 뒤의 순서(막기, 꺼내기, 되감기, 다시 넣기)를 손으로 한 번 해 두면, 실제로 그날이 왔을 때 되감기부터 눌러 주문을 날리는 실수를 하지 않습니다.

단계

  1. witness 에서 앱 계정으로 db1 에 insert into orders(note) values ('before') 를 하고, 그 행이 db2 에 보이는지 확인하세요.
  2. db1 에서 iptables 로 db2(172.30.1.3)와 오가는 패킷만 버리세요(들어오는 것과 나가는 것 둘 다). witness 는 여전히 두 서버에 닿아야 합니다. 15초쯤 지나 db2 의 PostgreSQL 로그에 walreceiver 가 타임아웃으로 끊겼다는 줄이 남아야 합니다.
  3. 단순한 자동화 스크립트처럼 판단합니다. db2 에서 db1 에 닿지 않으니 db2 를 승격하세요(select pg_promote()). db1 은 그대로 둡니다.
  4. witness 에서 앱 계정으로 db1 에 ('split-db1'), db2 에 ('split-db2') 주문을 하나씩 넣으세요. 두 서버 모두 쓰기를 받습니다. 각 서버에서 두 주문이 어떻게 보이는지 확인합니다.
  5. db1 이 더는 앱의 쓰기를 받지 못하게 네트워크로 막으세요. db1 에서 eth1 으로 들어오는 5432 포트 연결을 거절(REJECT)합니다. witness 에서 pg_isready -h db1 이 실패해야 합니다. ssh(22)는 막지 않습니다.
  6. 갈라진 뒤 db1 에만 들어간 주문을 /root/ha/lost.csv 로 꺼내세요. 열은 note,by_user,at 이고 첫 줄은 머리글입니다. 'before' 주문 뒤에 db1 에 들어온 행이 그 대상입니다.
  7. db1 을 멈추고, 2단계의 분할 규칙만 걷으세요(펜싱 규칙은 남깁니다). db2 에 슬롯 db1_slot 을 만들고, db1 을 pg_rewind 로 db2 에 맞춰 되감은 뒤(출력은 /root/ha/07-rewind.txt), host=db2·application_name=db1·슬롯 db1_slot 으로 db2 의 대기 서버로 띄우세요.
  8. /root/ha/lost.csv 의 주문을 db2 에 한 번만 다시 넣으세요(id 는 새로 받습니다). db2 에 before·split-db1·split-db2 가 하나씩 있고, 대기 서버 db1 에도 같은 것이 보여야 합니다.

참고

평소처럼 쓰고 복제를 확인한다

witness 에서 앱 계정으로 db1 에 insert into orders(note) values ('before') 를 하고, 그 행이 db2 에 보이는지 확인하세요.

psql -h db1 -U app -d appdb -c "insert ...". db2 에서는 ssh db2 로 들어가 sudo -iu postgres psql -d appdb 로 봅니다.

db1 과 db2 사이만 끊는다

db1 에서 iptables 로 db2(172.30.1.3)와 오가는 패킷만 버리세요(들어오는 것과 나가는 것 둘 다). witness 는 여전히 두 서버에 닿아야 합니다. 15초쯤 지나 db2 의 PostgreSQL 로그에 walreceiver 가 타임아웃으로 끊겼다는 줄이 남아야 합니다.

iptables -A INPUT -s 172.30.1.3 -j DROP 과 iptables -A OUTPUT -d 172.30.1.3 -j DROP 를 db1 에서 실행합니다. 이 실습은 wal_receiver_timeout 을 10초로 두었습니다. db2 의 로그는 /var/log/postgresql/postgresql-16-main.log 입니다. db1 을 멈추면 분할이 아니라 장애라서 로그의 말이 다릅니다.

db2 에서 보면 db1 은 죽었다

단순한 자동화 스크립트처럼 판단합니다. db2 에서 db1 에 닿지 않으니 db2 를 승격하세요(select pg_promote()). db1 은 그대로 둡니다.

db2 는 db1 이 죽었는지 선만 끊겼는지 알 방법이 없습니다. 승격하면 db2 는 타임라인 2 로 갈라집니다. 이 단계가 잘못된 판단이라는 것은 다음 단계에서 드러납니다.

두 주 서버가 각자 쓴다

witness 에서 앱 계정으로 db1 에 ('split-db1'), db2 에 ('split-db2') 주문을 하나씩 넣으세요. 두 서버 모두 쓰기를 받습니다. 각 서버에서 두 주문이 어떻게 보이는지 확인합니다.

두 서버 모두 pg_is_in_recovery() 가 f 입니다. 서로의 주문은 보이지 않습니다. 선이 다시 이어져도 이 둘은 저절로 합쳐지지 않습니다.

db1 을 펜싱한다

db1 이 더는 앱의 쓰기를 받지 못하게 네트워크로 막으세요. db1 에서 eth1 으로 들어오는 5432 포트 연결을 거절(REJECT)합니다. witness 에서 pg_isready -h db1 이 실패해야 합니다. ssh(22)는 막지 않습니다.

iptables -I INPUT -i eth1 -p tcp --dport 5432 -j REJECT. DROP 이 아니라 REJECT 로 두면 앱은 기다리지 않고 바로 실패합니다. db1 안에서는 로컬 소켓으로 계속 접속할 수 있어서 다음 단계의 꺼내기가 가능합니다.

되감기 전에 db1 에만 있는 주문을 꺼낸다

갈라진 뒤 db1 에만 들어간 주문을 /root/ha/lost.csv 로 꺼내세요. 열은 note,by_user,at 이고 첫 줄은 머리글입니다. 'before' 주문 뒤에 db1 에 들어온 행이 그 대상입니다.

펜싱 때문에 witness 에서는 db1 에 닿지 않습니다. db1 안에서 로컬로 읽어 표준 출력으로 내보내면 됩니다: ssh db1 "sudo -iu postgres psql -d appdb -XAc \"copy (select ...) to stdout with (format csv, header)\"" > /root/ha/lost.csv. id 는 꺼내지 않습니다. 다시 넣을 때 새로 받습니다.

db1 을 되감아 db2 의 대기 서버로

db1 을 멈추고, 2단계의 분할 규칙만 걷으세요(펜싱 규칙은 남깁니다). db2 에 슬롯 db1_slot 을 만들고, db1 을 pg_rewind 로 db2 에 맞춰 되감은 뒤(출력은 /root/ha/07-rewind.txt), host=db2·application_name=db1·슬롯 db1_slot 으로 db2 의 대기 서버로 띄우세요.

되감으려면 db1 이 db2 에 닿아야 해서 분할 규칙을 먼저 지웁니다(iptables -D ..., 넣을 때와 같은 꼴). 이 실습은 wal_log_hints 와 복제 계정의 되감기 권한을 미리 갖춰 두었습니다. 대기 서버 설정은 conf.d/20-standby.conf 와 standby.signal 입니다.

꺼낸 주문을 새 주 서버에 다시 넣는다

/root/ha/lost.csv 의 주문을 db2 에 한 번만 다시 넣으세요(id 는 새로 받습니다). db2 에 before·split-db1·split-db2 가 하나씩 있고, 대기 서버 db1 에도 같은 것이 보여야 합니다.

witness 에서 psql -h db2 -U app -d appdb -c "\copy orders(note, by_user, at) from '/root/ha/lost.csv' with (format csv, header)". \copy 는 파일을 클라이언트(witness)에서 읽습니다. 두 번 넣으면 같은 주문이 둘이 됩니다. 넣기 전에 이미 있는지 확인하세요.