PostgreSQL의 vacuum

konu·2025년 1월 26일

데이터베이스

목록 보기
4/8
post-thumbnail

(vacuum)

 

 

0. 배경

이 글에서도 밝혔듯 올해 목표는 DB 엔진 이해하기다.
그래서 그 대상이 되는 PostgreSQL의 vacuum에 대해 다루겠다.

(참고로, 이번 글도 exem의 DB 인사이드 글을 참고하여 작성했다.
따라서 이 글은 원본의 요약본 정도로 봐주었으면 하며, 더 깊은 이해를 원한다면 출처를 참조하라)

 

 

vacuum?

vacuum은 진공이라는 뜻을 가진다.
진공은 공기가 없다는 뜻이다.

PostgreSQL에서의 vacuum도 은유적으로 비슷한 의미를 지닌다.

vacuum은 데이터베이스 내 불필요한 공간(공기)을 제거하는 작업이다.

 

 

1. transaction id

목적

트랜잭션 id는 MVCC 모델을 구현하고, 트랜잭션 간 무결성을 보장하기 위해 필요하다.
또한 데이터의 생성 및 수정 시점을 반영하는 데에 사용된다.

 

예를 들어 id가 1이고 name이 fig인 user 데이터가 XID=1인 트랜잭션에 의해 저장되었다고 하자.
이 때, 이 레코드의 xmin 값은 트랜잭션 id인 1로 초기화된다.
(참고로, XID는 트랜잭션 id의 이름이다)

그리고 XID=2인 트랜잭션은 해당 레코드를 삭제한다.
이 때, 이 레코드는 실제로 삭제되지 않고 xmax의 값만 2로 초기화된다.

마지막으로 XID=3인 트랜잭션이 읽기 작업을 시작한다.
이 때, 이 레코드의 xmax 값이 null이 아니기 때문에 삭제된 것으로 간주되어 조회되지 않는다.

 

한계와 극복

한계

XID는 4바이트로 이뤄져있다. 따라서 약 43억개의 트랜잭션만 포함할 수 있으며,
그 이후의 트랜잭션에 대해서는 포기하거나 재사용할 수밖에 없다.

 

극복

위 사진과 같이 XID를 순환구조로 사용하면서 한계를 극복할 수 있다.

약 43억개의 XID에 대해, 현재 XID의
앞 절반은 이전의 트랜잭션을 저장하는 데,
뒤 절반은 이후의 트랜잭션을 저장하기 위해 사용한다.

그리고 트랜잭션이 1개 추가될 때마다 older ID window와 newer ID window가 움직여야 한다.
따라서 제일 오래된 XID는 newer ID로 사용하게 되면서 중복될 위험이 발생한다.

이러한 문제를 XID wraparound라고 한다.
이는, 뒤에서 배울 freezing을 통해 이전 트랜잭션을 처리하면서 해결할 수 있다.

 

TMI

0, 1, 2 XID는 특수한 목적으로 사용하기 때문에,
실질적으로 사용되는 XID는 3번부터라고 한다.

 

 

2. age

정의

현재 XID - 해당 XID 값이다.

위에서 밝혔듯, 임의의 트랜잭션 이후 21억개 이상 트랜잭션이 쌓일 경우 XID wraparound가 발생할 수 있다.
따라서 이 트랜잭션이 freezing의 대상인지 아닌지 식별하기 위해 age 개념이 필요하다.

 

XMIN & XMAX

  • XMIN
    : 해당 데이터를 생성한 XID

  • XMAX
    : 해당 데이터를 삭제한 XID

 

row age

row의 age는 현재 XID - XMIN을 통해 결정된다.
따라서 트랜잭션이 진행될수록 age는 지속적으로 증가한다.

그러다가 age가 VACUUM_FREEZE_MIN_AGE를 넘어서는 순간 data freezing의 대상이 된다.
다만, XMIN을 영구적인 ID로 변경하진 않고, t_infomask의 10번째 bit를 up시키며 처리한다.

VACUUM_FREEZE_MIN_AGE

PostgreSQL이 지정한 파라미터고, 기본값은 50,000,000이다.

t_infomask

tuple(= row)의 상태를 나타내기 위한 비트 마스크다.

 

table age

table의 age는 pg_class.relfrozenxid를 통해 계산할 수 있다.
그리고 당연히도, 테이블 내 그 어떤 row보다도 age가 같거나 더 많다.

