[PostgreSQL 4/12] VACUUM과 TXID 프리징: 공간과 오래된 트랜잭션 관리

심대용·3일 전
post-thumbnail

이 글에서 다룰 주제

  • 공간: 행 수가 일정한데 파일이 커지는 이유는?
  • 프리징: 삭제할 행이 적어도 유지보수가 필요한 이유는?
  • 운영: 어떤 지표와 방해 요인을 함께 점검해야 하는가?

주요 단어 · VACUUM · Bloat · Autovacuum · XID Age · Freeze · Visibility Map


행 수가 일정하다고 파일 크기까지 일정한 것은 아니다. 반대로 dead tuple이 거의 없다고 유지보수가 필요 없는 것도 아니다. PostgreSQL의 유지보수에는 더 이상 필요하지 않은 버전의 공간을 재사용하는 일과 오래된 트랜잭션 참조를 안전하게 처리하는 일이 함께 들어 있다. VACUUM을 단순한 파일 압축 도구로 보면 이 두 문제를 놓치기 쉽다.

VACUUM은 불필요한 버전 정리와 공간 재사용, 프리징 등을 수행한다. Freeze는 충분히 오래된 생성 트랜잭션을 일반 XID 비교의 대상에서 안전하게 제외하는 처리다.

자료와 예제 기준 — PostgreSQL 18을 중심으로 개인 학습 노트를 재구성했다. SQL·실행 계획·설정값은 설명 및 재현용 예제이며 이 글을 위해 운영 DB에서 새로 측정한 결과는 아니다. DDL/DML 예제는 독립적인 테스트 환경에서 사용한다.

1. Heap과 Index는 따로 부푼다

행 수가 그대로여도 이전 버전과 비효율적인 빈 공간 때문에 파일이 커질 수 있다.

구분Table BloatIndex Bloat
대상heap 페이지인덱스 페이지
원인 예정리 지연, 공간 재사용 불균형이전 TID 엔트리, 낮은 페이지 밀도
영향테이블 스캔·캐시 효율 저하인덱스 스캔·캐시 효율 저하
HOT와 관계새 버전은 여전히 생성됨새 일반 인덱스 엔트리 생성을 줄임

non-HOT UPDATE에서는 키 값이 그대로여도 새 TID를 가리키는 인덱스 엔트리가 필요할 수 있다. PK의 논리적 유일성을 지키면서 여러 MVCC 버전에 대응하는 물리 엔트리가 존재할 수 있다.

dead bytes + free bytes를 곧바로 OS에 반환 가능한 공간으로 해석하지 않는다. fillfactor 여유, 오버헤드, 페이지별 분포, 곧 재사용될 공간을 구분해야 한다.

Dead 비율 ≠ bloat 비율 — n_dead_tup은 행 개수 추정치다. 행 크기·빈 공간·인덱스·TOAST까지 설명하지 못한다.

빈 공간의 해석 — 곧 재사용하거나 fillfactor로 의도한 여유라면 모두 낭비는 아니다. 추이와 조회 비용을 함께 본다.

인덱스의 자체 관리 — B-tree에는 불필요한 엔트리 삭제·중복 제거 최적화도 있다. UPDATE마다 파일이 반드시 커지지는 않는다.

기억할 문장: 공간 크기보다 “다시 쓰이는가, 읽기 비용이 커지는가”를 확인하자.

2. 재사용할까, 파일을 줄일까?

세 방법의 선택 기준은 공간 반환의 필요성과 잠금 허용 시간이다.

기준VACUUMVACUUM FULLpg_repack
방식기존 페이지 정리전체 재작성복사·변경 반영·교체
목적내부 공간 재사용파일 축소온라인 재구성·축소
일반 접근대부분 읽기·쓰기 가능대상 테이블 접근 차단대부분 가능, 시작·마무리 강한 잠금
추가 공간전체 복사본 불필요새 파일 공간 필요복사본·인덱스·변경 로그 등 필요
선택일상 유지보수점검 시간 확보중단이 어려운 테이블

일반 VACUUM도 파일 끝 빈 페이지를 반환할 때 강한 잠금을 사용할 수 있다. TRUNCATE FALSE는 이 동작을 생략한다. 일반 VACUUM은 보통 파일 전체를 축소하지 않는다.

