PRO DBDATA PRO DBDATA

PostgreSQL Transaction ID Wraparound의 원리와 Autovacuum Freeze 진단 가이드

읽는 시간 약 17분
postgresql ec9ea5ec95a0 ec9888ebb0a9 1787466833

PostgreSQL을 장기간 운영하다 보면 로그에서 database must be vacuumed 또는 to prevent wraparound라는 문구를 발견할 수 있다. 평소 Autovacuum이 정상적으로 실행되고 있었다면 대수롭지 않게 넘기기 쉽지만, 이 메시지는 데이터베이스의 Transaction ID가 위험 수준에 가까워지고 있다는 신호다.

Transaction ID Wraparound는 단순한 테이블 용량 증가나 성능 저하 문제가 아니다. 오래된 행이 새 트랜잭션보다 미래에 생성된 데이터로 잘못 해석되는 것을 방지하기 위해 PostgreSQL이 쓰기 작업을 중단할 수 있는 중요한 운영 이슈다. 특히 트랜잭션 발생량이 많은 시스템에서는 실제 시간보다 누적된 트랜잭션 수를 기준으로 위험도가 증가하기 때문에 생성된 지 몇 달 되지 않은 데이터베이스에서도 발생할 수 있다.

안전하게 운영하려면 XID의 구조를 이해하고 datfrozenxid, relfrozenxid, 장기 트랜잭션, Replication Slot과 Autovacuum 진행 상태를 함께 점검해야 한다.

Transaction ID Wraparound가 발생하는 원리

PostgreSQL은 MVCC 방식을 사용해 여러 사용자가 동시에 데이터를 조회하고 변경할 수 있도록 관리한다. 각 행에는 해당 행을 생성한 트랜잭션을 나타내는 xmin과 삭제하거나 갱신한 트랜잭션을 나타내는 xmax가 기록된다. PostgreSQL은 이 정보를 현재 트랜잭션의 XID와 비교해 어떤 행을 보여줄지 결정한다.

일반적인 PostgreSQL Transaction ID는 32비트 값이므로 약 42억 개의 범위를 가진다. 하지만 PostgreSQL은 현재 XID를 기준으로 약 21억 개를 과거로, 나머지를 미래로 해석한다. XID가 계속 증가해 최대 범위를 넘어 처음부터 다시 시작하면 오래된 행의 XID가 미래의 트랜잭션으로 판단될 가능성이 생긴다. 이것이 Transaction ID Wraparound다.

예를 들어 오랫동안 변경되지 않은 행에 매우 오래된 XID가 남아 있다고 가정해 보자. 현재 XID가 한 바퀴 돌아 이 값에 가까워지면 PostgreSQL은 해당 행이 과거에 생성된 것인지 아직 시작되지 않은 미래의 트랜잭션이 만든 것인지 정상적으로 구분하기 어려워진다.

PostgreSQL은 이런 문제를 막기 위해 오래된 행의 XID를 특별한 Frozen 상태로 처리한다. Freeze된 행은 모든 정상 트랜잭션에서 볼 수 있는 오래된 행으로 간주되므로 XID가 순환해도 가시성 판단에 문제가 발생하지 않는다. 이 작업은 일반 VACUUM과 Wraparound 방지를 위한 공격적 Vacuum을 통해 수행된다.

기본 autovacuum_freeze_max_age는 일반적으로 2억 트랜잭션이다. 테이블의 XID 나이가 이 값에 가까워지면 PostgreSQL은 Dead Tuple 수가 많지 않더라도 Wraparound 방지를 위한 Autovacuum을 실행한다. Autovacuum 설정을 일반 테이블 정리 관점으로만 이해하면 안 되는 이유다. PostgreSQL Vacuum과 Transaction ID 관리

데이터베이스와 테이블의 XID 나이 진단하기

Transaction ID 위험도를 확인할 때는 먼저 데이터베이스 단위의 datfrozenxid 나이를 조회한다.

SELECT datname,
       age(datfrozenxid) AS xid_age,
       datfrozenxid
FROM pg_database
WHERE datallowconn = true
ORDER BY age(datfrozenxid) DESC;

age(datfrozenxid)는 현재 XID와 해당 데이터베이스에서 가장 오래된 Freeze 기준 XID의 차이를 보여준다. 데이터베이스 나이가 높다면 어느 테이블이 원인인지 추가로 확인해야 한다.

SELECT n.nspname AS schema_name,
       c.relname AS table_name,
       c.relkind,
       age(c.relfrozenxid) AS xid_age,
       c.relfrozenxid,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
JOIN pg_namespace n
  ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'm', 't')
  AND n.nspname NOT IN ('pg_toast', 'information_schema')
ORDER BY age(c.relfrozenxid) DESC
LIMIT 50;

pg_class.relfrozenxid는 해당 테이블에서 Freeze가 완료됐다고 보장할 수 있는 기준점이다. age(relfrozenxid)가 높을수록 오래된 XID가 남아 있을 가능성이 크며, Wraparound 방지를 위한 Vacuum 대상에 가까워졌다고 볼 수 있다.

