PostgreSQL 실전 강좌 #5 VACUUM과 autovacuum: dead tuple 청소의 원리와 튜닝
기초 6편에서 예고한 청구서가 도착했습니다. MVCC는 UPDATE·DELETE를 “새 버전 추가 + 옛 버전 표시"로 처리하므로, 표시된 옛 버전(dead tuple)이 계속 쌓입니다. 이것을 치우는 것이 VACUUM이고, PostgreSQL 운영 지식에서 단 하나의 중심을 꼽으라면 이것입니다. “튜닝 안 한 autovacuum + 갱신이 잦은 큰 테이블"의 조합은 운영 사고 목록의 최상위 단골입니다.
VACUUM이 하는 세 가지 일 #
- 공간 재사용 표시: dead tuple이 차지한 공간을 “다시 써도 됨"으로 표시합니다. 주의할 점은 파일이 줄어드는 것이 아니라 재사용 가능해질 뿐이라는 것입니다. 이미 부푼 테이블 크기는 그대로입니다.
- 가시성 지도 갱신: “이 블록의 행은 전부 모두에게 보임"을 기록합니다. 지난 편의 Index Only Scan이 성사되는 조건이 바로 이 지도의 신선도였습니다.
- 트랜잭션 ID 되감기 방지: MVCC의 버전 판별에 쓰는 트랜잭션 ID는 유한해서 주기적으로 “동결” 처리가 필요합니다. 이것이 밀리면 최악의 경우 DB가 쓰기를 거부하는 지경까지 갑니다. autovacuum을 꺼 버리면 안 되는 결정적 이유입니다.
여기에 통계 수집(ANALYZE)도 autovacuum이 함께 돌립니다. 기초 5편의 “예상 rows가 어긋나면 ANALYZE"가 평소에는 자동으로 굴러가는 이유입니다.
autovacuum의 문턱: 큰 테이블일수록 늦습니다 #
autovacuum은 테이블별로 “dead tuple이 문턱을 넘으면” 깨어납니다. 문턱의 기본값이 문제의 씨앗입니다.
문턱 = autovacuum_vacuum_threshold(50) + autovacuum_vacuum_scale_factor(0.2) × 행 수행 수의 20%라는 비율 문턱은 작은 테이블에는 적당하지만, 1억 행 테이블이라면 dead tuple이 2000만이 쌓여야 청소가 시작된다는 뜻입니다. 그동안 테이블과 인덱스는 부풀고, 스캔은 죽은 행 위를 헤엄치게 됩니다. 큰 테이블일수록 청소가 늦는 구조라서, 갱신이 잦은 큰 테이블에는 테이블 단위로 문턱을 내려 줍니다.
-- 이 테이블은 1%만 쌓여도 청소 시작
ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.01);“자주, 조금씩"이 방향입니다. 청소 주기가 짧을수록 한 번의 청소가 가볍고, 블로트가 자랄 틈이 없습니다. 참고로 PostgreSQL은 버전마다 VACUUM 자체를 꾸준히 빠르게 만들어 왔고(17의 메모리 구조 개선, 18의 비동기 I/O 등), 그래도 “언제 깨어나는가"는 여전히 위 문턱이 정하므로 이 튜닝의 가치는 그대로입니다.
청소를 막는 범인: 장기 트랜잭션 #
VACUUM이 돌고 있는데도 dead tuple이 안 줄어드는 날이 옵니다. 원리를 알면 당연합니다. 어떤 트랜잭션이 아직 볼 수 있는 버전은 치울 수 없습니다. 몇 시간짜리 스냅샷을 쥔 트랜잭션이 하나 있으면, 그 시점 이후의 모든 dead tuple이 전 테이블에서 보존됩니다. 실전 3편에서 idle in transaction을 경계하라던 이유가 이것입니다. 진단은 그때의 루틴 그대로 pg_stat_activity에서 가장 오래된 트랜잭션을 찾는 것이고, 예방은 idle_in_transaction_session_timeout 설정과 “트랜잭션 안에서 외부 대기 금지"라는 코드 규율입니다.
블로트 확인과 수습 #
현황 파악은 통계 뷰로 시작합니다.
SELECT relname, n_live_tup, n_dead_tup,
last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 10;n_dead_tup이 n_live_tup에 비해 비정상적으로 크거나 last_autovacuum이 아득히 옛날이라면 위의 두 원인(문턱, 장기 트랜잭션)을 순서대로 의심합니다. 이미 크게 부푼 테이블의 공간을 실제로 회수하려면 테이블을 다시 쓰는 수밖에 없는데, 표준 명령 VACUUM FULL은 작업 내내 테이블 전체를 배타 잠금하므로 운영 중 서비스에는 사실상 못 씁니다. 실무에서는 잠금 없이 온라인으로 다시 쓰는 pg_repack 같은 확장을 쓰거나, 점검 시간에 계획하고 돌립니다. 최선은 물론 여기까지 오지 않게 하는 것, 즉 위의 예방입니다.
정리 #
- VACUUM은 공간 재사용 표시, 가시성 지도 갱신, 트랜잭션 ID 동결의 세 가지 일을 합니다. autovacuum은 끄는 것이 아니라 튜닝하는 것입니다.
- 기본 문턱(20%)은 큰 테이블일수록 청소를 늦춥니다. 갱신 잦은 큰 테이블은 scale_factor를 테이블 단위로 내립니다.
- 방향은 “자주, 조금씩"입니다. 가벼운 청소가 잦은 것이 무거운 청소가 드문 것보다 낫습니다.
- 장기 트랜잭션은 전 테이블의 청소를 막습니다. idle in transaction 타임아웃과 코드 규율로 예방합니다.
- VACUUM FULL은 배타 잠금이라 운영 중에는 금물입니다. 다음 편은 이 잠금 이야기의 본체, 잠금과 동시성입니다.