pg_repack은 트리거로 변경을 기록하고, 복사본에 적용한 뒤 교체한다. --no-kill-backend는 잠금 충돌 시 다른 쿼리·세션을 강제로 취소·종료하는 기본 정책을 피하는 데 사용한다. 설치 버전과 옵션을 확인한다. FULL과 repack 이후에도 autovacuum이 필요하다.

일반 VACUUM — 인덱스 정리 후 DEAD 슬롯을 재사용 가능하게 할 수 있다. 프리징·Visibility Map도 관리한다.

추가 디스크 — FULL과 repack은 기존 파일을 유지한 채 새 파일을 만든다. 디스크가 가득 찬 상황의 즉시 해법이 아니다.

pg_repack 조건 — 전체 테이블 재구성에는 PK 또는 적격 NOT NULL UNIQUE 키가 필요하다. 확장·클라이언트 버전도 맞춘다.

기억할 문장: 인덱스만 비대하면 REINDEX CONCURRENTLY를 별도로 검토한다.

3. 공간 재사용과 autovacuum

VACUUM과 autovacuum

1 제거 가능한 시점을 판단해야 한다

최신 버전이 아니라는 이유만으로 이전 버전을 제거할 수 없다. 오래된 스냅샷에서 여전히 필요할 수 있기 때문이다. 더 이상 필요하지 않은 버전을 정리해 공간을 재사용한다.

기능결과
불필요한 튜플·인덱스 엔트리 정리재사용 공간 확보
Visibility Map 관리Index-only scan에서 Heap 확인 감소
Freeze오래된 XID의 순환 문제 방지
FSM 갱신새 튜플이 들어갈 여유 페이지 탐색 지원

VACUUM 자체도 영구 저장 구조를 바꾸므로 WAL과 Dirty page를 만들 수 있다. ‘청소는 I/O가 없다’고 가정하면 안 된다.

2 일반 VACUUM과 FULL

항목VACUUMVACUUM FULL
방식기존 파일에서 버전 정리새 파일로 재작성
공간주로 내부 재사용압축해 OS에 반환
DML일반 읽기·쓰기와 병행 가능ACCESS EXCLUSIVE로 차단
추가 공간전체 재작성 복사본 불필요새 테이블·인덱스를 위한 여유 필요
운영 목적지속적인 유지보수대규모 영구 감소 후 공간 회수

일반 VACUUM도 테이블 끝의 완전히 빈 페이지를 잘라내면 OS에 공간을 반환할 수 있다. 이 과정에는 강한 잠금이 필요할 수 있다. 따라서 ‘절대 파일이 줄지 않는다’, ‘어떤 잠금도 없다’는 설명은 부정확하다.

FULL은 특정 인덱스 순서로 정렬하는 기능이 아니다. 새 파일을 만들 공간이 필요하므로 디스크가 꽉 찬 상태에서 무조건 실행하는 해결책도 아니다. Autovacuum은 FULL을 자동으로 실행하지 않는다.

3 VACUUM과 ANALYZE

VACUUM orders;
ANALYZE orders;
VACUUM (ANALYZE) orders;

ANALYZE는 컬럼 분포·히스토그램·빈도 등 플래너 통계를 수집한다. VACUUM도 일부 관계 통계를 갱신하지만 ANALYZE를 대체하지는 않는다. 파티션 부모의 통계는 자식 변경만으로 자동 갱신되지 않는 경우가 있어 별도 ANALYZE를 검토한다.

4 Visibility Map과 Freeze

일반 인덱스 엔트리만으로 MVCC 가시성을 완전히 알 수 없다. Index-only scan도 대상 페이지가 all-visible이 아니면 Heap을 확인할 수 있다. 따라서 인덱스가 모든 컬럼을 포함해도 Heap Fetches가 발생한다.

Freeze는 행을 수정 불가능하게 만드는 기능이 아니다. 오래된 생성 트랜잭션을 모든 적절한 미래 스냅샷보다 과거인 것으로 취급하도록 표시한다. 내부 XID의 순환에 대비하는 유지보수이므로 UPDATE·DELETE가 거의 없는 테이블도 필요하다.

5 Autovacuum의 실행과 처리 능력

기본적인 변경량 조건:

base threshold + scale factor × estimated tuples

PostgreSQL 18에서는 최대 임계값 설정도 적용된다. INSERT 기반 조건과 Freeze 관련 조건은 별도로 존재한다.