대형 테이블만 확인해서는 안 된다. 데이터가 거의 변경되지 않는 오래된 테이블, 사용하지 않는 임시 테이블, Materialized View, TOAST 테이블이 최고 XID 나이를 보유하는 경우도 있다. 실제 운영 점검에서는 크기와 XID 나이를 함께 정렬해야 조치 우선순위를 정하기 쉽다.

Aurora PostgreSQL에서는 CloudWatch의 MaximumUsedTransactionIDs도 함께 확인하는 것이 좋다. 기본값을 기준으로 2억을 넘었다고 즉시 Wraparound가 발생하는 것은 아니지만, Autovacuum이 정상적으로 따라가고 있는지 점검해야 하는 단계다. AWS는 5억을 낮은 단계의 경고, 10억을 조치가 필요한 경고, 15억을 즉각적인 대응이 필요한 높은 위험 단계의 예시로 제시한다. 실제 경보 기준은 시스템의 시간당 XID 증가량과 가장 큰 테이블을 Vacuum하는 데 필요한 시간을 고려해 더 보수적으로 설정하는 편이 안전하다. Aurora PostgreSQL Vacuum 필요 여부 점검

Autovacuum Freeze가 늦어지는 원인 찾기

XID 나이가 계속 증가한다고 해서 Autovacuum이 실행되지 않는다고 단정할 수는 없다. 이미 공격적 Vacuum이 실행 중이지만 대형 테이블을 스캔하는 데 시간이 오래 걸리는 상황일 수 있다.

다음 쿼리로 현재 Vacuum 진행 상태를 확인할 수 있다.

SELECT p.pid,
       p.datname,
       p.relid::regclass AS table_name,
       p.phase,
       p.heap_blks_total,
       p.heap_blks_scanned,
       p.heap_blks_vacuumed,
       round(
         100.0 * p.heap_blks_scanned /
         NULLIF(p.heap_blks_total, 0), 2
       ) AS scan_percent,
       a.xact_start,
       now() - a.xact_start AS elapsed_time,
       a.wait_event_type,
       a.wait_event,
       a.query
FROM pg_stat_progress_vacuum p
JOIN pg_stat_activity a
  ON a.pid = p.pid
ORDER BY a.xact_start;

쿼리 내용에 to prevent wraparound가 표시되면 Wraparound 방지를 위한 공격적 Autovacuum이다. 일반 Vacuum은 Visibility Map에서 모든 행이 보이는 것으로 표시된 페이지를 건너뛸 수 있지만, 공격적 Vacuum은 오래된 XID를 Freeze하기 위해 더 많은 페이지를 검사할 수 있다. 따라서 평소보다 실행 시간이 길고 I/O 사용량이 높게 나타날 수 있다.

Autovacuum Freeze를 지연시키는 대표 원인은 다음과 같다.

  • 오랫동안 Commit 또는 Rollback되지 않은 트랜잭션
  • idle in transaction 상태로 남은 애플리케이션 세션
  • 장기간 유지되는 Prepared Transaction
  • 소비되지 않는 논리 Replication Slot
  • 장시간 실행되는 Reader 쿼리 또는 오래된 Snapshot
  • Autovacuum Worker 부족과 낮은 Cost Limit
  • 대형 테이블에 대한 반복적인 Vacuum 취소
  • 높은 디스크 I/O나 CPU 부하로 인한 처리 속도 저하

오래된 트랜잭션은 다음 쿼리로 확인한다.

SELECT pid,
       datname,
       usename,
       application_name,
       client_addr,
       state,
       xact_start,
       age(backend_xid) AS backend_xid_age,
       age(backend_xmin) AS backend_xmin_age,
       now() - xact_start AS transaction_duration,
       wait_event_type,
       wait_event,
       query
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL
   OR backend_xmin IS NOT NULL
ORDER BY greatest(
           age(backend_xid),
           age(backend_xmin)
         ) DESC NULLS LAST;

Prepared Transaction은 일반 세션이 종료된 뒤에도 남을 수 있다.

SELECT gid,
       prepared,
       owner,
       database,
       transaction,
       age(transaction) AS xid_age
FROM pg_prepared_xacts
ORDER BY age(transaction) DESC;

오래된 Prepared Transaction은 COMMIT PREPARED 또는 ROLLBACK PREPARED로 정리해야 하지만, 어떤 결과가 업무적으로 맞는지 확인하지 않고 임의로 처리해서는 안 된다. 백업을 복원한 환경에도 기존 Prepared Transaction이 남을 수 있으므로 2단계 Commit을 사용한다면 별도의 모니터링이 필요하다.

Replication Slot도 확인해야 한다.

SELECT slot_name,
       slot_type,
       database,
       active,
       xmin,
       catalog_xmin,
       age(xmin) AS xmin_age,
       age(catalog_xmin) AS catalog_xmin_age,
       restart_lsn,
       confirmed_flush_lsn
FROM pg_replication_slots
ORDER BY greatest(
           age(xmin),
           age(catalog_xmin)
         ) DESC NULLS LAST;

