본문 바로가기
엑셈 경쟁력/DB 인사이드

DB 인사이드 | PWI - WAL Buffer > WAL Record 저장

by exemtech 2025. 2. 10.

 

 

이전 글에서 Shared Buffer의 동작원리와 그와 관련된 Wait Event에 대해서 살펴보았습니다.

이어서 PostgreSQL의 WAL Buffer에 대해서 알아보고, WAL Buffer에 로그를 저장하는 과정에서 발생하는 Wait Event를 설명하겠습니다.

 

WAL Buffer

PostgreSQL에서는 WAL* 기법을 사용하여 데이터 변경 사항에 대한 로그를 생성된 시점에 따라 순차적으로 WAL Buffer에 저장합니다. Shared Buffer와 마찬가지로, WAL Buffer도 모든 프로세스가 공유해서 사용하는 Shared Memory 내부에 위치하고 있습니다.

 

PostgreSQL에서 WAL 기법을 사용하여 생성된 로그를 WAL Record(또는 XLog Record)라고 합니다. WAL Record는 실제 데이터(테이블, 인덱스 등)와 구분되어 WAL Buffer에 저장되며, 트랜잭션 Commit이나 Aborted 시 디스크로 Write 됩니다. 이렇게 디스크로 Write 된 여러 개의 WAL Record는 파일 단위로 묶어서 디스크에 저장되는데, 이를 WAL Segment라고 합니다.

📢 WAL(Write Ahead Logging)

WAL은 데이터베이스의 변경 사항을 디스크에 Write하기 전에, 변경 로그를 먼저 별도의 로그 파일에 기록(Logging)하는 방식으로 작동하는 메커니즘입니다. 이를 통해 시스템이 비정상적으로 종료되거나 장애가 발생했을 때도 데이터 손실을 방지하고 복구를 용이하게 만들어 데이터 무결성을 보장합니다.

WAL의 핵심 개념은 “변경 작업을 먼저 기록한다”는 것으로, 테이블이나 인덱스 실제 데이터를 Write 하기 전에 해당 변경 사항에 대한 로그를 먼저 기록하고, 이후 문제가 발생했을 때 이 로그를 바탕으로 변경 작업을 Replay 하여 데이터를 복구합니다.

 

이번 글에서는 데이터 변경 사항에 대한 로그가 WAL Buffer에 저장되는 과정에 대한 전체적인 흐름 설명하고, 각 단계의 처리 과정도 세부적으로 살펴보겠습니다.

 

WAL 동작 과정

Backend Process가 데이터 변경 작업을 수행한 후, WAL 기법에 따라 동작하는 과정을 간략히 표현하면 다음과 같습니다.

설명의 편의를 위해 데이터베이스 변경 사항에 대한 로그를 WAL Data, WAL Data에 관련된 메타 정보를 조합하여 생성한 데이터 구조를 WAL Record로 표현하겠습니다.

① WAL Data Create → ② WAL Data Register → ③ WAL Record Assembly → ④ WAL Record Copy → Disk Write

 

WAL 동작 과정에 대한 전체적인 흐름을 이해하였다면, 다음으로 각 단계의 처리 과정을 자세하게 알아보겠습니다.

(Disk I/O와 관련된 작업을 제외한, WAL Record를 WAL Buffer에 저장하는 동작에 초점을 두고 설명합니다.)

 

1. Create

Backend Process가 데이터베이스 변경 작업을 수행하면, 변경 사항에 대한 로그인 WAL Data가 생성(Create)됩니다.

 

2. Register

이전 단계에서 생성된 WAL Data와 관련된 Data Page에 대한 정보를 수집하고 등록(Register)합니다. WAL Data와 Data Page 정보를 서로 간 연결함으로써 WAL Record 구성에 필요한 정보가 모두 준비됩니다.

  1. WAL Data는 XLogRecData라는 구조체로 저장하며, XLogRecData는 연결된 리스트(Chain) 형태로 관리됩니다.
  2. WAL Data에 저장된 변경 사항과 관련된 Data Page에 대한 정보는 registerd_buffer(s)에 저장하며, registered_buffer(s)는 배열 형태로 관리됩니다.
  3. 연관된 XLogRecDataregistered_buffer(s)를 서로 연결함으로써 WAL Data에 저장된 변경 사항에 해당하는 Data Page 정보를 확인할 수 있습니다.

📢 각 registered_buffer(s)에는 연관된 XLogRecData 리스트를 가리키는 첫 번째(rdata_head)와 끝(rdata_tail)를 지칭하는 값이 저장되어 있습니다.

 

3. Assembly