1,000만 행, base=50, scale=0.2이면 약 2,000,050개의 obsolete tuple 발생 조건을 계산한다. 이는 실행 가능성을 판단하는 기준이지 즉시 시작·완료 보장이 아니다.

-- 부하 측정 후 조정할 교육용 예시
ALTER TABLE orders SET (
    autovacuum_vacuum_threshold = 1000,
    autovacuum_vacuum_scale_factor = 0.01,
    autovacuum_analyze_scale_factor = 0.02
);

임계값을 낮추면 더 자주 처리하지만 Worker·I/O·잠금 제약을 함께 봐야 한다. 오래된 스냅샷 때문에 제거 불가능한 버전은 Worker만 늘려도 정리되지 않는다. 복제 피드백, Slot이 유지하는 xmin, prepared transaction 등도 보존 경계에 영향을 줄 수 있다.

4. 오래된 미동결 XID를 관리해야 하는 이유

프리징 자체가 위험한 기능은 아니다. 오래된 미동결 XID가 계속 남아 있으면 번호 비교의 안전 범위를 벗어날 수 있고, PostgreSQL은 이를 막기 위해 신규 XID 발급을 차단할 수 있다. 애플리케이션에서는 INSERT·UPDATE 등의 실패로 나타난다.

이 문제의 시계는 날짜가 아니라 XID 소비량으로 움직인다. 부하가 높은 시스템은 같은 시간을 운영해도 훨씬 빨리 위험 구간에 접근한다.

일반적인 서버 재시작으로 XID 나이나 미동결 데이터가 초기화되지는 않는다. 유지보수 경계가 실제로 전진해야 한다.

5. MVCC와 XID의 관계

PostgreSQL은 UPDATE 시 새로운 행 버전을 만들고, 트랜잭션 스냅샷에 따라 보이는 버전을 결정한다.

항목의미주의할 점
xmin해당 행 버전을 생성한 트랜잭션원래 숫자만으로 동결 상태를 판정할 수 없음
xmax삭제·갱신 또는 행 잠금 관련 정보0이 아닌 값이 있다고 무조건 삭제된 행은 아님
Snapshot현재 조회에서 보이는 트랜잭션 범위와 진행 중 트랜잭션 정보단순 숫자 비교만으로 가시성이 결정되지는 않음
xid내부 32비트 트랜잭션 식별자번호가 순환함
xid8epoch를 포함한 64비트 식별자내부 튜플의 프리징 필요성을 없애지는 않음
Virtual XID트랜잭션을 식별하는 가상 식별자영구 XID와 구분

예를 들어 xmin = 500인 행 버전은 XID 500이 생성했다는 뜻이다. PostgreSQL은 그 트랜잭션의 커밋 여부, 스냅샷과의 관계, 삭제·갱신 상태 등을 함께 판단한다.

영구 XID는 일반적으로 최초 쓰기 시 발급된다. 따라서 SELECT 요청 수나 전체 커밋 건수를 XID 소비량과 동일하게 취급하면 안 된다. 서브트랜잭션이나 XID 할당을 유발하는 함수도 측정에 영향을 줄 수 있다.

XID 카운터는 같은 PostgreSQL 클러스터의 모든 데이터베이스가 공유한다. 한 테이블이 변경되지 않아도 다른 곳의 쓰기로 그 테이블의 XID 나이가 증가할 수 있다. 여기서 클러스터는 Kubernetes 클러스터가 아니라 하나의 PostgreSQL 데이터 디렉터리로 관리되는 데이터베이스 집합을 뜻한다.

6. 42.9억 번호 공간과 21.5억 비교 한계

32비트 번호 공간은 2^32 = 4,294,967,296, 약 42.9억이다. 일부 특수 XID는 예약되어 있지만 전체 크기를 이해할 때는 이 숫자로 생각하면 된다.

그러나 과거·미래 비교가 가능한 범위는 대략 절반인 2^31 = 2,147,483,648, 약 21.5억이다. 순환하는 번호 공간에서 한쪽 반 바퀴를 과거, 다른 반 바퀴를 미래로 해석하기 때문이다.

1 0~99 번호표로 축소한 예

생성 번호가 20인 행이 프리징되지 않은 채 남아 있다고 가정한다.

