PostgreSQL Vacuum 이해와 AutoVacuum 튜닝
PostgreSQL의 MVCC에서 Dead Tuple이 쌓이는 이유와 Vacuum 동작, AutoVacuum 실행 조건과 튜닝을 정리한다.
시리즈 · Database8 / 8
PostgreSQL에는 다른 RDBMS와 달리 Vacuum이라는 개념이 있다. 간단히 말하면 더 이상 사용되지 않는 데이터를 정리하는 명령이다.
Dead Tuple이 생기는 이유
PostgreSQL은 UPDATE나 DELETE를 수행할 때 디스크에 있던 기존 데이터를 그 자리에서 갱신하거나 삭제하지 않는다. UPDATE는 다음과 같이 진행된다.
- FSM(Free Space Map, 사용 가능한 공간을 표시하는 맵)에서 사용 가능한 공간을 확인하고, 없으면 공간을 추가로 확보한다.
- 그 공간에 변경된 데이터를 새로 기록한다(INSERT).
- 기록이 끝나면 원본 Tuple을 가리키던 포인터를 새 Tuple로 옮긴다.
이 과정을 거치면 기존 데이터는 어디에서도 참조되지 않는 Tuple이 된다. 이것이 Dead Tuple이다.
이렇게 번거롭게 동작하는 이유는 PostgreSQL의 MVCC(Multi-Version Concurrency Control) 구현 방식 때문이다.
PostgreSQL의 MVCC
- UPDATE가 발생하면 새로운 버전의 Tuple을 추가로 쌓는다.
- 어느 버전을 볼지는 Tuple에 저장된 Transaction ID인
xmin,xmax를 비교해 판단한다.

예를 들어 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이 한 번에 쓸 수 있는 creditautovacuum_vacuum_cost_delay: credit을 모두 소모한 뒤 쉬는 시간
credit을 다 쓰면 delay만큼 쉬었다가 다시 작업한다. credit이 너무 작으면 정리 속도가 Dead Tuple이 쌓이는 속도를 따라가지 못해 Dead Tuple이 계속 누적된다.
증상별로 볼 파라미터
AutoVacuum이 너무 드물게 돈다면
autovacuum_vacuum_scale_factor,autovacuum_vacuum_thresholdautovacuum_vacuum_insert_scale_factor,autovacuum_vacuum_insert_threshold
AutoVacuum이 너무 느리다면
autovacuum_vacuum_cost_delay,autovacuum_vacuum_cost_limitautovacuum_naptimeautovacuum_max_workersautovacuum_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: |