사용하지 않는 Slot이 오래된 xmin 또는 catalog_xmin을 유지하면 Vacuum이 필요한 데이터를 제거하지 못할 수 있다. 다만 Slot을 삭제하면 소비자가 필요한 WAL을 잃어 복제를 다시 구성해야 할 수 있으므로 소유 시스템과 사용 여부를 반드시 확인해야 한다.

안전한 Autovacuum Freeze 해결 절차

XID 나이가 높을 때 바로 VACUUM FULL부터 실행하는 것은 권장하지 않는다. VACUUM FULL은 테이블을 다시 작성하고 강한 잠금을 사용하기 때문에 서비스에 큰 영향을 줄 수 있다. Wraparound 예방에는 일반적으로 VACUUM (FREEZE)가 적합하다.

먼저 현재 XID 증가 속도와 남은 시간을 계산한다. 예를 들어 MaximumUsedTransactionIDs가 시간당 1,000만씩 증가한다면 현재 수치가 10억일 때 15억까지 약 50시간밖에 남지 않는다. 반대로 하루에 100만씩 증가하는 시스템은 같은 수치라도 대응 가능한 시간이 더 길다.

다음으로 장기 트랜잭션, Prepared Transaction, Replication Slot과 장시간 Reader 쿼리를 확인한다. 업무상 불필요한 세션만 안전하게 종료하고, 대량 배치와 신규 쓰기를 줄인 상태에서 기존 공격적 Autovacuum의 진행률을 관찰한다.

자동 작업이 지나치게 느리거나 긴급한 상황이라면 유지보수 시간에 위험도가 높은 테이블부터 수동 Vacuum을 실행할 수 있다.

VACUUM (FREEZE, VERBOSE) schema_name.table_name;

여러 대형 테이블을 동시에 실행하면 I/O와 CPU 부하가 급증할 수 있으므로 XID 나이가 가장 높은 테이블부터 순차적으로 진행하는 편이 안전하다. 실행 중에는 pg_stat_progress_vacuum, CPU 사용률, 읽기·쓰기 지연, 스토리지 처리량과 애플리케이션 응답 시간을 함께 확인해야 한다.

작업이 완료된 뒤에는 반드시 age(relfrozenxid)age(datfrozenxid)를 다시 조회한다. 특정 테이블의 Vacuum이 끝났더라도 다른 테이블이나 오래된 트랜잭션이 더 낮은 Freeze 기준점을 유지하면 데이터베이스 전체 XID 나이는 즉시 감소하지 않을 수 있다.

개인적으로 운영 장애를 점검할 때는 Autovacuum 프로세스가 보인다는 사실만으로 정상이라고 판단하지 않는다. 동일한 테이블의 진행률이 증가하는지, 재시작을 반복하지 않는지, 대기 이벤트가 지속되는지, MaximumUsedTransactionIDs의 증가 속도가 둔화되는지까지 함께 본다. Autovacuum이 계속 실행되지만 완료되지 않는 상태가 실행 자체가 없는 상태보다 발견하기 어려웠기 때문이다.

Aurora PostgreSQL에서는 rds.adaptive_autovacuum을 활성화 상태로 유지하는 것이 좋다. Adaptive Autovacuum은 위험도가 높아지면 autovacuum_vacuum_cost_delay, autovacuum_vacuum_cost_limit, autovacuum_work_mem, autovacuum_naptime 등을 메모리에서 더 공격적인 방향으로 조정한다. 다만 이 기능이 모든 차단 원인을 해결하는 것은 아니므로 CloudWatch 경보와 SQL 진단을 병행해야 한다. Aurora PostgreSQL Autovacuum 운영 가이드

마치며

PostgreSQL Transaction ID Wraparound는 XID가 단순히 최대 숫자에 도달해서 발생하는 문제가 아니다. 순환하는 XID 구조에서 오래된 행의 가시성을 안전하게 유지하기 위해 반드시 Freeze 작업이 필요하며, 이 작업이 장기간 완료되지 않을 때 데이터베이스 전체의 쓰기 가용성이 위험해진다.

핵심 점검 대상은 age(datfrozenxid), age(relfrozenxid), MaximumUsedTransactionIDs, 공격적 Autovacuum 진행률이다. 수치가 높다면 장기 트랜잭션, Prepared Transaction, Replication Slot, 오래된 Snapshot과 시스템 자원 부족을 순서대로 확인해야 한다.

가장 좋은 해결책은 위험 단계에 도달한 뒤 긴급 Vacuum을 수행하는 것이 아니라 평소 XID 증가 속도를 모니터링하는 것이다. 데이터베이스 및 테이블별 XID 나이를 정기적으로 수집하고 5억, 10억, 15억과 같은 단계별 경보를 운영 환경에 맞게 설정하면 대응 시간을 충분히 확보할 수 있다.

Autovacuum은 단순히 Dead Tuple을 청소하는 백그라운드 기능이 아니다. PostgreSQL의 데이터 가시성과 지속적인 쓰기 가용성을 지키는 핵심 보호 장치다. Autovacuum 설정을 과도하게 낮추거나 작업을 반복적으로 중단하기보다 완료 여부와 방해 요인을 지속적으로 관리하는 것이 안정적인 운영의 출발점이다.

PRODB

함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.