PostgreSQL Autovacuum 장애 대응기 (1)

Jimin Lee
29CM TEAM
Published in
12 min readMay 13, 2021

29CM에서는 Amazon RDS for PostgreSQL를 사용하고 있습니다. 최근에 경험한 PostgreSQL Autovacuum 장애와 Vaccum 최적화 방법에 대해서 설명하고자 합니다.

배경지식

본론에 앞서 필요한 배경지식인 PostgreSQL의 MVCC 개념과 Autovacuum에 대해서 설명하고자 합니다.

MVCC

PostgreSQL에서는 MVCC(Multi-Version Concurrency Control)를 사용하여 데이터베이스 동시성을 제어하고 있습니다. 이는 여러 트랜잭션에서 데이터를 동시에 수정하고, 조회할 수 있도록 하는 역할을 담당합니다.

MVCC은 사용자가 값을 변경할 때마다 새로운 데이터를 생성합니다. 그리고 커밋이 되면 새로운 데이터를 최신 데이터로 마킹하는 작업을 수행합니다. 이 과정에서 사용하지 않게 된 과거 데이터를 dead tuple이라고 부릅니다.

이때 dead tuple은 PostgreSQL의 저장 공간을 계속 차지하게 됩니다. 그래서 이 저장 공간을 다시 확보하고 재사용하는 작업이 필요합니다.

Vacuum

Vacuum은 MVCC 구조로 인하여 효율성이 떨어진 저장공간을 최적화하는 작업을 수행합니다.

vacuum이 담당하고 있는 역할은 크게 4가지가 있습니다.

  • 저장 공간 확보 — 테이블 별 dead tuple 정리
  • transaction wraparound 방지 — MVCC에 사용되는 Transaction ID 값이 계속 증가하지 않도록 정리
  • Query Planner 정확도 향상 — 통계 정보 갱신
  • index scan 성능 향상 — visibility map 정보 갱신

때문에 데이터베이스를 지속적으로 사용하려면 Vacuum을 주기적으로 실행해야합니다. Autovacuum은 이 역할을 담당합니다.

Autovacuum

Autovacuum을 사용하면 Autovacuum 데몬이 vacuum을 대신하게 됩니다. 데몬은 DB에 설정된 autovacuum parameter 값을 활용하여 주기적으로 vacuum을 실행합니다.

문제

29CM 시스템에는 2021년 2월부터 LWLock multixact_offset 대기가 급증하는 장애 패턴이 발생하기 시작했습니다.

Amazon RDS 성능도우미 > 상위대기와 Database Load 메트릭

해당 현상이 발생할 때에는 항상 DB Load가 증가하는 패턴이 있었습니다. 그리고 이는 쿼리 응답 지연을 일으켜, 결국 DB Connection이 밀려 서비스 장애로 전파되었습니다.

응답 지연으로 인하여 어드민/유저 서비스 모두 장애가 전파되는 경험을 하게 되었죠. 🥲

원인 파악

(1) Block 쿼리 의심하기

처음에는 특정 DML 요청이 다른 요청을 Block하고 있다고 가정했습니다. 그래서 다음 쿼리를 사용하여 Block된 쿼리들의 의존 관계를 확인하고자 했습니다.

SELECT
COALESCE(((blockingl.relation)::regclass)::text,
blockingl.locktype) AS locked_item, (now() - blockeda.query_start) AS waiting_duration,
blockeda.pid AS blocked_pid,
blockeda.query AS blocked_query,
blockedl.mode AS blocked_mode,
blockinga.pid AS blocking_pid,
blockinga.query AS blocking_query,
blockingl.mode AS blocking_mode
FROM (((pg_locks blockedl
JOIN pg_stat_activity blockeda ON ((blockedl.pid = blockeda.pid)))
JOIN pg_locks blockingl ON ((((blockingl.transactionid = blockedl.transactionid)
OR ((blockingl.relation = blockedl.relation)
AND (blockingl.locktype = blockedl.locktype)))
AND (blockedl.pid <> blockingl.pid))))
JOIN pg_stat_activity blockinga ON (((blockingl.pid = blockinga.pid)
AND (blockinga.datid = blockeda.datid))))
WHERE ((NOT blockedl.granted)
AND (blockinga.datname = current_database()));

그러나 특정 쿼리 Block으로 인한 지연 쿼리는 없었습니다. 이를 통해 문제의 원인은 다른 곳에 있다는 것을 알 수 있었습니다.

(2) 상위 부하 쿼리 살펴보기

Amazon RDS 성능도우미

Amazon RDS 성능도우미를 사용하여 상위 SQL을 살펴보았습니다. 특정 테이블에 접근하는 vacuum 명령문과 조회용 롱 쿼리에서 부하를 일으킨 다는 사실을 알 수 있었습니다.

(3) 실행중인 VACUUM 세션 살펴보기

다음 쿼리를 사용하여 실행중인 vacuum의 정보를 확인하였습니다.

SELECT
datname,
usename,
pid,
CURRENT_TIMESTAMP - xact_start AS xact_runtime,
query
FROM
pg_stat_activity
WHERE
upper(query)
LIKE '%VACUUM%'
ORDER BY
xact_start;

해당 쿼리를 통해 vacuum 실행이 지연되고 있는 테이블과 세션 정보를 확인할 수 있었습니다.

장애 대응 프로세스

몇 번의 개선 작업 이후에도 유사한 장애가 반복해서 발생했습니다. 이 과정에서 29CM 개발팀에서는 단기/중장기 측면에서 문제를 해결했습니다. 단기적으로 장애를 멈추고, 서비스를 안정화시킬 때 사용했던 장애대응 프로세스는 다음과 같습니다.

