• database
  • postgresql

PostgreSQL Vacuum 이해와 AutoVacuum 튜닝

PostgreSQL의 MVCC에서 Dead Tuple이 쌓이는 이유와 Vacuum 동작, AutoVacuum 실행 조건과 튜닝을 정리한다.

시리즈 · Database8 / 8
  1. E/R 모델 설계 원칙 정리
  2. 데이터베이스 키의 종류와 UNIQUE 제약
  3. 인덱스와 B-Tree, 그리고 스캔 방식
  4. JOIN의 종류와 구현 방식
  5. 트랜잭션의 ACID와 DBMS의 복구 전략: UNDO, REDO, WAL
  6. 데이터베이스 Lock: 공유 락·배타 락, 낙관적 락·비관적 락
  7. DB 클러스터링, 레플리케이션, 샤딩과 분산 트랜잭션
  8. PostgreSQL Vacuum 이해와 AutoVacuum 튜닝

PostgreSQL에는 다른 RDBMS와 달리 Vacuum이라는 개념이 있다. 간단히 말하면 더 이상 사용되지 않는 데이터를 정리하는 명령이다.

Dead Tuple이 생기는 이유

PostgreSQL은 UPDATE나 DELETE를 수행할 때 디스크에 있던 기존 데이터를 그 자리에서 갱신하거나 삭제하지 않는다. UPDATE는 다음과 같이 진행된다.

  1. FSM(Free Space Map, 사용 가능한 공간을 표시하는 맵)에서 사용 가능한 공간을 확인하고, 없으면 공간을 추가로 확보한다.
  2. 그 공간에 변경된 데이터를 새로 기록한다(INSERT).
  3. 기록이 끝나면 원본 Tuple을 가리키던 포인터를 새 Tuple로 옮긴다.

이 과정을 거치면 기존 데이터는 어디에서도 참조되지 않는 Tuple이 된다. 이것이 Dead Tuple이다.

이렇게 번거롭게 동작하는 이유는 PostgreSQL의 MVCC(Multi-Version Concurrency Control) 구현 방식 때문이다.

PostgreSQL의 MVCC

  • UPDATE가 발생하면 새로운 버전의 Tuple을 추가로 쌓는다.
  • 어느 버전을 볼지는 Tuple에 저장된 Transaction ID인 xmin, xmax를 비교해 판단한다.

UPDATE 시 기존 Tuple의 xmax가 채워지고 새 Tuple이 xmin과 함께 추가되는 과정

예를 들어 Tuple이 다음과 같다고 하자.

xmin xmax value
2010 2020 AAA
2012 0 BBB
2014 2030 CCC
2020 0 ZZZ
  • Transaction ID 2015는 AAA, BBB, CCC를 볼 수 있다. ZZZ는 xmin이 2020으로 2015보다 크므로 볼 수 없다.
  • Transaction ID 2021은 BBB, CCC, ZZZ를 볼 수 있다. AAA는 xmax가 2020이라 그때까지만 존재하던 값이므로 볼 수 없다.

결국 데이터가 변경될수록 Dead Tuple이 늘어난다. 쓸모없는 데이터까지 페이지 단위로 읽어 메모리에 올리게 되므로 성능이 떨어진다.

Vacuum과 Vacuum Full

Vacuum (AutoVacuum) Vacuum Full
공간 처리 Dead Tuple이 있던 공간을 FSM에 반환해 재사용만 가능하게 한다. OS에 디스크 공간을 반환하지는 않는다. OS에 디스크 공간까지 반환한다.
잠금 운영 중 수행 가능 테이블에 락이 걸려 운영 중에는 수행하기 어렵다.
디스크 추가 공간 불필요 대상 테이블을 복사하는 방식이라 여유 공간이 필요하다.

운영 중에 Vacuum Full의 효과가 필요하다면 오픈 소스인 pg_repack을 대안으로 쓸 수 있다.

-- DB 전체 Full 실행
VACUUM FULL ANALYZE;

-- DB 전체 일반 실행
VACUUM VERBOSE ANALYZE;

-- 특정 테이블만 일반 실행
VACUUM ANALYZE [테이블명];

-- 특정 테이블만 Full 실행
VACUUM FULL [테이블명];

