PostgreSQL (장애 상황, multiXact)

seongha_h·2025년 7월 16일

PostgreSQL

목록 보기
1/2
post-thumbnail

서버 장애 보고: LWLock과 MultiXact 문제 해결 사례

안녕하세요, seonghaaa입니다.

오늘은 PostgreSQL에서 발생했던 실제 서버 장애 상황과, 그 원인이었던 LWLock 및 MultiXact에 대해 학습한 내용을 바탕으로 문제 해결 과정을 공유하고자 합니다.


1. 장애 상황 개요

어느 날, 프로덕션 API의 에러율이 갑자기 30%까지 치솟는 위급 상황이 발생했습니다.

DB 지표를 확인한 결과, LWLock이 다량으로 발생하면서 쿼리들이 정상적으로 실행되지 못하고 지연되는 현상이 관찰되었습니다. 특히, 일부 무거운 쿼리는 20분 이상 지속되며 시스템 전체에 심각한 부하를 주고
있었습니다.

이 문제의 결정적인 원인은 바로 로그성 테이블에 설정된 외래 키(Foreign Key, FK) 연관관계였습니다.


2. 문제 발생 메커니즘 분석

외래 키(FK)로 인해 지속적으로 MultiXact가 누적되는 현상은 평상시에도 발생하며, 시스템을 불안정한 임계 상태로 만들고 있었습니다.

여기에 로그성 테이블의 통계를 조회하기 위한 무거운 SELECT 쿼리가 실행되면서 문제가 심화되었습니다. 이 쿼리는 CPU, 메모리, I/O 사용량을 급증시켰고, 이미 불안정했던 MultiXact 관리 시스템에 치명적인 추가 부하를
주었습니다.

결과적으로 트랜잭션 간의 자원 경합이 극도로 심화되었고, MultiXact가 감당할 수 있는 관리 범위를 초과하면서 서버 장애로 이어지게 되었습니다.


3. 장애 발생 시나리오