현재 번호생성 번호앞으로 진행한 거리해석
302010생성 번호는 과거
602040아직 과거로 비교 가능
802060반 바퀴를 넘어 정상적인 과거 해석이 불가능

즉, 최댓값에서 0으로 돌아가는 순간만 감시하면 안 된다. 현재 XID와 가장 오래된 미동결 XID 경계 사이의 거리가 중요하다.

XID 순환은 정상적인 장기 운영에서도 일어난다. 위험한 상황은 순환 자체보다 오래된 XID 참조를 안전하게 처리하지 못한 상태다.

7. 프리징의 실제 의미

VACUUM·FULL·pg_repack의 목적

학습 자료의 개념도 — VACUUM·FULL·pg_repack의 목적. 세부 조건은 본문 설명을 함께 읽는다.

프리징 전에는 행 생성 트랜잭션의 상태와 가시성을 일반 XID 규칙으로 판단한다. 프리징 후에는 생성 트랜잭션을 모든 일반 XID보다 오래된 것으로 취급한다.

구분행 생성 트랜잭션의 의미
프리징 전‘XID 500이 만든 행’이라는 일반 트랜잭션 참조
프리징 후‘충분히 오래전에 커밋된 생성 트랜잭션’으로 확정

개념적으로는 FrozenTransactionId로 취급한다. PostgreSQL 9.4 이후에는 원래 xmin을 보존하고 튜플 헤더의 동결 플래그로 표현하므로, 프리징 후에도 SELECT xmin 결과가 500으로 남을 수 있다.

프리징은 생성 트랜잭션의 가시성 문제를 해결하는 것이다. 행이 이후 삭제되거나 갱신되면 그 상태에 맞는 가시성 판단은 계속 필요하다. 데이터가 영구적으로 모든 조회에 보인다거나 수정 불가능해진다는 뜻이 아니다.

진행 중인 트랜잭션과 오래된 스냅샷 등이 안전한 처리 범위를 제한한다. FREEZE 옵션도 이 안전 조건을 무시하지 않는다.

8. VACUUM과 Visibility Map

1 VACUUM이 맡는 서로 다른 역할

작업해결하려는 문제
죽은 튜플 정리더 이상 필요 없는 행 버전이 차지하는 공간
프리징오래된 트랜잭션 참조와 번호 순환
Visibility Map 갱신페이지 상태를 이용한 조회·유지보수 최적화
ANALYZE플래너 통계 갱신. 별도 명령이며 VACUUM과 함께 실행 가능

따라서 dead tuple이 거의 없다는 사실만으로 프리징이 불필요하다고 결론 내릴 수 없다.

2 all-visible과 all-frozen

상태의미활용
all-visible페이지의 튜플이 모든 관련 트랜잭션에 보이는 상태Index Only Scan의 heap 접근 생략에 활용
all-frozen페이지의 튜플이 모두 동결된 상태동결을 위한 재검사 생략에 활용

all-visible이라고 all-frozen인 것은 아니다. 일반 VACUUM은 일부 페이지를 건너뛸 수 있지만, aggressive VACUUM은 미동결 XID가 있을 수 있는 페이지까지 확인한다. 이미 all-frozen인 페이지는 보통 다시 검사할 필요가 없다.

PostgreSQL 18에는 일반 VACUUM 중 일부 all-visible 페이지를 미리 동결하려는 eager freezing도 있다. 세부 검사 정책은 버전에 따라 다르므로, ‘일반 VACUUM은 프리징을 전혀 하지 않는다’고 이해하면 안 된다.

3 VACUUM FREEZE와 VACUUM FULL

-- 예시: 지정한 테이블을 적극적으로 동결
VACUUM (FREEZE, VERBOSE) public.target_table;

FREEZE는 동결 관련 나이 기준을 낮추어 안전하게 처리 가능한 튜플을 적극적으로 동결한다. VACUUM FULL은 테이블을 재작성하여 공간을 축소하는 작업으로, 목적과 잠금 비용이 다르다. XID 위험 대응에서 FULL을 기본 처방으로 선택하지 않는다.

일반 VACUUM은 보통 읽기·쓰기와 함께 실행할 수 있지만 I/O를 사용하고 DDL 등과 충돌할 수 있다. 테이블 끝의 빈 페이지를 잘라내는 단계에서는 더 강한 잠금이 필요할 수 있다.

9. 자동 유지보수 설정과 트레이드오프

