Published on

PostgreSQL RDS autovacuum 폭주로 CPU 100% 해결

Authors

서버가 멀쩡히 돌다가 어느 순간 RDS PostgreSQL CPU가 100%에 고정되고, 애플리케이션 지연이 급격히 커지며 커넥션까지 불안정해지는 사건은 대부분 autovacuum 폭주로 설명되는 경우가 많습니다. 특히 트래픽 피크 이후, 대량 UPDATE/DELETE 배치 이후, 혹은 장시간 열린 트랜잭션이 겹칠 때 autovacuum worker가 테이블을 붙잡고 CPU/IO를 태우면서 장애처럼 보입니다.

이 글은 “autovacuum을 꺼라” 같은 위험한 결론이 아니라, (1) 지금 당장 서비스부터 살리는 완화책(2) 재발을 막는 구조적 튜닝을 RDS 환경에서 실행 가능한 수준으로 정리합니다.

> 참고: CPU 100% 상황은 종종 커넥션 폭주와 함께 옵니다. 커넥션 슬롯 이슈까지 같이 터졌다면 Aurora PostgreSQL remaining connection slots are reserved… 체크리스트도 함께 점검하는 편이 안전합니다.

1) autovacuum 폭주가 CPU를 태우는 메커니즘

PostgreSQL은 MVCC 구조라서 UPDATE/DELETE가 즉시 물리 삭제되지 않고 **dead tuple(죽은 행 버전)**이 쌓입니다. 이를 정리하는 과정이 VACUUM이며, 자동으로 수행하는 것이 autovacuum입니다.

CPU 100%가 되는 전형적인 경로는 다음과 같습니다.

  • 대량 UPDATE/DELETE → dead tuple 급증
  • autovacuum이 테이블을 따라잡으려 함 → worker가 장시간 수행
  • 동시에 쿼리도 돌고 있어 buffer/IO 경쟁 → 전체 CPU 상승
  • long-running transaction이 있으면 vacuum이 dead tuple을 “지울 수 없는” 상태가 되어 더 오래 끌고 감
  • 통계가 부정확하면 플래너가 나쁜 플랜을 선택 → 쿼리까지 느려져 부하가 더 커짐

여기서 핵심은 “autovacuum이 원인”이라기보다, autovacuum이 따라잡아야 하는 부채(테이블 부패/트랜잭션 관리 문제)가 쌓인 결과라는 점입니다.

2) 지금 당장 CPU 100%를 낮추는 응급 완화

장애 대응에서는 “원인 규명”보다 서비스를 먼저 안정화해야 합니다. 아래는 RDS에서 비교적 안전한 순서입니다.

2.1 어떤 테이블/쿼리가 autovacuum을 붙잡는지 확인

먼저 autovacuum이 실제로 돌고 있는지, 어떤 테이블을 청소 중인지 확인합니다.

-- 실행 중인 autovacuum 확인
SELECT
  pid,
  now() - xact_start AS xact_age,
  state,
  wait_event_type,
  wait_event,
  query
FROM pg_stat_activity
WHERE query ILIKE '%autovacuum%'
ORDER BY xact_age DESC;

문제 테이블 후보는 dead tuple이 많은 테이블입니다.

-- dead tuple이 많은 테이블 TOP
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_pct,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;

또한 vacuum이 “못 지우는” 상황인지 보려면 오래 열린 트랜잭션을 확인합니다.

-- 오래 열린 트랜잭션/idle in transaction 찾기
SELECT
  pid,
  usename,
  now() - xact_start AS xact_age,
  state,
  query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_age DESC
LIMIT 20;

idle in transaction이 길게 보이면, 그 세션이 vacuum 진행을 막아 CPU/IO 낭비를 키울 수 있습니다.

2.2 “autovacuum을 끄기” 대신, 일시적으로 자원 사용을 제한

운영 중 autovacuum을 완전히 끄면(특히 RDS에서) 트랜잭션 ID wraparound 같은 더 큰 사고로 이어질 수 있습니다. 대신 cost-based throttling과 worker 수를 조절해 “폭주”를 완화합니다.

RDS Parameter Group에서 아래를 점검/조정합니다(즉시 적용 여부는 파라미터에 따라 다릅니다).

  • autovacuum_max_workers : 너무 크면 동시에 여러 테이블을 청소하며 CPU를 태움
  • autovacuum_vacuum_cost_limit, autovacuum_vacuum_cost_delay : vacuum 속도를 제한/지연
  • maintenance_work_mem : 너무 작으면 vacuum이 비효율적으로 오래 걸림(단, 너무 크게 하면 메모리 압박)

