PostgreSQL Autovacuum과 Lock

PostgreSQL Autovacuum과 Lock

1. 들어가며

운영 중인 PostgreSQL 데이터베이스에서 CPU 사용률이 100% 상태로 지속되는 상황이 발생했고, 원인을 파고들어 보니 그 중심에는 autovacuum이 있었습니다. 평소에는 신경 쓸 일이 거의 없는 백그라운드 프로세스라고만 생각했는데, 이 이슈를 계기로 autovacuum이 어떤 원리로 동작하고 왜 lock 경합까지 일으킬 수 있는지를 어느 정도 알게 되었습니다. 겪었던 문제를 먼저 정리하고, 이를 분석하고 해결하는 과정에서 알게 된 autovacuum과 lock에 대한 내용을 정리하겠습니다.

2. 무슨 일이 있었나

트래픽이 몰리는 구간에서 백그라운드 정리 작업이 지연되며 특정 테이블의 xid 나이가 점점 누적되고 있었습니다. 결국 어느 시점에 임계값을 넘어서면서 PostgreSQL은 자체적으로 하나의 autovacuum을 강제로 실행시켰고, 공교롭게 그 시점에 데이터 일부에 문제가 있었던 테이블을 이 프로세스가 처리하게 되면서 좀처럼 끝나지 않고 장시간 CPU를 점유하는 상황으로 이어졌습니다.

처음에는 단순한 트래픽 급증이나 비효율적인 쿼리 때문이라고 생각했지만, 확인해보니 원인은 훨씬 근본적인 곳에 있었습니다. 실행 중인 쿼리를 조회해보니 CPU를 점유하고 있던 것은 일반적인 애플리케이션 쿼리가 아니라 'autovacuum: VACUUM ... (to prevent wraparound)' 형태의 autovacuum이었습니다. 찾아보니 이는 트랜잭션 ID wraparound를 막기 위해 PostgreSQL이 강제로 실행한 것이었고, 일반적인 방법(프로세스 종료, autovacuum 비활성화)으로는 멈출 수 없는 프로세스였습니다.

이 과정에서 상태가 어떻게 변화했는지 간단히 정리하면 다음과 같습니다.

구간

상태

평상시

정상 운영, xid_age 정상 범위

문제 누적 구간

백그라운드 정리 작업 지연 → xid_age가 임계값에 근접

장애 발생 시점

wraparound 방지 autovacuum 강제 실행, 문제 있는 테이블 처리 중 정체 → CPU 100% 지속

조치 완료 후

임계값 원복, CPU 사용률 및 서비스 정상화

대응은 크게 세 단계로 진행되었습니다. 먼저 CPU를 점유하고 있는 프로세스가 wraparound 방지 autovacuum이라는 것을 확인한 뒤, 앞서 확인했듯 이 모드는 일반적인 방법으로 중단할 수 없었기 때문에 프로세스를 직접 종료하는 대신 임계값 설정을 일시적으로 상향해 해당 테이블이 강제 실행 대상에서 제외되도록 유도했습니다. 이후 CPU 사용률이 정상 수준으로 내려온 것을 확인한 뒤에는, 문제가 있던 테이블을 백업 및 초기화해 근본적인 손상을 정리했습니다. 마지막으로 임시로 올려두었던 설정값을 원래대로 되돌리고, CPU와 서비스 상태를 재확인하며 대응을 마무리했습니다.

-- 1) 원인 진단: CPU를 점유 중인 프로세스 확인
SELECT pid, query FROM pg_stat_activity WHERE state = 'active';
 
-- 2) 긴급 조치: wraparound 방지 대상에서 제외되도록 임계값 임시 상향
ALTER SYSTEM SET autovacuum_freeze_max_age = 500000000;
 
-- 3) 문제 테이블 정리 후 임시 설정 원복
TRUNCATE TABLE target_table;
ALTER SYSTEM SET autovacuum_freeze_max_age = 200000000;

1번 조회 결과의 query 값에 'autovacuum: VACUUM ... (to prevent wraparound)'라는 문구가 붙어 있는지가 핵심 판단 기준입니다. 이 문구가 있으면 일반 autovacuum이 아니라 강제 실행된 wraparound 방지 모드라는 뜻이며, 앞서 설명한 것처럼 프로세스를 직접 종료해도 소용이 없습니다. 반대로 이 문구 없이 'autovacuum: VACUUM ...'만 보인다면 일반적인 정리 작업이므로 크게 걱정할 필요는 없습니다.

3. Autovacuum이란

3.1 Autovacuum, 무엇을 하는 프로세스인가

PostgreSQL은 UPDATE나 DELETE가 발생해도 기존 행을 즉시 지우지 않고, dead tuple이라는 형태로 남겨두는 구조를 사용합니다(MVCC). Autovacuum은 이렇게 쌓인 dead tuple을 정리하고, 트랜잭션 ID를 재사용할 수 있도록 freeze 처리하는 백그라운드 프로세스입니다. 언뜻 DBA의 영역처럼 느껴지지만, 실제로는 애플리케이션이 만들어내는 쿼리 패턴과 트랜잭션 사용 방식이 autovacuum의 부하와 lock 경합에 직접 영향을 줍니다. 그래서 개발자 입장에서도 최소한의 동작 원리는 알아두는 것이 도움이 됩니다.