(1) 응답 지연 중인 쿼리 세션 종료하기

SELECT
pg_terminate_backend(pid)
FROM
pg_stat_activity
WHERE
state IN ('active')
AND (now() - query_start) > interval '5 minutes'
AND wait_event IN ('MultiXactOffsetControlLock', 'multixact_offset', 'SLRURead');

특정 쿼리의 응답 지연이 전체 서비스 장애로 전파되는 것을 멈추고자 했습니다. 그래서 Vacuum으로 인하여 대기가 발생하는 지연 쿼리 세션을 명시적으로 종료하여 서비스 안정화를 꾀하였습니다.

(2) Autovacuum 종료하기

ALTER TABLE public.t_product SET (autovacuum_enabled = false);

그러나 vacuum 부하로 인하여 응답 지연 쿼리는 계속 양산되었습니다. 그래서 문제가 되는 테이블의 autovacuum을 명시적으로 일시 정지하였습니다. 이후, 서비스 장애가 빠르게 회복되었습니다.

autovacuum을 일시 정지할 테이블은 다음 쿼리를 사용하여 선정하였습니다.

(2–A) 테이블의 나이가 오래된 상위 20개 조회 SQL

autovacuum은 transaction wraparound 문제를 방지하기 위하여, 데이터베이스의 나이가 특정 임계값 (autovacuum_freeze_max_age)에 이르면 vacuum을 실행합니다.

SELECT
c.oid::regclass AS table_name,
greatest(age(c.relfrozenxid), age(t.relfrozenxid)) AS age,
pg_size_pretty(pg_table_size(c.oid)) AS table_size
FROM
pg_class c
LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
WHERE
c.relkind = 'r'
ORDER BY
2 DESC
LIMIT 20;

테이블의 나이가 가장 많은 테이블이 autovacuum 대상으로 선정될 가능성이 높기 때문에 다음 쿼리를 사용하였습니다.

(2–B) 테이블별 Dead tuple 현황 조회

autovacuum에서 vacuum 대상 테이블을 선정할 때에는 다음의 autovacuum setting parameter를 사용합니다.

  • autovacuum_vacuum_threshold : vacuum을 실행할 최소 dead tuple 수. 기본 값은 50입니다.
  • autovacuum_vacuum_scale_factor : vacuum을 실행할 최소 dead tuple 비율. 기본 값은 0.2이며, 일반적으로 dead tuple 비율이 20%을 넘어선 테이블이 vacuum 대상 테이블로 선정됩니다.

그리고 PostgreSQL은 위의 값으로 다음과 같은 계산식을 사용하여 vacuum 대상을 선정합니다.

vacuum threshold = vacuum base threshold + vacuum scale factor * number of live tuples.

다음 쿼리를 사용하면 dead tuple의 크기와 비율이 높은 상위 테이블을 알 수 있습니다. 이를 통해 vacuum 대상으로 선정될 테이블을 유추할 수 있습니다.

SELECT
relname AS TableName,
n_live_tup AS LiveTuples,
n_dead_tup AS DeadTuples,
n_dead_tup / n_live_tup AS ratio,
last_autovacuum AS Autovacuum,
last_autoanalyze AS Autoanalyze,
*
FROM
pg_stat_user_tables
WHERE
n_dead_tup > 0;

그리고 서비스가 안정화된 이후에는 테이블별 autovacuum을 다시 활성화했습니다. 다음 쿼리를 사용하면, 테이블별 setting parameter를 한눈에 확인할 수 있습니다.

SELECT
relname,
reloptions
FROM
pg_class
WHERE
reloptions IS NOT NULL;

(3) 수동으로 Vacuum 실행하기

vacuum VERBOSE public.t_product;

수동으로 vacuum을 실행하면 다음과 같은 이점을 얻을 수 있습니다.

첫째, vacuum이 지연되어 부하를 일으키는 테이블이 계속 autovacuum 대상 테이블로 선정되어 문제가 반복되는 현상을 해결할 수 있습니다.

수동 Vacuum 이후, 대기 상태인 DB Session 수가 감소한 케이스

둘째, vacuum이 지연되는 원인을 파악할 수 있습니다. vacuum 실행 시, verbose 옵션을 사용하면 vacuum 실행과정을 출력할 수 있습니다. 출력 결과를 살펴보면 vacuum을 block하고 있는 요소, 지연 사유를 확인할 수 있습니다.

29CM 개발팀에서는 이를 통해 vacuum 최적화에 대한 몇가지 힌트를 얻을 수 있었습니다. 이에 대한 예시로 인덱스 색인 최적화 포인트, idle in transaction 상태로 XID를 점유하고 있는 세션 검토 등이 있었습니다.

본 글에서는 autovacuum으로 인한 장애 인지와 단기적인 장애 대응 프로세스에 대해서 다루었습니다. 다음 글에서는 장애가 발생한 근본적인 원인 해결을 위한 과정에 대해서 설명하고자 합니다. 🙂

함께 성장할 동료를 찾습니다

29CM ((주)무신사)는 3년 연속 거래액 2배의 성장을 이루었습니다.
이제 더 큰 성장을 위해 기존 모놀리틱 서비스 구조를 마이크로서비스 구조로 전환하고, 앵귤러 기반 프론트엔드 코드를 리액트로 전환하는 등의 기술적인 시도를 진행하고 있습니다. 모바일 앱 내부 구조도 모듈러 아키텍처로 개선하는 과정에 있습니다. 함께 성장하고 유저 가치를 만들어낼 동료 개발자분들을 찾습니다.

🚀 29CM 채용 페이지 : https://www.29cmcareers.co.kr/

--

--