IT/DB 운영

죽은 replication slot 하나가 Postgres WAL을 500GB로 불려놓은 밤

gfrog 2026. 9. 7. 15:14

지난 주 새벽에 페이저가 울렸다. Postgres 프라이머리 디스크 사용률 91%. 평소에는 40% 근처를 왔다갔다 하던 놈이라 뭔가 확실히 이상했다.

로그인해서 df -h 찍어보니 데이터 볼륨이 정말 500GB 넘게 차 있었다. 어제까지만 해도 220GB 정도였는데 하룻밤 사이에 두 배가 됐다. 이 시점에서 이미 멘탈이 반쯤 나갔다. 새벽 3시에 눈 뜬 사람이 할 수 있는 사고력은 뻔하다.

범인 찾기

디스크가 어디서 부풀었는지부터 봐야 했다. du -sh /var/lib/postgresql/* 대충 훑으니 답은 명확했다. pg_wal 디렉토리가 380GB. 데이터 자체는 별로 안 커졌는데 WAL만 미친듯이 쌓여 있었다.

Postgres에서 WAL이 안 지워질 이유는 몇 개 없다. 아카이빙이 실패하고 있거나, replication slot이 소비를 못 하고 있거나. 우리는 archive_mode = off니까 답은 뻔했다.

SELECT slot_name, active, restart_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots;

결과를 보고 한숨이 나왔다. slot_analytics_v2라는 놈이 active = false인 상태로 lag가 340GB로 찍혀 있었다. 우리 팀이 두 달 전에 CDC 파이프라인 재작업하면서 새 slot 만들고 옛날 slot을 안 지운 거였다. Debezium 컨슈머는 이미 다른 slot을 물고 있었고, 얘는 그냥 유령처럼 남아서 WAL만 물고 있었다.

왜 max_slot_wal_keep_size가 안 막았나

여기서 조금 부끄러운 얘기를 해야 한다. 우리 Postgres는 max_slot_wal_keep_size = -1로 설정돼 있었다. 이게 뭔지 몰라서 그런 게 아니라, RDS 옮기기 전 셀프 호스팅 시절에 "slot이 뒤처지면 그냥 무한정 기다린다"고 세팅한 걸 아무도 리뷰 안 하고 그대로 가져온 상태였다.

이 옵션의 의미는 이렇다. -1이면 slot이 얼마나 뒤처져도 WAL을 안 지운다. 양수(예: 100GB)로 설정하면 그 크기를 넘어가는 순간부터 slot을 무효화(invalidate)하고 WAL을 회수한다. 우리는 무한대를 골라놓고 slot 하나 지운 걸 까먹은 셈이다.

솔직히 이 조합이 왜 위험한지 다시 정리하면:

  • -1은 안전해 보이지만 실은 방화벽 하나를 꺼두는 것과 같다
  • 어떤 이유로든 컨슈머가 죽으면 프라이머리 디스크가 대신 대가를 치른다
  • 특히 CDC처럼 애플리케이션 팀이 관리하는 slot은 소유권이 모호해서 유실되기 쉽다

응급 처치와 후처리

일단 급했으니 slot을 지워야 했다. 그런데 여기서 한 번 더 흔들렸다. 그 slot이 진짜 안 쓰는 게 맞나? 잘못 지우면 CDC 컨슈머가 다시 붙었을 때 처음부터 읽으려 하다 실패한다. 팀 슬랙에 물어봤고, "그거 두 달 전에 버린 거 맞음"이라는 확인을 받고서야 손을 댔다.

SELECT pg_drop_replication_slot('slot_analytics_v2');

drop 자체는 순식간에 끝났지만 WAL이 바로 삭제되지는 않는다. 다음 체크포인트가 돌면서 정리되기 시작했다. checkpoint_timeout 기다리기 답답해서 수동으로 CHECKPOINT 한 번 때렸다. 5분쯤 지나니 pg_wal 크기가 눈에 띄게 줄기 시작했고, 30분 뒤에는 40GB 정도로 안정화됐다.

앞으로 이걸 어떻게 안 만들 것인가

사후에 팀 내부 논의 끝에 세 가지를 바꿨다.

첫째, max_slot_wal_keep_size를 무한대에서 실제 값으로 바꿨다. 우리 환경에선 128GB로 시작했다. 이 값을 넘기면 slot이 invalidate되지만, 그건 disk full로 프라이머리가 멈추는 것보다 훨씬 낫다. Invalidate된 slot은 재생성해서 초기 스냅샷부터 다시 뜨면 되니까 회복 가능하다.

둘째, pg_replication_slots를 Prometheus로 뽑기 시작했다. postgres_exporter에 이미 메트릭이 있어서 alerting만 추가했다. 룰은 두 개. slot lag가 20GB 넘으면 warning, 60GB 넘으면 page. active=false인 slot이 24시간 이상 남아 있어도 warning.

셋째, replication slot 소유권 문서를 만들었다. slot 이름 규칙에 팀 prefix를 강제하고 (cdc_analytics_*, standby_ops_* 이런 식), Confluence 페이지에 owner/purpose/created_at을 적게 했다. 사람 프로세스라 완벽하진 않지만, 유령 slot 하나 없애는 데는 이런 게 필요하다.

요약이 필요한가 싶지만

기술적으로 새로운 걸 배운 사건은 아니다. 문서에 다 나와 있는 내용이었고, 이런 사고가 흔한 것도 안다. 근데 "안다"와 "우리 환경에 적용해뒀다"는 다른 얘기다. Postgres logical replication 쓰는 팀이라면 오늘 밤 자기 전에 SELECT * FROM pg_replication_slots 한 번 찍어보시는 걸 권한다. active = false인 놈이 있고 lag가 GB 단위면, 그게 다음 새벽에 페이저 울릴 후보다.

혹시 다른 팀은 slot 관리를 어떻게 하는지 궁금하다. 특히 여러 CDC 파이프라인을 병렬로 굴리는 팀에서 slot 소유권을 어떻게 추적하는지, 댓글이나 DM 환영이다.