아래는 PostgreSQL 18 기본값이다. 단위는 초·일·행 수가 아닌 XID 나이다.

설정기본값역할
vacuum_freeze_min_age50,000,000프리징 여부를 결정하는 나이 기준
vacuum_freeze_table_age150,000,000실행되는 VACUUM이 적극적인 검사 전략을 선택하는 기준
autovacuum_freeze_max_age200,000,000wraparound 방지용 autovacuum 실행 대상이 되는 기준
vacuum_failsafe_age1,600,000,000VACUUM이 비상 전략으로 동결 진전을 우선하는 기준

2억은 쓰기 중단 한계가 아니라 예방 작업을 시작하는 기준이다. vacuum_freeze_table_age는 별도의 스케줄러가 아니며, VACUUM의 실행 방식에 영향을 준다.

failsafe는 비용 기반 지연을 중단하고 인덱스 정리 등 일부 부가 작업을 건너뛰어 위험을 줄이려 한다. 설정 간에는 유효 범위 보정도 있으므로 숫자를 각각 독립적으로 해석하지 않는다.

조정 방향기대 효과비용·주의점
더 일찍 프리징나중에 몰리는 작업량 완화곧 변경될 행까지 처리하면 불필요한 작업 증가
방지용 실행 기준을 늦춤일부 테이블의 강제 유지보수 빈도 감소상태 보존량 증가, 대응 여유와 처리량 검토 필요
VACUUM 처리량 확대오래된 XID 경계가 빨리 전진서비스 I/O·캐시 사용과 경쟁

autovacuum을 꺼도 wraparound 방지용 프로세스는 실행될 수 있다. 그러나 이 마지막 보호 장치에만 의존하는 운영은 바람직하지 않다.

10. 프리징이 밀리는 원인

1 오래 열린 트랜잭션

장기 트랜잭션이나 오래된 스냅샷은 VACUUM의 안전한 정리 경계를 붙잡을 수 있다. 단순히 연결이 오래 살아 있다는 것과 오래된 XID·스냅샷을 보유한다는 것은 구분해야 한다.

특히 idle in transaction은 쿼리를 실행하지 않더라도 트랜잭션이 끝나지 않은 상태다. 실제 영향은 backend_xid, backend_xmin, 잠금 상태를 함께 보고 판단한다.

2 Prepared transaction

2단계 커밋의 prepared transaction은 일반 세션이 종료되어도 남을 수 있다. 업무 결과를 확인하고 COMMIT 또는 ROLLBACK해야 하며, 단순히 오래되었다는 이유로 일괄 처리해서는 안 된다.

3 복제 슬롯

슬롯 필드주로 붙잡는 대상
restart_lsn재시작에 필요한 WAL
xminVACUUM의 사용자 데이터 정리 경계
catalog_xmin시스템 카탈로그 정리 경계

WAL 디스크 증가와 XID 프리징 정체는 관련될 수 있지만 같은 문제는 아니다. 슬롯의 실제 필드와 소비자 상태를 확인한다.

4 처리량 부족과 잠금

대형 테이블, 느린 스토리지, 제한된 worker·I/O 예산, 충돌하는 잠금, 반복 취소는 유지보수를 지연시킨다. ‘VACUUM이 실행 중’이라는 사실보다 완료 후 경계가 전진하는지가 중요하다.

11. 읽기 전용 점검 SQL

아래 쿼리는 진단 예시다. 일부 세션 정보는 pg_monitor 등 적절한 권한이 필요하다. 데이터베이스 전체 진단과 테이블 진단의 범위를 구분한다.

1 실제 설정 확인

SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN (
    'autovacuum',
    'autovacuum_freeze_max_age',
    'vacuum_freeze_min_age',
    'vacuum_freeze_table_age',
    'vacuum_failsafe_age'
)
ORDER BY name;

테이블별 storage parameter가 전역 설정을 덮어쓸 수 있다는 점도 확인한다.

2 데이터베이스별 XID 나이

SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;

datfrozenxid는 해당 DB에 남은 미동결 XID의 하한 경계다. 사용자 테이블뿐 아니라 카탈로그 등도 영향을 준다.

3 현재 DB의 테이블과 TOAST 나이

SELECT
    c.oid::regclass AS table_name,
    age(c.relfrozenxid) AS main_xid_age,
    age(t.relfrozenxid) AS toast_xid_age,
    GREATEST(age(c.relfrozenxid), age(t.relfrozenxid)) AS xid_age,
    pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
    c.reloptions
