DBA 실무/PostgreSQL

autovacuum 방치했다가 DB 멈춘 썰 (PostgreSQL VACUUM 완벽 정리, 그림 5개)

isony 2026. 9. 4. 07:42
반응형

autovacuum 방치했다가 DB 멈춘 썰 (PostgreSQL VACUUM 완벽 정리, 그림 5개)

테스트 환경: PostgreSQL 17, Rocky Linux 9 / Oracle 19c

Oracle DBA 시절엔 신경 쓸 필요 없던 게 PostgreSQL엔 하나 있습니다. 바로 VACUUM입니다.

처음엔 저도 무시했습니다. "자동으로 돈다며? 그럼 놔두면 되지." 그런데 어느 날 디스크가 이유 없이 계속 차오르고, 잘 돌던 쿼리가 느려지기 시작했습니다. 알고 보니 autovacuum이 제대로 안 돌고 있었고, dead tuple이 산더미처럼 쌓여 있었습니다. 최악의 경우엔 트랜잭션 ID wraparound로 DB가 통째로 멈출 수도 있다는 걸 나중에 알고 식은땀을 흘렸습니다.

이번 편은 운영 시리즈의 두 번째 글이자, PostgreSQL 운영의 핵심입니다. 3편(MVCC)에서 예고한 dead tuple 문제를 드디어 제대로 다룹니다. Oracle엔 없는 개념이라, PostgreSQL 운영자라면 반드시 알아야 합니다. 그림 5개로 확실히 정리합니다.

  • dead tuple은 왜 쌓이나
  • VACUUM vs VACUUM FULL 차이
  • autovacuum은 언제 자동 실행되나
  • ★ 트랜잭션 ID wraparound (DB 셧다운 위험)
  • 운영 모니터링과 튜닝

 

 

dead tuple은 왜 쌓이나

3편에서 배운 MVCC를 복습하면서 시작합니다. PostgreSQL은 UPDATE/DELETE 시 옛 행을 지우지 않고 "죽었다(dead)"고 표시만 합니다.

문제는 이 dead tuple이 계속 쌓인다는 겁니다. UPDATE를 많이 하는 테이블은 실제 데이터보다 몇 배씩 커집니다. 이걸 테이블 bloat(팽창) 이라고 합니다.

-- dead tuple 확인 (3편에서도 봤던 쿼리)
SELECT relname, n_live_tup, n_dead_tup 
FROM pg_stat_user_tables 
ORDER BY n_dead_tup DESC;

n_dead_tup이 n_live_tup에 육박하거나 넘으면 심각한 상태입니다. 100만 개의 dead tuple이 있으면, 그 테이블은 200만 행짜리처럼 느리게 동작합니다. 죽은 행도 스캔해야 하니까요.

이 dead tuple을 청소하는 게 바로 VACUUM입니다.

 

 

VACUUM vs VACUUM FULL - 헷갈리면 사고 난다

VACUUM에는 두 종류가 있고, 이걸 헷갈리면 운영 사고가 납니다.

-- 일반 VACUUM: dead 공간을 "재사용 가능"으로 표시
VACUUM employees;

-- VACUUM ANALYZE: 통계까지 갱신 (권장)
VACUUM ANALYZE employees;

-- VACUUM FULL: 테이블 통째로 재작성 (주의!)
VACUUM FULL employees;

결정적 차이

일반 VACUUM
  → dead 공간을 재사용 가능으로 표시
  → 테이블 크기는 그대로 (빈 슬롯 재활용)
  → 락 없음 (온라인) · 빠름 · 평소 운영용
  → 단, 디스크는 OS로 반환 안 됨

VACUUM FULL
  → 테이블을 통째로 새로 씀
  → 테이블 크기 축소, 디스크 OS로 반환
  → 테이블 전체 잠금! (읽기/쓰기 모두 차단)
  → 대용량은 몇 시간 걸림

