- Published on
PostgreSQL VACUUM 안 돌아갈 때 - autovacuum 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 느려지고 디스크가 꾸준히 늘어나는데 VACUUM 로그는 안 보이고, pg_stat_user_tables의 last_autovacuum도 비어 있다면 “VACUUM이 안 돈다”라고 느끼기 쉽습니다. 하지만 실제로는 다음 중 하나인 경우가 많습니다.
- autovacuum이 트리거 조건을 아직 못 만남
- autovacuum이 돌긴 도는데 너무 느림(I/O 제한, 작업자 부족)
- 긴 트랜잭션/복제 슬롯/오래된 스냅샷 때문에 dead tuple을 치울 수 없음
- vacuum이 락/충돌 회피로 계속 밀림
이 글은 “왜 안 도는지”를 먼저 진단하고, 그 다음에 안전하게 autovacuum을 튜닝하는 순서로 정리합니다.
운영 환경에서 디스크가 꽉 차는 상황이라면, 원인 분석과 함께 OS 레벨 inode/공간 점검도 같이 해야 합니다. 디스크가 남는데도 에러가 나는 케이스는 No space left on device인데 용량 남을 때 - inode 0% 해결도 함께 참고하세요.
1) “VACUUM이 안 돈다”를 확인하는 최소 체크리스트
autovacuum 자체가 꺼져 있지 않은지
SHOW autovacuum;
SHOW track_counts;
autovacuum은on이어야 합니다.track_counts가off면 통계가 쌓이지 않아 autovacuum이 정상 동작하기 어렵습니다.
최근 vacuum/autovacuum 기록 확인
SELECT
relname,
n_live_tup,
n_dead_tup,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
last_autovacuum가 계속NULL이면 “트리거가 안 걸렸거나”, “작업자가 못 잡았거나”, “통계가 갱신되지 않거나” 중 하나입니다.
autovacuum 작업자가 실제로 돌고 있는지
SELECT
pid,
backend_type,
state,
wait_event_type,
wait_event,
now() - xact_start AS xact_age,
query
FROM pg_stat_activity
WHERE backend_type = 'autovacuum worker'
ORDER BY xact_start NULLS LAST;
wait_event_type가IO또는Lock이면 “돌고는 있는데 막힘/느림” 가능성이 큽니다.
로그로 “조용히” 돌고 있는지 확인
운영에서 vacuum이 눈에 안 띄는 이유는 기본 설정이 로그를 안 남기기 때문입니다.
SHOW log_autovacuum_min_duration;
-1이면 autovacuum 로그가 기본적으로 안 찍힙니다.
권장(진단용): 짧게 켜서 원인을 잡고, 안정화 후에 다시 조정합니다.
ALTER SYSTEM SET log_autovacuum_min_duration = '1s';
SELECT pg_reload_conf();
2) autovacuum 트리거: “조건이 안 맞아서” 안 도는 경우
autovacuum은 테이블별로 대략 다음 조건에서 vacuum을 예약합니다.
- dead tuple 수가
autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * reltuples를 초과
즉, 테이블이 크면 기본 scale_factor가 너무 커서 dead tuple이 엄청 쌓여야만 autovacuum이 시작됩니다.
현재 테이블별 트리거 임계치 계산해보기
SELECT
c.relname,
s.n_dead_tup,
c.reltuples::bigint AS est_rows,
current_setting('autovacuum_vacuum_threshold')::int AS base_threshold,
current_setting('autovacuum_vacuum_scale_factor')::float AS scale_factor,
(current_setting('autovacuum_vacuum_threshold')::int
+ current_setting('autovacuum_vacuum_scale_factor')::float * c.reltuples)::bigint AS vacuum_trigger
FROM pg_class c
JOIN pg_stat_user_tables s ON s.relid = c.oid
WHERE c.relkind = 'r'
ORDER BY s.n_dead_tup DESC
LIMIT 20;
이 결과에서 n_dead_tup가 vacuum_trigger보다 작으면, “안 도는 게 정상”일 수 있습니다. 문제는 bloat는 그보다 더 일찍 성능을 망칠 수 있다는 점이라, 대형 테이블은 테이블 단위로 scale factor를 낮추는 게 일반적입니다.
3) 테이블 단위 autovacuum 튜닝(가장 효과적)
전역 설정을 무작정 세게 올리면 I/O가 폭증해 전체 성능이 흔들릴 수 있습니다. 대신 문제 테이블만 타겟팅하는 게 안전합니다.
예: 업데이트/삭제가 많은 대형 테이블
ALTER TABLE public.events SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 5000,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_analyze_threshold = 3000
);
scale_factor를 낮추면 더 자주 vacuum/analyze가 걸립니다.threshold는 “최소 dead tuple/변경 row 수” 바닥값입니다.
테이블 단위 설정 확인
SELECT
n.nspname,
c.relname,
c.reloptions
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname IN ('events');
4) autovacuum이 “돌긴 도는데 너무 느린” 경우
느린 원인은 크게 3가지 축입니다.
- 작업자 수가 부족함
- 작업자당 속도가 제한됨(딜레이, cost limit)
- I/O 경합으로 wait이 길어짐
4-1) 작업자 수: autovacuum_max_workers
SHOW autovacuum_max_workers;
SHOW autovacuum_naptime;
autovacuum_max_workers가 너무 작으면, vacuum 대기열이 쌓입니다.autovacuum_naptime이 길면(예: 1min) 반응성이 떨어집니다.
진단 팁: vacuum가 필요한 테이블이 많은데 동시에 실행되는 worker가 늘 1~2개라면 병목일 가능성이 큽니다.
4-2) 속도 제한: autovacuum_vacuum_cost_limit / autovacuum_vacuum_cost_delay
SHOW autovacuum_vacuum_cost_limit;
SHOW autovacuum_vacuum_cost_delay;
- delay가 크면 autovacuum이 의도적으로 “천천히” 돕니다.
- 운영 부하가 낮은 시간대에 bloat가 심각하다면 cost limit을 올리고 delay를 줄여 더 빨리 끝내도록 조정할 수 있습니다.
예시(상대적으로 공격적, 신중히 적용):
ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 4000;
ALTER SYSTEM SET autovacuum_vacuum_cost_delay = '2ms';
SELECT pg_reload_conf();
권장 접근:
- 먼저 테이블 단위 scale factor 조정으로 “자주 조금씩” 청소
- 그래도 밀리면 worker 수/속도 제한을 조정
5) vacuum이 못 치우는 진짜 원인: 긴 트랜잭션과 스냅샷
dead tuple은 “아무도 그 옛날 버전을 볼 일이 없다”가 확정되어야 제거됩니다. 아래 케이스가 있으면 vacuum이 돌아도 reclaim이 안 됩니다.
5-1) 오래 열린 트랜잭션 찾기
SELECT
pid,
usename,
application_name,
client_addr,
now() - xact_start AS xact_age,
state,
query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start ASC
LIMIT 20;
xact_age가 비정상적으로 긴 세션이 있으면, vacuum이 치워야 할 튜플의 “최소 경계선”이 과거에 묶입니다.- 특히 커넥션 풀/배치/관리자 콘솔에서
BEGIN만 열어두고 놀고 있는 경우가 흔합니다.
5-2) replication slot이 WAL 정리를 막는지
SELECT
slot_name,
slot_type,
active,
restart_lsn,
confirmed_flush_lsn
FROM pg_replication_slots;
- 논리 복제 슬롯이 쌓이면 WAL이 안 지워져 디스크가 늘 수 있습니다.
- 이건 vacuum 문제처럼 보이지만 실제로는 WAL 보관 문제입니다.
5-3) old_snapshot_threshold 및 장기 스냅샷
장기 스냅샷(특히 오래 도는 리포트 쿼리)이 있으면 vacuum 진행이 제한될 수 있습니다. 환경에 따라 설정/정책으로 해결합니다.
6) 락 때문에 vacuum이 밀리는 경우
일반 VACUUM은 강한 락을 오래 잡지 않지만, 테이블 상태/옵션/특정 단계에서 락 대기가 생길 수 있습니다.
대기 상황 확인:
SELECT
a.pid,
a.backend_type,
a.wait_event_type,
a.wait_event,
a.query,
l.locktype,
l.mode,
l.granted,
c.relname
FROM pg_stat_activity a
LEFT JOIN pg_locks l ON l.pid = a.pid
LEFT JOIN pg_class c ON c.oid = l.relation
WHERE a.backend_type = 'autovacuum worker'
ORDER BY a.pid;
granted = false가 보이면 락을 기다리는 중입니다.
7) “VACUUM FULL 해야 하나요?”에 대한 현실적인 답
VACUUM은 dead tuple을 청소하지만, OS 파일 크기를 즉시 줄이지는 않습니다(재사용 가능 공간으로 남음).- 파일 크기를 줄이려면
VACUUM FULL또는CLUSTER같은 재작성 작업이 필요하지만, 이는 테이블에 강한 락을 걸고 시간이 오래 걸립니다.
운영에서의 일반적인 전략:
- autovacuum이 제때 돌도록 트리거/속도/worker를 튜닝
- bloat가 이미 심각한 일부 테이블만 점검 창에
VACUUM FULL또는pg_repack같은 도구로 재작성
VACUUM FULL 예시(주의: 락 큼):
VACUUM (FULL, VERBOSE, ANALYZE) public.events;
8) 실전 튜닝 절차(안전한 순서)
1단계: 관측 가능성 확보
log_autovacuum_min_duration을 짧게 설정(예:1s)pg_stat_user_tables,pg_stat_activity로 대기/락/IO 확인
2단계: 문제 테이블부터 테이블 단위로
- 대형 테이블의
autovacuum_vacuum_scale_factor를 낮춤 analyze도 같이 따라오게autovacuum_analyze_scale_factor조정
3단계: 시스템 전역 설정은 마지막에
- vacuum backlog가 명확할 때만
autovacuum_max_workers증가 - 야간 배치 시간 등 여유 구간에
cost_limit상향,cost_delay하향
4단계: “치울 수 없는” 원인 제거
- 장기 트랜잭션 제거(앱/배치/풀 설정)
- 불필요한 replication slot 정리
운영 장애 대응의 관점에서는, 원인이 DB 내부가 아니라 노드 리소스/파일시스템 이슈인 경우도 많습니다. 예를 들어 디스크 경고가 떠서 vacuum이 문제처럼 보일 때, 실제로는 inode 고갈이 원인일 수 있습니다. 이런 케이스는 앞서 언급한 No space left on device인데 용량 남을 때 - inode 0% 해결을 같이 확인하는 게 빠릅니다.
또한 DB가 느려져 애플리케이션이 연쇄적으로 죽는 상황에서는, 쿠버네티스 환경이라면 장애 전파를 줄이기 위해 Pod 상태를 빠르게 진단해야 합니다. DB 부하가 트리거가 되어 애플리케이션이 CrashLoopBackOff로 넘어가는 경우도 있으니 Kubernetes CrashLoopBackOff 원인별 10분 진단 같은 체크리스트를 병행하면 전체 복구 시간이 줄어듭니다.
9) 자주 하는 실수와 체크 포인트
실수 1: 전역 scale factor만 낮춰서 I/O 폭주
- 모든 테이블이 더 자주 vacuum되어 디스크가 바빠지고, 오히려 쿼리가 느려질 수 있습니다.
- “문제 테이블부터”가 기본입니다.
실수 2: VACUUM만 하고 ANALYZE를 놓침
- vacuum으로 dead tuple은 줄어도 통계가 낡으면 플래너가 잘못된 실행 계획을 선택할 수 있습니다.
- autovacuum 튜닝 시 analyze 트리거도 같이 조정하세요.
실수 3: 긴 트랜잭션을 방치
- autovacuum을 아무리 공격적으로 해도 reclaim이 안 됩니다.
pg_stat_activity에서 오래 열린 트랜잭션을 먼저 잡아야 합니다.
10) 결론: “안 돈다”가 아니라 “조건/속도/장애물” 문제다
PostgreSQL에서 vacuum 문제는 대부분 다음 공식으로 정리됩니다.
- 트리거가 늦다:
autovacuum_vacuum_scale_factor가 큼(특히 대형 테이블) - 돌긴 도는데 느리다: worker 부족, cost 제한, I/O 경합
- 아예 못 치운다: 긴 트랜잭션, 오래된 스냅샷, replication slot
먼저 pg_stat_user_tables와 pg_stat_activity로 “정말 안 도는지, 아니면 조용히 느린지”를 구분하고, 테이블 단위 옵션으로 좁혀 튜닝한 뒤 전역 파라미터를 조정하는 순서가 가장 안전하고 재현성 있는 접근입니다.