또한, table은 data freezing의 대상이 아니다.
그러나 table의 age를 통해 테이블 내 row의 data freezing 여부를 결정할 수 있다.

예를 들어 위와 같이 table의 age가 VACUUM_FREEZE_MIN_AGE보다 낮다면,
당연히 모든 row의 age가 VACUUM_FREEZE_MIN_AGE 미만이므로 data freezing이 불필요하다.

 

 

3. visibility map (VM)

정의

visibility mapheap relation(= 테이블)을 구성하는 페이지의 상태를 나타내는 2bit짜리 메타데이터 파일이다.
VM 파일은 VACUUM, VACUUM FREEZE 명령 혹은 autovacuum 작업에 의해 갱신된다.

 

내용

1번째 bit (ALL_VISIBLE)

  • if 1,
    : 페이지 내 dead tuple이 없다는 뜻이다.

  • if 0,
    : 페이지 내 dead tuple이 존재하거나, vacuum 이전이므로 여부를 알지 못하는 상태다.

dead tuple?

위에서 밝혔듯, PostgreSQL은 레코드를 수정, 삭제할 때 이전 튜플(레코드)을 바로 제거하지 않는다.
다만, 이러한 튜플을 논리적으로 dead tuple로 처리한다.

 

2번째 bit (ALL_FROZEN)

  • if 1,
    : 페이지 내 모든 튜플이 data freezing을 거친 상태다.

 

사용

vacuum

ALL_VISIBLE인 경우 vacuum의 대상에서 제외된다.
모든 튜플이 data freezing도 필요하지 않고 트랜잭션에 의한 수정도 거치지 않았다는 뜻이기 때문이다.

freeze

ALL_FROZEN인 경우 freezing의 대상에서 제외된다.
모든 튜플이 freezing 과정을 거쳤다는 뜻이기 때문이다.

query

INDEX SCAN ONLY가 적용되었을 때 해당 페이지가 ALL_VISIBLE인 경우 lookup 대상에서 제외된다.
참고로, 인덱스를 타더라도 dead tuple로 인해 lookup이 필수적이다.

 

 

4. manual vacuum

의미

사용자가 직접적으로 명령어를 입력하여 vacuum 작업을 실행하는 경우를 뜻한다.
명령어에 따라 VACUUM, VACUUM FULL, VACUUM FREEZE로 나뉜다.

 

필요성

XID wraparound 현상을 방지하고 MVCC의 비효율성을 방지하고자 사용한다.

 

목표

  • 디스크 공간을 최적화하기 위해
  • 오래된 데이터를 freeze하기 위해
  • VM을 갱신하기 위해
  • 테이블에 대한 통계를 갱신하기 위해

 

디스크 공간 최적화

원인

위의 트랜잭션 ID 예시에서도 설명했듯, 삭제 트랜잭션이 커밋되더라도 즉시 삭제되진 않는다.
그리고 수정 트랜잭션은 레코드의 값을 직접 수정하지 않고 수정된 값의 레코드를 새로 생성한다.

따라서 더이상 참조되지 않는 레코드를 언젠가는 삭제해야 하는데, 이를 위해 vacuum이 필요하다.

 

작업

오래된 버전의 데이터가 여전히 사용 가능하다고 표시하거나 사용 가능하지 않으므로 OS에 반환한다.
각각 전자는 standard vacuum, 후자는 vacuum full로 칭한다.

 

standard vacuum

VACUUM 명령어에 의해 실행되며, VM을 통해 vacuum 실행 대상을 먼저 선정한 후
페이지를 순회하며 더이상 참조되지 않는 튜플을 dead tuple로 표시한다.

따라서 삭제 작업이 아니기 때문에 lock 없이 수행되므로 연산 속도가 빠르다.
다만, 삭제 작업이 아니기 때문에 디스크 공간을 실질적으로 최적화하진 않는다.

 

vacuum full

VACUUM FULL 명령어에 의해 실행되며,
live tuple을 대상으로 테이블을 새롭게 구성하고 인덱스도 이에 맞춰 재정의한다.

즉, 레코드와 인덱스에 대한 직접적인 수정이 수반되므로 테이블에 대한 독점 락을 필요로 한다.
따라서 연산 과정에서 성능에 악영향을 미칠 수 있으나 실질적으로 디스크를 최적화할 수 있다.

 

standard vacuum mode

정의

standard vacuum에는 2가지 모드가 존재한다.
바로 eagerlazy 모드다.