FROM pg_class AS c
LEFT JOIN pg_class AS t ON t.oid = c.reltoastrelid
WHERE c.relkind IN ('r', 'm')
ORDER BY xid_age DESC
LIMIT 30;

relfrozenxid는 그보다 오래된 XID가 안전하게 처리되었다고 보장하는 경계다. 원래 xmin 값의 단순 최솟값과 다르다. total_size는 인덱스 등을 포함하므로 실제 heap 스캔량과 동일하지 않다. 파티션 부모보다 실제 데이터를 담는 자식 테이블의 상태를 확인한다.

4 오래된 트랜잭션·스냅샷 후보

SELECT
    pid, datname, usename, application_name, state,
    now() - xact_start AS transaction_duration,
    backend_xid, age(backend_xid) AS xid_age,
    backend_xmin, age(backend_xmin) AS xmin_age,
    wait_event_type, wait_event,
    left(query, 200) AS query_sample
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
  AND (backend_xid IS NOT NULL OR backend_xmin IS NOT NULL
       OR state = 'idle in transaction')
ORDER BY GREATEST(age(backend_xid), age(backend_xmin))
         DESC NULLS LAST;

이 결과는 조사 후보 목록이다. 나이가 높다고 무조건 세션을 종료하지 않고 작업 목적과 영향 범위를 확인한다.

5 Prepared transaction과 복제 슬롯

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

SELECT slot_name, slot_type, database, active,
       xmin, age(xmin) AS xmin_age,
       catalog_xmin, age(catalog_xmin) AS catalog_xmin_age,
       restart_lsn, confirmed_flush_lsn
FROM pg_replication_slots;

6 진행 중인 VACUUM

SELECT pid, datname, relid, phase,
       heap_blks_total, heap_blks_scanned, heap_blks_vacuumed
FROM pg_stat_progress_vacuum;

heap 스캔 비율만으로 전체 완료 시간을 확정하지 않는다. 이후 인덱스 작업 등 다른 단계가 남을 수 있다.

12. 모니터링과 장애 대응

1 대시보드에서 함께 볼 것

항목질문
DB·테이블 최대 XID 나이어디에 가장 오래된 경계가 남았는가?
XID 소비 속도여유가 얼마나 빠르게 줄어드는가?
VACUUM 완료 전후 나이실제로 유지보수가 진전했는가?
장기 트랜잭션·슬롯경계를 붙잡는 대상이 있는가?
VACUUM 시간·잠금·I/O처리량 문제인가, 대기 문제인가?

age / autovacuum_freeze_max_age는 예방 작업 기준 대비 비율이지 실제 고갈률이 아니다. 반대로 age / 2^31도 운영 중단까지의 정확한 비율은 아니다. PostgreSQL은 이론적 한계 전에 안전 여유를 두고 동작한다.

2 남은 시간 추정

예상 여유 시간(초)
≈ (운영상 정한 위험 경계 - 현재 XID 나이) / 초당 XID 소비량

가상의 여유가 10억 XID이고 소비량이 초당 1만이라면:

1,000,000,000 / 10,000 = 100,000초 ≈ 27.8시간

이 계산은 그동안 동결 경계가 전진하지 않고 소비 속도가 일정하다는 가정이다. 단일 측정값을 SLA처럼 사용하지 않는다. 실제 경보는 XID 나이, 소비량 추세, VACUUM의 예상 완료 시간과 함께 설계한다.

3 대응 순서

수동 처리 예시는 다음과 같다. BEGIN 블록 밖에서 적절한 권한으로 실행한다.

VACUUM (FREEZE, VERBOSE) public.target_table;

일반 VACUUM으로 충분한 경우도 있다. 동결이 불가능한 경계를 붙잡는 원인이 남아 있다면 FREEZE만 반복해도 해결되지 않는다.

신규 XID 발급이 차단된 상황에서는 서버 오류 메시지와 해당 버전의 공식 절차를 따른다. 읽기와 VACUUM은 가능한 경우가 많다. 문제 DB 전체와 시스템 카탈로그까지 처리할 수 있는 권한이 중요하다. 현대 버전의 일반적인 복구에서는 무조건 재시작하거나 single-user 모드로 전환할 필요가 없다.