문제 발생 시나리오는 다음 단계로 진행되었습니다.

  1. 트랜잭션 ID 대량 생성 — 각 INSERT 작업마다 새로운 트랜잭션 ID가 할당되며, 32비트 트랜잭션 ID의 한계 소모를 가속화했습니다.

  2. MultiXact 폭증 — 동일한 행을 참조하는 여러 트랜잭션에서 FOR KEY SHARE 락이 발생함에 따라 MultiXact ID가 대량으로 생성되기 시작했습니다.

  3. 무거운 SELECT 쿼리의 촉매 역할 — 통계성 SELECT 쿼리의 긴 실행 시간은 FOR KEY SHARE 락의 점유 시간을 증가시켰습니다. 이는 시스템 리소스 경합을 유발하며, 이미 임계 상태였던 MultiXact 관리 시스템에 추가
    부하를 주어 마비시켰습니다. 직접적인 락 충돌은 없었지만, 이 쿼리가 시스템을 임계점을 넘어서게 하는 결정적인 방아쇠 역할을 했습니다.

  4. SLRU 버퍼 과부하 — MultiXact 관리를 위한 SLRU 버퍼, 특히 MultiXactOffsetBuffer가 과부하되어 시스템 리소스가 고갈되었습니다.

  5. 전체 시스템 마비 — 모든 쿼리가 MultiXactOffsetBuffer 락을 기다리는 병목 현상이 발생하며, 전체 시스템의 성능이 급격히 저하되고 마비 상태에 이르렀습니다.


    4. 해결 방안

    이 문제의 근본적인 해결을 위해 로그성 테이블의 외래 키(FK)를 제거하였습니다.

    이로써 참조 무결성 검사를 위한 FOR KEY SHARE 락 발생을 원천적으로 차단할 수 있었습니다. 결과적으로 MultiXact ID 생성량이 자연스럽게 줄어들었고, 그에 따라 발생하던 LWLock 역시 크게 감소하여 시스템이 정상
    상태를 회복
    할 수 있었습니다.


    PostgreSQL 주요 개념 학습

    이번 장애를 계기로 PostgreSQL의 핵심 동작 방식과 관련 개념에 대해 깊이 있게 학습할 기회를 얻었습니다.

    PostgreSQL이란?

    PostgreSQL은 고급 객체-관계형 데이터베이스 시스템(ORDBMS)입니다. 엔터프라이즈급 성능을 목표로 설계되었으며, 복잡한 쿼리와 데이터 무결성이 필요한 시스템에 특히 적합합니다. 또한, 다양한 데이터 유형과 확장
    기능을 지원하여 유연한 데이터 관리가 가능합니다.


    MVCC (Multi-Version Concurrency Control)

    MVCC는 하나의 레코드(행)에 대해 여러 버전을 유지함으로써, 읽기와 쓰기를 동시에 수행할 수 있도록 지원하는 동시성 제어 방식입니다.

    동작 원리

  6. 데이터 변경이 발생하면(트랜잭션 1), 원본 데이터에 대한 snapshot을 백업하여 보관합니다.

  7. 이때, 새로운 유저가 해당 데이터를 읽는다면(트랜잭션 2) snapshot을 읽습니다.

  8. 데이터 변경이 취소되면 snapshot을 바탕으로 데이터를 복구하고, 변경이 완료되면 디스크에 쓰기 작업을 수행합니다.

    이처럼 여러 버전의 데이터를 유지하는 것이 MVCC입니다. MySQL은 Undo Log를 통해 이를 지원합니다.

    장점

  • 여러 버전의 데이터를 바탕으로 읽기/쓰기를 수행하기 때문에 lock이 필요하지 않습니다. 따라서 일반적인 RDBMS보다 읽기 성능이 좋습니다.

  • 다른 트랜잭션에서 해당 데이터를 수정하더라도, 영향을 받지 않고 읽기가 가능합니다.

    주의점

    MVCC로 인하여 하나의 데이터가 여러 버전으로 존재하기에, 사용하지 않는 데이터를 정리하는 GC 역할이 필요합니다. → VACUUM


    트랜잭션 관리 (Transaction ID)

    PostgreSQL에서는 각 트랜잭션에 고유한 Transaction ID (txid)가 부여됩니다. 이 txid는 32비트 정수값(약 42억)이며, 따라서 wraparound가 발생할 수 있습니다.

    wraparound란? ID가 순환하여 다시 0부터 시작하는 현상

    행 정보의 구성

    각 row에는 xmin, xmax 두 가지 값이 존재합니다.

    필드설명
    xmin이 row를 생성한 트랜잭션 ID
    xmax이 row를 삭제/갱신한 트랜잭션 ID

    데이터를 갱신하면 이전 row의 xmax가 갱신 트랜잭션 ID로 세팅됩니다.

    예시:

    100번 트랜잭션으로 데이터 생성, 101번 트랜잭션으로 데이터 갱신
    → 100번에서 생성한 row의 xmax = 101
    → 새로운 row의 xmin = 101

    데이터 가시성 판단 규칙

    여러 버전의 데이터가 존재하므로, 읽기 트랜잭션은 어떤 데이터를 읽을지 판단이 필요합니다.

    조건결과
    xmin ≤ txid < xmaxrow 보임
    txid < xminrow 아직 생성되지 않음
    xmax ≤ txidrow 삭제됨

    읽을 수 있는 데이터가 여러 개라면 가장 최신 데이터를 읽습니다.


    MultiXact

    PostgreSQL에서 MultiXact는 여러 트랜잭션이 동시에 동일한 행의 특정 버전을 공유하는 것을 관리하는 메커니즘입니다. row 레벨 잠금과 관련하여 여러 트랜잭션이 접근할 수 있도록 하여 동시성을 향상시키기 위해
    사용합니다.

    왜 MultiXact가 필요한가?

    PostgreSQL의 row는 xmin, xmax라는 트랜잭션 ID를 가질 수 있으며, xmax에는 1개의 트랜잭션 ID만 저장할 수 있습니다.

    그러나 다음과 같은 공유 잠금(shared lock) 상황에서는 하나의 row에 여러 트랜잭션이 동시에 잠금을 걸어야 할 때가 있습니다.

  • SELECT FOR SHARE

  • SELECT FOR KEY SHARE

  • FK 검사

    Foreign Key 제약 조건은 단순히 참조 무결성만 검사하는 것이 아니라, PostgreSQL 내부에서는 실제로 공유 잠금(FOR KEY SHARE Lock)을 사용하여 데이터 무결성을 보장합니다.

    xmax에 여러 개의 트랜잭션 ID를 넣을 수 없으므로, 하나의 MultiXact ID를 생성하여 xmax 자리에 저장합니다. 즉, MultiXact ID 하나는 여러 개의 트랜잭션을 의미합니다.

    MultiXact의 저장 구조

    PostgreSQL은 MultiXact 정보를 두 개의 SLRU 파일로 나눠 저장합니다. (디스크 + 메모리 버퍼로 운영)

  • pg_multixact/offsets — MultiXact ID → members 배열 시작 위치 매핑 (인덱스 역할)

    예시: mxid123 → members[200]
  • pg_multixact/members — 실제 트랜잭션 ID 목록

    예시: members[200] = [txid1, txid2, txid3]

    MultiXactOffsetBuffer

    MultiXactOffsetBufferpg_multixact/offsets 영역에 접근할 때 사용하는 공유 버퍼 페이지에 대한 락(LWLock)을 의미합니다.

    MultiXact ID가 생성될 때, 아래와 같은 작업이 수행됩니다.

  1. 새로 생성할 MultiXact ID가 가리키는 members 배열의 시작 위치(오프셋)를 계산합니다. → 이 위치는 pg_multixact/members 영역의 어느 지점에 해당 ID의 트랜잭션 목록이 저장되는지를 나타냅니다.

  2. 이 offset 값을 pg_multixact/offsets에 기록합니다. → 이 파일은 SLRU 구조를 가지며, shared buffer에 페이지 단위로 매핑되어 관리됩니다.

  3. offset 정보를 기록하거나 읽기 위해 해당 페이지를 shared buffer로 로드합니다. → 이미 버퍼에 없는 경우 디스크에서 로딩하며, SLRU 캐시 정책에 따릅니다.

  4. 이 buffer 페이지는 여러 트랜잭션이 동시에 접근할 수 있으므로, 충돌을 방지하기 위해 LWLock:MultiXactOffsetBuffer 락을 획득해야 합니다. → 이 락은 해당 buffer page의 consistency를 보장합니다.

  5. 기록이 완료되면 LWLock을 해제하고, 이후 단계로 MultiXact ID와 member 정보를 pg_multixact/members에 기록합니다.

    결론적으로, 이 lock은 동시에 동일한 row에 대해 SELECT FOR SHARE / FK INSERT로 공유 잠금이 발생할 때 MultiXact ID가 대량으로 생성되며 발생합니다.


    SLRU (Simple Least Recently Used)

    PostgreSQL 내부에서 사용하는 디스크 기반 저장소 구조입니다. 여러 내부 시스템 카탈로그 정보를 저장하고 읽을 때, 효율적인 I/O를 위해 SLRU를 사용합니다.

    SLRU 종류

    SLRU 이름용도설명
    pg_multixact/offsetsMultiXact Offset각 MultiXact ID가 어느 member에서 시작하는지 기록
    pg_multixact/membersMultiXact Member해당 MultiXact가 포함하는 트랜잭션들의 ID 저장
    pg_subtransSubTransaction트랜잭션의 부모/자식 관계 추적
    pg_commit_tsCommit Timestamp트랜잭션의 커밋 시간 저장 (선택적 기능)
    pg_notifyLISTEN/NOTIFYNOTIFY 이벤트 관리

    SLRU의 특징

    각 시스템 카탈로그(pg_multixact, pg_subtrans 등)는 슬롯 단위로 페이지 파일(디스크)에 저장됩니다.

  • 각 페이지는 SharedBuffer가 아닌 SLRU 전용 bufferPool에서 관리

  • 메모리에 유지할 수 있는 페이지 수가 제한되어 있고, LRU 방식으로 캐시 교체 수행

  • SLRU에는 동시 접근 제어를 위해 LWLock을 사용


    아키텍처

    PostgreSQL 아키텍처

    Lock에 대해서는 내용이 방대하므로 다음 게시글에서 작성해 보겠습니다.


    출처

  • AWS 공식 문서 - Aurora PostgreSQL LWLock:MultiXact

profile
https://github.com/Fixtar

0개의 댓글