일시 완화의 방향은 보통 다음 중 하나입니다.

  • CPU가 이미 100%로 서비스가 죽는다면: cost_delay 증가 / cost_limit 감소 / worker 수 감소
  • vacuum이 너무 느려 부채가 쌓이는 타입이라면: 피크 시간대만 완화하고, 비피크에 worker/limit을 올려 빨리 청소

> 팁: “항상 느리게”가 답이 아니라, 업무 시간에는 억제하고 야간에 따라잡는 스케줄이 현실적인 경우가 많습니다.

2.3 장시간 트랜잭션을 종료(최소한의 범위로)

vacuum이 진행 불가 상태라면, 가장 효과적인 응급조치는 오래 열린 트랜잭션을 끊는 것입니다.

-- 위험: 실제 영향이 있으므로 반드시 대상 pid를 확인 후 수행
SELECT pg_terminate_backend(<pid>);

특히 다음 패턴이 자주 원인입니다.

  • 애플리케이션이 커넥션을 잡고 BEGIN 후 커밋을 안 함
  • 배치가 큰 트랜잭션으로 수십 분~수 시간 유지
  • 리포팅/관리 쿼리가 트랜잭션 스냅샷을 오래 유지

애플리케이션 레벨에서 커넥션/트랜잭션 폭주가 함께 보이면, 앞서 언급한 커넥션 튜닝 체크리스트도 같이 보는 것이 좋습니다.

3) 재발을 막는 핵심 진단 포인트 6가지

여기부터는 “왜 이런 일이 반복되는가”를 해결하는 영역입니다.

3.1 테이블별 dead tuple 폭증 원인: UPDATE 패턴과 HOT 업데이트

UPDATE가 많을 때, 인덱스 구조에 따라 **HOT 업데이트(Heap-Only Tuple)**가 깨지면 인덱스까지 갱신되어 더 많은 쓰기/부하가 발생합니다.

  • 불필요한 인덱스가 많으면 UPDATE 비용이 증가
  • 자주 변경되는 컬럼에 인덱스가 있으면 vacuum 부담 증가

대응:

  • 인덱스 재검토(실제 사용되는지)
  • UPDATE를 “바뀐 값만” 하도록(불필요한 UPDATE 제거)

3.2 Autovacuum 트리거가 늦는 경우: scale factor가 큰 테이블

기본 autovacuum 트리거는 대략:

  • autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * reltuples

테이블이 크면 scale factor 때문에 “너무 늦게” vacuum이 시작되어 한 번 시작하면 장시간 달리는 형태가 됩니다. 이때는 테이블별로 scale factor를 낮추는 게 정석입니다.

-- 특정 테이블에 대해 autovacuum을 더 자주 돌게 설정(예시)
ALTER TABLE public.big_table SET (
  autovacuum_vacuum_scale_factor = 0.02,
  autovacuum_vacuum_threshold = 5000,
  autovacuum_analyze_scale_factor = 0.01,
  autovacuum_analyze_threshold = 5000
);

핵심은 “전체 파라미터를 건드리기 전에” 문제 테이블만 핀포인트로 조정하는 것입니다.

3.3 autovacuum이 느린 경우: IO 병목과 cost 파라미터

RDS에서 vacuum은 결국 디스크와 버퍼 캐시 경쟁을 합니다.

  • autovacuum_vacuum_cost_delay가 너무 크면 청소가 끝나지 않아 부채가 누적
  • 너무 작으면 피크에 CPU/IO를 과점유

권장 접근:

  • 피크 시간대: delay를 늘려 서비스 쿼리 우선
  • 비피크 시간대: delay를 줄이거나 cost_limit을 올려 “빚을 갚는” 시간 확보

3.4 통계 부정확으로 쿼리 플랜 악화 → 부하 증폭

autovacuum은 vacuum뿐 아니라 analyze도 수행합니다. analyze가 제때 안 되면 플래너가 잘못된 카디널리티를 추정해 느린 플랜을 선택하고, 그 느린 쿼리가 다시 dead tuple을 더 쌓게 됩니다.

확인:

SELECT
  schemaname,
  relname,
  last_analyze,
  last_autoanalyze,
  n_mod_since_analyze
FROM pg_stat_user_tables
ORDER BY n_mod_since_analyze DESC
LIMIT 20;

대응:

  • 문제 테이블에 analyze scale factor를 낮춤
  • 대량 배치 직후 ANALYZE를 명시적으로 수행(배치 파이프라인에 포함)

