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

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

계획된 전환과 pg_rewind

TT Lab 에서 이어서 보기

목표

아무것도 잃지 않는 전환으로 db2 를 주 서버로 올리고, 실수로 갈라진 db1 을 pg_rewind 로 되감아 다시 붙입니다.

왜 중요한가

주 서버를 내려야 하는 날은 반드시 옵니다. 그날의 절차가 '대기 서버를 승격한다' 한 줄이면, 마지막 몇 초의 커밋이 사라지거나 옛 주 서버가 다시 붙지 못합니다. 깨끗한 종료와 LSN 비교 하나가 그 차이를 만듭니다. 그리고 역사가 갈라진 서버를 베이스백업 없이 되살리는 pg_rewind 는 준비(wal_log_hints)가 되어 있을 때만 쓸 수 있습니다.

단계

  1. db1 과 db2 둘 다 wal_log_hints = on 으로 두고 재시작하세요. 설정은 /etc/postgresql/16/main/conf.d/ 의 파일로 둡니다.
  2. 전환이 끝나면 db1 이 db2 에서 WAL 을 받습니다. 그때 쓸 물리 슬롯 db1_slot 을 지금 db2 에 만드세요.
  3. 앱 쓰기를 멈췄다고 보고 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 의 값보다 작지 않아야 합니다.
  4. db2 를 승격하세요(select pg_promote()). db2 가 쓰기를 받는 주 서버가 되고 타임라인이 2 로 갈라져야 합니다.
  5. db1 을 db2 의 대기 서버로 띄우세요. primary_conninfo 는 host=db2·user=replicator·application_name=db1, 슬롯은 db1_slot 입니다. db2 의 pg_stat_replication 에 db1 이 streaming 으로 보이고 db1_slot 이 active 여야 합니다. pg_rewind 는 쓰지 않습니다.
  6. 사고를 재현합니다. db1(대기 서버)에서 select pg_promote() 를 실행하고, witness 에서 앱 계정으로 db1 에 insert into orders(note) values ('oops') returning id, note 를 하세요. 그 출력을 /root/ha/06-oops.txt 에 저장합니다. 이제 쓰기를 받는 서버가 둘입니다.
  7. db2 에서 복제 계정 replicator 에게 pg_rewind 에 필요한 함수 실행 권한을 주고, db1 을 멈춘 뒤 pg_rewind 로 db2 에 맞춰 되감으세요(--source-server='host=db2 user=replicator dbname=postgres'). pg_rewind 의 출력 전체를 /root/ha/07-rewind.txt 에 저장합니다.
  8. 되감은 db1 을 다시 db2 의 대기 서버로 띄우세요. db2 의 pg_stat_replication 에 db1 이 streaming 으로 보여야 하고, 6단계에서 db1 에 들어갔던 oops 행은 어느 서버에도 없어야 합니다.

참고

되감기를 미리 가능하게

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 행이 어디로 갔는지 생각해 보세요.