2단계를 통해 준비된, XLogRecDataregistered_buffer(s)를 조합(Assembly)하여 다음과 같이 일반화된 구조에 맞춰 WAL Record를 구성합니다.

📢 WAL Record는 PostgreSQL 9.5 버전 이후, Header와 Data 영역으로 나뉘는 일반화된 구조를 사용합니다.

더보기

 

WAL Record는 크게 XLogRecord struct와 data 영역으로 나뉘며, 일반적으로 XLogRecord Header, Block Header, Data Header, Block Data, Main Data 5가지로 구성 요소로 정의합니다.

WAL Record를 식별하는 주요 정보는 XLogRecord Header에 저장되며, 나머지 4가지 구성 요소는 작업 성격에 따라 일부 제외되거나 세분화될 수 있습니다.

 

a. XLogRecord Header

XLogRecord라는 구조체로 정의되어 있으며, WAL Record의 메타데이터 역할을 합니다. 직전에 생성된 WAL Record의 위치(xl_prev), 데이터 크기(xl_tot_len), 작업 유형(xl_rmid, xl_info), 트랜잭션 ID(xl_xid) 등을 포함합니다.

 # postgres/src/include/access/xlogrecord.h

 typedef struct XLogRecord
 {
     uint32        xl_tot_len;        /* total len of entire record */
     TransactionId xl_xid;        /* xact id */
     XLogRecPtr    xl_prev;        /* ptr to previous record in log */
     uint8        xl_info;        /* flag bits, see below */
     RmgrId        xl_rmid;        /* resource manager for this record */
     /* 2 bytes of padding here, initialize to zero */
     pg_crc32c    xl_crc;            /* CRC for this record */

     /* XLogRecordBlockHeaders and XLogRecordDataHeader follow, no padding */
 } XLogRecord;

 

b. Block Header와 Block Data

Block Data는 ①변경 사항이 포함된 블록과 관련된 데이터(상태나 크기 정보) ②변경 사항이 포함된 블록 전체 데이터(full_page_write=on)를 저장합니다. 그리고 Block Header는 Block Data에 대한 Header 영역으로 Block Data에 대한 메타 정보를 가지고 있으며, XLogRecordBlockHeader라는 구조체로 정의되어 있습니다.
Block Data와 Block Header는 블록을 기반으로 하는 데이터이기 때문에, 여러 개의 블록을 포함하는 변경 사항이라면 Block Data와 BlockHeader가 여러 개로 구성됩니다. 반면 블록과 무관한 변경 사항이라면 이 부분은 0개, 즉 없을 수 있습니다.

# postgres/src/include/access/xlogrecord.h

typedef struct XLogRecordBlockHeader
{
  uint8        id;                /* block reference ID */
  uint8        fork_flags;        /* fork within the relation, and flags */
  uint16        data_length;    /* number of payload bytes (not including page
                               * image) */

  /* If BKPBLOCK_HAS_IMAGE, an XLogRecordBlockImageHeader struct follows */
  /* If BKPBLOCK_SAME_REL is not set, a RelFileLocator follows */
  /* BlockNumber follows */
} XLogRecordBlockHeader;

📢 full_page_write (default: on)
PostgreSQL에서 데이터베이스 복구 시 사용되는 중요한 설정으로, Data Page에 대한 첫 번째 변경 작업 시 해당 페이지의 전체 내용을 포함할 것인지 결정합니다. 이를 통해 데이터베이스가 비정상적으로 종료되어 Data Page가 손상된 상황이 발생해도 페이지 복구가 가능하게 합니다.

  • on : Checkpoint 이후 첫 번째로 수행되는 데이터베이스 변경 작업에 해당하는 WAL Record의 Block Data에는 블록 전체 데이터를 저장합니다. 이때 Block Header 영역에는 XLogRecordBlockImageHeader이 추가됩니다.
  • off : Full Page Write 기능을 비활성화, 즉 WAL Record에는 변경된 데이터만 저장합니다.

 

c. Data Header와 Main Data

Main Data는 트랜잭션 메타데이터, 변경된 데이터의 값(실제 데이터), 함수 호출 또는 추가 정보, 변경 내역에 대한 설명 등을 포함하고 있으며, XLogRecData 구조체가 여러 개 연결된 리스트 형태로 구성됩니다. 그리고 Main Data에 대한 메타데이터를 가지고 있는 Data Header는 Main Data의 길이에 따라 XLogRecordDataHeaderShort 또는 XLogRecordDataHeaderLong 구조체로 구분됩니다.

