PostgreSQL VACUUM FULL 후 relfrozenxid age가 1로 줄어드는 이유

반응형
PostgreSQL VACUUM FULL 후 relfrozenxid age가 1로 줄어드는 이유

PostgreSQL VACUUM FULL 후 relfrozenxid age가 1로 줄어드는 이유

PostgreSQL에서 VACUUM FULL을 수행한 뒤 pg_class.relfrozenxidage 값이 크게 낮아지는 현상은 정상적인 동작이다. 일반 VACUUM이 기존 테이블 파일 안에서 불필요한 튜플을 정리하는 방식이라면, VACUUM FULL은 테이블을 새로 재작성하는 방식에 가깝기 때문이다.

핵심 요약 VACUUM FULL은 일반 VACUUM처럼 일부 페이지만 정리하는 작업이 아니라 테이블을 새 relfilenode로 재작성한다.
이 과정에서 살아 있는 튜플만 새 테이블로 복사되고, 새 테이블의 relfrozenxid도 최근 기준으로 갱신된다.
따라서 수행 후 age(relfrozenxid)가 1처럼 매우 낮은 값으로 보일 수 있다.
스트리밍 복제 환경에서는 이 변경이 WAL을 통해 replica에도 반영되므로 primary와 replica에서 동일하게 age가 낮아진다.

질문의 핵심은 VACUUM FULL이 freeze인지 rewrite인지다

질문에서 제시한 테스트 결과는 다음과 같은 흐름이다. primary node에서 pgbench_accounts 테이블의 age(relfrozenxid)가 434158이었고, VACUUM FULL 수행 후 age가 1로 감소했다. replica node에서도 동일하게 age가 434158에서 1로 변경됐다.

primary node

postgres=# select relname, age(relfrozenxid)
           from pg_class
           where relname = 'pgbench_accounts';

     relname      |  age
------------------+--------
 pgbench_accounts | 434158

postgres=# vacuum full pgbench_accounts;
VACUUM

postgres=# select relname, age(relfrozenxid)
           from pg_class
           where relname = 'pgbench_accounts';

     relname      | age
------------------+-----
 pgbench_accounts |   1

이 결과만 보면 VACUUM FULLVACUUM FREEZE처럼 freeze를 수행한 것인지, 아니면 Oracle의 reorg처럼 테이블을 재구성하면서 age가 초기화된 것인지가 궁금해질 수 있다.

결론부터 말하면 VACUUM FULL은 일반 VACUUM의 EAGER 또는 LAZY 처리와 동일한 관점으로 보기보다는, 테이블을 새로 재작성하는 작업으로 이해하는 것이 맞다. 그 결과 새로 만들어진 테이블의 relfrozenxid가 최근 트랜잭션 기준으로 갱신되면서 age가 매우 낮아질 수 있다.

일반 VACUUM과 VACUUM FULL의 차이

일반 VACUUM은 테이블에 남아 있는 dead tuple을 정리하고, 재사용 가능한 공간으로 표시한다. 기본적으로 테이블 파일 자체를 완전히 새로 만들지는 않는다. 그래서 일반 VACUUM은 디스크 파일 크기를 운영체제에 즉시 반환하기보다는 PostgreSQL 내부에서 재사용 가능한 공간을 확보하는 성격이 강하다.

반면 VACUUM FULL은 테이블 전체를 새 파일로 재작성한다. 살아 있는 row만 새 테이블 구조로 옮기고, 인덱스도 다시 구성된다. 이 과정에서 기존 테이블의 물리적 bloat가 제거되고, 작업이 끝나면 새 relfilenode가 기존 테이블을 대체한다.

구분 일반 VACUUM VACUUM FULL
처리 방식 기존 테이블 내부 정리 테이블 재작성
공간 반환 대부분 내부 재사용 공간 확보 물리적 파일 축소 가능
잠금 영향 상대적으로 낮음 강한 잠금 필요
relfrozenxid 변화 조건에 따라 제한적으로 전진 재작성 결과로 크게 전진 가능

실무 기준으로 보면 VACUUM FULL은 단순한 청소 작업이라기보다 테이블 재구성 작업에 가깝다. 따라서 age 변화도 일반 VACUUM의 freeze 조건만으로 해석하면 혼동이 생길 수 있다.

relfrozenxid와 age가 의미하는 것

