밀린 대기 서버를 재고, 붙잡힌 WAL 을 푼다
목표
대기 서버가 「붙어 있다」 와 「따라오고 있다」 를 구별해 잽니다. 받기와 적용의 차이를 바이트로 재고, 대기 서버 조회와 적용의 충돌·피드백의 대가·버려진 슬롯이 붙잡는 WAL 을 실제로 일으킨 뒤, 연결은 멀쩡한데 데이터가 오래된 대기 서버 셋을 고칩니다.
왜 중요한가
복제 모니터링은 대개 연결 상태만 봅니다. 그래서 대기 서버가 몇 시간 뒤처져도 초록불이고, 보고서를 뽑는 사람이 먼저 알아챕니다. 반대로 지워진 대기 서버의 슬롯 하나가 몇 주 동안 WAL 을 붙잡아 주 서버 디스크를 채우기도 합니다. 둘 다 「어디서 멈췄는지」 를 위치(LSN)로 재면 바로 보이는 일입니다. 그리고 지연을 줄이는 손잡이마다 대가를 다른 곳으로 넘긴다는 것 — 조회를 끊거나, 대기 서버가 뒤처지거나, 주 서버의 청소가 밀리거나 — 을 직접 겪어 두면 설정을 고를 때 덜 헤맵니다.
환경
이 파드는 postgres 계정으로 돕니다(su·sudo 없이 그냥 치면 됩니다). 주 서버는 5432 에 떠 있습니다.
psql 주 서버(5432, labdb)에 바로 붙습니다
psql -h 127.0.0.1 -p 5433 이 실습에서 세울 대기 서버
대기 서버에는 -h 127.0.0.1 을 꼭 붙이세요(유닉스 소켓 디렉터리를 옮겨 두었습니다). 이 실습의 파일은 /root/db/lag/ 에 둡니다.
단계
- 대기 서버 lag1 을 슬롯 lag1 로 5433 에
- 적용만 멈추고 밀린 양을 바이트로 →
/root/db/lag/02-paused.txt - 재개하고 따라잡은 것을 확인 →
/root/db/lag/03-caughtup.txt - 대기 서버 조회와 적용의 충돌 →
/root/db/lag/04-conflict.txt - 피드백을 켜면 주 서버의 청소가 밀린다 →
/root/db/lag/05-feedback.txt - 아무도 안 쓰는 슬롯이 WAL 을 붙잡는다 →
/root/db/lag/06-ghost.txt - 상한을 걸어 버려진 슬롯을 끊는다 →
/root/db/lag/07-slots.txt - 사건: 연결은 멀쩡한데 보고서용 대기 서버 셋의 데이터가 오래됐다
참고
- 대기 서버 로그는
/var/log/labhub/lag1.log, 주 서버 로그는/var/log/postgresql.log입니다. - 대기 서버에서만 되는 함수(pg_wal_replay_pause 등)와 주 서버에서만 보이는 뷰(pg_stat_replication)를 헷갈리기 쉽습니다.
- 사건 도구:
incident list,incident show <name>. 같은 사건을 다시 내면 처음 고장 상태로 돌아갑니다. 보고서는/root/incident/<name>/report.txt에 대상마다<target>: <code>한 줄과evidence-<target>: <observed line>을 씁니다. - 실습 파드에는 볼륨이 없어 세션이 끝나면 파일과 서버가 모두 사라집니다.
대기 서버 lag1 을 슬롯으로 붙인다
주 서버(5432)에 물리 슬롯 lag1 을 만들고, 베이스백업을 /var/lib/postgresql/lag1 에 떠서 대기 서버로 5433 에 띄우세요(로그 /var/log/labhub/lag1.log). 대기 서버는 슬롯 lag1 로 주 서버를 따라와야 합니다.
방향: 첫 실습에서 한 그대로입니다 — 슬롯을 먼저 만들고, 그 슬롯으로 사본을 뜨고, 사본을 대기 서버로 띄웁니다. 도구: pg_create_physical_replication_slot, pg_basebackup 의 -X stream·-S·-R, postgresql.auto.conf 의 port, pg_ctl start, 대기 서버의 pg_stat_wal_receiver. 구체: 슬롯을 만들 때 두 번째 인자로 true 를 주면 만드는 순간부터 WAL 을 붙잡습니다(대기 서버가 처음 붙기 전에 필요한 WAL 이 지워지지 않게). 계정은 lab(이 실습 서버는 루프백 복제를 허용해 둡니다). 대기 서버 접속에는 -h 127.0.0.1 -p 5433 을 붙이세요.
적용만 멈추고, 밀린 양을 바이트로 잰다
대기 서버(5433)의 적용(replay)만 멈추세요. 그다음 주 서버에 표 ticks 를 만들고 1000행 이상 넣습니다. 대기 서버에서 ticks 가 아직 보이지 않는 것을 확인하고, 대기 서버가 받아 둔 위치와 적용한 위치, 그 차이를 /root/db/lag/02-paused.txt 에 receive_lsn: <lsn>·replay_lsn: <lsn>·gap_bytes: <bytes> 세 줄로 적으세요. 대기 서버는 멈춘 채로 둡니다(다음 단계에서 재개합니다).
방향: 대기 서버 안에서는 받는 일과 적용하는 일이 따로 돕니다. 적용만 멈추면 받기는 계속되므로 둘 사이가 벌어집니다. 도구: pg_wal_replay_pause(), pg_get_wal_replay_pause_state(), pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn(), pg_wal_lsn_diff(). 구체: 두 위치는 같은 select 한 번으로 함께 뜨세요 — 따로 재면 그사이 수신 위치가 움직여 차이가 맞지 않습니다. 멈춤은 대기 서버에서 부르는 함수이고, 주 서버에서 부르면 오류가 납니다.
적용을 재개하고 따라잡은 것을 확인한다
대기 서버의 적용을 재개하고, 받은 위치와 적용한 위치가 같아질 때까지(따라잡을 때까지) 기다리세요. 그다음 /root/db/lag/03-caughtup.txt 에 replay_lsn: <lsn>(따라잡은 뒤의 적용 위치)·ticks_rows: <rows>(대기 서버에서 센 ticks 행 수) 두 줄을 적으세요.
방향: 멈춘 동안 받아 둔 WAL 은 디스크에 있으므로, 재개하면 다시 받지 않고 그대로 적용만 합니다. 도구: pg_wal_replay_resume(), pg_last_wal_receive_lsn() 과 pg_last_wal_replay_lsn() 의 비교, count(*). 구체: 재개 직후에는 아직 적용 중일 수 있습니다 — 두 위치가 같아진 것을 본 뒤에 적으세요. 채점기는 양쪽 ticks 를 다시 세어 봅니다.
대기 서버 조회가 적용과 부딪혀 취소된다
대기 서버의 max_standby_streaming_delay 를 5s 로 바꾸세요(재시작 없이). 주 서버에 10만 행짜리 표 big 을 만들고, 대기 서버에서 반복 읽기(repeatable read) 트랜잭션으로 big 을 센 뒤 30초쯤 머무는 조회를 띄워 둔 채, 주 서버에서 big 의 절반을 지우고 VACUUM 하세요. 대기 서버 조회가 받은 출력(오류 포함)을 /root/db/lag/04-conflict.txt 에 저장합니다.
방향: 주 서버의 VACUUM 은 「이제 아무도 안 보는」 행을 지우는데, 대기 서버의 조회는 주 서버가 모르는 곳에서 그 행을 보고 있습니다. 그 청소 기록을 적용하려면 대기 서버는 조회가 끝나길 기다리거나 조회를 끊어야 합니다. 도구: 대기 서버에서의 alter system set·pg_reload_conf(), psql 의 -c 를 여러 번(트랜잭션을 이어 간다), 백그라운드 실행(&), 대기 서버의 pg_stat_database_conflicts. 구체: 대기 서버의 조회는 백그라운드로 띄우고 출력을 2>&1 로 파일에 받으세요. 몇 초 뒤 주 서버에서 delete 와 vacuum 을 하면, 5초 한도가 지나 조회가 취소되고 충돌 통계가 하나 오릅니다. 채점기는 그 통계를 봅니다.
피드백을 켜면 대가는 주 서버로 간다
대기 서버의 hot_standby_feedback 을 on 으로 바꾸세요(재시작 없이). 04 처럼 대기 서버에서 big 을 읽는 긴 조회를 띄워 둔 채, 주 서버에서 big 의 행을 더 지우고 VACUUM (VERBOSE) big 을 돌려 그 출력을 /root/db/lag/05-feedback.txt 에 저장하세요.
방향: 피드백은 대기 서버가 「내 조회가 아직 이 시점의 행을 본다」 를 주 서버에 알리는 장치입니다. 조회는 살아남지만, 그 행을 지우지 못하는 쪽은 주 서버입니다. 도구: 대기 서버의 alter system set·pg_reload_conf(), VACUUM (VERBOSE) 의 tuples 줄, 주 서버 pg_replication_slots 의 xmin. 구체: VERBOSE 출력은 INFO 로 표준 오류에 나옵니다 — 2>&1 로 받으세요. 「N are dead but not yet removable」 의 N 이 0 보다 크면 피드백이 청소를 막은 것입니다.
아무도 안 쓰는 슬롯이 WAL 을 붙잡는다
주 서버에 아무도 쓰지 않을 물리 슬롯 ghost 를 만드세요(만드는 순간부터 WAL 을 붙잡게). 표 bulk 에 큰 쓰기를 해서 ghost 가 붙잡은 WAL 이 100MB(104857600바이트) 넘게 쌓이게 하고 CHECKPOINT 하세요. /root/db/lag/06-ghost.txt 에 ghost_restart_lsn: <lsn>·current_lsn: <lsn>(주 서버의 지금 위치)·retained_bytes: <bytes>(두 위치의 차이) 세 줄을 적습니다.
방향: 슬롯의 restart_lsn 은 「여기부터는 지우지 마라」 는 약속입니다. 쓰는 대기 서버가 없으면 그 위치는 영영 움직이지 않습니다. 도구: pg_create_physical_replication_slot(이름, true), insert … select generate_series, CHECKPOINT, pg_replication_slots 의 restart_lsn·wal_status, pg_current_wal_lsn(), pg_wal_lsn_diff(), pg_ls_waldir(). 구체: 200바이트짜리 행 수십만 개면 WAL 이 100MB 를 넘습니다. 체크포인트를 해도 pg_wal 디렉터리 크기가 줄지 않는 것을 pg_ls_waldir() 합계로 확인해 보세요. 두 위치는 한 번에 뜨세요.
상한을 걸어 버려진 슬롯을 끊는다
주 서버의 max_slot_wal_keep_size 를 64MB 로 거세요(재시작 없이). CHECKPOINT 로 ghost 가 끊기는(wal_status 가 lost) 것을 확인하고, 그때의 pg_replication_slots 출력을 /root/db/lag/07-slots.txt 에 저장한 뒤 ghost 를 지우세요. 살아 있는 대기 서버의 슬롯 lag1 은 그대로 따라와야 합니다.
방향: 상한은 「슬롯 하나가 붙잡을 수 있는 WAL 의 최대치」 입니다. 넘은 슬롯을 끊어 주 서버의 디스크를 지키고, 끊긴 대기 서버는 다시 떠야 합니다. 도구: alter system set·pg_reload_conf(), CHECKPOINT, pg_replication_slots 의 wal_status·safe_wal_size, 주 서버 로그 /var/log/postgresql.log, pg_drop_replication_slot. 구체: 상한 검사는 설정을 바꾼 순간이 아니라 체크포인트 때 돕니다. 끊길 때 주 서버 로그에 그 슬롯 이름이 들어간 줄이 남습니다. lost 를 확인한 출력은 지우기 전에 저장하세요.
사건 — 연결은 멀쩡한데 보고서용 대기 서버 셋의 데이터가 오래됐다
incident rp-stale-standby 를 실행하면 사건이 납니다(티켓을 먼저 읽으세요). 증상: 주 서버 rp-src(127.0.0.1:5450)를 따라가는 보고서용 대기 서버 셋 rpt1·rpt2·rpt3(5451·5452·5453)이 모두 streaming 으로 붙어 있는데, 조회하면 주 서버에 방금 쓴 것이 보이지 않습니다. 셋 다 주 서버를 실시간으로 따라오게 하고, 같은 일이 다시 생기지 않게 원인이 된 설정까지 바로잡으세요. 대기 서버는 대기 서버로 남아야 하고, 재시작은 재시작해야만 바뀌는 설정을 고칠 때만 합니다(대기 서버마다 대시보드 세션이 붙어 있습니다). /root/incident/rp-stale-standby/report.txt 에 대기 서버마다 rpt1: <code> 한 줄과 evidence-rpt1: <observed line> 한 줄을 적습니다(rpt2·rpt3 도 같은 꼴). 코드는 대기 서버마다 다음 중 하나입니다: apply-delay(적용을 일부러 늦추는 설정) · conflict-wait(대기 서버 조회와의 충돌을 끝없이 기다림) · recovery-target(복구 목표 지점에 닿아 멈춤) · replay-paused(누군가 적용을 손으로 멈춤).
방향: 연결이 멀쩡하면 받기는 되고 있다는 뜻입니다 — 문제는 적용입니다. 먼저 「멈춤 상태인가 아닌가」 로 둘씩 나누고, 각 쌍 안에서 무엇이 다른지 보세요. 도구: 주 서버 pg_stat_replication 의 flush_lsn·replay_lsn 과 pg_wal_lsn_diff, 대기 서버의 pg_get_wal_replay_pause_state(), show(또는 pg_settings)로 적용·복구 관련 설정, pg_stat_activity(startup 프로세스의 wait_event, 오래 머문 조회의 backend_xmin), 대기 서버 로그(/var/log/labhub/rp-stale/<이름>.log). 구체: 멈춤 상태인 둘 중 하나는 resume 하면 복구가 끝나 승격해 버립니다 — 로그의 HINT 가 무엇을 하라고 하는지 끝까지 읽으세요. replay_lag 같은 시간 숫자는 적용이 서 있으면 낡은 값을 보여 줍니다. 대부분의 적용 설정은 reload 로 바뀌지만 recovery_target_* 는 재시작해야 바뀝니다.