PostgreSQL 고가용성: 서버 세 대로 하는 active-standby
서버 둘 사이의 복제와 동기 커밋
목표
db1 과 db2 를 서로 다른 VM 에서 스트리밍 복제로 묶고, 동기 복제가 무엇을 지키고 무엇을 멈추게 하는지 직접 봅니다.
왜 중요한가
앞 코스에서 한 서버 안에 주·대기 서버를 띄워 보았다면, 이번에는 그것이 진짜 네트워크를 건넙니다. 듣는 주소, pg_hba 의 처음 맞는 줄, 대기 서버의 이름처럼 한 서버 안에서는 보이지 않던 것들이 복제를 막는 첫 원인이 됩니다. 그리고 동기 복제는 설정 한 줄로 유실을 없애 주는 대신, 대기 서버가 멈추는 순간 주 서버의 쓰기를 함께 멈춥니다. 멈춘 커밋을 취소해도 데이터가 남는다는 것을 한 번 겪어 두면, 재시도하는 앱이 왜 같은 주문을 두 번 넣는지 설명할 수 있습니다.
단계
- db1 이 세션망(eth1)에서도 접속을 받게 하고(
listen_addresses), pg_hba.conf 에 복제 연결 줄을 세션망 대역172.30.1.0/24·scram-sha-256으로 더하세요. 복제 계정replicator(REPLICATION·LOGIN, 비밀번호replpass)를 만들고 db1 을 재시작합니다. db2 에서 이 계정으로 복제 연결이 되어야 합니다. - db1 에 물리 복제 슬롯
db2_slot을 만드세요. - db2 의 클러스터를 멈추고 데이터 디렉터리(
/var/lib/postgresql/16/main)를 db1 의 베이스백업으로 바꾸세요(db2_slot사용). 그리고 db2 가 대기 서버로 뜨도록standby.signal을 두고,primary_conninfo에host=db1·user=replicator·application_name=db2를,primary_slot_name에db2_slot을 설정합니다. 아직 띄우지는 않습니다. - db2 를 띄우세요. db1 의
pg_stat_replication에application_name이 db2 인 연결이streaming으로 보이고, 그 연결이 db2 의 세션망 주소에서 왔으며,db2_slot이 그 연결로 active 여야 합니다. - db1 에서
synchronous_standby_names를 설정해 db2 를 동기 대기 서버로 만드세요.pg_stat_replication의 db2sync_state가sync(또는quorum)여야 하고,synchronous_commit은 커밋이 대기 서버를 기다리는 값이어야 합니다. - db2 를 멈추세요(
systemctl stop postgresql@16-main). 그다음 witness 에서 앱 계정으로insert into orders(note) values ('sync-stall')을 실행하면 커밋이 멈춥니다. 몇 초 기다린 뒤 Ctrl+C 로 취소하고, psql 이 낸 출력 전체를/root/ha/06-stall.txt에 저장하세요. 그리고 그 행이 db1 에 남았는지 확인합니다. - db2 를 다시 띄우세요. 6단계의
sync-stall행이 db2 에도 보이고, db2 가 다시 동기(sync) 대기 서버여야 합니다. - db1 에 앱 계정의 권한을 물려받는 로그인 계정
reporter(비밀번호reppass)를 만들고, 이 계정에만synchronous_commit = remote_apply를 거세요(alter role). 그다음 reporter 로insert into orders(note) values ('ryw')를 하고 db2 에서 바로 보이는지 확인합니다. 서버 전체의synchronous_commit은 바꾸지 않습니다.
참고
- 이 터미널은 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 에 서로 무관한 빈 클러스터가 하나씩 떠 있고, db1 에만 앱 데이터베이스(
appdb, 표orders)가 있습니다. db1 은 아직 localhost 에서만 듣습니다. - 흔한 실수: pg_hba 의 database 칸에
all을 적는 것. 복제 연결에는 맞지 않아no pg_hba.conf entry for replication connection이 납니다. - 흔한 실수:
application_name을 빼고 동기 복제를 거는 것. 오류 없이 async 로 남습니다.
db1 이 세션망에서 복제 연결을 받게 한다
db1 이 세션망(eth1)에서도 접속을 받게 하고(listen_addresses), pg_hba.conf 에 복제 연결 줄을 세션망 대역 172.30.1.0/24·scram-sha-256 으로 더하세요. 복제 계정 replicator(REPLICATION·LOGIN, 비밀번호 replpass)를 만들고 db1 을 재시작합니다. db2 에서 이 계정으로 복제 연결이 되어야 합니다.
listen_addresses 는 재시작해야 바뀝니다. pg_hba 의 database 칸은 replication 이라고 적어야 복제 연결에 맞습니다(all 은 맞지 않습니다). 설정 조각은 /etc/postgresql/16/main/conf.d/ 에 파일로 두면 원본을 건드리지 않습니다. 확인은 witness 에서 pg_isready -h db1 입니다.
db2 몫의 복제 슬롯
db1 에 물리 복제 슬롯 db2_slot 을 만드세요.
select pg_create_physical_replication_slot('db2_slot') 입니다. 논리 슬롯(logical)은 스트리밍 복제가 아니라 논리 디코딩용입니다. pg_replication_slots 에서 slot_type 을 확인하세요.
db2 를 db1 의 사본으로 갈아 끼운다
db2 의 클러스터를 멈추고 데이터 디렉터리(/var/lib/postgresql/16/main)를 db1 의 베이스백업으로 바꾸세요(db2_slot 사용). 그리고 db2 가 대기 서버로 뜨도록 standby.signal 을 두고, primary_conninfo 에 host=db1·user=replicator·application_name=db2 를, primary_slot_name 에 db2_slot 을 설정합니다. 아직 띄우지는 않습니다.
패키지가 만든 빈 클러스터는 db1 과 시스템 식별자가 달라 대기 서버가 될 수 없습니다. 지우고 sudo -iu postgres pg_basebackup -h db1 -U replicator -D /var/lib/postgresql/16/main -X stream -S db2_slot 로 받습니다. 비밀번호는 db2 의 /var/lib/postgresql/.pgpass 에 두면 나중에 WAL 을 받는 프로세스도 같은 파일을 씁니다. 설정은 /etc/postgresql/16/main/conf.d/ 에 파일로 둡니다.
db2 를 띄워 슬롯으로 붙인다
db2 를 띄우세요. db1 의 pg_stat_replication 에 application_name 이 db2 인 연결이 streaming 으로 보이고, 그 연결이 db2 의 세션망 주소에서 왔으며, db2_slot 이 그 연결로 active 여야 합니다.
ssh db2 systemctl start postgresql@16-main. 붙었는지는 주 서버(db1) 에서 봅니다. 대기 서버에서 보면 pg_stat_replication 은 비어 있는 것이 정상입니다. 슬롯이 active 가 아니라면 primary_slot_name 을 확인하세요. 로그는 /var/log/postgresql/postgresql-16-main.log 입니다.
db2 를 동기 대기 서버로
db1 에서 synchronous_standby_names 를 설정해 db2 를 동기 대기 서버로 만드세요. pg_stat_replication 의 db2 sync_state 가 sync(또는 quorum)여야 하고, synchronous_commit 은 커밋이 대기 서버를 기다리는 값이어야 합니다.
alter system set synchronous_standby_names = 'FIRST 1 (db2)' 뒤에 select pg_reload_conf(). 목록의 이름은 대기 서버의 application_name 입니다. 이름이 틀리면 오류 없이 async 로 남습니다.
대기 서버가 멈추면 커밋도 멈춘다
db2 를 멈추세요(systemctl stop postgresql@16-main). 그다음 witness 에서 앱 계정으로 insert into orders(note) values ('sync-stall') 을 실행하면 커밋이 멈춥니다. 몇 초 기다린 뒤 Ctrl+C 로 취소하고, psql 이 낸 출력 전체를 /root/ha/06-stall.txt 에 저장하세요. 그리고 그 행이 db1 에 남았는지 확인합니다.
psql -h db1 -U app -d appdb -c "insert ..." 2>&1 | tee /root/ha/06-stall.txt 로 실행하고 멈춘 뒤 Ctrl+C 를 누르면 화면과 파일에 함께 남습니다. statement_timeout 은 커밋 대기를 끊지 못합니다. 취소 경고를 읽어 보세요. 클라이언트에는 실패처럼 보였는데 행은 어디에 있습니까?
돌아온 대기 서버가 따라잡는다
db2 를 다시 띄우세요. 6단계의 sync-stall 행이 db2 에도 보이고, db2 가 다시 동기(sync) 대기 서버여야 합니다.
슬롯 덕분에 db1 은 db2 가 멈춘 동안의 WAL 을 지우지 않고 기다렸습니다. 다시 띄우면 그 자리부터 받아 갑니다. db2 에서는 sudo -iu postgres psql -d appdb 로 확인합니다.
보고서 계정만 remote_apply 로
db1 에 앱 계정의 권한을 물려받는 로그인 계정 reporter(비밀번호 reppass)를 만들고, 이 계정에만 synchronous_commit = remote_apply 를 거세요(alter role). 그다음 reporter 로 insert into orders(note) values ('ryw') 를 하고 db2 에서 바로 보이는지 확인합니다. 서버 전체의 synchronous_commit 은 바꾸지 않습니다.
create role reporter login password 'reppass' in role app 로 만들면 app 의 권한을 물려받습니다. alter role reporter set synchronous_commit = remote_apply 는 그 계정의 새 세션부터 적용됩니다. remote_apply 는 대기 서버가 적용까지 마쳤다고 답할 때까지 기다리므로, 커밋이 돌아온 순간 db2 에서 읽힙니다.