이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • PostgreSQL의 vacuum, bloat, WAL은 백엔드 개발과 어떤 관련이 있는가?
  • 오래 열린 트랜잭션이 운영 DB에 어떤 문제를 만드는가?
  • 인프라/DBA가 vacuum과 WAL을 민감하게 보는 이유는 무엇인가?

개요

Vacuum, bloat, WAL은 PostgreSQL 운영에서 자주 등장하는 단어다. 백엔드 개발자가 내부 구현을 DBA 수준으로 외울 필요는 없지만, 자신의 코드가 이 문제를 만들 수 있다는 점은 알아야 한다.

핵심은 MVCC다. PostgreSQL은 업데이트와 삭제 시 기존 row version을 즉시 덮어쓰기보다 새 version을 만들고, 더 이상 필요 없는 old version을 나중에 정리한다. 이 정리에 vacuum이 관여한다.

원리

PostgreSQL에서 UPDATE는 기존 행을 제자리 수정하는 느낌으로 보이지만, 내부적으로는 새 row version을 만들 수 있다.

UPDATE orders
SET status = 'CANCELED'
WHERE id = 100;

기존 version은 아직 과거 스냅샷을 보는 트랜잭션에 필요할 수 있다. 그래서 DB는 아무도 필요로 하지 않는 시점이 되어야 정리할 수 있다.

오래 열린 트랜잭션이 있으면 old version을 오래 보존해야 하고, vacuum이 충분히 정리하지 못한다. 이때 테이블과 인덱스가 부풀어 오르는 bloat가 생길 수 있다.

오래 열린 트랜잭션

백엔드 코드에서 흔한 원인은 다음이다.

@Transactional(readOnly = true)
public void streamLargeReport(OutputStream out) {
    orderRepository.streamAll().forEach(order -> {
        reportWriter.write(out, order); // 오래 걸리는 I/O
    });
}

이 코드는 읽기 전용이어도 트랜잭션을 오래 열 수 있다. PostgreSQL MVCC에서는 오래 열린 트랜잭션이 vacuum의 정리를 방해할 수 있다.

idle in transaction도 위험하다. 트랜잭션을 열어 놓고 아무 작업 없이 대기하는 세션이다.

SELECT pid, state, now() - xact_start AS tx_age, query
FROM pg_stat_activity
WHERE state = 'idle in transaction';

WAL

WAL은 Write-Ahead Log다. DB는 변경 내용을 먼저 로그에 기록해 장애 복구와 복제에 사용한다. 많은 쓰기, 대량 update, 인덱스 생성, backfill은 WAL을 크게 만들 수 있다.

WAL 증가는 다음 문제로 이어질 수 있다.

  • 디스크 사용량 증가
  • replica가 따라오지 못하는 lag
  • 백업과 복구 비용 증가
  • I/O 부하 증가

백엔드에서 대량 backfill을 작은 배치로 나누라는 이유 중 하나가 WAL과 replica lag다.

WAL 아카이브가 밀리거나 디스크가 부족해지면 단순 성능 저하를 넘어 백업/PITR과 복제 안정성에도 영향을 준다. 그래서 인프라/DBA는 “대량 update가 몇 분 걸리는가”뿐 아니라 “그동안 WAL이 얼마나 생성되고 replica가 따라오는가”를 본다.

Bloat

Bloat는 dead tuple이나 불필요한 공간 때문에 테이블과 인덱스가 커지는 현상이다. 같은 데이터를 읽어도 더 많은 페이지를 읽어야 하므로 성능이 떨어질 수 있다.

다음 패턴은 bloat를 키울 수 있다.

  • 같은 row를 자주 update한다.
  • 대량 update/delete가 자주 있다.
  • 오래 열린 트랜잭션이 많다.
  • autovacuum이 부하를 따라가지 못한다.

hot row 문제와도 연결된다. 같은 row를 매우 자주 update하면 row version과 index churn이 증가할 수 있다.

