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

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

서버 둘 사이의 복제와 동기 커밋

TT Lab 에서 이어서 보기

목표

db1 과 db2 를 서로 다른 VM 에서 스트리밍 복제로 묶고, 동기 복제가 무엇을 지키고 무엇을 멈추게 하는지 직접 봅니다.

왜 중요한가

앞 코스에서 한 서버 안에 주·대기 서버를 띄워 보았다면, 이번에는 그것이 진짜 네트워크를 건넙니다. 듣는 주소, pg_hba 의 처음 맞는 줄, 대기 서버의 이름처럼 한 서버 안에서는 보이지 않던 것들이 복제를 막는 첫 원인이 됩니다. 그리고 동기 복제는 설정 한 줄로 유실을 없애 주는 대신, 대기 서버가 멈추는 순간 주 서버의 쓰기를 함께 멈춥니다. 멈춘 커밋을 취소해도 데이터가 남는다는 것을 한 번 겪어 두면, 재시도하는 앱이 왜 같은 주문을 두 번 넣는지 설명할 수 있습니다.

단계

  1. db1 이 세션망(eth1)에서도 접속을 받게 하고(listen_addresses), pg_hba.conf 에 복제 연결 줄을 세션망 대역 172.30.1.0/24·scram-sha-256 으로 더하세요. 복제 계정 replicator(REPLICATION·LOGIN, 비밀번호 replpass)를 만들고 db1 을 재시작합니다. db2 에서 이 계정으로 복제 연결이 되어야 합니다.
  2. db1 에 물리 복제 슬롯 db2_slot 을 만드세요.
  3. 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 을 설정합니다. 아직 띄우지는 않습니다.
  4. db2 를 띄우세요. db1 의 pg_stat_replication 에 application_name 이 db2 인 연결이 streaming 으로 보이고, 그 연결이 db2 의 세션망 주소에서 왔으며, db2_slot 이 그 연결로 active 여야 합니다.
  5. db1 에서 synchronous_standby_names 를 설정해 db2 를 동기 대기 서버로 만드세요. pg_stat_replication 의 db2 sync_state 가 sync(또는 quorum)여야 하고, synchronous_commit 은 커밋이 대기 서버를 기다리는 값이어야 합니다.
  6. db2 를 멈추세요(systemctl stop postgresql@16-main). 그다음 witness 에서 앱 계정으로 insert into orders(note) values ('sync-stall') 을 실행하면 커밋이 멈춥니다. 몇 초 기다린 뒤 Ctrl+C 로 취소하고, psql 이 낸 출력 전체를 /root/ha/06-stall.txt 에 저장하세요. 그리고 그 행이 db1 에 남았는지 확인합니다.
  7. db2 를 다시 띄우세요. 6단계의 sync-stall 행이 db2 에도 보이고, db2 가 다시 동기(sync) 대기 서버여야 합니다.
  8. db1 에 앱 계정의 권한을 물려받는 로그인 계정 reporter(비밀번호 reppass)를 만들고, 이 계정에만 synchronous_commit = remote_apply 를 거세요(alter role). 그다음 reporter 로 insert into orders(note) values ('ryw') 를 하고 db2 에서 바로 보이는지 확인합니다. 서버 전체의 synchronous_commit 은 바꾸지 않습니다.

참고

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 에서 읽힙니다.