Transaction ID Wraparound

Transaction ID는 약 40억 개를 표현할 수 있고, 현재 트랜잭션을 기준으로 20억 개는 과거, 20억 개는 미래로 취급한다.

Transaction ID를 계속 소비해 한 바퀴를 돌면, 오래된 데이터가 미래에 있는 것처럼 취급되어 보이지 않게 된다. 이 현상이 Transaction ID Wraparound다.

이를 막기 위해 오래된 Tuple의 Transaction ID를 frozen XID(= 2)라는 특별한 값으로 바꾼다. 이 동작을 freeze 또는 Anti-Wraparound Vacuum이라고 한다. AutoVacuum을 꺼 두더라도 관련 임계치를 초과하면 DB가 강제로 Vacuum을 수행한다.

AutoVacuum 실행 조건

Dead Tuple 누적치가 임계치에 도달했을 때

vacuum threshold = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * number of tuples

기본값 기준으로 Dead Tuple이 테이블 전체 행의 20% + 50개를 초과하면 AutoVacuum이 실행된다.

테이블의 age가 임계치에 도달했을 때

  • age가 autovacuum_freeze_max_age(기본 2억)를 초과한 경우
  • vacuum_freeze_table_age < 테이블의 age < autovacuum_freeze_max_age 범위에 있으면서 해당 테이블에 Vacuum이 호출된 경우

AutoVacuum 튜닝

Vacuum이 드물게 수행될수록 정리할 Dead Tuple과 freeze 작업이 쌓여 한 번에 큰 부하를 유발한다. 그렇다고 너무 자주 수행하면 DDL과 겹쳐 장애 상황이 생길 수 있다. Dead Tuple 정리와 Vacuum 부하 사이에서 균형을 찾아야 한다.

실행 빈도

파라미터 기본값 권장
autovacuum_vacuum_threshold 50 50
autovacuum_vacuum_scale_factor 0.2 0.1

실행 속도

파라미터 기본값 권장
autovacuum_vacuum_cost_limit 200 1000 또는 200 * (autovacuum_max_workers / 3)
autovacuum_vacuum_cost_delay 20ms (PostgreSQL 12부터 2ms) 5ms
  • autovacuum_vacuum_cost_limit: AutoVacuum이 한 번에 쓸 수 있는 credit
  • autovacuum_vacuum_cost_delay: credit을 모두 소모한 뒤 쉬는 시간

credit을 다 쓰면 delay만큼 쉬었다가 다시 작업한다. credit이 너무 작으면 정리 속도가 Dead Tuple이 쌓이는 속도를 따라가지 못해 Dead Tuple이 계속 누적된다.

증상별로 볼 파라미터

AutoVacuum이 너무 드물게 돈다면

  • autovacuum_vacuum_scale_factor, autovacuum_vacuum_threshold
  • autovacuum_vacuum_insert_scale_factor, autovacuum_vacuum_insert_threshold

AutoVacuum이 너무 느리다면

  • autovacuum_vacuum_cost_delay, autovacuum_vacuum_cost_limit
  • autovacuum_naptime
  • autovacuum_max_workers
  • autovacuum_work_mem

PostgreSQL 모니터링 메모

우아한테크세미나의 PostgreSQL 모니터링 발표에서 참고할 만한 기준을 옮겨 둔다.

Aurora PostgreSQL 알람 기준

지표 Warning Critical
CPU Utilization 50 이상 70 이상
DML Latency 200 이상 500 이상
Select Latency 200 이상 500 이상
Commit Latency 20 이상 50 이상
Maximum Used Transaction IDs - 50,000,000 이상
Database Connections 인스턴스 스펙에 따라 다름

PostgreSQL은 슬로우 쿼리를 포함한 모든 로그가 postgresql.log 하나에 쌓인다. 따라서 필요한 이벤트를 키워드로 필터링하고, 그 키워드를 count하는 metric을 만들어 알람을 건다.

대상 필터링 키워드
vacuum ERROR: VACUUM, ERROR: MultiXactId, ERROR: index
lock timeout Lock on transaction
statement timeout statement timeout
long query(슬로우 쿼리) LOG: duration:

참고