인프라/DBA와의 접점

DBA가 vacuum과 WAL을 민감하게 보는 이유는 이것이 단순 성능 문제가 아니라 복구와 안정성 문제이기 때문이다.

개발자가 backfill을 계획할 때는 다음을 공유한다.

  • 대상 row 수
  • update 비율
  • batch size
  • 실행 시간대
  • 중단 가능성
  • replica lag 허용 범위
  • 검증 쿼리

DBA는 autovacuum 설정, 디스크 여유, WAL 보관, replication 상태를 함께 본다.

개발자는 다음 질문에 답할 수 있어야 한다.

이 작업은 몇 row를 update/delete하는가?
한 row를 몇 번 다시 update하는가?
트랜잭션 하나가 얼마나 오래 유지되는가?
replica lag가 몇 초를 넘으면 멈출 것인가?
작업 후 vacuum이나 analyze가 필요한가?

이 질문은 인프라를 귀찮게 하는 절차가 아니라 장애 확률을 줄이는 설계다.

MySQL 관점

MySQL InnoDB도 undo log, purge, redo log, buffer pool 같은 운영 개념이 있다. PostgreSQL의 vacuum과 이름은 다르지만, 오래 열린 트랜잭션과 대량 변경이 정리 작업과 로그, 복제에 영향을 준다는 큰 원리는 비슷하다.

따라서 DB 엔진별 용어는 다르더라도 백엔드 원칙은 같다.

  • 트랜잭션을 짧게 유지한다.
  • 대량 변경은 작게 나눈다.
  • 운영 지표를 보며 속도를 조절한다.
  • 읽기 스트리밍과 외부 I/O를 조심한다.

실전 팁

  • 긴 report/export는 운영 DB 트랜잭션을 오래 열지 않게 설계한다.
  • Backfill은 batch size와 sleep을 두고 replica lag를 보며 진행한다.
  • idle in transaction 알림을 둔다.
  • update가 잦은 카운터는 별도 구조를 검토한다.
  • DBA에게 대량 변경 계획을 사전에 공유한다.
  • 대량 변경 전후로 table size, dead tuple, replica lag, WAL 증가량을 확인한다.
  • 긴 read transaction을 만드는 export/report는 별도 replica나 snapshot 전략을 검토한다.
  • WAL/PITR 보관 정책과 디스크 알림은 백업 전략과 함께 본다.

위험 신호!

  • 운영 DB에서 대량 update를 한 트랜잭션으로 실행한다.
  • read-only transaction이 수십 분 유지된다.
  • idle in transaction 세션이 자주 보인다.
  • replica lag가 커지는데 backfill을 계속 밀어붙인다.
  • WAL 디스크 사용량 알림이 없다.
  • 대량 update 후 성능 저하를 인덱스 부족으로만 보고 bloat 가능성을 보지 않는다.
  • export 기능이 read-only라 안전하다고 생각하고 긴 transaction을 방치한다.

확인 질문

  • 오래 열린 트랜잭션이 vacuum을 방해하는 이유는 무엇인가?
    • 과거 스냅샷을 볼 가능성이 있어 old row version을 정리하지 못하게 만들 수 있기 때문이다.
  • WAL이 증가하면 어떤 운영 문제가 생길 수 있는가?
    • 디스크 사용량, I/O 부하, replica lag, 백업과 복구 비용이 증가할 수 있다.
  • 백엔드 개발자가 vacuum과 WAL을 알아야 하는 이유는 무엇인가?
    • 긴 트랜잭션과 대량 변경, hot row 업데이트 같은 코드 패턴이 DB 정리와 복제, 복구 안정성에 영향을 주기 때문이다.
  • WAL 증가를 백업/PITR과 함께 봐야 하는 이유는 무엇인가?
    • WAL은 복제와 장애 복구에 쓰이므로 생성량과 아카이브 지연이 커지면 replica lag, 디스크 사용량, 복구 가능성에 영향을 주기 때문이다.

참고 문서