pg_class.relfrozenxid는 해당 테이블에서 이 값보다 오래된 트랜잭션 ID가 더 이상 wraparound 위험을 만들지 않는다고 볼 수 있는 기준값이다. age(relfrozenxid)는 현재 트랜잭션 ID 기준으로 이 기준값이 얼마나 오래되었는지를 보여준다.

예를 들어 age(relfrozenxid)가 높다는 것은 해당 테이블의 freeze 기준이 오래되었다는 뜻이다. 이 값이 계속 커지면 PostgreSQL은 트랜잭션 ID wraparound를 막기 위해 더 적극적인 vacuum을 수행해야 한다.

age(relfrozenxid)는 테이블의 데이터가 오래됐다는 의미가 아니다.
테이블 안에 남아 있는 트랜잭션 ID 기준점이 현재 트랜잭션 ID와 얼마나 떨어져 있는지를 보여주는 값이다.
따라서 운영 환경에서는 테이블 bloat와 별도로 wraparound 관점의 관리 지표로 봐야 한다.

VACUUM FULL 후 age가 1로 줄어드는 이유

VACUUM FULL을 수행하면 PostgreSQL은 기존 테이블을 그대로 압축하는 것이 아니라 새 테이블 파일을 만들고 살아 있는 튜플을 다시 적재한다. 이때 새로 만들어진 relation의 freeze 기준 정보도 함께 갱신된다.

그래서 기존 테이블의 age(relfrozenxid)가 434158이었다 하더라도, VACUUM FULL 이후 새 relation의 relfrozenxid가 최근 기준으로 설정되면 age는 1처럼 낮은 값으로 나타날 수 있다.

이 동작은 “기존 테이블의 오래된 age 값이 그대로 유지된 채 공간만 줄어든다”는 방식이 아니다. 재작성된 테이블은 새로운 물리 구조를 가지며, 그 과정에서 relfrozenxid도 현재 기준에 가깝게 이동한다.

-- 확인 쿼리 예시
select
    relname,
    relfrozenxid,
    age(relfrozenxid)
from pg_class
where relname = 'pgbench_accounts';

VACUUM FREEZE와 같은 의미로 봐도 되는가

VACUUM FULL 결과로 age(relfrozenxid)가 낮아진다고 해서, 이를 일반적인 VACUUM FREEZE와 완전히 같은 작업으로 이해하면 안 된다. 두 작업은 목적과 수행 방식이 다르다.

VACUUM FREEZE는 기존 테이블을 대상으로 가능한 튜플의 트랜잭션 ID를 freeze 처리하는 데 초점이 있다. 반면 VACUUM FULL은 테이블을 새로 작성하면서 공간 회수와 물리적 재구성을 수행한다.

항목 VACUUM FREEZE VACUUM FULL
주요 목적 트랜잭션 ID wraparound 방지 bloat 제거와 테이블 물리 재작성
테이블 재작성 기본적으로 재작성하지 않음 재작성함
age 감소 freeze 범위에 따라 감소 재작성 결과로 크게 감소 가능
운영 영향 상대적으로 낮음 잠금과 I/O 영향이 큼

따라서 “VACUUM FULL이 freeze 파라미터 조건에 걸린 오래된 XID만 freeze해서 age가 줄어든다”기보다는, “테이블 재작성 과정에서 새 relation의 relfrozenxid가 최근 기준으로 갱신되어 age가 낮아진다”고 보는 편이 더 정확하다.

vacuum_freeze_min_age와 vacuum_freeze_table_age의 영향

일반 VACUUM에서는 vacuum_freeze_min_age, vacuum_freeze_table_age, autovacuum_freeze_max_age 같은 설정이 중요하다. 이 설정들은 언제 튜플을 freeze 대상으로 볼지, 언제 더 적극적인 vacuum을 수행할지에 영향을 준다.

하지만 질문의 테스트처럼 VACUUM FULL을 수행해 테이블이 재작성되는 경우에는, 일반 VACUUM이 특정 페이지나 특정 오래된 튜플만 대상으로 freeze를 수행하는 상황과 다르게 해석해야 한다.

일반 VACUUM에서는 freeze 관련 파라미터가 “어느 정도 오래된 XID를 freeze할 것인가”에 영향을 준다.
VACUUM FULL에서는 테이블 재작성 결과로 relation 기준 정보가 새로 잡히므로 age가 급격히 낮아질 수 있다.
따라서 테스트 결과의 age 1은 비정상 값이 아니라 재작성 후 자연스럽게 나타날 수 있는 값이다.

replica에서도 age가 1로 바뀌는 이유