Main Data와 Data Header는 블록 수준이 아닌 논리적인 데이터 변경 사항을 대상으로, 여러 개가 아닌 각각 1개로 구성됩니다. COMMIT / ROLLBACK Command과 같이 트랜잭션 상태만 저장하는 작업 같은 특수한 경우에는, 별도로 Main Data를 저장하지 않는 것을 허용하므로 해당 영역은 존재하지 않을 수 있습니다. 다시 말해 Main Data와 Data Header는 특수한 경우 0개가 될 수 있지만, 일반적으로 하나씩 존재합니다.

 # postgres/src/include/access/xlogrecord.h

 typedef struct **XLogRecordDataHeaderShort**
 {
     uint8        id;                /* XLR_BLOCK_ID_DATA_SHORT */
     uint8        data_length;    /* number of payload bytes */
 }            XLogRecordDataHeaderShort;

 typedef struct **XLogRecordDataHeaderLong**
 {
     uint8        id;                /* XLR_BLOCK_ID_DATA_LONG */
     /* followed by uint32 data_length, unaligned */
 }            XLogRecordDataHeaderLong;

📢 WAL Record에 기록할 정보의 Level을 설정하는 wal_level (Default, replica)도 WAL Record의 크기를 결정하는데 영향을 줍니다.

wal_level Description
minimal Crash 또는 Immediate Shutdown로부터 복구하는데 필요한 정보, 최소한의 정보만 기록
replica Standby 서버에서 Read-only 쿼리 실행을 포함하여, WAL Archiving과 Replication를 지원하기에 충분한 데이터를 기록
logical Logical Replication을 위해 Logical Decoding을 지원하는데 필요한 정보를 추가 기록

각 Level은 모든 하위 Level에서 기록된 내용을 포함합니다.

wal_level의 설정에 따라 기록해야 할 데이터가 많아질수록, WAL Record 크기가 증가할 수 있으며(minimal < replica < logical), 그에 따라 WAL Record 생성하는 작업 시간도 길어질 수 있습니다. 특히 대규모 트랜잭션을 처리하는 시스템에서는 WAL Record의 양이 많아지면 WAL Buffer가 더 빨리 채워지므로 관련된 Wait Event 발생에도 영향을 미칠 수 있습니다.

 

4. Copy

구성된 WAL Record는 다음과 같이 두 단계를 거쳐 Shared Memory의 WAL Buffer에 Copy 하는 방식으로 저장합니다.

  • WAL Page 확인 - 먼저 WAL Buffer에 WAL Record를 저장할 공간이 있는지 확인하고 공간을 예약합니다.
  • WAL Record 저장 - 예약한 공간에 WAL Record를 Copy 방식으로 저장합니다.

[WAL Page 확인]

a. WAL Record를 저장하고자 공간을 확인하고, 대상 WAL Page에 대한 WALInsert Lock*을 획득합니다.

b. 직전에 저장된 WAL Record의 위치 정보(PreBytePos)와 새로운 WAL Record를 저장할 위치 정보(CurBytePos)를 확인하기 위해 XLogCtlInsert 구조체에 대한 spinlock을 획득합니다.

📢 XLogCtlInsert 구조체는 WAL Record를 삽입하고자 위치를 추적하는데 활용합니다. 새로운 WAL Record를 저장할 때 현재 사용 중인 WAL Page의 위치와 WAL Buffer 내의 남은 공간 정보를 관리하며, 이를 보호하기 위해 spinlock이 사용됩니다.

더보기
 typedef struct XLogCtlInsert
  {
      slock_t        insertpos_lck;    /* protects CurrBytePos and PrevBytePos */

      /*
       * CurrBytePos is the end of reserved WAL. The next record will be
       * inserted at that position. PrevBytePos is the start position of the
       * previously inserted (or rather, reserved) record - it is copied to the
       * prev-link of the next record. These are stored as "usable byte
       * positions" rather than XLogRecPtrs (see XLogBytePosToRecPtr()).
       */
      uint64        CurrBytePos;
      uint64        PrevBytePos;

      /*
       * Make sure the above heavily-contended spinlock and byte positions are
       * on their own cache line. In particular, the RedoRecPtr and full page
       * write variables below should be on a different cache line. They are
       * read on every WAL insertion, but updated rarely, and we don't want
       * those reads to steal the cache line containing Curr/PrevBytePos.
       */
      char        pad[PG_CACHE_LINE_SIZE];

      /*
       * fullPageWrites is the authoritative value used by all backends to
       * determine whether to write full-page image to WAL. This shared value,
       * instead of the process-local fullPageWrites, is required because, when
       * full_page_writes is changed by SIGHUP, we must WAL-log it before it
       * actually affects WAL-logging by backends.  Checkpointer sets at startup
       * or after SIGHUP.
       *
       * To read these fields, you must hold an insertion lock. To modify them,
       * you must hold ALL the locks.
       */
      XLogRecPtr    RedoRecPtr;        /* current redo point for insertions */
      bool        fullPageWrites;

      /*
       * runningBackups is a counter indicating the number of backups currently
       * in progress. lastBackupStart is the latest checkpoint redo location
       * used as a starting point for an online backup.
       */
      int            runningBackups;
      XLogRecPtr    lastBackupStart;

      /*
       * WAL insertion locks.
       */
      WALInsertLockPadded *WALInsertLocks;
  } XLogCtlInsert;

 

