PostgreSQL 고가용성: 서버 세 대로 하는 active-standby
계획된 전환과 pg_rewind
목표
아무것도 잃지 않는 전환으로 db2 를 주 서버로 올리고, 실수로 갈라진 db1 을 pg_rewind 로 되감아 다시 붙입니다.
왜 중요한가
주 서버를 내려야 하는 날은 반드시 옵니다. 그날의 절차가 '대기 서버를 승격한다' 한 줄이면, 마지막 몇 초의 커밋이 사라지거나 옛 주 서버가 다시 붙지 못합니다. 깨끗한 종료와 LSN 비교 하나가 그 차이를 만듭니다. 그리고 역사가 갈라진 서버를 베이스백업 없이 되살리는 pg_rewind 는 준비(wal_log_hints)가 되어 있을 때만 쓸 수 있습니다.
단계
- db1 과 db2 둘 다
wal_log_hints = on으로 두고 재시작하세요. 설정은/etc/postgresql/16/main/conf.d/의 파일로 둡니다. - 전환이 끝나면 db1 이 db2 에서 WAL 을 받습니다. 그때 쓸 물리 슬롯
db1_slot을 지금 db2 에 만드세요. - 앱 쓰기를 멈췄다고 보고 db1 을 깨끗이 멈추세요(
systemctl stop postgresql@16-main, fast 모드). 그다음 db1 의 마지막 체크포인트 위치(pg_controldata의 Latest checkpoint location)와 db2 가 재생한 위치(pg_last_wal_replay_lsn())를/root/ha/03-lsn.txt에db1_checkpoint <LSN>,db2_replay <LSN>두 줄로 적습니다. db2 의 값이 db1 의 값보다 작지 않아야 합니다. - db2 를 승격하세요(
select pg_promote()). db2 가 쓰기를 받는 주 서버가 되고 타임라인이 2 로 갈라져야 합니다. - db1 을 db2 의 대기 서버로 띄우세요.
primary_conninfo는host=db2·user=replicator·application_name=db1, 슬롯은db1_slot입니다. db2 의pg_stat_replication에 db1 이streaming으로 보이고db1_slot이 active 여야 합니다. pg_rewind 는 쓰지 않습니다. - 사고를 재현합니다. db1(대기 서버)에서
select pg_promote()를 실행하고, witness 에서 앱 계정으로 db1 에insert into orders(note) values ('oops') returning id, note를 하세요. 그 출력을/root/ha/06-oops.txt에 저장합니다. 이제 쓰기를 받는 서버가 둘입니다. - db2 에서 복제 계정
replicator에게 pg_rewind 에 필요한 함수 실행 권한을 주고, db1 을 멈춘 뒤 pg_rewind 로 db2 에 맞춰 되감으세요(--source-server='host=db2 user=replicator dbname=postgres'). pg_rewind 의 출력 전체를/root/ha/07-rewind.txt에 저장합니다. - 되감은 db1 을 다시 db2 의 대기 서버로 띄우세요. db2 의
pg_stat_replication에 db1 이streaming으로 보여야 하고, 6단계에서 db1 에 들어갔던oops행은 어느 서버에도 없어야 합니다.
참고
- 이 터미널은 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으로 붙은 비동기 대기 서버입니다(앞 실습에서 손으로 한 상태). 두 서버 모두 세션망에서 듣고, 복제 계정의 비밀번호는 각 서버의/var/lib/postgresql/.pgpass에 있습니다. - 두 서버 모두
wal_keep_size = 256MB로 옛 WAL 을 남겨 둡니다. 되감기가 대상 서버의 WAL 을 마지막 공통 체크포인트부터 읽기 때문입니다. - 흔한 실수: 3단계에서
-m immediate로 내리는 것. 종료 체크포인트가 없어 db1 이 앞서 나간 채로 남을 수 있습니다. - 흔한 실수: 되감기 전에 db1 을 띄워 둔 채로 pg_rewind 를 돌리는 것. 대상은 깨끗이 멈춰 있어야 합니다.
되감기를 미리 가능하게
db1 과 db2 둘 다 wal_log_hints = on 으로 두고 재시작하세요. 설정은 /etc/postgresql/16/main/conf.d/ 의 파일로 둡니다.
되감기(pg_rewind)는 갈라진 뒤에 바뀐 블록을 WAL 에서 찾습니다. 힌트 비트만 바뀐 블록은 이 설정이 없으면 WAL 에 남지 않습니다. 재시작해야 바뀌는 설정이라 사고 뒤에 켜면 늦습니다. 확인은 show wal_log_hints 입니다.
전환 뒤에 쓸 슬롯을 db2 에 미리
전환이 끝나면 db1 이 db2 에서 WAL 을 받습니다. 그때 쓸 물리 슬롯 db1_slot 을 지금 db2 에 만드세요.
물리 슬롯은 대기 서버에서도 만들 수 있고, 승격한 뒤에도 남습니다. select pg_create_physical_replication_slot('db1_slot') 을 db2 에서 실행합니다.
db1 을 깨끗이 내리고 두 위치를 비교한다
앱 쓰기를 멈췄다고 보고 db1 을 깨끗이 멈추세요(systemctl stop postgresql@16-main, fast 모드). 그다음 db1 의 마지막 체크포인트 위치(pg_controldata 의 Latest checkpoint location)와 db2 가 재생한 위치(pg_last_wal_replay_lsn())를 /root/ha/03-lsn.txt 에 db1_checkpoint <LSN>, db2_replay <LSN> 두 줄로 적습니다. db2 의 값이 db1 의 값보다 작지 않아야 합니다.
fast 종료는 남은 WAL 을 대기 서버에 다 보내고 종료 체크포인트를 쓴 뒤 내려갑니다. 그 체크포인트까지 db2 가 받았다면 db1 의 역사 전체가 db2 안에 있습니다. ssh db1 /usr/lib/postgresql/16/bin/pg_controldata /var/lib/postgresql/16/main 으로 봅니다.
db2 를 승격한다
db2 를 승격하세요(select pg_promote()). db2 가 쓰기를 받는 주 서버가 되고 타임라인이 2 로 갈라져야 합니다.
승격하면 db2 의 pg_wal 에 00000002.history 가 생깁니다. 그 파일에 갈림 지점이 적혀 있습니다. select pg_is_in_recovery() 가 f 여야 합니다.
db1 을 되감기 없이 붙인다
db1 을 db2 의 대기 서버로 띄우세요. primary_conninfo 는 host=db2·user=replicator·application_name=db1, 슬롯은 db1_slot 입니다. db2 의 pg_stat_replication 에 db1 이 streaming 으로 보이고 db1_slot 이 active 여야 합니다. pg_rewind 는 쓰지 않습니다.
3단계에서 확인한 조건 덕분에 db1 은 갈림 지점보다 앞서 있지 않습니다. standby.signal 을 두고 설정 조각(conf.d/20-standby.conf)만 쓰면 새 타임라인을 따라갑니다. 비밀번호는 db1 의 /var/lib/postgresql/.pgpass 에 이미 있습니다.
누군가 db1 을 승격해 버렸다
사고를 재현합니다. db1(대기 서버)에서 select pg_promote() 를 실행하고, witness 에서 앱 계정으로 db1 에 insert into orders(note) values ('oops') returning id, note 를 하세요. 그 출력을 /root/ha/06-oops.txt 에 저장합니다. 이제 쓰기를 받는 서버가 둘입니다.
승격은 되돌릴 수 없는 명령입니다. db1 은 db2 의 타임라인 2 에서 타임라인 3 으로 갈라집니다. db1 의 로그(/var/log/postgresql/postgresql-16-main.log)에서 selected new timeline ID 줄을 찾아보세요.
pg_rewind 로 db1 을 되감는다
db2 에서 복제 계정 replicator 에게 pg_rewind 에 필요한 함수 실행 권한을 주고, db1 을 멈춘 뒤 pg_rewind 로 db2 에 맞춰 되감으세요(--source-server='host=db2 user=replicator dbname=postgres'). pg_rewind 의 출력 전체를 /root/ha/07-rewind.txt 에 저장합니다.
필요한 함수는 pg_ls_dir(text, boolean, boolean), pg_stat_file(text, boolean), pg_read_binary_file(text), pg_read_binary_file(text, bigint, bigint, boolean) 넷입니다. pg_rewind 는 postgres 사용자로 /usr/lib/postgresql/16/bin/pg_rewind --target-pgdata=/var/lib/postgresql/16/main ... 으로 돌립니다. 대상(db1)은 깨끗이 멈춰 있어야 합니다.
되감은 db1 을 다시 붙인다
되감은 db1 을 다시 db2 의 대기 서버로 띄우세요. db2 의 pg_stat_replication 에 db1 이 streaming 으로 보여야 하고, 6단계에서 db1 에 들어갔던 oops 행은 어느 서버에도 없어야 합니다.
승격하면서 standby.signal 이 지워졌습니다. 다시 두고 띄우면 5단계의 설정 조각대로 db2 를 따라갑니다. 되감기는 갈림 지점 뒤의 변경을 버립니다. oops 행이 어디로 갔는지 생각해 보세요.