3.5 long-running transaction을 만드는 코드/운영 습관 제거

autovacuum 폭주의 “진짜 원인”이 장기 트랜잭션인 경우가 매우 많습니다.

  • 웹 요청에서 트랜잭션을 열고 외부 API 호출/대기
  • 큐 소비자가 한 번에 너무 큰 범위를 한 트랜잭션으로 처리
  • 관리 도구에서 실수로 트랜잭션을 열어둠

애플리케이션 측 가드레일 예시(개념):

-- 세션 단위로 statement timeout, idle_in_transaction_session_timeout 설정 가능
SET statement_timeout = '30s';
SET idle_in_transaction_session_timeout = '10s';

RDS Parameter Group에서 전역으로도 설정 가능하며, 운영 안정성에 큰 도움이 됩니다.

3.6 bloating이 심하면 VACUUM만으로는 부족: REINDEX / VACUUM FULL 검토

  • 일반 VACUUM은 공간을 OS에 반환하지 않습니다(재사용만 가능).
  • 테이블/인덱스 bloat가 심하면 쿼리도 느려지고 vacuum도 더 오래 걸립니다.

옵션:

  • REINDEX (CONCURRENTLY)로 인덱스 bloat 완화(가능한 버전/제약 확인)
  • VACUUM FULL은 강력하지만 테이블 잠금이 커서 운영 중에는 매우 위험
  • RDS에서는 대체로 “새 테이블로 재작성 + 스왑” 같은 온라인 마이그레이션 전략이 안전할 때가 많음

4) 실전 대응 플레이북(장애 시나리오별)

4.1 대량 UPDATE/DELETE 배치 후 CPU 100%

  1. pg_stat_activity로 autovacuum 대상 테이블 확인
  2. 해당 테이블 n_dead_tup 확인
  3. 배치가 끝났다면, 비피크에 vacuum/analyze가 따라잡도록 파라미터를 “일시적으로” 완화
  4. 다음부터는 배치를 쪼개고(작은 트랜잭션), 배치 후 ANALYZE 포함

4.2 평소엔 괜찮다가 특정 시간에만 autovacuum 폭주

  • 트래픽 피크 시간대에 autovacuum이 시작되는 구조일 가능성
  • 테이블별 scale factor를 낮춰 피크 전에 조금씩 청소되게 만들거나
  • 반대로 피크에는 cost_delay를 늘려 autovacuum이 덜 공격적으로 동작하도록 조정

4.3 autovacuum이 계속 도는데 dead tuple이 줄지 않음

  • long-running transaction이 vacuum을 막고 있을 확률이 큼
  • idle in transaction 세션을 찾아 종료/원인 코드 수정
  • idle_in_transaction_session_timeout 도입 검토

5) RDS에서 점검하면 좋은 지표/로그

  • Enhanced Monitoring/CloudWatch:
    • CPUUtilization, ReadIOPS/WriteIOPS, ReadLatency/WriteLatency
    • FreeableMemory, FreeStorageSpace
  • Performance Insights:
    • Top SQL, wait event(IO/Lock), autovacuum 관련 세션
  • PostgreSQL 뷰:
    • pg_stat_user_tables, pg_stat_activity, pg_stat_progress_vacuum(버전 지원 시)
-- vacuum 진행 상황(지원 버전에서 유용)
SELECT *
FROM pg_stat_progress_vacuum;

6) 결론: “autovacuum을 꺼서”가 아니라 “부채를 없애서” 해결

RDS PostgreSQL에서 autovacuum 폭주로 CPU 100%가 되는 문제는 대개 다음 3가지를 함께 손봐야 재발이 멈춥니다.

  1. 장기 트랜잭션 제거(idle in transaction, 과도하게 큰 트랜잭션)
  2. 문제 테이블 중심의 autovacuum 튜닝(scale factor/threshold/analyze)
  3. 쓰기 패턴/인덱스 구조 개선(불필요한 UPDATE, 과도한 인덱스, 배치 쪼개기)

운영 환경에서는 “전체 파라미터를 크게 바꾸는 튜닝”보다, 어떤 테이블이 부채를 만드는지 정확히 집어내고 테이블 단위로 조정하는 방식이 가장 안전하고 효과적입니다.

부하가 커지며 애플리케이션 레벨에서 커넥션 폭주까지 함께 보인다면, autovacuum과 별개로 커넥션 풀/프록시까지 포함한 영구 차단 전략도 함께 점검해 두는 것이 좋습니다: Aurora PostgreSQL remaining connection slots are reserved… 체크리스트