피해야 할 대응은 다음과 같다.

  • 원인 조사 없이 임계값만 올려 문제를 숨기기.
  • wraparound 방지 VACUUM을 반복 취소하기.
  • 복제 소비자 영향 확인 없이 슬롯을 삭제하기.
  • 세션·prepared transaction을 일괄 종료하거나 롤백하기.
  • VACUUM FULL을 XID 장애의 기본 해결책으로 사용하기.
  • pg_xact 같은 내부 파일을 수동 삭제하기.

13. 번역 작업 플랫폼에 적용하기

FastAPI·Celery·PostgreSQL 기반 번역 플랫폼을 가정한 설계 예시다. 실제 운영 진단 결과는 아니다.

대상위험 시나리오설계·운영 포인트
translation_jobs빈번한 상태 변경으로 XID 소비와 dead tuple 증가autovacuum 처리량·HOT·bloat를 함께 관찰
언어 코드·정책 테이블거의 변경되지 않아 유지보수 관심에서 누락크기·변경량과 별개로 XID 나이를 감시
번역 본문·결과 TOAST본문 테이블만 점검하여 오래된 TOAST를 놓침테이블·TOAST 나이를 함께 조회
Celery 작업DB 트랜잭션을 연 채 긴 LLM 호출 대기상태 저장과 외부 호출을 분리하고 트랜잭션을 짧게 유지
CDC·논리 복제중단된 소비자의 슬롯이 오래된 경계 유지소비자 상태와 xmin·catalog_xmin·WAL을 구분 감시

예를 들어 ‘작업 상태를 RUNNING으로 저장 → 커밋 → LLM 호출 → 결과를 별도 짧은 트랜잭션으로 저장’하는 흐름을 고려할 수 있다. 이때 재시도·중복 실행·부분 실패를 처리할 idempotency와 상태 전이 규칙도 함께 설계한다.

14. 오해 정리와 복습 질문

오해올바른 이해
프리징하면 DB가 멈춘다프리징은 XID 순환 장애를 예방한다
42.9억까지는 아무 문제가 없다오래된 미동결 XID와의 비교 한계는 약 21.5억
2억에 도달하면 쓰기가 중단된다기본 방지용 autovacuum 기준이다
프리징된 행은 수정할 수 없다UPDATE·DELETE 가능하며 새 행 버전은 새 정보를 가짐
xmin이 2가 아니면 미동결이다현대 버전은 플래그로 동결을 표시할 수 있음
SELECT 1회마다 영구 XID 1개를 소비한다일반 읽기와 영구 XID 할당은 다름
변경 없는 테이블은 안전하다클러스터의 다른 쓰기로 나이가 증가할 수 있음
VACUUM 실행 이력이 있으면 안전하다완료 후 동결 경계가 전진했는지 확인해야 함
64비트 xid8이 프리징을 대체한다내부 32비트 XID 처리 필요성은 남아 있음

복습 질문

  1. 번호 공간은 약 42.9억인데 왜 비교 한계는 약 21.5억인가?
  2. dead tuple이 거의 없는 테이블에도 왜 프리징이 필요한가?
  3. all-visible과 all-frozen의 차이는 무엇인가?
  4. autovacuum_freeze_max_age 대비 비율을 ‘실제 고갈률’이라고 부르면 왜 부정확한가?
  5. VACUUM이 실행 중인데도 XID 나이가 계속 증가할 때 무엇부터 확인해야 하는가?

이어서 학습할 질문

  • 장기 트랜잭션이 VACUUM과 프리징을 제한하는 과정을 xmin과 스냅샷으로 설명해줘.
  • Visibility Map의 all-visible과 all-frozen을 Index Only Scan과 연결해 설명해줘.
  • PostgreSQL XID 위험을 감지하는 Prometheus·Grafana 지표와 경보를 설계해줘.
  • MultiXact wraparound는 일반 XID wraparound와 어떻게 다른지 설명해줘.

자료 기준과 참고 문서

개인 PostgreSQL 학습 노트를 바탕으로 정리했다. 첨부 그림은 제공된 학습 자료를 사용했으며, 버전이나 설정에 따른 조건은 본문에 덧붙였다.

이어서 읽기 · ← 이전 편 · 다음 편 → · 전체 시리즈 목차

profile
어제보다 더 성장하는 나

0개의 댓글