- Published on
PostgreSQL VACUUM 안 돼 bloat 폭증? 즉시 복구
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 중인 PostgreSQL에서 갑자기 디스크가 빨리 차고, 쿼리가 느려지고, pg_stat_user_tables의 dead tuple이 폭증한다면 대개 VACUUM이 제대로 돌지 않거나(혹은 돌지만 부족하거나) autovacuum이 밀린 상황입니다. 이때는 “원인 확인 → 즉시 응급 처치 → 근본 재발 방지” 순서로 접근해야 합니다. 무턱대고 VACUUM FULL부터 실행하면 장시간 락으로 장애를 키울 수 있으니, 아래 절차대로 단계적으로 복구하세요.
비슷하게 시스템 자원이 꽉 차는 상황에서는 OS 레벨 진단도 병행해야 합니다. 디스크가 이미 100%에 근접했다면 먼저 큰 파일을 정리해 숨통을 트는 게 우선입니다. 필요하면 리눅스 디스크 100%? 큰 파일 5분내 찾아 삭제도 함께 참고하세요.
1) bloat 폭증의 정체: 왜 VACUUM이 필요한가
PostgreSQL은 MVCC 특성상 UPDATE/DELETE 시 기존 튜플을 즉시 지우지 않고 “죽은 튜플(dead tuple)”로 남겨둡니다. VACUUM은 이 dead tuple을 회수해 재사용 가능 공간으로 표시하고, 통계도 갱신해 플래너가 올바른 실행계획을 세우도록 돕습니다.
문제는 다음 중 하나라도 발생하면 dead tuple이 계속 쌓이면서 bloat가 폭증합니다.
- autovacuum이 아예 꺼져 있음
- autovacuum이 너무 느림(설정이 보수적이거나 IO 병목)
- 장시간 트랜잭션이 vacuum을 막음(특히
xmin이 오래 유지) - 테이블이 너무 큰데 vacuum 비용 제한 때문에 따라가지 못함
- 인덱스 bloat가 심해져 조회/쓰기 모두 느려짐
bloat가 심해지면 다음 증상이 연쇄적으로 나타납니다.
- 디스크 사용량 증가(특히 WAL/테이블 파일)
- 인덱스/테이블 스캔 비용 증가로 지연
- autovacuum이 더 오래 걸려 backlog가 더 쌓임
- 결국 OOM, 디스크 full, 장애로 번짐
2) 지금 당장 확인할 체크리스트(5분 진단)
아래 쿼리로 “어디가 터졌는지”부터 잡습니다.
2.1 autovacuum이 켜져 있는지
SHOW autovacuum;
SHOW track_counts;
autovacuum은on이어야 하고track_counts가off면 통계가 없어 autovacuum/통계 기능이 제한됩니다.
2.2 dead tuple이 폭증한 테이블 찾기
SELECT
schemaname,
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;
여기서 n_dead_tup 상위 테이블이 응급 처치 대상입니다.
2.3 vacuum이 막히는 “장기 트랜잭션” 확인
SELECT
pid,
usename,
state,
xact_start,
now() - xact_start AS xact_age,
query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start ASC
LIMIT 20;
xact_age가 수십 분~수시간 이상이면 vacuum이 회수할 수 있는 범위가 줄어듭니다.- 특히 “idle in transaction”은 최악입니다.
2.4 현재 vacuum이 돌고 있는지, 어디서 오래 멈췄는지
SELECT
pid,
relid::regclass AS relation,
phase,
heap_blks_scanned,
heap_blks_total,
index_vacuum_count,
max_dead_tuples,
num_dead_tuples
FROM pg_stat_progress_vacuum;
진행률이 거의 안 움직이거나 특정 테이블에서 오래 머물면 해당 테이블/인덱스/락을 집중 확인합니다.
2.5 락 때문에 vacuum이 못 도는지
SELECT
a.pid,
a.usename,
a.query,
l.locktype,
l.mode,
l.granted,
a.state,
a.query_start
FROM pg_locks l
JOIN pg_stat_activity a ON a.pid = l.pid
WHERE l.relation IS NOT NULL
ORDER BY a.query_start ASC;
granted = false가 많으면 락 경합이 큽니다.- 응급 상황에서는 “누가 막고 있는지”를 찾아 조치해야 합니다.
3) 즉시 복구(응급 처치) 시나리오
여기서 목표는 “서비스를 살리기”입니다. bloat를 완전히 제거하는 것과, 당장 성능/디스크 압박을 완화하는 것은 다릅니다.
3.1 1단계: 가장 안전한 응급 처치 VACUUM (ANALYZE)
우선 락 부담이 적은 일반 vacuum으로 dead tuple 회수와 통계 갱신을 합니다.
VACUUM (ANALYZE, VERBOSE) public.big_table;
- 일반
VACUUM은 테이블을 재작성하지 않으므로, 공간을 OS에 반환하지는 못해도 “재사용 가능”으로 만들어 성능 악화를 완화합니다. ANALYZE를 같이 돌려 플랜이 망가진 상황을 빠르게 복구합니다.
운영에서는 큰 테이블 여러 개를 한 번에 돌리기보다, dead tuple 상위 테이블부터 순차적으로 처리하세요.
3.2 2단계: autovacuum이 밀렸다면 “해당 테이블만” 공격적으로 튜닝
전역 설정을 바꾸기 전에, 문제 테이블에만 storage parameter로 응급 튜닝을 적용할 수 있습니다.
ALTER TABLE public.big_table SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 10000,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_analyze_threshold = 5000
);
- scale factor를 낮추면 더 자주 vacuum/analyze가 실행됩니다.
- 임계값은 워크로드에 맞게 조정합니다.
적용 후에는 수동 vacuum으로 한 번 “초기 정리”를 해주는 게 좋습니다.
VACUUM (ANALYZE) public.big_table;
3.3 3단계: 인덱스 bloat 의심 시 REINDEX CONCURRENTLY
테이블 자체보다 “인덱스가 비대해져서” 느려지는 경우가 많습니다. 특히 업데이트가 잦은 테이블에서 인덱스 bloat가 흔합니다.
가능하면 서비스 중단을 피하기 위해 concurrent 옵션을 우선 고려합니다.
REINDEX INDEX CONCURRENTLY public.big_table_some_idx;
또는 테이블 단위로도 가능합니다.
REINDEX TABLE CONCURRENTLY public.big_table;
- concurrent reindex는 시간이 더 걸릴 수 있지만, 일반적으로 강한 락을 피합니다.
- 단, 실패 시 재시도 전략이 필요할 수 있습니다(임시 파일/디스크 여유도 확인).
3.4 4단계: 정말 급하면 “문제 세션 종료”로 길막 제거
장기 트랜잭션이 vacuum을 막는다면, 업무 영향도를 판단해 종료해야 할 수도 있습니다.
SELECT pg_terminate_backend(12345);
- 종료 전 해당 세션의 쿼리/트랜잭션 영향 범위를 반드시 확인하세요.
- 애플리케이션에서 커넥션 풀 설정/트랜잭션 범위가 과도하게 길지 않은지도 함께 점검해야 합니다.
3.5 최후의 수단: VACUUM FULL은 언제 쓰나
VACUUM FULL은 테이블을 재작성해 OS에 공간을 반환할 수 있지만, 강한 락으로 인해 해당 테이블을 사실상 “장시간 멈추게” 만들 수 있습니다.
VACUUM (FULL, ANALYZE) public.big_table;
권장 사용 조건:
- 점검 시간(maintenance window)이 확보됨
- 테이블 단위로 영향 범위를 제한할 수 있음
- 디스크가 매우 부족해 “반환”이 꼭 필요함
대안으로는 pg_repack 같은 온라인 재작성 도구를 검토할 수 있지만, 여기서는 PostgreSQL 기본 기능 중심으로 설명합니다.
4) autovacuum이 안 도는 진짜 원인들(자주 놓치는 포인트)
4.1 autovacuum_max_workers 부족
큰 테이블이 여러 개 동시에 터지면 worker 수가 적어 backlog가 쌓입니다.
SHOW autovacuum_max_workers;
SHOW autovacuum_naptime;
- worker를 늘리면 동시에 처리 가능하지만 IO가 버티는지도 같이 봐야 합니다.
4.2 vacuum cost 제한으로 “너무 착하게” 동작
기본값은 운영에서 보수적인 편이라, 폭증 상황에서는 따라가지 못할 수 있습니다.
SHOW autovacuum_vacuum_cost_limit;
SHOW autovacuum_vacuum_cost_delay;
- cost limit을 올리거나 delay를 줄이면 더 공격적으로 vacuum을 수행합니다.
- 대신 디스크 IO를 더 사용하므로, 스토리지 성능과 함께 조정해야 합니다.
4.3 long transaction과 idle in transaction
가장 흔한 운영 실수입니다.
- API가 트랜잭션을 열고 외부 호출을 기다림
- 배치가 커서를 오래 잡고 있음
- 관리자가 콘솔에서 트랜잭션 열고 방치
애플리케이션 레벨에서는 “트랜잭션 범위를 최소화”하고, DB 레벨에서는 타임아웃을 걸어 방지합니다.
SHOW idle_in_transaction_session_timeout;
필요 시(신중히) 설정:
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
SELECT pg_reload_conf();
4.4 디스크/메모리 압박이 vacuum을 더 느리게 만든다
vacuum은 IO를 많이 씁니다. 디스크가 이미 포화면 vacuum도 느려지고, 느려진 vacuum 때문에 bloat가 더 늘어나는 악순환이 생깁니다.
- 디스크가 가득 찼다면 먼저 공간을 확보하세요.
- 커널 OOM이나 메모리 압박이 있었다면 DB 프로세스/워크로드도 같이 점검해야 합니다.
필요하면 리눅스 OOM Killer로 프로세스 죽음 원인 추적처럼 OS 로그 기반으로 함께 추적하는 것이 빠릅니다.
5) 재발 방지: “터지기 전에” 감지하고 자동으로 막는 법
5.1 모니터링 지표
다음 지표를 대시보드/알람에 올리면 bloat 폭증을 조기에 잡을 수 있습니다.
pg_stat_user_tables.n_dead_tup상위 테이블 변화량last_autovacuum이 오래 비어있는 테이블pg_stat_activity의 장기 트랜잭션 수- 디스크 사용량(데이터 디렉터리, WAL 디렉터리)
예: dead tuple 비율이 큰 테이블 찾기
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio_pct
FROM pg_stat_user_tables
WHERE (n_live_tup + n_dead_tup) > 0
ORDER BY dead_ratio_pct DESC
LIMIT 20;
5.2 테이블별 autovacuum 정책을 “차등 적용”
모든 테이블에 동일한 scale factor를 적용하면, 작은 테이블에는 과하고 큰 테이블에는 부족할 수 있습니다.
- 업데이트/삭제가 잦은 핵심 테이블: scale factor 낮게
- 거의 변경 없는 로그성 테이블: 기본값 유지 또는 파티셔닝 고려
5.3 파티셔닝과 데이터 수명주기 정리
대량 로그/이벤트 테이블은 UPDATE/DELETE로 지우기보다 “파티션 드롭”이 훨씬 효율적입니다.
- 오래된 파티션을
DROP TABLE로 제거하면 bloat를 근본적으로 줄일 수 있습니다. - VACUUM 부담이 줄어들어 전체 성능이 안정화됩니다.
6) 운영자가 바로 쓰는 응급 Runbook(요약)
아래 순서로 실행하면 대부분의 “VACUUM 안 돼서 bloat 폭증” 상황을 안전하게 완화할 수 있습니다.
- 디스크 여유 확인(가득 찼으면 먼저 확보)
pg_stat_user_tables로 dead tuple 상위 테이블 선정pg_stat_activity로 장기 트랜잭션/idle in transaction제거- 상위 테이블부터
VACUUM (ANALYZE)수동 실행 - 인덱스 bloat 의심 시
REINDEX CONCURRENTLY - 테이블별 autovacuum 파라미터를 낮춰 재발 방지
- 모니터링/알람에 dead tuple, 장기 트랜잭션, last autovacuum 추가
7) 마무리
bloat 폭증은 단순히 “디스크가 늘었다”가 아니라, MVCC 구조에서 정리 작업이 밀렸다는 신호입니다. 응급 상황에서는 일반 VACUUM (ANALYZE)와 장기 트랜잭션 제거로 서비스를 먼저 살리고, 이후 인덱스 재구성과 autovacuum 튜닝, 파티셔닝 같은 구조적 개선으로 재발을 막는 것이 정석입니다.
운영 환경에서 가장 중요한 것은 “강한 락을 유발하는 선택(VACUUM FULL 등)을 마지막에” 두고, 먼저 안전한 조치로 회복 탄력성을 확보하는 것입니다.