⚠️ 절대 주의: VACUUM FULL은 테이블 전체를 잠급니다. 운영 중인 테이블에 무심코 실행하면 서비스가 멈춥니다. 반드시 유지보수 시간에만, 또는 pg_repack(락 없이 재구성하는 확장)을 쓰세요.

평소: 일반 VACUUM (autovacuum이 자동으로)
디스크 반환 필요: 유지보수 시간에 VACUUM FULL 또는 pg_repack

 

 

 

★ autovacuum은 언제 자동 실행되나

다행히 평소엔 autovacuum이 자동으로 VACUUM을 돌립니다. 그런데 "언제" 도는지 알아야 문제를 진단할 수 있습니다.

기본 발동 조건은 이렇습니다.

dead tuple > 50 + (autovacuum_vacuum_scale_factor × 전체 행 수)
                    ↑ 기본값 0.2 (20%)

예시로 이해하기:

1,000행 테이블:
  50 + (0.2 × 1,000) = 250
  → dead tuple 250개 쌓이면 autovacuum 실행

★ 큰 테이블의 함정

여기에 큰 테이블의 함정이 있습니다.

10억 행 테이블:
  50 + (0.2 × 10억) = 2억!
  → dead tuple이 2억 개 쌓일 때까지 청소 안 함
  → 그 사이 테이블은 bloat로 폭발

이래서 큰 테이블일수록 scale_factor를 낮춰야 합니다. 기본값 0.2는 작은 테이블 기준이라, 대용량 운영 DB엔 부적합합니다. (튜닝은 아래에서)

 

 

★ 트랜잭션 ID Wraparound - DB가 멈추는 최악의 사고

이게 제가 식은땀 흘린 부분입니다. autovacuum을 오래 방치하면 단순히 느려지는 걸 넘어 DB가 통째로 멈출 수 있습니다.

원리

PostgreSQL의 트랜잭션 ID는 32비트 (약 21억 개)
→ 계속 증가하다가 한계에 도달하면 "한 바퀴 돌음(wraparound)"
→ 옛 데이터가 미래 것으로 보여 사라질 위험
→ 이를 막으려고 PostgreSQL이 스스로 셧다운!

VACUUM은 dead tuple 청소뿐 아니라, 오래된 행을 "freeze(동결)" 해서 트랜잭션 ID를 안전하게 재사용하게 합니다. 이 freeze를 안 하면 wraparound로 치닫습니다.

모니터링 (정기 확인 필수)

-- 각 DB의 wraparound 위험도 확인
SELECT datname, 
       age(datfrozenxid) AS xid_age,
       ROUND(age(datfrozenxid)::numeric / 2147483647 * 100, 2) AS pct
FROM pg_database 
ORDER BY xid_age DESC;
xid_age가:
  ~15억 미만: 정상
  15억 이상: 경보 (autovacuum 점검 필요)
  20억 근접: 위험 (곧 셧다운)

healthy한 시스템에선 거의 안 생기지만, 터지면 재앙입니다. 그래서 정기 모니터링이 필수입니다.

 

 

★ 운영 모니터링과 튜닝

그럼 실무에서 뭘 챙겨야 할까요? 방치가 사고를 부르니, 주기적 확인 3가지와 튜닝을 정리합니다.

정기 확인 3가지

-- ① dead tuple 비율 (5~10% 이하 유지)
SELECT relname, n_live_tup, n_dead_tup,
       ROUND(n_dead_tup::numeric / NULLIF(n_live_tup,0) * 100, 1) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 20;

-- ② 마지막 autovacuum 시각 (오래됐으면 문제)
SELECT relname, last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY last_autovacuum NULLS FIRST LIMIT 20;

-- ③ XID 나이 (위 wraparound 쿼리)

튜닝 핵심

기본값은 소규모 워크로드용이라 운영 DB엔 부족합니다. 핵심 튜닝 포인트입니다.