용어

설명

dead tuple

UPDATE/DELETE로 인해 더 이상 유효하지 않지만, 아직 물리적으로 삭제되지 않고 남아있는 행입니다.

MVCC

여러 트랜잭션이 서로 다른 시점의 데이터를 동시에 볼 수 있도록 하는 PostgreSQL의 동시성 제어 방식입니다.

autovacuum

dead tuple을 정리하고 트랜잭션 ID를 freeze 처리하는 PostgreSQL의 백그라운드 자동 정리 프로세스입니다.

xid / wraparound

PostgreSQL은 32비트 트랜잭션 ID(xid)를 사용하며, 오래된 xid를 정리하지 않으면 ID가 순환(wraparound)되어 데이터 정합성이 깨질 수 있습니다.

lock

여러 세션이 동시에 같은 대상에 접근할 때 정합성을 지키기 위해 거는 잠금으로, 요청 종류에 따라 서로 다른 수준의 lock이 사용됩니다.

3.2 xid_age는 왜, 어떻게 쌓이는가 — Wraparound 방지 Autovacuum

xid_age는 트랜잭션이 발생할 때마다 계속 증가하며, 이를 낮추는 유일한 방법은 autovacuum이 해당 테이블에 대한 freeze 작업을 끝까지 완료하는 것뿐입니다. 그런데 쓰기 트래픽이 많아 xid_age가 쌓이는 속도는 빨라지는데, 정작 autovacuum은 I/O 경합이나 처리 지연으로 제때 완주하지 못하는 상황이 겹치면, 쌓이는 속도와 비우는 속도의 격차가 계속 벌어지게 됩니다. 이 격차가 좁혀지지 않고 트랜잭션 ID(xid)의 나이가 결국 임계값을 넘어서면, PostgreSQL은 wraparound를 막기 위해 해당 테이블에 대한 autovacuum을 강제로 실행합니다. 이 모드는 데이터 정합성을 지키기 위한 최후의 방어선이기 때문에, 일반적인 방법(프로세스 종료, autovacuum 비활성화)으로는 멈출 수 없습니다. 이번 이슈에서도 문제가 있던 테이블과 맞물리면서, 장시간 리소스를 점유하며 서비스에 영향을 주는 상황으로 번졌습니다.

image1.png

그림 1. Wraparound 방지 Autovacuum까지의 단계

4. Autovacuum이 만들 수 있는 또 다른 Lock 경합 상황

이번 이슈의 직접적인 원인은 아니었지만, 이 일을 계기로 autovacuum이 lock 경합을 일으킬 수 있는 다른 상황들도 함께 정리해 보았습니다.

4.1 일반 autovacuum과 DDL의 충돌

일반적인 autovacuum은 SHARE UPDATE EXCLUSIVE라는 비교적 가벼운 lock만 사용하기 때문에, 평소 SELECT / INSERT / UPDATE 같은 쿼리와는 대부분 문제없이 공존합니다. 문제는 배포 중 ALTER TABLE처럼 ACCESS EXCLUSIVE lock을 요구하는 DDL을 실행할 때 발생합니다. autovacuum이 이미 해당 테이블을 처리하고 있다면 DDL은 autovacuum이 끝날 때까지 대기하게 되고, 그 뒤로 들어오는 일반 쿼리들까지 줄줄이 대기 상태에 걸리게 됩니다. "배포하다가 갑자기 서비스가 멈췄다"는 현상의 흔한 원인 중 하나가 바로 이것입니다.

Lock 모드

대표 사용 예

autovacuum과의 관계

ACCESS SHARE

SELECT

충돌 없음

ROW EXCLUSIVE

INSERT / UPDATE / DELETE

충돌 없음

SHARE UPDATE EXCLUSIVE

autovacuum, CREATE INDEX CONCURRENTLY

동일 모드끼리 충돌 (동시 실행 불가)

ACCESS EXCLUSIVE

ALTER TABLE, DROP TABLE, TRUNCATE

충돌 (autovacuum 종료까지 대기)

image2.png

그림 2. Autovacuum과 DDL의 Lock 충돌

4.2 오래 열린 트랜잭션이 만드는 문제

애플리케이션의 커넥션 누수나 디버깅 중 열어둔 트랜잭션을 오래 방치하면, 그 트랜잭션이 시작된 시점 이전의 dead tuple을 autovacuum이 정리하지 못하게 됩니다. 그 결과 테이블과 인덱스의 크기가 필요 이상으로 부풀어 오르는 bloat 현상이 발생하고, 조회 성능 저하로 이어질 수 있습니다.

5. 마치며

"autovacuum은 알아서 잘 돌아가는 백그라운드 프로세스"라고 방치하면, 평소에는 아무 문제가 없다가 트래픽 증가, 오래 열린 트랜잭션, 설정 미흡 같은 조건이 한꺼번에 겹치는 순간 서비스 전체에 영향을 줄 수 있습니다. xid_age나 dead tuple 비율 같은 지표를 평소에 모니터링하지 않기 때문에, 문제가 임계 수준에 도달한 뒤에야 뒤늦게 알아차릴 수 있습니다. 이런 지표들에 대해서도 모니터링을 하면, 강제 실행 조건에 도달하기 전에 좀 더 여유 있게 대응할 수 있을 것 같습니다.

Sean

Site footer