(KO) PostgreSQL pg_waldump — WAL 디코딩 및 검사 유틸리티
목차
- 학술적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 가이드
- 소스 검증 (2026-06-06 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
학술적 배경
섹션 제목: “학술적 배경”write-ahead log(WAL, 미리-쓰기 로그)는 설계상 불투명하다. 순차 추가에 최적화된 이진 바이트 스트림이므로 사람이 직접 읽기 위한 구조가 아니다. “이 바이트들이 살아남았다면 이 변경도 살아남았다”는 정확성 보장은 정상 운영 중 엔진이 바이트를 해석할 필요 없이 작동한다는 사실에서 나온다. 문제가 생기는 순간 이 불투명함은 부채로 바뀐다. 스탠바이 지연을 보는 DBA, 예상치 못한 VACUUM 중단을 디버깅하는 엔지니어, WAL 레코드를 남기는 익스텐션을 개발하는 개발자 모두 같은 것이 필요하다. 로그를 읽고 무엇이 들어 있는지 파악하는 방법이다.
핵심 문제는 WAL 내성 검사(introspection)다. 디스크의 물리 로그 파일을 받아 각 레코드의 식별자·위치·크기·페이로드 요약을 사람이 읽을 수 있는 형태로 출력하는 것이다. 이를 안전하고 유용하게 만들려면 세 가지 속성이 필요하다.
오프라인 읽기 전용 접근. 데이터베이스가 온라인이 아닌 상태에서도 WAL 세그먼트를 읽을 수 있어야 한다. 주된 사용 사례는 크래시 서버의 사후 분석이거나 라이브 인스턴스에 연결되지 않은 아카이브 WAL 스트림 검사다. 버퍼 매니저·공유 메모리·잠금 매니저 등 실행 중인 서버를 가정하는 어떤 인프라도 호출할 수 없다. 파일 위에서 동작하는 순수한 리더여야 한다.
충실한 레코드 디코딩. WAL 레코드는 자기 기술적인 텍스트가 아니다. 페이로드는 그것을 쓴 리소스 매니저(rmgr, resource manager)가 레이아웃을 정의하는 이진 블롭이다. Heap INSERT 레코드, B-트리 페이지 분할 레코드, 체크포인트 레코드 모두 동일한 와이어 프레임을 쓰지만 페이로드는 다르다. 내성 검사 도구는 레코드의 redo 함수를 실행하지 않으면서도 복구 드라이버가 사용하는 것과 같은 desc·identify 로직을 호출해야 한다. rmgr 테이블이 도구에 존재해야 하되 설명 콜백만 있고 복구 로직은 없어야 한다는 뜻이다.
위치와 크기 집계. 복제 지연이나 체크포인트 압박을 디버깅하는 운영자에게는 레코드 텍스트만으로 부족하다. 누적 카운트와 크기가 필요하다. Heap rmgr이 WAL 세그먼트당 얼마나 많은 바이트를 소비하는지, 전체 페이지 이미지(FPI, full-page image)가 차지하는 비율은 얼마인지, 어떤 레코드 타입이 가장 많은 볼륨을 발생시키는지 같은 정보다. 기본 레코드 스캔 위에 통계 누적 모드를 별도로 두어야 한다.
이 세 가지 속성이 pg_waldump의 세 가지 동작 모드에 정확히 대응한다. 일반 레코드 표시, 통계(--stats), 팔로우 모드(--follow)가 그것이다.
ARIES 논문(Mohan et al. 1992)은 로그를 prevLSN 역방향 포인터로 연결된 자기 기술적 레코드의 시퀀스로 정의한다. 역방향 포인터는 순차 역방향 순회를 가능하게 하는 구조다. pg_waldump는 순방향 링크 구조를 활용한다. 각 레코드가 자신의 길이를 담고 있으므로 다음 레코드는 현재 LSN에 패딩된 레코드 길이를 더한 위치에서 시작한다. xl_prev 역방향 포인터(XLogRecGetPrev)는 각 표시 줄에서 이전 레코드 LSN을 보고하는 데 쓴다. 레코드 프레임 구조는 postgres-xlog-wal.md를, rmgr 디스패치 테이블은 postgres-wal-records-rmgr.md를 참고한다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”모든 프로덕션 수준 DBMS는 WAL 검사 도구를 제공한다. 구체적인 구현은 달라도 공통적으로 수렴하는 구조적 선택이 있다. PostgreSQL 고유 구현을 보기 전에 이 선택들을 정리해 두면 이해에 도움이 된다.
공유 라이브러리를 사용하는 프런트엔드 바이너리
섹션 제목: “공유 라이브러리를 사용하는 프런트엔드 바이너리”표준 패턴은 서버와 동일한 소스 트리에서 컴파일되는 독립 바이너리다. WAL 리더와 rmgr 설명 코드를 공유하되 복구 실행기와 서버 측 인프라는 제외한다. MySQL/InnoDB는 mysqlbinlog를 제공한다(binlog 파일에 동작하며 MySQL의 논리적 WAL 유사체다). Oracle은 LogMiner를 제공하는데 외부 바이너리가 아닌 서버 측 패키지로 동작한다. SQL Server는 라이브 연결에서 호출하는 T-SQL 테이블 반환 함수 fn_dblog와 fn_dump_dblog를 제공한다. PostgreSQL의 선택 — 서버와 같은 C 라이브러리에 링크하지만 완전히 프런트엔드 프로세스로 실행되는 외부 바이너리 — 은 Oracle의 완전 서버 통합 방식과 가상의 써드파티 파서 사이 어딘가에 위치한다.
플러그어블 페이지 읽기 콜백
섹션 제목: “플러그어블 페이지 읽기 콜백”서버 내부(WAL 버퍼나 스트리밍 복제 수신 버퍼에서 읽기)와 외부(디스크 세그먼트 파일에서 직접 읽기) 모두에서 동작하는 WAL 리더는 I/O 경로를 하드코딩할 수 없다. 보편적 해법은 호출자가 제공하는 페이지 읽기 콜백이다. 서버 내부에서는 공유 WAL 버퍼에서, 외부에서는 세그먼트 파일을 직접 열어 읽는다. 레코드 프레임 처리, CRC 검사, 블록 참조 디코딩 등 WAL 리더의 핵심 루프는 두 경우 모두 동일하다. pg_waldump는 파일 기반 경로를 위해 자체 WALDumpReadPage 콜백을 제공한다.
필터-후-표시 파이프라인
섹션 제목: “필터-후-표시 파이프라인”WAL 원시 출력은 방대하다. WAL 세그먼트의 기본 크기는 16 MB이고 수천 개의 레코드를 담을 수 있다. 모든 검사 도구는 출력을 좁히는 필터를 제공한다. Oracle LogMiner는 START_TIME/END_TIME과 OBJECT_NAME으로, pg_waldump는 rmgr·XID·릴레이션(테이블스페이스 OID / 데이터베이스 OID / 릴레이션 파일노드)·블록 번호·포크·전체 페이지 쓰기 존재 여부로 필터링한다. 필터는 XLogReadRecord가 성공한 직후 타이트한 메인 루프 안에서 적용된다. 매칭된 레코드는 표시하고 건너뛴 레코드는 출력 없이 루프를 진행한다.
독립 출력 경로로서의 통계 모드
섹션 제목: “독립 출력 경로로서의 통계 모드”WAL 내용의 집계 뷰 — “이 세그먼트에서 레코드 타입별로 몇 바이트씩 기여하는가?” — 는 레코드 단위 표시와 질적으로 다르다. 스캔 전체에 걸쳐 카운트와 크기를 누적한 뒤 요약 표를 출력해야 한다. 누적 경로와 표시 경로를 분리하면 둘 다 깔끔하게 유지된다. 표시 경로는 무상태(레코드 하나 입력, 줄 하나 출력)이고 통계 경로는 유상태(N개 레코드 입력, 표 하나 출력)다. pg_waldump는 동일한 메인 루프 안에서 XLogDumpDisplayRecord와 XLogDumpDisplayStats를 상호 배타적인 두 출력 경로로 구현한다.
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”pg_waldump는 단일 파일 프런트엔드 바이너리다(src/bin/pg_waldump/pg_waldump.c, 1323줄). 동반 파일인 rmgrdesc.c는 RmgrDescTable을 제공한다. 이는 서버 측 rmgr 테이블의 축약판으로 rm_name·rm_desc·rm_identify 콜백만 담고 redo·startup·cleanup 콜백은 없다. 바이너리는 유틸리티 함수를 위해 libpgcommon과 libpgport에 링크하지만 서버 백엔드 라이브러리에는 링크하지 않는다.
flowchart TB
CLI["main()<br/>옵션 파싱<br/>XLogDumpConfig + XLogDumpPrivate"]
DIR["identify_target_directory()<br/>WAL 세그먼트 디렉터리 탐색<br/>., pg_wal, $PGDATA/pg_wal 순서 시도"]
ALLOC["XLogReaderAllocate()<br/>WalSegSz, waldir<br/>XL_ROUTINE 콜백 등록"]
FIND["XLogFindNextRecord()<br/>첫 번째 유효한 레코드 위치로 순방향 탐색"]
LOOP["메인 루프<br/>XLogReadRecord()"]
FILTER{"필터 통과?<br/>rmgr / xid / relation<br/>block / fork / fpw"}
DISPLAY["XLogDumpDisplayRecord()<br/>rm_name + rm_identify + rm_desc<br/>+ XLogRecGetBlockRefInfo"]
STATS["XLogRecStoreStats()<br/>XLogStats 누적"]
FPW["XLogRecordSaveFPWs()<br/>RestoreBlockImage → 파일 저장"]
SUMMARY["XLogDumpDisplayStats()<br/>rmgr/레코드별 요약 표"]
END["XLogReaderFree()"]
CLI --> DIR --> ALLOC --> FIND --> LOOP
LOOP --> FILTER
FILTER -->|"no"| LOOP
FILTER -->|"yes, --stats"| STATS --> FPW --> LOOP
FILTER -->|"yes, display"| DISPLAY --> FPW --> LOOP
LOOP -->|"end or --limit"| SUMMARY --> END
그림 1 — pg_waldump 메인 제어 흐름. 표시와 통계 두 출력 경로는 루프 안에서 상호 배타적이다. --save-fullpage는 모드와 무관하게 매칭된 모든 레코드에서 실행된다.
시작: WAL 위치 확인과 세그먼트 헤더 읽기
섹션 제목: “시작: WAL 위치 확인과 세그먼트 헤더 읽기”main은 identify_target_directory를 호출해 WAL 세그먼트 파일이 있는 디렉터리를 찾는다. 탐색 순서는 (1) 명시적 --path 인수, (2) 해당 경로에 /pg_wal을 붙인 것, (3) --path가 없으면 현재 디렉터리, pg_wal, $PGDATA/pg_wal 순이다. 각 후보에서 search_directory가 WAL 파일명 패턴에 맞는 파일을 열고 첫 8 KB 페이지를 읽어 XLogLongPageHeader에서 WalSegSz를 추출한다.
// search_directory — pg_waldump.c (condensed)r = read(fd, buf.data, XLOG_BLCKSZ);if (r == XLOG_BLCKSZ){ XLogLongPageHeader longhdr = (XLogLongPageHeader) buf.data; WalSegSz = longhdr->xlp_seg_size; if (!IsValidWalSegSize(WalSegSz)) /* error: not a power of two between 1 MB and 1 GB */ exit(1);}IsValidWalSegSize는 크기가 [1 MB, 1 GB] 범위의 2의 거듭제곱인지 확인한다. 세그먼트 크기는 컴파일 타임 기본값(16 MB)이지만 initdb 시점에 재정의할 수 있다. pg_waldump는 컴파일 기본값을 가정하는 대신 파일에서 직접 읽어야 한다. 기본값이 아닌 --wal-segsize로 초기화된 클러스터에서 이 처리가 의미를 갖는다.
XLogReaderState 초기화와 세 가지 콜백
섹션 제목: “XLogReaderState 초기화와 세 가지 콜백”WAL 디렉터리와 세그먼트 크기를 확인한 뒤 main은 XLogReaderAllocate를 호출해 XLogReaderState를 생성한다. 세 가지 파일 I/O 콜백은 XL_ROUTINE 매크로로 전달된다. 이 매크로는 XLogReaderRoutine 타입의 복합 리터럴을 초기화한다.
// main — pg_waldump.c (condensed)xlogreader_state = XLogReaderAllocate(WalSegSz, waldir, XL_ROUTINE(.page_read = WALDumpReadPage, .segment_open = WALDumpOpenSegment, .segment_close = WALDumpCloseSegment), &private);WALDumpOpenSegment는 XLogFileName으로 (tli, segno, segsize)에서 세그먼트 파일명을 만든 뒤 읽기 전용으로 연다. --follow 모드에서는 500 ms 간격으로 최대 10번 재시도한다. 서버가 이전 세그먼트 쓰기를 마친 뒤 다음 세그먼트를 생성하기 전까지의 짧은 구간을 처리하기 위한 바쁜 대기(busy-wait)다.
WALDumpReadPage는 핵심 I/O 콜백이다. 서버 내부의 read_local_xlog_page에 해당하는 파일 기반 구현체다. 요청이 설정된 끝 포인터(private.endptr) 범위 안에 있는지 확인하고, 읽을 바이트 수를 계산한 뒤 WALRead를 호출한다. WALRead가 실패하면 WALReadError 구조체에서 세그먼트 파일명과 오프셋을 보고한다. endptr을 넘는 읽기는 private.endptr_reached = true를 설정하고 -1을 반환해 루프를 멈춘다.
// WALDumpReadPage — pg_waldump.c (condensed)static intWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, XLogRecPtr targetPtr, char *readBuff){ XLogDumpPrivate *private = state->private_data; int count = XLOG_BLCKSZ; WALReadError errinfo;
if (private->endptr != InvalidXLogRecPtr) { if (targetPagePtr + XLOG_BLCKSZ <= private->endptr) count = XLOG_BLCKSZ; else if (targetPagePtr + reqLen <= private->endptr) count = private->endptr - targetPagePtr; else { private->endptr_reached = true; return -1; /* stops the main loop */ } }
if (!WALRead(state, readBuff, targetPagePtr, count, private->timeline, &errinfo)) { WALOpenSegment *seg = &errinfo.wre_seg; char fname[MAXPGPATH];
XLogFileName(fname, seg->ws_tli, seg->ws_segno, state->segcxt.ws_segsize); /* pg_fatal with fname + errinfo.wre_off ... */ }
return count;}endptr에 대한 세 가지 분기가 핵심이다. 전체 페이지가 경계 안에 들어오면 XLOG_BLCKSZ 전체를 읽는다. 경계까지 reqLen 바이트만 필요하면 부분 페이지(count = endptr - targetPagePtr)를 읽는다. 경계 자체를 넘으면 endptr_reached를 세트한다. WALRead는 내부적으로 WALDumpOpenSegment·WALDumpCloseSegment 콜백을 호출하므로 이 함수 자체는 파일을 직접 열지 않는다.
WALDumpCloseSegment는 파일 디스크립터를 닫고 -1로 초기화하는 단순한 함수다.
메인 읽기 루프
섹션 제목: “메인 읽기 루프”XLogFindNextRecord가 요청된 시작 LSN 이후 첫 번째 유효한 레코드 위치로 리더를 옮긴 뒤, 메인 루프는 XLogReadRecord를 반복 호출한다.
// main — pg_waldump.c (condensed)for (;;){ if (time_to_stop) break; /* SIGINT handler set this */
record = XLogReadRecord(xlogreader_state, &errormsg); if (!record) { if (!config.follow || private.endptr_reached) break; pg_usleep(1000000L); /* follow mode: sleep 1 s and retry */ continue; }
/* apply filters */ if (config.filter_by_rmgr_enabled && !config.filter_by_rmgr[record->xl_rmid]) continue; if (config.filter_by_xid_enabled && config.filter_by_xid != record->xl_xid) continue; if (config.filter_by_extended && !XLogRecordMatchesRelationBlock(...)) continue; if (config.filter_by_fpw && !XLogRecordHasFPW(xlogreader_state)) continue;
/* output */ if (!config.quiet) { if (config.stats) { XLogRecStoreStats(&stats, xlogreader_state); stats.endptr = ...; } else XLogDumpDisplayRecord(&config, xlogreader_state); } if (config.save_fullpage_path) XLogRecordSaveFPWs(xlogreader_state, config.save_fullpage_path);
config.already_displayed_records++; if (config.stop_after_records > 0 && config.already_displayed_records >= config.stop_after_records) break;}XLogReadRecord는 페이지 경계 처리, 레코드 분할, CRC 검증, 역방향 링크 검사를 내부에서 처리한다. pg_waldump 루프는 디코딩된 XLogRecord *(또는 WAL 끝이나 오류 시 NULL)와 XLogReaderState에 언패킹된 블록 참조만 본다.
필터를 통과한 레코드마다 루프 본문이 수행하는 실제 처리는 다음과 같다.
// main (loop body) — pg_waldump.c (verbatim)/* perform any per-record work */if (!config.quiet){ if (config.stats == true) { XLogRecStoreStats(&stats, xlogreader_state); stats.endptr = xlogreader_state->EndRecPtr; } else XLogDumpDisplayRecord(&config, xlogreader_state);}
/* save full pages if requested */if (config.save_fullpage_path != NULL) XLogRecordSaveFPWs(xlogreader_state, config.save_fullpage_path);
/* check whether we printed enough */config.already_displayed_records++;if (config.stop_after_records > 0 && config.already_displayed_records >= config.stop_after_records) break;두 가지 세부 사항이 중요하다. 첫째, stats.endptr은 통계 레코드마다 갱신된다. 루프가 --limit으로 일찍 끝날 수 있고, 요약 표의 바이트 범위 헤더는 실제로 집계된 마지막 레코드를 반영해야 하기 때문이다. 둘째, XLogRecordSaveFPWs는 if (!config.quiet) 블록 바깥에 있다. --save-fullpage --quiet를 함께 쓰면 텍스트 출력을 억제하면서도 이미지 추출은 계속 동작한다. FPI 사이드 채널이 표시 경로와 의도적으로 분리된 결과다.
flowchart TB
TOP["루프 상단"]
SIG{"time_to_stop?<br/>SIGINT 핸들러"}
READ["record = XLogReadRecord()"]
NULLQ{"record == NULL?"}
FOLLOW{"follow이고<br/>endptr_reached 아님?"}
SLEEP["pg_usleep 1 s<br/>continue"]
EXIT["루프 종료"]
FRMGR{"rmgr 필터 통과?"}
FXID{"xid 필터 통과?"}
FEXT{"릴레이션/블록/포크<br/>필터 통과?"}
FFPW{"fpw 필터 통과?"}
OUT{"stats 모드?"}
STORE["XLogRecStoreStats()<br/>stats.endptr = EndRecPtr"]
DISP["XLogDumpDisplayRecord()"]
FPW["save_fullpage_path?<br/>XLogRecordSaveFPWs()"]
LIMIT{"stop_after_records<br/>도달?"}
TOP --> SIG
SIG -->|"yes"| EXIT
SIG -->|"no"| READ --> NULLQ
NULLQ -->|"yes"| FOLLOW
FOLLOW -->|"yes"| SLEEP --> TOP
FOLLOW -->|"no"| EXIT
NULLQ -->|"no"| FRMGR
FRMGR -->|"no"| TOP
FRMGR -->|"yes"| FXID
FXID -->|"no"| TOP
FXID -->|"yes"| FEXT
FEXT -->|"no"| TOP
FEXT -->|"yes"| FFPW
FFPW -->|"no"| TOP
FFPW -->|"yes"| OUT
OUT -->|"yes"| STORE --> FPW
OUT -->|"no, display"| DISP --> FPW
FPW --> LIMIT
LIMIT -->|"yes"| EXIT
LIMIT -->|"no"| TOP
그림 2 — 디코드 루프. 각 반복은 레코드 하나를 읽고, 네 가지 필터 게이트를 고정된 순서(rmgr, xid, 확장 릴레이션/블록/포크, fpw)로 통과시킨다. 게이트 하나라도 실패하면 continue로 루프 상단으로 돌아가 출력은 없다. 그 뒤 통계 또는 표시 경로 중 정확히 하나로 디스패치한다. --save-fullpage와 --limit 검사는 어느 출력 경로 이후에도 실행된다. 필터는 좌에서 우로 단락 평가되므로 가장 저렴한 검사(단일 배열 인덱스, 단일 정수 비교)가 블록 단위 반복 검사보다 먼저 온다.
XLogDumpConfig가 모든 필터 상태를 보관한다. 다섯 가지 독립 필터 차원을 조합할 수 있다.
- rmgr (
--rmgr): 불리언 배열filter_by_rmgr[RM_MAX_ID + 1].--rmgr=list옵션은print_rmgr_list를 호출해0..RM_MAX_BUILTIN_ID범위의 빌트인 rmgr 이름을 출력한다. - xid (
--xid): 단일TransactionId. - 릴레이션 + 블록 + 포크 (
--relation,--block,--fork):XLogRecordMatchesRelationBlock이 처리한다.XLogRecMaxBlockId·XLogRecGetBlockTagExtended로 레코드 내 모든 블록 ID를 순회해RelFileLocatorEquals, 블록 번호, 포크 번호를 검사한다. - 전체 페이지 이미지 존재 여부 (
--fullpage):XLogRecordHasFPW가 처리한다. 블록 ID를 순회해XLogRecHasBlockImage를 검사한다.
filter_by_extended 플래그는 릴레이션·블록·포크 검사의 게이트다. --relation·--block·--fork 가운데 하나라도 지정되면 세트된다. --block은 --relation을 요구하며 코드가 옵션 파싱 후 이를 강제한다.
--rmgr 옵션(getopt_long 스위치의 case 'r')은 파싱 시점에 이름을 rmgr ID로 변환해 filter_by_rmgr[] 불리언 배열의 해당 슬롯을 세트한다. "list" 리터럴(이름 출력 후 종료), "custom###" 형식의 숫자 ID(로드되지 않은 커스텀 rmgr), 대소문자를 구분하지 않고 rmgr 테이블과 매칭하는 빌트인 이름 세 가지 형식을 받는다.
// main (case 'r') — pg_waldump.c (condensed)if (pg_strcasecmp(optarg, "list") == 0){ print_rmgr_list(); exit(EXIT_SUCCESS);}
/* "custom###": the module isn't loaded, so match by numeric ID */if (sscanf(optarg, "custom%03d", &rmid) == 1){ if (!RmgrIdIsCustom(rmid)) /* error: custom resource manager does not exist */ ; config.filter_by_rmgr[rmid] = true; config.filter_by_rmgr_enabled = true;}else{ /* then look for builtin rmgrs by name */ for (rmid = 0; rmid <= RM_MAX_BUILTIN_ID; rmid++) if (pg_strcasecmp(optarg, GetRmgrDesc(rmid)->rm_name) == 0) { config.filter_by_rmgr[rmid] = true; config.filter_by_rmgr_enabled = true; break; } if (rmid > RM_MAX_BUILTIN_ID) /* error: resource manager does not exist */ ;}파싱 시점에 이름을 정수 ID로 변환하기 때문에 메인 루프의 필터 검사가 레코드당 단일 O(1) 배열 인덱스(config.filter_by_rmgr[record->xl_rmid])로 끝난다. 문자열 비교를 레코드마다 반복하지 않아도 된다. --rmgr을 여러 번 지정하면 여러 슬롯이 세트되어 필터는 합집합으로 동작한다(나열된 rmgr 중 하나라도 매칭되면 통과).
표시: rm_desc와 rm_identify
섹션 제목: “표시: rm_desc와 rm_identify”XLogDumpDisplayRecord는 레코드당 한 줄을 출력한다. GetRmgrDesc(XLogRecGetRmid(record))로 해당 레코드 rmgr의 RmgrDescData를 얻은 뒤 고정 필드를 출력하고 두 텍스트 콜백을 호출한다.
// XLogDumpDisplayRecord — pg_waldump.c (condensed)const RmgrDescData *desc = GetRmgrDesc(XLogRecGetRmid(record));XLogRecGetLen(record, &rec_len, &fpi_len);
printf("rmgr: %-11s len (rec/tot): %6u/%6u, tx: %10u, lsn: %X/%08X, prev %X/%08X, ", desc->rm_name, rec_len, XLogRecGetTotalLen(record), XLogRecGetXid(record), LSN_FORMAT_ARGS(record->ReadRecPtr), LSN_FORMAT_ARGS(xl_prev));
id = desc->rm_identify(info);if (id == NULL) printf("desc: UNKNOWN (%x) ", info & ~XLR_INFO_MASK);else printf("desc: %s ", id);
initStringInfo(&s);desc->rm_desc(&s, record);printf("%s", s.data);
XLogRecGetBlockRefInfo(record, true, config->bkp_details, &s, NULL);printf("%s", s.data);rm_identify는 4비트 옵코드(xl_info 상위 니블)를 "INSERT", "UPDATE", "HEAP_LOCK" 같은 문자열로 변환한다. rm_desc는 페이로드 요약을 덧붙인다. Heap INSERT라면 대상 블록 번호와 오프셋, 체크포인트라면 redo 포인터와 타임라인이다. 두 콜백 모두 rmgr의 *desc.c 파일(예: heapdesc.c, nbtdesc.c)에 정의되어 있으며, 동일한 PG_RMGR X-매크로 확장으로 서버와 pg_waldump의 rmgrdesc.c 양쪽에 컴파일된다.
XLogRecGetBlockRefInfo는 블록 참조 목록을 덧붙인다. 등록된 각 블록마다 쉼표로 구분된 blk N: rel T/D/R fork main blk B 요약을 출력하며, 블록에 전체 페이지 이미지가 있으면 FPW를 표시한다.
통계 모드
섹션 제목: “통계 모드”--stats를 지정하면 XLogDumpDisplayRecord 대신 XLogRecStoreStats(src/include/access/xlogstats.h)가 호출된다. 이 함수는 XLogStats.rmgr_stats[rmid]를 증가시키고 옵션으로 record_stats[rmid][xl_info >> 4]도 증가시킨다. 스캔이 끝나면 XLogDumpDisplayStats가 요약을 출력한다.
// XLogDumpDisplayStats — pg_waldump.c (condensed)for (ri = 0; ri <= RM_MAX_ID; ri++){ if (!RmgrIdIsValid(ri)) continue; desc = GetRmgrDesc(ri); if (!config->stats_per_record) { count = stats->rmgr_stats[ri].count; /* ... */ XLogDumpStatsRow(desc->rm_name, count, total_count, rec_len, total_rec_len, fpi_len, total_fpi_len, tot_len, total_len); } else { for (rj = 0; rj < MAX_XLINFO_TYPES; rj++) { /* skip zero-count entries */ id = desc->rm_identify(rj << 4); XLogDumpStatsRow(psprintf("%s/%s", desc->rm_name, id), ...); } }}MAX_XLINFO_TYPES는 16이다(rmgr당 4비트 옵코드 공간). 통계 표는 네 열로 구성된다. 레코드 카운트(%), 레코드 본문 바이트(%), FPI 바이트(%), 합산 바이트(%)이며 각각 열 합계에 대한 퍼센트를 함께 표시한다. --stats=record는 rmgr/옵코드 행을, --stats만 지정하면 rmgr 행을 출력한다. 행별 퍼센트는 열 합계 대비이며 최종 Total 행은 FPI 비율을 [xx.xx%] 형식으로 행 합계 대비 표시한다.
전체 페이지 이미지 추출
섹션 제목: “전체 페이지 이미지 추출”--save-fullpage=DIR을 지정하면 XLogRecordSaveFPWs가 활성화된다. 매칭된 레코드마다 표시·통계 모드와 무관하게 실행된다. 블록 ID를 순회하며 XLogRecHasBlockImage가 없는 블록은 건너뛰고, RestoreBlockImage를 호출해 저장된 이미지를 PGAlignedBlock 버퍼에 압축 해제한 뒤 아래 이름의 파일에 쓴다.
<DIR>/<tli>-<LSN_hi>/<LSN_lo>.<spcOid>.<dbOid>.<relNumber>.<blk>_<fork>복구 드라이버가 redo 중 데이터 페이지에 적용하는 것과 동일한 이미지다. 파일은 원시 8 KB(또는 BLCKSZ 바이트) 페이지다. PostgreSQL 페이지 레이아웃을 이해하는 도구라면 무엇이든 열 수 있다(postgres-page-layout.md 참고).
커스텀 rmgr 폴백
섹션 제목: “커스텀 rmgr 폴백”pg_waldump는 익스텐션 모듈을 로드하지 않는다. 커스텀 rmgr ID(RM_MAX_BUILTIN_ID 초과)를 가진 레코드를 만나면 GetRmgrDesc는 첫 번째 조회 시 initialize_custom_rmgrs를 호출해 CustomRmgrDesc[]를 custom000..customNNN 숫자 이름과 스텁 default_desc·default_identify 콜백으로 채운다. 스텁 rm_identify는 NULL을 반환해 표시에서 "UNKNOWN"으로 이어지고, default_desc는 원시 rmgr ID만 덧붙인다. --rmgr 옵션은 같은 "custom###" 명명 규칙으로 커스텀 rmgr 이름을 받으므로 모듈이 로드되지 않아도 숫자 커스텀 rmgr ID로 필터링할 수 있다.
팔로우 모드
섹션 제목: “팔로우 모드”--follow는 WAL 끝에 도달해도 종료하지 않고 루프를 유지한다. XLogReadRecord가 NULL을 반환하고(현재 세그먼트 끝) endptr_reached가 거짓이면 루프는 1초 대기 후 재시도한다. WALDumpOpenSegment도 다음 세그먼트 파일이 아직 없으면 500 ms 간격으로 최대 10번 재시도한다. 서버가 이전 세그먼트의 마지막 레코드를 기록한 뒤 새 세그먼트를 생성하기 전까지의 구간을 처리하기 위해서다. SIGINT는 volatile sig_atomic_t time_to_stop 플래그를 세트하며, 메인 루프는 각 반복 맨 위에서 이를 확인한다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼 이름을 기준으로 삼는다. 줄 번호는 힌트일 뿐이다. 아래 표의 줄 번호는 커밋
273fe94(REL_18_STABLE, 2026-06-06) 기준이다.
메인 드라이버 (pg_waldump.c)
섹션 제목: “메인 드라이버 (pg_waldump.c)”main—getopt_long으로 18개 long 옵션 파싱, 디렉터리 탐색,XLogReaderAllocate,XLogFindNextRecord, 메인 읽기 루프,--stats시 최종XLogDumpDisplayStats,XLogReaderFree.sigint_handler—time_to_stop = true세트. 루프 맨 위에서 확인.identify_target_directory— 세 후보 탐색(명시적 경로, 경로 +/pg_wal, 그 다음.,pg_wal,$PGDATA/pg_wal).search_directory— WAL 파일(이름으로 또는IsXLogFileName매칭으로 스캔)을 열고 첫 페이지를 읽어WalSegSz를 발견.verify_directory/open_file_in_directory/split_path— 경로 헬퍼 함수들.create_fullpage_directory—--save-fullpage출력 디렉터리를pg_check_dir·pg_mkdir_p로 생성.WALDumpOpenSegment—XLogReaderRoutine.segment_open콜백. 팔로우 모드에서 10 × 500 ms 재시도.WALDumpCloseSegment—XLogReaderRoutine.segment_close콜백.WALDumpReadPage—XLogReaderRoutine.page_read콜백.endptr검사와 함께WALRead호출.XLogRecordMatchesRelationBlock—XLogRecGetBlockTagExtended로 블록 ID 순회,RelFileLocatorEquals·블록 번호·포크 번호 검사.XLogRecordHasFPW— 블록 ID 순회,XLogRecHasBlockImage검사.XLogRecordSaveFPWs— 블록 ID 순회,RestoreBlockImage, 원시 페이지를--save-fullpage디렉터리에 저장.XLogDumpDisplayRecord— 레코드별 표시 줄 포맷.rm_name, 길이, xid, LSN, 이전 LSN,rm_identify(info),rm_desc(s, record),XLogRecGetBlockRefInfo.XLogDumpStatsRow— 네 개 카운트/퍼센트 열로 통계 표 한 행 포맷.XLogDumpDisplayStats— 2회 통과 통계 출력. 첫 번째 통과에서 합계 계산, 두 번째 통과에서 rmgr별(또는 rmgr/옵코드별) 행 출력, 마지막 FPI 비율 포함Total행.print_rmgr_list—--rmgr=list용 빌트인 rmgr 이름 출력.usage— 18개 옵션 전체를 나열하는 도움말 텍스트.
rmgr 설명 레이어 (rmgrdesc.c, rmgrdesc.h)
섹션 제목: “rmgr 설명 레이어 (rmgrdesc.c, rmgrdesc.h)”RmgrDescTable[RM_N_BUILTIN_IDS]—access/rmgrlist.h에 대한PG_RMGRX-매크로로 구축된 정적 배열. 각 항목은rm_name,rm_desc,rm_identify를 보유.redo·startup·cleanup없음.CustomRmgrDesc[RM_N_CUSTOM_IDS]— 첫 번째 커스텀 rmgr 조회 시initialize_custom_rmgrs가 지연 초기화. 이름은"custom000".."custom127".GetRmgrDesc(rmid)— 빌트인이면&RmgrDescTable[rmid]반환. 커스텀이면 초기화 후&CustomRmgrDesc[rmid - RM_MIN_CUSTOM_ID]반환.
핵심 데이터 구조
섹션 제목: “핵심 데이터 구조”XLogDumpPrivate—{timeline, startptr, endptr, endptr_reached}.XLogReaderState에private_data로 전달.XLogDumpConfig— 모든 옵션 상태. 표시 플래그(quiet,bkp_details,follow,stats,stats_per_record), 필터 필드(filter_by_rmgr[],filter_by_xid,filter_by_relation,filter_by_relation_block,filter_by_relation_forknum,filter_by_fpw),stop_after_records,save_fullpage_path.XLogStats(xlogstats.h) —{count, startptr, endptr, rmgr_stats[RM_MAX_ID+1], record_stats[RM_MAX_ID+1][MAX_XLINFO_TYPES]}. 각XLogRecStats슬롯은{count, rec_len, fpi_len}보유.RmgrDescData(pg_waldump의rmgrdesc.h, 서버 측rmgr.h미러링) —{rm_name, rm_desc, rm_identify}.XLogReaderRoutine(xlogreader.h) —{page_read, segment_open, segment_close}함수 포인터.XL_ROUTINE(...)이 복합 리터럴을 초기화.
위치 힌트 (2026-06-06, REL_18 273fe94 기준)
섹션 제목: “위치 힌트 (2026-06-06, REL_18 273fe94 기준)”| 심볼 | 파일 | 줄 |
|---|---|---|
XLogDumpPrivate (typedef) | pg_waldump.c | 47 |
XLogDumpConfig (typedef) | pg_waldump.c | 55 |
sigint_handler | pg_waldump.c | 91 |
print_rmgr_list | pg_waldump.c | 97 |
verify_directory | pg_waldump.c | 113 |
create_fullpage_directory | pg_waldump.c | 127 |
split_path | pg_waldump.c | 160 |
open_file_in_directory | pg_waldump.c | 188 |
search_directory | pg_waldump.c | 209 |
identify_target_directory | pg_waldump.c | 291 |
WALDumpOpenSegment | pg_waldump.c | 337 |
WALDumpCloseSegment | pg_waldump.c | 379 |
WALDumpReadPage | pg_waldump.c | 388 |
XLogRecordMatchesRelationBlock | pg_waldump.c | 437 |
XLogRecordHasFPW | pg_waldump.c | 469 |
XLogRecordSaveFPWs | pg_waldump.c | 489 |
XLogDumpDisplayRecord | pg_waldump.c | 545 |
XLogDumpStatsRow | pg_waldump.c | 584 |
XLogDumpDisplayStats | pg_waldump.c | 625 |
usage | pg_waldump.c | 755 |
main | pg_waldump.c | 792 |
RmgrDescTable[] | rmgrdesc.c | 37 |
initialize_custom_rmgrs | rmgrdesc.c | 68 |
GetRmgrDesc | rmgrdesc.c | 86 |
XLogStats (struct) | xlogstats.h | 28 |
XLogRecStoreStats | xlogstats.h | 41 |
XLogReaderRoutine (struct) | xlogreader.h | 72 |
XL_ROUTINE (macro) | xlogreader.h | 117 |
XLogReaderAllocate | xlogreader.h | 331 |
소스 검증 (2026-06-06 기준)
섹션 제목: “소스 검증 (2026-06-06 기준)”확인된 사실
섹션 제목: “확인된 사실”-
WalSegSz는 컴파일 기본값을 가정하지 않고 항상 세그먼트 파일 첫 페이지에서 발견된다.search_directory에서 확인.XLOG_BLCKSZ바이트를 읽고XLogLongPageHeader로 캐스팅한 뒤xlp_seg_size를 읽는다.IsValidWalSegSize가 2의 거듭제곱 제약을 검사한 뒤에야WalSegSz를 사용한다. -
세 가지 I/O 콜백은
XL_ROUTINE으로 등록되며, 이 매크로는XLogReaderRoutine의 복합 리터럴로 확장된다.xlogreader.h에서 확인.#define XL_ROUTINE(...) &(XLogReaderRoutine){__VA_ARGS__}.XLogReaderAllocate시그니처는XLogReaderRoutine *routine을 받는다. -
--follow는 두 지점에서 대기한다.WALDumpOpenSegment에서 500 ms 간격 최대 10회 재시도, 메인 루프에서XLogReadRecord가 NULL을 반환할 때 1초 대기.WALDumpOpenSegment(tries < 10,pg_usleep(500 * 1000))와 메인 루프(pg_usleep(1000000L))에서 확인. -
filter_by_rmgr[]검사는 보조 디코딩 필드가 아닌record->xl_rmid(XLogRecord헤더의xl_rmid필드)를 직접 사용한다. 메인 루프의!config.filter_by_rmgr[record->xl_rmid]에서 확인.XLogReadRecord는 원시XLogRecord *를 반환하므로xl_rmid가 실제 헤더 바이트다. -
--block은--relation을 요구한다.main에서getopt_long이후 확인.config.filter_by_relation_block_enabled && !config.filter_by_relation_enabled이면pg_log_error후goto bad_argument. -
--rmgr의 커스텀 rmgr 이름은 세 자리"custom###"ID로 받으며, 이는rmgrdesc.c의initialize_custom_rmgrs와 일관된다.case 'r'분기에서 빌트인 이름 루프 전에sscanf(optarg, "custom%03d", &rmid)시도에서 확인.initialize_custom_rmgrs도 같은"custom%03d"포맷을 사용한다. -
통계 누적은 로컬 코드가 아닌
xlogstats.h의XLogRecStoreStats를 사용한다. 메인 루프의XLogRecStoreStats(&stats, xlogreader_state)에서 확인. 함수는xlogstats.h에 extern으로 선언되어 있으며pg_walinspect(기여 모듈의 SQL 접근 가능 유사체,pg_waldump.c37행 소스 주석에 언급됨)와 공유된다. -
XLogRecordSaveFPWs는 쓰기 전에RestoreBlockImage를 호출해 FPI를 압축 해제한다.fwrite전에if (!RestoreBlockImage(record, block_id, page)) pg_fatal(...)에서 확인. 기록되는 파일은 압축된 WAL 표현이 아닌 압축 해제된 8 KB 페이지다. -
통계 표는 카운트가 0인 커스텀 rmgr 행을 건너뛰지만, 빌트인 rmgr은 카운트가 0이어도 행을 출력한다. 통계 루프 안의
if (RmgrIdIsCustom(ri) && count == 0) continue;에서 확인. 이 가드는 커스텀 rmgr에만 적용되므로 빌트인은 항상 행이 나온다.
열린 질문
섹션 제목: “열린 질문”-
pg_walinspect와의 동등성.pg_waldump.c맨 위 소스 주석은 코드 변경이나 수정 시pg_walinspect기여 모듈에도 같은 작업을 고려하도록 권고한다. 두 도구 사이의 코드 공유 대 중복 정도는 여기서 완전히 추적하지 않았다.pg_walinspect중심의 후속 문서가 경계를 명확히 할 것이다. -
히스토리 파일에 걸친 타임라인 처리.
WALDumpOpenSegment는 현재 타임라인에서 세그먼트를 찾지 못하면*tli_p를 진행시킨다. 타임라인 히스토리 파일(.history파일)을 읽고 스캔 중 타임라인을 전환하는 로직은XLogReader인프라에서 상속되며 여기서는 상세히 다루지 않았다.XLogReadRecord내부 타임라인 전환을 중심으로 한 후속 작업이 전체 그림을 완성할 것이다. -
기본 포크가 아닌 경우의
--save-fullpage파일 명명. 파일명에는forkNames[fork]에서 파생된forkname이 밑줄 접두어와 함께 포함된다.[0, MAX_FORKNUM]범위 밖의 포크 번호가 나타나면 건너뛰지 않고 즉시pg_fatal이 발생한다. 초기화 포크의 정상적인 FPW로 이 상황이 발생할 수 있는지는 열린 질문으로 남겨둔다.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”MySQL mysqlbinlog
섹션 제목: “MySQL mysqlbinlog”MySQL의 바이너리 로그는 논리적 로그다. ROW 포맷에서는 행 이미지를, STATEMENT 포맷에서는 SQL 문을 기록하며 물리적 페이지 편집이 아니다. mysqlbinlog는 구조적으로 pg_waldump와 유사하다. 독립 바이너리, 파일 기반 I/O, 필터 파이프라인 방식이다. 다만 MySQL의 로그가 페이지 지향이 아닌 행 지향이므로 mysqlbinlog는 rmgr별 페이로드 요약 대신 읽기 쉬운 DML(INSERT INTO … VALUES …)을 재구성할 수 있다. MySQL의 binlog는 스토리지 엔진 크래시 복구에 적합하지 않다. InnoDB는 별도의 redo 로그를 사용하기 때문이다. MySQL은 두 가지 별개의 관심사를 위해 두 개의 별도 로그를 가진다. PostgreSQL의 WAL은 크래시 복구와 논리적 디코딩 모두를 단일 스트림에서 처리한다(postgres-logical-decoding.md 참고).
Oracle LogMiner
섹션 제목: “Oracle LogMiner”Oracle LogMiner(Oracle 8i에서 도입)는 pg_waldump와 반대되는 구조적 선택을 한 사례다. 서버 측 PL/SQL 패키지(DBMS_LOGMNR)로 redo 로그 파일을 읽고 디코딩된 결과를 SQL 뷰(V$LOGMNR_CONTENTS)로 제공한다. 전체 데이터 딕셔너리에 접근할 수 있으므로 SQL 수준 DML을 재구성하고 스키마와 오브젝트 이름으로 필터링할 수 있다. 라이브 데이터베이스 연결이 필요하며, 보조 설정 없이는 오프라인 크래시 인스턴스의 redo 로그를 검사할 수 없다. pg_waldump는 파일만 있으면 되므로 서버가 기동되지 않는 상황에서도 사용 가능하다.
pg_walinspect (PG14+)
섹션 제목: “pg_walinspect (PG14+)”pg_walinspect는 pg_waldump와 동등한 기능을 SQL 함수로 노출하는 기여 익스텐션이다. pg_get_wal_records_info, pg_get_wal_stats, pg_get_wal_block_info를 제공한다. 서버 내부에서 실행되므로 WAL 레코드 데이터를 라이브 카탈로그와 조인할 수 있고, 올바른 권한이 있는 클라이언트라면 누구나 접근 가능하다. 모니터링 대시보드와 자동화 도구에 유용하다. pg_waldump.c의 소스 주석은 pg_waldump의 버그 수정을 pg_walinspect에도 포팅해야 한다고 명시적으로 권고한다. 두 도구는 XLogStats와 XLogRecStoreStats 집계 함수를 공유하지만 그 외에는 별개의 코드베이스다.
연구: 성능 튜닝을 위한 WAL 분석
섹션 제목: “연구: 성능 튜닝을 위한 WAL 분석”--stats가 생성하는 누적 WAL 통계는 로그 내용을 워크로드 신호로 활용하는 넓은 연구 분야의 실용적 사례다. 이 분야의 논문들은 체크포인트 빈도를 워크로드 믹스에 따라 조정해 FPW의 I/O 증폭을 최소화하는 적응적 체크포인트 스케줄링과, 인접 체크포인트 사이 FPI의 중복을 활용하는 WAL 압축 방식을 다룬다. Greenplum과 CockroachDB는 워크로드 단계별 레코드 타입의 볼륨과 분포를 특성화하는 구조적 WAL 분석에 관한 엔지니어링 보고서를 발표한 바 있다. 이는 용량 계획의 레버로 사용된다. pg_waldump의 --stats=record 모드는 이 분석의 수동 버전이고, pg_walinspect의 SQL 인터페이스가 자동화 수집을 가능하게 하는 방향이다.
src/bin/pg_waldump/pg_waldump.c— 메인 바이너리 (REL_18_STABLE, 커밋 273fe94)src/bin/pg_waldump/rmgrdesc.c/rmgrdesc.h— pg_waldump용 rmgr 설명 테이블src/include/access/xlogreader.h—XLogReaderState,XLogReaderRoutine,XL_ROUTINE,XLogReaderAllocate,XLogFindNextRecord,XLogReadRecord,XLogReaderFreesrc/include/access/xlogstats.h—XLogStats,XLogRecStats,XLogRecStoreStatssrc/include/access/xlog_internal.h—XLogLongPageHeader,XLogLongPageHeaderData,IsXLogFileName,XLogFileName,XLogFromFileName,XLogSegNoOffsetToRecPtr,XLByteInSegsrc/include/access/xlogrecord.h—XLogRecord헤더 필드postgres-xlog-wal.md— WAL 삽입, LSN 메커니즘, 플러시 워터마크postgres-wal-records-rmgr.md— rmgr 디스패치 테이블, 레코드 구조, FPIpostgres-page-layout.md— 페이지 레이아웃 (--save-fullpage출력 해석에 필요)postgres-logical-decoding.md— 논리적 디코딩, 별개의 WAL 소비자