PostgreSQL 고가용성: 서버 세 대로 하는 active-standby
분할, 두 주 서버, 펜싱
목표
두 서버 사이만 끊긴 상황에서 승격해 두 주 서버(split-brain)를 직접 만들고, 펜싱과 꺼내기·되감기·다시 넣기로 정리합니다.
왜 중요한가
'주 서버에 닿지 않으면 승격한다' 는 규칙은 주 서버가 정말 죽었을 때만 맞습니다. 선만 끊긴 것이라면 두 서버가 각자 쓰기를 받고, 그 둘은 합쳐지지 않습니다. 이 장면을 한 번 만들어 보면 HA 도구가 왜 다수결과 펜싱에 그렇게 집착하는지 이해하게 됩니다. 그리고 사고가 난 뒤의 순서(막기, 꺼내기, 되감기, 다시 넣기)를 손으로 한 번 해 두면, 실제로 그날이 왔을 때 되감기부터 눌러 주문을 날리는 실수를 하지 않습니다.
단계
- witness 에서 앱 계정으로 db1 에
insert into orders(note) values ('before')를 하고, 그 행이 db2 에 보이는지 확인하세요. - db1 에서 iptables 로 db2(172.30.1.3)와 오가는 패킷만 버리세요(들어오는 것과 나가는 것 둘 다). witness 는 여전히 두 서버에 닿아야 합니다. 15초쯤 지나 db2 의 PostgreSQL 로그에 walreceiver 가 타임아웃으로 끊겼다는 줄이 남아야 합니다.
- 단순한 자동화 스크립트처럼 판단합니다. db2 에서 db1 에 닿지 않으니 db2 를 승격하세요(
select pg_promote()). db1 은 그대로 둡니다. - witness 에서 앱 계정으로 db1 에
('split-db1'), db2 에('split-db2')주문을 하나씩 넣으세요. 두 서버 모두 쓰기를 받습니다. 각 서버에서 두 주문이 어떻게 보이는지 확인합니다. - db1 이 더는 앱의 쓰기를 받지 못하게 네트워크로 막으세요. db1 에서 eth1 으로 들어오는 5432 포트 연결을 거절(REJECT)합니다. witness 에서
pg_isready -h db1이 실패해야 합니다. ssh(22)는 막지 않습니다. - 갈라진 뒤 db1 에만 들어간 주문을
/root/ha/lost.csv로 꺼내세요. 열은note,by_user,at이고 첫 줄은 머리글입니다. 'before' 주문 뒤에 db1 에 들어온 행이 그 대상입니다. - db1 을 멈추고, 2단계의 분할 규칙만 걷으세요(펜싱 규칙은 남깁니다). db2 에 슬롯
db1_slot을 만들고, db1 을 pg_rewind 로 db2 에 맞춰 되감은 뒤(출력은/root/ha/07-rewind.txt),host=db2·application_name=db1·슬롯db1_slot으로 db2 의 대기 서버로 띄우세요. /root/ha/lost.csv의 주문을 db2 에 한 번만 다시 넣으세요(id 는 새로 받습니다). db2 에before·split-db1·split-db2가 하나씩 있고, 대기 서버 db1 에도 같은 것이 보여야 합니다.
참고
- 이 터미널은 witness 입니다. 앱 서버이자 관리 콘솔이라고 생각하세요. 다른 서버는
ssh db1·ssh db2로 들어가고exit로 돌아옵니다. 세 서버 모두 root 입니다. - 서버 안에서 PostgreSQL 에 관리자로 붙을 때는
sudo -iu postgres psql입니다. witness 에서 앱 계정으로는psql -h db1 -U app -d appdb이고 비밀번호는/root/.pgpass에 들어 있습니다. - 주소는 세션망(eth1)의 것입니다: db1
172.30.1.2, db2172.30.1.3, witness172.30.1.4. 이름(db1·db2)으로 써도 이 주소로 풀립니다. 첫 NIC 의 주소(10.0.2.2)는 세 서버가 모두 같아서 서로를 부르는 데 쓸 수 없습니다. - 채점기는 witness 에서
ssh db1·ssh db2로 서버를 들여다봅니다. 22번 포트와 root 의 ssh 열쇠는 건드리지 마세요. - 세 VM 은 세션이 끝나면 함께 사라지고 남는 것이 없습니다. 60분을 넘기면 화면의 +시간으로 연장하세요.
- 시작할 때 db1 이 주 서버, db2 가
db2_slot으로 붙은 비동기 대기 서버입니다. 두 서버 모두 wal_log_hints 가 켜져 있고 옛 WAL 을 남겨 두며(wal_keep_size), 복제 타임아웃은 10초로 짧게 두었습니다. 복제 계정은 pg_rewind 에 필요한 권한을 이미 갖고 있습니다. - 이 실습은 일부러 잘못된 판단(3단계)을 합니다. 실제 운영에서 3단계를 하기 전에 해야 했던 일이 5단계입니다.
- 흔한 실수: 6단계보다 7단계를 먼저 하는 것. 되감기가 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)에서 읽습니다. 두 번 넣으면 같은 주문이 둘이 됩니다. 넣기 전에 이미 있는지 확인하세요.