스트리밍 복제 환경에서 primary에서 수행한 VACUUM FULL의 결과는 WAL을 통해 replica에 전달된다. VACUUM FULL은 단순히 primary 메모리 안에서만 상태를 바꾸는 작업이 아니라 relation 파일과 시스템 카탈로그 변경을 동반하는 작업이다.

따라서 primary에서 새 relfilenode가 만들어지고 pg_class의 relation 메타데이터가 변경되면, replica도 WAL replay를 통해 같은 변경을 반영한다. 그 결과 replica에서 다시 조회했을 때도 age(relfrozenxid)가 1로 보일 수 있다.

replica node

postgres=# select relname, age(relfrozenxid)
           from pg_class
           where relname = 'pgbench_accounts';

     relname      |  age
------------------+--------
 pgbench_accounts | 434158

-- primary의 VACUUM FULL 변경이 WAL replay로 반영된 뒤

postgres=# select relname, age(relfrozenxid)
           from pg_class
           where relname = 'pgbench_accounts';

     relname      | age
------------------+-----
 pgbench_accounts |   1

즉 replica에서 age가 낮아진 것은 replica가 별도로 VACUUM FULL을 실행했기 때문이 아니라, primary의 재작성 결과와 카탈로그 변경이 복제로 반영됐기 때문이다.

Oracle reorg와 비교할 때 주의할 점

Oracle의 reorg와 PostgreSQL의 VACUUM FULL은 모두 물리 구조를 정리하거나 재구성한다는 점에서는 비슷하게 보일 수 있다. 하지만 PostgreSQL에서는 MVCC와 트랜잭션 ID wraparound 관리가 강하게 연결되어 있기 때문에 relfrozenxid, age, freeze 개념까지 함께 봐야 한다.

PostgreSQL의 VACUUM FULL은 bloat 제거를 위해 테이블을 재작성하는 작업이며, 이 과정에서 새 relation의 트랜잭션 ID 기준 정보가 갱신된다. 그래서 단순히 “공간 재정렬”로만 이해하면 age가 왜 1로 바뀌는지 설명하기 어렵다.

운영 시 확인해야 할 사항

VACUUM FULL은 age를 낮추고 물리적 bloat를 제거하는 데 효과가 있을 수 있지만, 운영 중인 서비스에서는 신중하게 사용해야 한다. 테이블에 강한 잠금이 필요하고, 테이블 크기에 따라 많은 I/O와 임시 디스크 공간이 필요할 수 있다.

  • 대상 테이블의 크기와 bloat 수준을 먼저 확인한다.
  • 서비스 영향이 적은 시간대에 수행한다.
  • replica 지연이 발생할 수 있으므로 replication lag를 함께 모니터링한다.
  • age(datfrozenxid), age(relfrozenxid)를 정기적으로 점검한다.
  • 단순 wraparound 예방 목적이라면 VACUUM FREEZE 또는 autovacuum 튜닝을 먼저 검토한다.
-- 테이블별 relfrozenxid age 확인
select
    n.nspname as schema_name,
    c.relname as table_name,
    age(c.relfrozenxid) as relfrozenxid_age
from pg_class c
join pg_namespace n
  on n.oid = c.relnamespace
where c.relkind in ('r', 't', 'm')
order by age(c.relfrozenxid) desc
limit 20;

-- 데이터베이스별 datfrozenxid age 확인
select
    datname,
    age(datfrozenxid) as datfrozenxid_age
from pg_database
order by age(datfrozenxid) desc;

정리하면

질문의 테스트에서 VACUUM FULL pgbench_accounts 수행 후 primary와 replica 모두 age(relfrozenxid)가 434158에서 1로 줄어든 것은 정상적인 결과로 볼 수 있다.

핵심은 VACUUM FULL이 일반 VACUUM처럼 기존 테이블 안에서 제한적으로 정리만 하는 작업이 아니라, 살아 있는 튜플을 기반으로 테이블을 새로 재작성하는 작업이라는 점이다. 이 재작성 결과로 relation의 relfrozenxid가 최근 기준으로 갱신되면서 age가 낮아진다.

replica에서도 동일한 값이 보이는 이유는 primary에서 발생한 테이블 재작성과 시스템 카탈로그 변경이 WAL을 통해 복제되기 때문이다. 따라서 이 현상은 “replica에서 별도의 freeze가 수행됐다”기보다는 “primary의 VACUUM FULL 결과가 복제로 반영됐다”고 이해하는 것이 맞다.

반응형