복제 슬롯 하나가 디스크 17GB를 붙들고 있었습니다
PostgreSQL은 복제 슬롯이 남아 있는 한 WAL을 지우지 않습니다. standby가 죽은 뒤 두 달간 쌓인 WAL과, 공간을 되찾은 뒤에야 드러난 2차 손상까지의 기록입니다.

디스크 사용률 91퍼센트. 30GB 중 26GB가 차 있었습니다. 그런데 데이터베이스 본체는 650MB였습니다.
남은 공간을 차지한 것은 pg_wal 디렉터리였습니다. 18GB, 파일 1,099개. 가장 오래된 파일의 날짜는 두 달 전이었습니다.
복제 슬롯은 WAL을 붙잡아 두는 계약입니다
PostgreSQL의 복제 슬롯은 standby가 아직 받아가지 못한 WAL을 primary가 지우지 못하게 막습니다. standby가 잠시 끊어져도 재접속하면 이어받을 수 있게 하는 안전장치입니다.
문제는 이 계약에 기본적으로 만료가 없다는 점입니다. standby가 영영 돌아오지 않아도 슬롯이 남아 있는 한 primary는 WAL을 계속 보관합니다.
이 서버의 설정값이 그 관계를 그대로 보여줍니다.
max_wal_size = 1024MB
max_slot_wal_keep_size = -1
max_wal_size가 1GB인데 실제 pg_wal은 18GB였습니다. max_wal_size는 체크포인트 간격을 조절하는 목표치이지 상한이 아니며, 슬롯이 붙잡은 WAL 앞에서는 아무 힘이 없습니다.
max_slot_wal_keep_size는 PostgreSQL 13에서 추가된 설정으로, 슬롯이 붙잡을 수 있는 WAL의 상한입니다. 이 값을 넘기면 슬롯이 무효화되고 WAL은 정상적으로 삭제됩니다. standby는 못 따라오게 되지만 primary는 살아남습니다. 기본값은 -1, 무제한입니다. 이 서버도 기본값 그대로였습니다.
즉 standby를 폐기할 때 슬롯을 지우지 않으면, 그 순간부터 디스크가 차오르기 시작합니다. 그리고 그 사실은 디스크가 다 찰 때까지 아무 신호도 내지 않습니다.
standby가 여러 대이거나 운영자가 여러 명인 환경이라면, 슬롯을 일일이 챙기는 것보다 max_slot_wal_keep_size에 감당 가능한 상한을 걸어두는 편이 확실합니다. 복제가 끊어지는 것과 primary의 디스크가 차는 것 중 무엇이 더 나쁜지를 미리 정해두는 설정이라고 보면 됩니다.
진단은 쿼리 한 줄입니다
SELECT slot_name, slot_type, active, wal_status,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;
결과는 이랬습니다.
slot_name | slot_type | active | wal_status | retained
-----------+-----------+--------+------------+----------
standby_1 | physical | f | extended | 17 GB
active = f는 이 슬롯을 쓰는 접속이 없다는 뜻입니다. 확인차 pg_stat_replication을 조회하니 0행이었습니다. 붙어 있는 standby가 하나도 없는데 슬롯만 남아 17GB를 잡고 있었습니다.
wal_status가 extended인 것도 신호입니다. 이 값은 슬롯 때문에 max_wal_size를 넘겨 보관 중이라는 표시입니다.
회수
standby를 되살릴 계획이 없다면 슬롯을 지우면 됩니다.
SELECT pg_drop_replication_slot('standby_1');
CHECKPOINT;
CHECKPOINT는 두 번 실행했습니다. 체크포인트가 돌아야 더 이상 필요 없어진 WAL 세그먼트가 실제로 삭제되거나 재활용됩니다.
| 항목 | 조치 전 | 조치 후 |
|---|---|---|
| WAL 파일 수 | 1,099개 | 34개 |
pg_wal 크기 |
18GB | 513MB |
| 디스크 사용률 | 91퍼센트 (여유 2.7GB) | 31퍼센트 (여유 20GB) |
한 가지 실무적인 함정이 있었습니다. 이 명령을 SSH와 컨테이너 실행을 거쳐 한 줄로 넘겼더니 큰따옴표가 PostgreSQL 식별자로 해석되면서 column "standby_1" does not exist로 실패했습니다. 셸이 여러 겹 중첩될 때는 인용부호를 신뢰하지 말고 SQL을 파일로 만들어 psql -f로 넘기는 편이 안전합니다.
공간을 되찾아도 끝이 아니었습니다
디스크를 비운 뒤에도 캐시 컨테이너 하나가 여전히 올라오지 않았습니다. 로그를 보니 원인이 바뀌어 있었습니다.
Bad file format reading the append only file appendonly.aof.122.incr.aof
디스크가 가득 찬 동안 append-only 파일에 쓰기가 잘리면서 파일이 깨진 것입니다. 재시작 횟수는 8,480회였고, 11일 동안 이 상태였습니다. 복구는 공식 도구로 했습니다.
valkey-check-aof --fix appendonly.aof.manifest
24,171,248바이트 중 손상된 뒷부분 18,392,008바이트를 잘라내고 5,779,240바이트만 남기자 정상 기동했습니다. 캐시라 잘려나간 데이터는 문제가 되지 않았습니다.
여기서 얻은 교훈은 이렇습니다. 디스크 포화는 1차 증상이고, 그 사이 쓰기를 하던 컴포넌트에는 2차 손상이 남습니다. 공간을 되찾은 것으로 상황이 끝났다고 판단하면 안 됩니다. 포화 기간 동안 쓰기가 있었던 프로세스를 목록으로 만들고 하나씩 정합성을 확인해야 합니다.
두 달 동안 아무 경보가 없었던 이유
이 서버가 자동 점검 대상에 들어 있지 않았습니다. 그래서 디스크 91퍼센트도, 컨테이너가 8,480번 재시작한 것도 아무도 몰랐습니다.
점검 항목을 추가하면서 디스크 사용률만 넣지 않고 원인 지표를 직접 넣었습니다.
| 점검 항목 | 임계 | 왜 넣었는가 |
|---|---|---|
| 비활성 복제 슬롯이 보유한 WAL | 2GB | 이번 사고의 직접 원인. 디스크가 차기 전에 잡힙니다 |
pg_wal 총량 |
8GB | 슬롯 외의 누적 원인도 포착 |
| 디스크 사용률 | 85퍼센트 | 최후의 방어선 |
| running이 아닌 컨테이너 | 존재 시 | 재시작 루프를 조기에 발견 |
디스크 사용률만 감시했다면 이번에도 91퍼센트가 되어서야 알았을 것입니다. 결과가 아니라 원인을 감시하면 훨씬 이르게 잡힙니다.
경보 0건은 정상이라는 뜻이 아닙니다
새 점검을 넣고 실행했더니 경보가 0건이었습니다. 여기서 멈추면 안 됩니다. 0건은 “정상”일 수도 있고 “점검이 값을 못 읽고 조용히 실패했다”일 수도 있습니다. 둘은 로그에서 똑같이 보입니다.
그래서 임계치를 일부러 낮춰 다시 돌렸습니다.
디스크 31퍼센트 (임계 1퍼센트) → 경보 발생
비활성 슬롯 WAL 0MB (임계 0MB) → 경보 발생
값을 실제로 읽고 있고 판정 로직도 동작한다는 것이 확인됐습니다. 그다음 임계치를 원래대로 되돌리니 0건이었습니다. 이번 0건은 정상이라는 뜻입니다.
새로 넣은 감시는 반드시 한 번은 일부러 울려봐야 합니다. 울려보지 않은 감시는 감시가 아니라 그냥 코드입니다.
체크리스트
복제를 쓰는 시스템이라면 다음 두 가지를 지금 확인해 볼 만합니다.
pg_replication_slots에active = f인 슬롯이 있는지. 있다면 그 standby가 실재하는지- standby를 폐기하는 절차서에 슬롯 삭제가 들어 있는지
슬롯을 지우면 그 standby는 base backup부터 다시 만들어야 합니다. 되살릴 계획이 있다면 슬롯을 지우는 대신 standby를 먼저 복구하는 쪽이 맞습니다. 다만 그동안에도 디스크는 계속 차오르므로, 남은 여유 공간과 복구에 걸릴 시간을 함께 놓고 판단해야 합니다.