XLogCtlInsert에서 확인한 위치 정보와 WAL Record의 크기를 기반으로 WAL Page 내 정확한 위치(시작과 끝 지점)를 확인합니다. 이때 시작 지점에 대한 정보는 WAL Record의 LSN*으로 활용됩니다.

새로운 WAL Record를 기록할 위치가 결정되면 XLogCtlInsert 구조체의 내용을 업데이트하고, 더 이상 위치 정보를 보호할 필요가 없다고 판단해 spinlock을 해제합니다.

📢 LSN(Log Sequence Number)은 WAL Buffer 혹은 WAL Segment 상의 위치를 나타내는 값으로, 저장된 순서에 따라 순차적으로 WAL Record에 부여됩니다. WAL Record가 LSN을 가지고 있기 때문에 순서를 식별하여 Replay 할 수 있습니다.

 

c. <optional> 만약 위 과정에서 WAL Page에 충분한 공간이 없다고 확인되면, WALBufMapping Lock*을 획득하여 새로운 페이지를 확보하거나 기존 페이지를 디스크에 Write하여 공간을 확보하는 작업을 수행합니다. 이 작업을 마친 후, WALBufMapping Lock을 해제합니다.

[WAL Record 저장]

d. 확인한 WAL Page 위치에 WAL Record를 실제로 Copy하는 작업을 진행합니다.

e. Copy 과정이 모두 끝나면 마침내 WALInsert Lock을 해제합니다.

📢 WAL Buffer는 Shared Memory에 위치해있으므로, 프로세스 간 경합을 관리하기 위해 WALInsert Lock, WALBufMapping Lock이 사용되었습니다.

  • WALInsert Lock : WAL Record를 WAL Buffer로 Copy하는 작업 시 사용하는 LWLock 유형의 Lock입니다. 다중화(NUM_XLOGINSERT_LOCKS=8)되어 여러 프로세스에서 동시에 사용될 수 있지만, 프로세스들이 서로 충돌하지 않도록 위치나 상태를 관리합니다.
  • WALBufMapping Lock : WAL Page 확장이 필요한 경우 획득해야 하는 LWLock 유형의 Lock입니다. 단일 Lock으로 관리되기 때문에 하나의 프로세스만 WALBufMapping Lock을 획득할 수 있습니다.

WALInsert Lock과 WALBufMapping Lock에 대해서는 "WAL Buffer > Wait Event”를 주제로 하여 다음 글에서 다루겠습니다.

 

5. Disk Write

Backend Process가 트랜잭션 Commit을 수행하면 WAL Record는 Disk Write 됩니다.

📢 WAL Record가 디스크로 Write되는 조건은 Commit 수행 외에 다양하며, 관련 내용은 “IO”를 주제로 이후 글에서 다루도록 하겠습니다.

 

마무리

  • PostgreSQL은 WAL 기법을 사용하여 데이터 변경 사항에 대한 로그를 생성된 시점에 따라 순차적으로 WAL Buffer에 저장한다.
  • WAL 동작 과정은 ① WAL Data Create →  WAL Data Register → ③ WAL Record Assembly →  WAL Record Copy →  Disk Write 순서로 진행된다.
  • WAL은 WAL Record 구조로 WAL Buffer에 저장되며, WAL Record는 XLogRecord Header, Block Header, Data Header, Block Data, Main Data 5가지 요소로 구성된다.
  • WAL Record를 WAL Buffer에 저장하는 과정에서 WALInsert LockWALBufMapping Lock을 사용한다.
  • WALInsert Lock은 WAL Buffer로 WAL Record를 Copy할 때 획득해야 하는 Lock으로, WAL Record의 동시 삽입을 관리하고 보호하여 데이터 무결성을 보장하는데 중요한 역할을 한다.
  • WALBufMapping Lock은 WAL Record를 저장하기 위한 WAL Page 공간이 부족한 경우에 한하여 페이지 확장 작업 시 추가적으로 필요한 Lock이다.

 

 

댓글