적용될 모드는 vacuum_freeze_table_age 파라미터에 따라 결정되며,
해당 테이블의 age가 이 파라미터를 넘을 경우 eager 모드, 넘지 못할 경우 lazy 모드가 적용된다.

 

eager vs. lazy

eager 모드는 VM의 ALL_FROZEN이 0인 모든 페이지를 대상으로 하며,
lazy 모드는 VM의 ALL_VISIBLE이 0인 모든 페이지를 대상으로 한다.

이는 eager 모드가 lazy 모드에 비해 더 넓은 범위를 대상으로 하고 있음을 뜻한다.
예시를 들어보자.

위와 같이 테이블의 age가 VACUUM_FREEZE_MIN_AGE보다 낮기 때문에 ALL_FROZEN이 0인 상태를 가정하자.
그리고 page 1과 2만 수정을 거쳐 ALL_VISIBLE이 0로 변경되었다.

이때, eager 모드는 ALL_FROZEN이 0인 모든 페이지, 즉 테이블 내 모든 페이지를 대상으로 하지만
lazy 모드는 ALL_VISIBLE이 1인 모든 페이지, 즉 page 1과 2만 대상으로 선정한다.

 

relfrozenid

eager 모드는 ALL_FROZEN이 0인, 즉 freezing이 필요할 수 있는 모든 페이지를 방문한다.
따라서 eager 모드 vacuum이 적용된 직후엔 테이블 내 freezing이 필요한 페이지가 더 이상 없다.

freezing 대상의 age는 최소 VACUUM_FREEZE_MIN_AGE이기 때문에,
eager 모드 적용 이후 relfrozenxid현재 XID - VACUUM_FREEZE_MIN_AGE로 수정된다.
또한, 테이블 내 freezing 되지 않은 모든 row 중 가장 큰 age 값이 VACUUM_FREEZE_MIN_AGE임이 보장된다.

 

vacuum freeze

정의

vacuum freeze는 vacuum standard와 정확히 동일하게 작동하지만,
VACUUM_FREEZE_MIN_AGEVACUUM_FREEZE_TABLE_AGE를 0로 초기화하고 나서 작동된다는 점이 다르다.

 

작동

VACUUM_FREEZE_TABLE_AGE가 0이기 때문에 eager 모드가 무조건 적용된다.
VACUUM_FREEZE_MIN_AGE가 0이기 때문에 모든 페이지가 freeze의 대상이 된다.

즉, 테이블 내 모든 데이터를 freeze 해야 할 때 적용할 만하다.

 

 

5. autovacuum

정의

autovacuumvacuum daemon으로 불리는 여러 프로세스에 의해 자동으로 수행되는 vacuum이다.
그리고 autovacuum launcher라고 불리는 프로세스가 워커 프로세스들을 관리하는 방식이다.

 

파라미터

AUTOVACUUM, TRACK_COUNTS 파라미터가 반드시 설정되어 있어야 한다.

그리고 autovacuum launcher가 파라미터 AUTOVACUUM_NAPTIME마다
파라미터 AUTOVACUUM_MAX_WORKERS만큼 워커 프로세스를 깨워서 autovacuum을 수행한다.

 

트리거

테이블 내 변화(삽입, 수정, 삭제) 임계치를 초과한 경우에 autovacuum의 대상으로 선정된다.
비교하는 공식은 간단하게 풀어 쓰면 다음과 같다.

  1. 수정, 삭제 임계값 + factor 값 * 전체 튜플 개수 vs. 수정, 삭제 연산 개수
  2. 삽입 임계값 + factor 값 * 전체 튜플 개수 vs. 삽입 연산 개수

 

TMI

삽입 연산은 dead tuple을 만들어내지 않기 때문에 따로 다뤄진다.

 

anti-wraparound vacuum

autovacuum이 자동으로 수행되지 않더라도 XID wraparound 현상은 막아야 한다.
그래서 테이블의 age가 VACUUM_FREEZE_MAX_AGE를 넘은 경우에는 별다른 설정 없이도 autovacuum이 수행된다.

 

 

6. 결론

이번 시간을 통해 vacuum에 대해 좀 더 자세하게 알 수 있게 되었다.
정말 간단히 정리해 보면 vacuum은,

수정 및 삭제 등에 의해 발생한 dead tuple을 정리해서 디스크를 최적화하고,
XID wraparound를 방지하기 위해 data를 freeze 처리한다.

가만 생각해보면 이 모든 게 결국은 MVCC 때문이라는 것을 ...

profile
日日是好日

0개의 댓글