-- 큰/바쁜 테이블은 per-table로 공격적으로 (권장)
ALTER TABLE big_table SET (
    autovacuum_vacuum_scale_factor = 0.05,   -- 기본 0.2 → 0.05
    autovacuum_vacuum_cost_limit = 2000       -- 더 빠르게
);
postgresql.conf 전역 튜닝:
autovacuum_max_workers = 6         # 기본 3, 워커 증가
autovacuum_naptime = 30s           # 더 자주 체크
maintenance_work_mem = 1GB         # VACUUM 속도 향상

# SSD/NVMe라면 더 공격적으로
autovacuum_vacuum_cost_delay = 0   # 지연 없이 풀속도

핵심 원칙:

1. 전역을 무작정 낮추지 말고, 바쁜 테이블만 per-table 튜닝
2. dead 비율 5~10% 이하 유지 목표
3. XID 나이 15억 넘으면 경보 설정
4. VACUUM FULL 대신 pg_repack (온라인)
5. 스테이징에서 먼저 테스트 (autovacuum이 I/O 경쟁 유발 가능)

 

 

★ AI로 VACUUM 상태 진단하기

VACUUM 관련 지표가 복잡하면 Claude에게 진단을 맡기세요.

프롬프트:
"PostgreSQL 17 운영 중인데 특정 테이블이 느려졌어.
pg_stat_user_tables 결과야:
[결과 붙여넣기]

1. dead tuple 상태 진단
2. autovacuum이 잘 도는지
3. bloat이 심각한지
4. 튜닝 방안 (per-table 설정 포함)
Oracle DBA가 이해하기 쉽게 설명해줘."

특히 "이 테이블에 맞는 autovacuum 설정을 계산해줘"라고 하면 테이블 크기·변경 빈도에 맞는 값을 제안해줍니다.

⚠️ 검증 필수: autovacuum 튜닝은 I/O에 영향을 줍니다. AI 제안값은 반드시 스테이징에서 먼저 적용하고, 운영 워크로드와 경쟁하지 않는지 확인하세요.


Oracle DBA가 VACUUM에서 저지르는 실수 TOP 5

1. VACUUM을 아예 신경 안 씀

"자동이니까 괜찮겠지" → autovacuum이 못 따라가는 걸 모름. 정기 모니터링 필수.

2. 운영 중 VACUUM FULL 실행

테이블 전체 잠금으로 서비스 중단. 유지보수 시간에만.

3. 큰 테이블에 기본 설정 방치

scale_factor 0.2면 큰 테이블은 청소가 너무 늦음. per-table로 낮춰야.

4. XID wraparound 무시

"설마 그런 일이" → 터지면 DB 셧다운. 정기 확인 필수.

5. 긴 트랜잭션 방치

오래 열린 트랜잭션은 VACUUM을 방해함(옛 행을 못 지움). idle_in_transaction_session_timeout 설정 권장.

 

 

마무리

PostgreSQL VACUUM의 핵심입니다.

  1. dead tuple은 MVCC의 대가 — UPDATE/DELETE마다 쌓임
  2. 일반 VACUUM은 온라인, FULL은 락 — FULL은 유지보수 시간에만
  3. autovacuum 공식 — 큰 테이블은 scale_factor 낮춰야
  4. XID wraparound — 방치하면 DB 셧다운, 정기 모니터링 필수
  5. 튜닝은 per-table로 — 바쁜 테이블만 공격적으로

Oracle엔 없던 개념이라 처음엔 낯설지만, VACUUM은 PostgreSQL 운영자의 필수 교양입니다. "UNDO 관리에서 VACUUM 관리로"의 사고 전환, 기억나시죠? 3편에서 예고한 그 관리 포인트가 바로 이겁니다. 저처럼 방치하다 식은땀 흘리지 마시고, 오늘 위 모니터링 쿼리 3개를 본인 DB에 돌려보세요.

다음 편은 운영 중 자주 나오는 문제 TOP 10입니다. VACUUM 문제를 포함해, 연결 부족·락·느린 쿼리 같은 실전 장애를 한 번에 정리합니다.

여러분이 VACUUM으로 겪은 사고나, bloat를 극적으로 줄인 경험이 있다면 댓글로 공유해주세요.

 

반응형