(KO) PostgreSQL WAL 아카이빙 & 요약 — PITR과 증분 백업의 기반 구조
목차
- 학술적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 가이드
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
학술적 배경
섹션 제목: “학술적 배경”WAL(Write-Ahead Log, 미리-쓰기 로그)은 구조적으로 데이터베이스가 지금까지 만든 모든 내구성 변경의 완전하고 순서 있는 기록이다. 이 기록이 있으면 두 가지 능력이 거의 자동으로 따라온다. 로그를 충분히 오래 보존할 수 있다면, 그리고 로그가 건드린 블록을 찾을 수 있다면 말이다. **연속 아카이빙(continuous archiving)**은 첫 번째 조건을 충족하고, **WAL 요약(WAL summarization)**은 두 번째 조건을 충족한다.
PITR(Point-In-Time Recovery, 특정-시점 복구). 크래시 복구는 마지막 체크포인트의
redo 포인터부터 로그 끝까지 WAL을 재실행해 데이터베이스가 죽은 시점을 복원한다.
PITR은 이를 일반화한다. 베이스 백업(데이터 디렉터리 사본과 일관성이 보장된
LSN의 기록)을 찍은 뒤, 그 위에 무한한 WAL 이력을 재실행해 임의의 목표 지점에서
멈출 수 있다. 목표 지점은 벽시계 시각, 명명된 복원 포인트, LSN, 또는 트랜잭션
ID다. 베이스 백업이 “체크포인트”고, 아카이브된 WAL이 “재실행할 로그”다. 크래시
복구에 더해 필요한 것은 딱 하나다. 백업과 목표 지점 사이의 WAL이 어딘가에 여전히
존재해야 한다는 것이다. pg_wal은 적극적으로 재활용되기 때문에 재활용 전에
내구적·독립적 저장소로 복사해야 한다. 그 복사 작업이 연속 아카이빙이다.
증분 백업(incremental backup). 전체 베이스 백업은 마지막 백업 이후 변경되지
않은 블록까지 클러스터의 모든 블록을 복사한다. 기준 백업 이후 어떤 블록이 변경되었는지
알면 그 블록들과 나머지를 참조로 재구성하는 데 필요한 메타데이터만 복사하면 된다.
WAL은 이미 수정된 블록마다 이름을 붙인다. 각 WAL 레코드는 자신이 건드린
RelFileLocator, 포크 번호, 블록 번호를 담고 있다. WAL 요약은 이 정보를 증류한다.
WAL을 한 번 스캔해 “LSN 범위 X..Y에서 이 릴레이션 포크들의 이 블록들이 수정되었다”는
콤팩트한 파일을 기록한다. 증분 백업은 기준 백업부터 현재까지의 요약들을 합집합해
WAL 자체를 다시 읽지 않고도 변경 집합을 파악한다.
Database Internals(Petrov, 2019)는 WAL을 시스템의 “진실의 원천”으로 규정한다. 페이지 캐시와 데이터 파일은 도출된·재구성 가능한 상태지만 로그가 기준이다. 아카이빙과 요약 모두 이 관점을 진지하게 받아들인 결과다. 아카이빙은 임의로 오래된 베이스 백업이 여전히 롤포워드될 수 있도록 로그의 수명을 재활용 수평선 너머로 연장한다. 요약은 로그 위에 보조 색인 — 블록 참조 테이블(block-reference table) — 을 구축해 “무엇이 변경되었는가?”라는 질문을 전체 로그가 아닌 답에 비례하는 시간으로 답할 수 있게 한다.
ARIES(Mohan et al., 1992)는 롤포워드가 올바르게 이루어질 수 있는 redo 규율을 제공한다. 모든 변경은 페이지가 기록되기 전에 로그에 먼저 기록되고(미리-쓰기), redo는 멱등적이다. 이미 페이지에 반영된 레코드를 재실행하면 페이지 LSN 검사로 no-op이 된다. 따라서 베이스 백업 위에 아카이브된 WAL을 재실행하면 백업 시점이 아무리 오래되었더라도 일관된 상태로 수렴한다.
아카이빙과 요약의 설계 공간에는 몇 가지 축이 있다.
-
푸시(push) vs. 풀(pull). 데이터베이스가 완성된 로그 세그먼트를 아카이브에 직접 밀어 넣는가(아카이버 데몬), 아니면 외부 도구가 꺼내 가는가? 대부분의 시스템은 데이터베이스가 세그먼트 완성 시점을 정확히 알고 있기 때문에 밀어 넣는 방식을 택한다.
-
전체 세그먼트 vs. 레코드 단위. 아카이빙은
pg_wal이 재활용하는 단위인 전체 WAL 세그먼트(기본 16 MB)를 전송한다. 요약은 레코드 단위로 읽지만 체크포인트 단위로 요약 파일을 배출한다. 체크포인트가 redo를 시작할 수 있는 유일한 LSN이기 때문이다. -
블록-델타 vs. 파일-델타 vs. 바이트-델타 증분 백업. PostgreSQL은 블록-델타 방식을 선택했다. 변경 추적의 단위는 8 KB 페이지다. 이는 페이지-LSN redo 모델과 WAL 자체의 블록 참조 단위와 일치한다.
-
인밴드(in-band) vs. 아웃오브밴드(out-of-band) 변경 추적. 일부 시스템은 모든 쓰기에 동기적으로 변경 레코드를 갱신하는 라이브 “변경 블록 비트맵”을 유지한다(인밴드, 저지연이지만 쓰기 경로에 비용 추가). PostgreSQL은 반대를 선택했다. 별도 프로세스가 WAL로부터 사후에 변경 집합을 도출해 쓰기 경로를 손대지 않는 대신, 증분 백업이 기다려야 하는 요약 지연이 생긴다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”연속 아카이빙과 증분 백업은 오래되고 잘 닦인 아이디어다. 성숙한 시스템들은 비슷한 뼈대로 수렴한다. 공통 관례를 먼저 명확히 하면 PostgreSQL의 구체적 심볼이 공통 플레이북 안의 선택지로 읽힌다.
로그 라이터와 아카이버 사이의 알림 파일 핸드셰이크
섹션 제목: “로그 라이터와 아카이버 사이의 알림 파일 핸드셰이크”WAL 세그먼트를 채우는 프로세스와 아카이빙하는 프로세스는 분리되어 있다. 보편적
패턴은 작은 상태 파일 프로토콜이다. 세그먼트가 완성되면 라이터가 마커를 떨어뜨린다
(“이 세그먼트가 준비되었다”). 아카이버가 이를 집어 세그먼트를 복사하고 마커를
다른 것으로 교체한다(“이 세그먼트가 아카이빙되었다”). 제3자(체크포인터)가 두 번째
마커를 읽어 세그먼트를 재활용할 수 있는지 판단한다. PostgreSQL은 정확히
archive_status/NNN.ready와 NNN.done 파일로 이를 구현한다. 이 핸드셰이크는
세 주체를 서로 블록하지 않도록 분리하고 크래시에서도 살아남는다. 크래시 후
다시 나타난 .ready 파일은 단순히 재아카이빙되므로 아카이브 명령은 멱등적이어야
한다.
재시도와 배압을 갖춘 전용 아카이버 프로세스
섹션 제목: “재시도와 배압을 갖춘 전용 아카이버 프로세스”아카이빙은 느리고, 원격이며, 불안정할 수 있는 저장소와 통신한다. 커밋 경로 위에
있어서는 안 되고, 일시적 아카이브 실패가 서버를 중단시키거나 WAL을 잃어서도
안 된다. 관례는 경계 있는 횟수만큼 재시도하는 전용 프로세스다. 실패하면
.ready 파일을 그대로 두어 세그먼트가 재활용되지 않게 하고, 자연적 배압을 적용한다.
아카이빙이 뒤처지면 .ready 파일이 쌓이고, pg_wal이 재활용되지 않으며, 디스크가
가득 찬다. 조용한 데이터 손실이 아닌 크고 복구 가능한 장애다.
플러그인 가능한 아카이브 전송
섹션 제목: “플러그인 가능한 아카이브 전송”초기 시스템은 “파일마다 이 셸 명령을 실행하라”를 하드코딩했다. 단순하지만
파일마다 fork가 발생해 단일 스트림 처리량이 제한되고 셸 인용은 보안·정확성 위험이다.
현대적 관례는 모듈 인터페이스를 추가한다. 로드 가능한 라이브러리가 프로세스 내
콜백을 노출해 fork와 셸을 피한다. PostgreSQL은 둘 다 유지한다. archive_command
(셸)와 archive_library(모듈). 셸 경로 자체는 내장 모듈로 구현되어 아카이버가
항상 하나의 콜백만 호출한다.
로그에서 도출된 변경 추적 색인
섹션 제목: “로그에서 도출된 변경 추적 색인”증분 백업은 “LSN X 이후 변경된 것” 오라클이 필요하다. 로그에서 구축하는 시스템은
배경 리더가 WAL을 소비해 콤팩트한 변경 색인을 생성한다. 색인은 (릴레이션, 포크,
블록) 키와 LSN 범위로 구성되어 백업이 기준 시점부터 현재까지 정확한 범위를 선택할
수 있다. PostgreSQL의 WAL 요약 파일 — 각각 TLI-startLSN-endLSN.summary로 명명되고
직렬화된 BlockRefTable을 담음 — 이 색인이다.
복구 가능 지점에서의 경계 정렬
섹션 제목: “복구 가능 지점에서의 경계 정렬”변경 추적 색인은 LSN 경계가 복구가 실제로 시작하고 멈출 수 있는 지점과 일치할 때만
유용하다. 관례는 체크포인트 redo 포인터에서 색인 세그먼트를 자르는 것이다.
PostgreSQL은 XLOG_CHECKPOINT_REDO와 XLOG_CHECKPOINT_SHUTDOWN 레코드에서
요약 파일을 자른다. 그 LSN들만이 redo가 시작될 수 있는 유일한 지점이기 때문이다.
이렇게 하면 증분 백업의 기준 LSN이 반드시 요약 파일 경계에 떨어진다.
flowchart LR
subgraph primary["프라이머리 클러스터"]
WAL["pg_wal/<br/>WAL 세그먼트"] -->|세그먼트 가득 참| RDY["archive_status/<br/>NNN.ready"]
RDY -->|래치 알림| ARCH["아카이버 프로세스<br/>(PgArchiverMain)"]
ARCH -->|archive_file_cb| STORE["아카이브 저장소<br/>(명령 또는 라이브러리)"]
ARCH -->|이름 변경| DONE["archive_status/<br/>NNN.done"]
WAL -->|xlogreader 스캔| SUMM["WAL 요약기<br/>(WalSummarizerMain)"]
SUMM -->|BlockRefTable| SUMFILES["pg_wal/summaries/<br/>TLI-LSN-LSN.summary"]
end
STORE -->|restore_command| RESTORE["복구 / PITR<br/>(RestoreArchivedFile)"]
SUMFILES -->|변경 집합| INCR["증분 백업<br/>(pg_basebackup)"]
그림 1 — WAL 스트림을 공유하는 두 수집 파이프라인. 아카이버는 완성된 세그먼트를
PITR용 내구 저장소에 밀어 넣는다. 요약기는 증분 백업을 위해 수정된 블록에 색인을
붙인다. 둘 다 pg_wal에서 읽으며, 어느 쪽도 커밋 경로 위에 없다.
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”PostgreSQL은 작업을 포스트마스터가 fork하는 두 개의 독립 보조 프로세스 — 아카이버와
WAL 요약기 — 와 xlogarchive.c의 복구 측 복원 로직으로 분리한다. 세 모두
GUC(archive_mode + archive_command/archive_library; summarize_wal)로 활성화되고,
어느 것도 백엔드의 커밋 경로 위에 없다.
아카이버: 래치 대기, 스캔, 우선순위 정렬, 전송
섹션 제목: “아카이버: 래치 대기, 스캔, 우선순위 정렬, 전송”아카이버는 pg_wal/archive_status의 .ready 파일을 소진하는 것이 전부인 보조
프로세스다. PgArchiverMain은 시그널 핸들러를 설정하고(특히 SIGUSR2를 “마지막
사이클 후 종료”로), 백엔드가 깨울 수 있도록 자신의 proc 번호를 공유 메모리에
광고하고, max-heap 워크스페이스를 할당하고, 아카이브 모듈을 로드한 뒤 pgarch_MainLoop에
진입한다. 메인 루프는 알림, 60초 자동 깨움, 또는 셧다운에 반응하는 래치 대기다.
// pgarch_MainLoop — src/backend/postmaster/pgarch.cdo{ ResetLatch(MyLatch); time_to_stop = ready_to_stop; /* SIGUSR2 핸들러가 설정 */ ProcessPgArchInterrupts(); /* ... SIGTERM-but-no-SIGUSR2 그레이스풀 처리 ... */ pgarch_ArchiverCopyLoop(); /* 미결 항목 전부 아카이빙 */ if (!time_to_stop) rc = WaitLatch(MyLatch, WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH, PGARCH_AUTOWAKE_INTERVAL * 1000L, WAIT_EVENT_ARCHIVER_MAIN);} while (!time_to_stop);깨움은 PgArchWakeup에서 온다. WAL 기계는 새 .ready 파일이 생길 때마다
XLogArchiveNotify로 이를 호출한다. 60초 자동 깨움(PGARCH_AUTOWAKE_INTERVAL)은
깨움을 놓쳤을 때의 안전망이다. “아카이버는 데이터를 보호하기 위해 존재한다”는
원칙 하에 능동적으로 폴링한다.
pgarch_ArchiverCopyLoop는 사이클마다 돌아가는 핵심 루틴이다. pgarch_readyXlog에서
다음 파일을 받아 경계 있는 재시도 횟수만큼 아카이빙한다. 주목할 처리가 하나 있다.
**고아 정리(orphan cleanup)**다. .ready 파일이 pg_wal에 더 이상 존재하지 않는
세그먼트를 가리키면(크래시로 인해 이미 재활용된 세그먼트에 남은 마커), 아카이버는
영원히 실패하는 대신 .ready 파일을 삭제하고 계속 진행한다.
// pgarch_ArchiverCopyLoop — src/backend/postmaster/pgarch.cwhile (pgarch_readyXlog(xlog)){ int failures = 0, failures_orphan = 0; for (;;) { if (ShutdownRequestPending || !PostmasterIsAlive()) return; ProcessPgArchInterrupts(); /* 설정 변경을 빠르게 반영 */ snprintf(pathname, MAXPGPATH, XLOGDIR "/%s", xlog); if (stat(pathname, &stat_buf) != 0 && errno == ENOENT) { /* 이미 재활용된 세그먼트의 고아 .ready: 삭제 */ StatusFilePath(xlogready, xlog, ".ready"); if (unlink(xlogready) == 0) break; /* ... NUM_ORPHAN_CLEANUP_RETRIES 내 재시도 ... */ } if (pgarch_archiveXlog(xlog)) /* 실제 복사 */ { pgarch_archiveDone(xlog); /* .ready -> .done */ pgstat_report_archiver(xlog, false); break; } else if (++failures >= NUM_ARCHIVE_RETRIES) return; /* 이번 사이클 포기, 나중에 재시도 */ }}우선순위 정렬. 많은 세그먼트가 대기 중일 때 아카이버는 알파벳순 디렉터리
항목을 단순히 집어 들지 않는다. pgarch_readyXlog는 경계 있는 max-heap
(NUM_FILES_PER_DIRECTORY_SCAN = 64)을 채우는 디렉터리 스캔을 수행해 한 번의
스캔으로 최대 64개 파일을 우선순위 순으로 처리하며, 비싼 디렉터리 스캔 비용을
분산한다. 우선순위는 ready_file_comparator에 인코딩된다. 타임라인 이력 파일이
항상 우선이고(승격된 스탠바이의 새 타임라인이 즉시 아카이빙), 그다음은 가장 오래된
세그먼트 순이다(WAL 체인 연속성 유지 및 가장 빨리 재활용할 세그먼트 해방).
// ready_file_comparator — src/backend/postmaster/pgarch.cbool a_history = IsTLHistoryFileName(a_str);bool b_history = IsTLHistoryFileName(b_str);/* 타임라인 이력 파일은 항상 최고 우선순위. */if (a_history != b_history) return a_history ? -1 : 1;/* 오래된 파일에 우선순위 부여. */return strcmp(a_str, b_str);PgArchForceDirScan은 WAL 기계가 다음 스캔에서 캐시된 heap을 무시하도록 강제한다.
XLogArchiveNotify는 타임라인 이력 파일이 있을 때 이를 호출해 버퍼된 세그먼트 이름
64개 뒤를 기다리지 않고 즉시 큐의 앞으로 이동한다.
플러그인 전송: 셸 명령 vs. 아카이브 라이브러리
섹션 제목: “플러그인 전송: 셸 명령 vs. 아카이브 라이브러리”아카이버는 절대로 system()을 직접 호출하지 않는다. LoadArchiveLibrary는 단일
ArchiveModuleCallbacks vtable을 결정한다. archive_library가 비어 있으면 내장
shell_archive_init(archive_command를 래핑)을 사용하고, 아니면 지정된 라이브러리를
dlopen해 _PG_archive_module_init을 호출한다. archive_command와 archive_library
중 정확히 하나만 설정할 수 있다.
// LoadArchiveLibrary — src/backend/postmaster/pgarch.cif (XLogArchiveLibrary[0] != '\0' && XLogArchiveCommand[0] != '\0') ereport(ERROR, ... "both \"archive_command\" and \"archive_library\" set" ...);if (XLogArchiveLibrary[0] == '\0') archive_init = shell_archive_init;else archive_init = (ArchiveModuleInit) load_external_function(XLogArchiveLibrary, "_PG_archive_module_init", false, NULL);ArchiveCallbacks = (*archive_init) ();if (ArchiveCallbacks->archive_file_cb == NULL) ereport(ERROR, ... "archive modules must register an archive callback" ...);pgarch_archiveXlog는 자체 sigsetjmp 예외 배리어 안에서 콜백을 호출한다.
아카이브 모듈이 던진 ERROR가 아카이버 전체를 재시작하는 FATAL이 되는 대신
“false 반환, 이 파일 재시도”로 바뀐다. 백엔드에서 커스텀 PG_exception_stack을
설치해 오류를 강등시키는 유일한 지점이다.
복구 측: restore_command와 .ready/.done 프로토콜
섹션 제목: “복구 측: restore_command와 .ready/.done 프로토콜”아카이빙의 반대편은 복원이다. 아카이브 복구(PITR 또는 스탠바이 부트스트랩) 중에
스타트업 프로세스는 pg_wal에 더 이상 없는 WAL 세그먼트가 필요하다.
RestoreArchivedFile은 restore_command를 실행해 명명된 세그먼트를 아카이브에서
pg_wal의 임시 파일로 복사하고, 크기를 교차 확인하며, 성공/실패를 보고한다.
중요한 점은, “복원 실패”가 “WAL 끝에 도달”의 정상 신호라는 것이다. 복구는
복원이 실패할 때까지 롤포워드한다.
// RestoreArchivedFile — src/backend/access/transam/xlogarchive.cxlogRestoreCmd = BuildRestoreCommand(recoveryRestoreCommand, xlogpath, xlogfname, lastRestartPointFname);PreRestoreCommand(); /* SIGTERM 빠른 종료 준비 */rc = system(xlogRestoreCmd);PostRestoreCommand();if (rc == 0 && stat(xlogpath, &stat_buf) == 0) { /* 성공: xlogpath 사용 */ }/* 시그널로 죽었으면 중단; 아니면 "파일 없음, WAL 끝"으로 처리 */if (wait_result_is_signal(rc, SIGTERM)) proc_exit(1);.ready/.done 상태 파일은 WAL 라이터, 아카이버, 체크포인터 사이의 계약이다.
XLogArchiveNotify는 세그먼트가 가득 찰 때 NNN.ready를 생성한다.
pgarch_archiveDone은 성공적인 복사 후 .ready를 .done으로 이름 변경한다.
XLogArchiveCheckDone은 체크포인터가 세그먼트를 재활용하기 전에 호출하는 함수다.
.done이 있으면 true(재활용 가능)를 반환하고, 둘 다 없으면 .ready를 재생성해
세그먼트를 잃지 않는다.
// XLogArchiveCheckDone — src/backend/access/transam/xlogarchive.cif (!XLogArchivingActive()) return true; /* archive_mode=off: 항상 재활용 가능 */StatusFilePath(archiveStatusPath, xlog, ".done");if (stat(archiveStatusPath, &stat_buf) == 0) return true; /* 아카이빙됨: 재활용 */StatusFilePath(archiveStatusPath, xlog, ".ready");if (stat(archiveStatusPath, &stat_buf) == 0) return false; /* 대기 중: 보존 *//* 둘 다 없음 -> .ready (재)생성 후 보존 */XLogArchiveNotify(xlog);return false;flowchart LR S0([시작]) --> A[세그먼트 가득 참] A -->|XLogArchiveNotify가 NNN.ready 생성| B[Ready 상태] B -->|pgarch_readyXlog가 파일 선택| C[아카이빙 중] C -->|archive_file_cb 실패\nNUM_ARCHIVE_RETRIES까지 재시도| B C -->|pgarch_archiveDone이 NNN.done으로 이름 변경| D[Done 상태] D -->|체크포인트가 .done 확인\nXLogArchiveCheckDone 반환 true| E[재활용됨] E --> S1([끝])
그림 2 — 아카이브 상태 프로토콜을 통한 WAL 세그먼트 하나의 수명. 복사 실패는
세그먼트를 Ready 상태로 되돌린다. .done 마커만이 체크포인터에게 세그먼트 재활용
권한을 부여한다.
WAL 요약기: 수정된 블록 색인 만들기
섹션 제목: “WAL 요약기: 수정된 블록 색인 만들기”summarize_wal이 켜져 있으면 포스트마스터는 두 번째 보조 프로세스인
WalSummarizerMain을 실행한다. XLogReaderState로 WAL을 읽고, 수정된 (릴레이션,
포크, 블록) 튜플의 BlockRefTable을 축적하고, 주기적으로 .summary 파일을
기록한다. 공유 메모리 WalSummarizerData는 summarized_lsn(디스크에 내구적),
pending_lsn(메모리에 읽혔지만 아직 기록되지 않음), 타임라인을 WALSummarizerLock
아래 광고한다. 증분 백업은 summarized_lsn을 읽어 변경 색인이 어디까지 확장되었는지
확인하고, 백업 시작 LSN까지 요약기가 따라잡기를 WaitForWalSummarization으로
기다린다.
요약기는 모든 체크포인트 redo 포인터에서 새 요약 파일을 자르고, wal_level=minimal
에서 기록된 WAL을 건너뛰며(증분 백업의 안전 기반이 될 수 없음), 타임라인 전환을
따라가고, 기저 WAL이 사라지고 파일이 wal_summary_keep_time보다 오래되면 오래된
요약을 제거한다. 다음 절에서 호출 흐름을 자세히 살펴본다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”이 절은 세 하위시스템의 호출 흐름을 따라간다. 아카이버 프로세스(pgarch.c),
복구 측 복원 및 상태 프로토콜(xlogarchive.c), WAL 요약기(walsummarizer.c) 순이다.
심볼이 안정적 앵커다. 위치 힌트 테이블은 각 심볼을 REL_18 commit 273fe94 기준
(파일, 행) 쌍으로 매핑한다.
아카이버 프로세스 시작과 메인 루프
섹션 제목: “아카이버 프로세스 시작과 메인 루프”PgArchiverMain은 XLogArchivingActive()가 참일 때 포스트마스터가 호출하는
진입점이다. MyBackendType = B_ARCHIVER를 설정하고, 시그널 핸들러를 설치하고,
프로세스 번호를 공유 메모리에 게시해 백엔드가 깨울 수 있게 하고, 스캔당
워크스페이스를 구성하고, 아카이브 모듈을 로드한 뒤 루프에 진입한다.
// PgArchiverMain — src/backend/postmaster/pgarch.cMyBackendType = B_ARCHIVER;AuxiliaryProcessMainCommon();pqsignal(SIGUSR2, pgarch_waken_stop); /* "마지막 사이클 후 종료" */Assert(XLogArchivingActive());on_shmem_exit(pgarch_die, 0);PgArch->pgprocno = MyProcNumber; /* PgArchWakeup 용 광고 */arch_files = palloc(sizeof(struct arch_files_state));arch_files->arch_heap = binaryheap_allocate(NUM_FILES_PER_DIRECTORY_SCAN, ready_file_comparator, NULL);LoadArchiveLibrary();pgarch_MainLoop();깨움 경로는 PgArchWakeup이다. 광고된 프로세스 번호로 아카이버의 래치를 설정한다.
의도적으로 ProcArrayLock을 잡지 않는다. procLatch는 해제되지 않으므로 낡은
프로세스 번호가 최악의 경우 엉뚱한 프로세스를 깨우는 정도고, 아카이버는 곧
재실행된다.
// PgArchWakeup — src/backend/postmaster/pgarch.cint arch_pgprocno = PgArch->pgprocno;if (arch_pgprocno != INVALID_PROC_NUMBER) SetLatch(&ProcGlobal->allProcs[arch_pgprocno].procLatch);ProcessPgArchInterrupts는 두 루프 모두에서 호출된다. 일반적인 배리어·설정 처리
외에, archive_library 변경을 감지하면 아카이버를 proc_exit(0)시켜 재실행 시
새 모듈이 로드된다. 라이브러리 언로드 메커니즘이 없으므로 재시작이 유일한 안전한
경로다.
파일 선택: 디렉터리 스캔, max-heap, 우선순위
섹션 제목: “파일 선택: 디렉터리 스캔, max-heap, 우선순위”pgarch_readyXlog는 pgarch.c에서 가장 복잡한 루틴이다. 단일 최고우선순위
.ready 파일을 반환하지만, 파일마다 한 번씩 스캔하는 대신 한 번 스캔해 최대 64개
이름을 버퍼링한다. 진입 시 먼저 버퍼된 이름을 제공하려 시도하고(.ready가 여전히
존재하는지 재-stat), 버퍼가 비어 있거나 force_dir_scan이 설정되었을 때만
재스캔한다.
// pgarch_readyXlog — src/backend/postmaster/pgarch.c (요약)if (pg_atomic_exchange_u32(&PgArch->force_dir_scan, 0) == 1) arch_files->arch_files_size = 0; /* 강제 재스캔 */while (arch_files->arch_files_size > 0) /* 버퍼된 이름 제공 */{ arch_file = arch_files->arch_files[--arch_files->arch_files_size]; StatusFilePath(status_file, arch_file, ".ready"); if (stat(status_file, &st) == 0) { strcpy(xlog, arch_file); return true; }}/* 아니면: archive_status/ 스캔, .ready 베이스네임을 max-heap에 추가, ... */rldir = AllocateDir(XLogArchiveStatusDir);while ((rlde = ReadDir(rldir, XLogArchiveStatusDir)) != NULL){ /* 이름 길이/문자 검증, ".ready" 접미사 요구 */ if (arch_files->arch_heap->bh_size < NUM_FILES_PER_DIRECTORY_SCAN) binaryheap_add_unordered(arch_files->arch_heap, CStringGetDatum(arch_file)); else if (ready_file_comparator(binaryheap_first(arch_files->arch_heap), CStringGetDatum(basename), NULL) > 0) { /* 최저 우선순위 퇴출, 이 항목 삽입 */ }}/* heap을 오름차순 우선순위로 arch_files[]에 드레인, 최고 우선순위 반환 */heap은 64개 최고우선순위 후보를 보유한다. 나머지 항목은 이것들이 소진된 후 다음 스캔에서 발견된다. WAL 파일명은 타임라인을 가장 유효 숫자로 정렬하므로 “더 작은 타임라인 = 더 오래됨 = 더 높은 우선순위”가 되어 과거 타임라인이 우선된다. 복구 체인 연속성을 유지하는 데 바람직한 특성이다.
파일 전송과 완료 표시
섹션 제목: “파일 전송과 완료 표시”pgarch_archiveXlog에서 아카이브 콜백이 실제로 실행된다. sigsetjmp 블록이
주목할 세부 사항이다. 아카이버는 예외 스택의 맨 아래에 있으므로 모듈의 평범한
ereport(ERROR)는 FATAL이 되어 프로세스를 재시작한다. 직접 만든 핸들러가 이를
잡아, 일반적으로 PG_CATCH가 하는 정리(LWLockReleaseAll, AtEOXact_Files,
pgaio_error_cleanup 등)를 실행하고 false를 반환한다.
// pgarch_archiveXlog — src/backend/postmaster/pgarch.c (요약)if (sigsetjmp(local_sigjmp_buf, 1) != 0){ error_context_stack = NULL; HOLD_INTERRUPTS(); EmitErrorReport(); LWLockReleaseAll(); ConditionVariableCancelSleep(); pgaio_error_cleanup(); ReleaseAuxProcessResources(false); AtEOXact_Files(false); MemoryContextSwitchTo(oldcontext); FlushErrorState(); RESUME_INTERRUPTS(); ret = false; /* 파일 재시도, 재시작 안 함 */}else{ PG_exception_stack = &local_sigjmp_buf; ret = ArchiveCallbacks->archive_file_cb(archive_module_state, xlog, pathname); PG_exception_stack = NULL;}성공 시 pgarch_archiveDone이 NNN.ready를 NNN.done으로 이름 변경한다.
이름 변경은 의도적으로 내구적이지 않다(durable_rename이 아닌 rename). 크래시로
이름 변경을 잃으면 .ready가 다시 나타나고 세그먼트는 단순히 재아카이빙된다.
아카이브 명령은 멱등적이어야 하므로 무해하다.
복구 측: RestoreArchivedFile과 상태 프로토콜
섹션 제목: “복구 측: RestoreArchivedFile과 상태 프로토콜”복구 경로에서 RestoreArchivedFile은 BuildRestoreCommand로 명령을 구성하고,
외부 명령 실행 중 SIGTERM으로 빠른 종료가 가능하도록 system() 전후에
PreRestoreCommand/PostRestoreCommand를 적용하고, 신호가 아닌 실패는 “WAL 끝”으로
처리한다. 복원에 성공한 파일은 KeepFileRestoredFromArchive가 정식 이름으로 이동시키는데,
이때 .done을 강제하고(archive_mode=always가 아닌 경우 재아카이빙 방지)
walsender를 깨운다.
// KeepFileRestoredFromArchive — src/backend/access/transam/xlogarchive.cdurable_rename(path, xlogfpath, ERROR);if (XLogArchiveMode != ARCHIVE_MODE_ALWAYS) XLogArchiveForceDone(xlogfname);else XLogArchiveNotify(xlogfname);if (reload) WalSndRqstFileReload();WalSndWakeup(true, false);XLogArchiveNotify는 상태 프로토콜의 생산자 끝이다. NNN.ready를 생성하고,
이력 파일을 강제 스캔하고, 아카이버를 찌른다. 소비자 끝은
XLogArchiveCheckDone(체크포인터가 “재활용해도 되나?” 질의),
XLogArchiveIsBusy(아직 미아카이빙 상태인지 — 재활용 경쟁을 처리하기 위해
세그먼트 없음을 busy하지 않음으로 처리), XLogArchiveCleanup(세그먼트가 최종적으로
사라지면 두 상태 파일 모두 unlink)이다.
WAL 요약기: 메인 루프와 타임라인 처리
섹션 제목: “WAL 요약기: 메인 루프와 타임라인 처리”WalSummarizerMain은 아카이버와 같은 형태다. sigsetjmp 오류 배리어, 공유 메모리에
프로세스 번호 광고. 그다음 무한 루프에 진입한다. 각 반복에서 안전한 상한선
(GetLatestLSN)을 찾고, 현재 타임라인이 과거가 됐다면 타임라인 전환 지점을 계산하고,
요약 파일 하나 분량의 WAL을 요약하고, 진행 상황을 공개한다.
// WalSummarizerMain — src/backend/postmaster/walsummarizer.c (요약)current_lsn = GetOldestUnsummarizedLSN(¤t_tli, &exact);if (XLogRecPtrIsInvalid(current_lsn)) proc_exit(0); /* summarize_wal 꺼짐 */for (;;){ ProcessWalSummarizerInterrupts(); MaybeRemoveOldWalSummaries(); latest_lsn = GetLatestLSN(&latest_tli); if (current_tli != latest_tli && XLogRecPtrIsInvalid(switch_lsn)) switch_lsn = tliSwitchPoint(current_tli, readTimeLineHistory(latest_tli), &switch_tli); end_of_summary_lsn = SummarizeWAL(current_tli, current_lsn, exact, switch_lsn, latest_lsn); current_lsn = end_of_summary_lsn; exact = true; LWLockAcquire(WALSummarizerLock, LW_EXCLUSIVE); WalSummarizerCtl->summarized_lsn = end_of_summary_lsn; WalSummarizerCtl->pending_lsn = end_of_summary_lsn; LWLockRelease(WALSummarizerLock); ConditionVariableBroadcast(&WalSummarizerCtl->summary_file_cv);}GetLatestLSN은 역할에 따라 올바른 상한선을 선택한다. 프라이머리에서는
GetFlushRecPtr(플러시되지 않은 WAL은 요약하지 않는다). 복구 중에는
walreceiver의 플러시 위치와 재실행 위치 중 더 큰 값이다. 재실행된 WAL은 반드시
플러시된 것이기 때문이다.
GetOldestUnsummarizedLSN은 가장 최신 기존 .summary 파일의 끝 LSN에서
summarized_lsn을 부트스트랩하거나, 없으면 디스크상의 가장 오래된 세그먼트에서
시작한다. 요약기 외의 백엔드도 이 값을 읽어 요약기가 아직 필요한 WAL의 재활용을
막는다.
SummarizeWAL: 레코드 읽기 루프와 BlockRefTable
섹션 제목: “SummarizeWAL: 레코드 읽기 루프와 BlockRefTable”SummarizeWAL은 요약 파일 하나에 대한 실제 작업을 수행한다. 페이지 읽기 콜백으로
summarizer_read_local_xlog_page를 지정한 XLogReaderState를 할당하고,
(exact가 false이면 XLogFindNextRecord로) 위치를 잡은 뒤 레코드를 하나씩 읽는다.
레코드 처리는 세 경우로 나뉜다. RM_XLOG_ID 레코드(체크포인트/파라미터/복구-끝)는
파일 경계와 fast-forward 모드를 결정한다. RM_DBASE_ID/RM_SMGR_ID/RM_XACT_ID
레코드는 limit-block 기장이 필요하다. 나머지는 블록 참조를 기여한다.
// SummarizeWAL — src/backend/postmaster/walsummarizer.c (요약)BlockRefTable *brtab = CreateEmptyBlockRefTable();bool fast_forward = true;/* ... XLogBeginRead 또는 XLogFindNextRecord로 summary_start_lsn 설정 ... */while (1){ record = XLogReadRecord(xlogreader, &errormsg); if (record == NULL) { /* 과거 TLI의 WAL 끝 -> break */ } if (!XLogRecPtrIsInvalid(switch_lsn) && xlogreader->ReadRecPtr >= switch_lsn) { summary_end_lsn = switch_lsn; break; } rmid = XLogRecGetRmid(xlogreader); if (rmid == RM_XLOG_ID) { if (SummarizeXlogRecord(xlogreader, &new_fast_forward)) { if (xlogreader->ReadRecPtr > summary_start_lsn) { summary_end_lsn = xlogreader->ReadRecPtr; break; } /* 파일 자름 */ else fast_forward = new_fast_forward; /* 첫 레코드 */ } } else if (!fast_forward) switch (rmid) { case RM_DBASE_ID: SummarizeDbaseRecord(xlogreader, brtab); break; case RM_SMGR_ID: SummarizeSmgrRecord(xlogreader, brtab); break; case RM_XACT_ID: SummarizeXactRecord(xlogreader, brtab); break; } if (!fast_forward) for (block_id = 0; block_id <= XLogRecMaxBlockId(xlogreader); block_id++) { if (!XLogRecGetBlockTagExtended(xlogreader, block_id, &rlocator, &forknum, &blocknum, NULL)) continue; if (forknum != FSM_FORKNUM) BlockRefTableMarkBlockModified(brtab, &rlocator, forknum, blocknum); } summary_end_lsn = xlogreader->EndRecPtr; /* WALSummarizerLock 아래 pending_lsn 공개 */}루프가 끝나면 비자명한 범위가 커버됐고 fast-forward가 아닌 경우, BlockRefTable을
임시 파일에 직렬화하고 최종 이름으로 durable_rename한다.
pg_wal/summaries/TLITLITLI-STARTLSN-ENDLSN.summary 형식이다(TLI와 두 LSN 반쪽의
%08X 5중 튜플).
// SummarizeWAL — src/backend/postmaster/walsummarizer.csnprintf(final_path, MAXPGPATH, XLOGDIR "/summaries/%08X%08X%08X%08X%08X.summary", tli, LSN_FORMAT_ARGS(summary_start_lsn), LSN_FORMAT_ARGS(summary_end_lsn));io.file = PathNameOpenFile(temp_path, O_WRONLY | O_CREAT | O_TRUNC);WriteBlockRefTable(brtab, WriteWalSummary, &io);FileClose(io.file);durable_rename(temp_path, final_path, ERROR);Limit blocks: 삭제, 잘림, 새 포크 처리
섹션 제목: “Limit blocks: 삭제, 잘림, 새 포크 처리”블록을 “수정됨”으로 표시하는 것만으로는 충분하지 않다. 릴레이션이 삭제·재생성·
잘리면 증분 백업은 이전 블록이 여전히 유효하다고 가정해서는 안 된다. 요약기는
이를 limit block으로 인코딩한다. SummarizeSmgrRecord는 포크 생성 시
limit block을 0으로 설정하고(모든 것이 새 것), 잘림 시 잘림 지점으로 설정한다.
// SummarizeSmgrRecord — src/backend/postmaster/walsummarizer.cif (info == XLOG_SMGR_CREATE) { xlrec = (xl_smgr_create *) XLogRecGetData(xlogreader); if (xlrec->forkNum != FSM_FORKNUM) BlockRefTableSetLimitBlock(brtab, &xlrec->rlocator, xlrec->forkNum, 0);} else if (info == XLOG_SMGR_TRUNCATE) { xlrec = (xl_smgr_truncate *) XLogRecGetData(xlogreader); if ((xlrec->flags & SMGR_TRUNCATE_HEAP) != 0) BlockRefTableSetLimitBlock(brtab, &xlrec->rlocator, MAIN_FORKNUM, xlrec->blkno); /* ... visibilitymap_truncation_length를 통한 VM 포크 ... */}SummarizeDbaseRecord는 CREATE DATABASE ... (FILE_COPY)와 DROP DATABASE를
데이터베이스/테이블스페이스 단위로 동일하게 처리한다. SummarizeXactRecord는
커밋 또는 중단 시 삭제된 릴레이션 추적을 지운다. 소스에 명시한 불변 원칙은
*“전체 백업이 필요한 것으로 더 많은 것을 표시해도 데이터를 잃을 수 없다”*는 것이다.
limit block은 항상 보수적이다.
Fast-forward와 wal_level=minimal
섹션 제목: “Fast-forward와 wal_level=minimal”SummarizeXlogRecord는 체크포인트·파라미터 변경·복구-끝 레코드를 검사해
유효한 wal_level을 추출하고, 여기서 파일을 자를지(redo 포인트와 셧다운 체크포인트에서
true. redo는 그 지점에서만 시작할 수 있다)와 fast-forward 여부(
wal_level=minimal일 때 요약 배출 건너뜀. 최소 수준 WAL에 걸쳐 있는 증분 백업은
안전하지 않음)를 결정한다.
// SummarizeXlogRecord — src/backend/postmaster/walsummarizer.cif (info == XLOG_CHECKPOINT_REDO) memcpy(&record_wal_level, XLogRecGetData(xlogreader), sizeof(int));else if (info == XLOG_CHECKPOINT_SHUTDOWN) { /* ... rec_ckpt.wal_level ... */ }else if (info == XLOG_PARAMETER_CHANGE) { /* ... xlrec.wal_level ... */ }else if (info == XLOG_END_OF_RECOVERY) { /* ... xlrec.wal_level ... */ }else return false; /* 경계 레코드 아님 */*new_fast_forward = (record_wal_level == WAL_LEVEL_MINIMAL);return true;따라잡기 대기와 WAL 끝 백오프
섹션 제목: “따라잡기 대기와 WAL 끝 백오프”두 대기 루틴이 요약기를 소비자와 연결한다. 레코드 읽기 내에서
summarizer_read_local_xlog_page는 현재 타임라인의 WAL 끝에 도달하면
summarizer_wait_for_wal을 호출한다. 이 루틴은 200ms 단위의 적응형 백오프를
구현한다(유휴 시 최대 30초까지 두 배 증가, 부하 시 감소). 외부에서는
WaitForWalSummarization이 증분 백업의 호출을 summarized_lsn이 백업 시작
LSN에 도달할 때까지 블록한다. 진행 감시 장치가 pending_lsn이 1분 동안 전진하지
않으면 오류를 낸다.
// WaitForWalSummarization — src/backend/postmaster/walsummarizer.c (요약)while (1){ if (!summarize_wal) return; /* 대기 중 비활성화: 포기 */ summarized_lsn = WalSummarizerCtl->summarized_lsn; /* 락 아래 */ pending_lsn = WalSummarizerCtl->pending_lsn; if (summarized_lsn >= lsn) break; /* 10초 주기마다: deadcycles 추적; ~60초 진전 없으면 오류 */ if (deadcycles >= 6) ereport(ERROR, ... "WAL summarization is not progressing" ...); ConditionVariableTimedSleep(&WalSummarizerCtl->summary_file_cv, timeout_in_ms, WAIT_EVENT_WAL_SUMMARY_READY);}요약 파일 정리
섹션 제목: “요약 파일 정리”MaybeRemoveOldWalSummaries는 체크포인트 주기마다 한 번 실행된다(redo 포인터
전진 여부로 게이팅). .summary 파일 제거는 두 가지 조건이 모두 충족될 때만
이루어진다. 기저 WAL이 사라졌을 것(ws->end_lsn <= oldest_lsn)과 파일이
wal_summary_keep_time보다 오래되었을 것. 그래서 요약은 WAL보다 설정된 보존
기간만큼만 더 살아남고, 그것을 여전히 필요로 하는 증분 백업은 완료할 수 있다.
flowchart TB
A["WalSummarizerMain 루프"] --> B["GetLatestLSN<br/>(프라이머리: flush 위치;<br/>스탠바이: replay/recv flush 최대값)"]
B --> C{current_tli<br/>!= latest_tli?}
C -- 예 --> D["tliSwitchPoint<br/>switch_lsn 계산"]
C -- 아니오 --> E["SummarizeWAL"]
D --> E
E --> F["XLogReadRecord 루프"]
F --> G{RM_XLOG_ID<br/>경계?}
G -- redo/셧다운 --> H["ReadRecPtr에서 파일 자름<br/>또는 fast_forward 설정"]
G -- 다른 rmgr --> I["BlockRefTableMarkBlockModified<br/>+ limit-block 기장"]
H --> J["WriteBlockRefTable<br/>durable_rename .summary"]
I --> F
J --> K["summarized_lsn 공개<br/>summary_file_cv 브로드캐스트"]
K --> A
그림 3 — 요약기의 파일당 흐름. 경계 레코드(redo 포인트, 셧다운 체크포인트)가
요약 파일을 자르고 fast-forward를 전환한다. 다른 모든 레코드는 블록 참조를
메모리상의 BlockRefTable에 공급하고, 이것이 자름 지점에서 직렬화된다.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 행 |
|---|---|---|
PgArchiverMain | src/backend/postmaster/pgarch.c | 218 |
PgArchWakeup | src/backend/postmaster/pgarch.c | 281 |
pgarch_MainLoop | src/backend/postmaster/pgarch.c | 311 |
pgarch_ArchiverCopyLoop | src/backend/postmaster/pgarch.c | 381 |
pgarch_archiveXlog | src/backend/postmaster/pgarch.c | 517 |
pgarch_readyXlog | src/backend/postmaster/pgarch.c | 645 |
ready_file_comparator | src/backend/postmaster/pgarch.c | 781 |
PgArchForceDirScan | src/backend/postmaster/pgarch.c | 804 |
pgarch_archiveDone | src/backend/postmaster/pgarch.c | 818 |
LoadArchiveLibrary | src/backend/postmaster/pgarch.c | 913 |
RestoreArchivedFile | src/backend/access/transam/xlogarchive.c | 54 |
ExecuteRecoveryCommand | src/backend/access/transam/xlogarchive.c | 295 |
KeepFileRestoredFromArchive | src/backend/access/transam/xlogarchive.c | 358 |
XLogArchiveNotify | src/backend/access/transam/xlogarchive.c | 444 |
XLogArchiveForceDone | src/backend/access/transam/xlogarchive.c | 510 |
XLogArchiveCheckDone | src/backend/access/transam/xlogarchive.c | 565 |
XLogArchiveIsBusy | src/backend/access/transam/xlogarchive.c | 619 |
XLogArchiveCleanup | src/backend/access/transam/xlogarchive.c | 712 |
WalSummarizerMain | src/backend/postmaster/walsummarizer.c | 214 |
GetWalSummarizerState | src/backend/postmaster/walsummarizer.c | 451 |
GetOldestUnsummarizedLSN | src/backend/postmaster/walsummarizer.c | 509 |
WaitForWalSummarization | src/backend/postmaster/walsummarizer.c | 664 |
GetLatestLSN | src/backend/postmaster/walsummarizer.c | 804 |
SummarizeWAL | src/backend/postmaster/walsummarizer.c | 910 |
SummarizeSmgrRecord | src/backend/postmaster/walsummarizer.c | 1319 |
SummarizeXlogRecord | src/backend/postmaster/walsummarizer.c | 1429 |
summarizer_read_local_xlog_page | src/backend/postmaster/walsummarizer.c | 1502 |
summarizer_wait_for_wal | src/backend/postmaster/walsummarizer.c | 1616 |
MaybeRemoveOldWalSummaries | src/backend/postmaster/walsummarizer.c | 1662 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”모든 발췌는 commit 273fe94852b3a7e34fd171e8abdf1481beb302fa의 REL_18_STABLE
(PG 18.x)에서 가져왔다. 다음 사실들을 소스 트리에 직접 대조했다.
-
아카이버는 래치 구동 보조 프로세스이며 커밋 경로 위에 없다. 확인:
PgArchiverMain이MyBackendType = B_ARCHIVER를 설정하고pgarch_MainLoop이WaitLatch(... WAIT_EVENT_ARCHIVER_MAIN)에서 블록한다. 백엔드는.ready파일을 생성하고PgArchWakeup을 호출할 뿐,pgarch.c로 직접 진입하지 않는다. -
.ready/.done상태 프로토콜. 확인:xlogarchive.c의XLogArchiveNotify가<xlog>.ready를 생성한다.pgarch_archiveDone(pgarch.c)이.ready를.done으로 이름 변경한다.XLogArchiveCheckDone은.done이 있어야만 재활용을 허가하고, 둘 다 없으면.ready를 재생성한다. 이름 변경은 설계상 비내구적(plainrename)이다. -
64파일 max-heap, 이력 파일 우선·오래된 파일 우선 우선순위. 확인:
NUM_FILES_PER_DIRECTORY_SCAN은 64다.pgarch_readyXlog가ready_file_comparator로 정렬된arch_files->arch_heap(binaryheap)을 채운다. 이력 파일이 세그먼트보다 먼저, 이름 오름차순으로 더 오래된 것이 우선이다. -
archive_command/archive_library중 정확히 하나. 확인:LoadArchiveLibrary와ProcessPgArchInterrupts모두 두 GUC가 비어 있지 않으면ereport(ERROR)한다. 빈archive_library는 내장shell_archive_init을 선택한다. -
아카이브 모듈 오류는 강등되며 치명적이지 않다. 확인:
pgarch_archiveXlog가 자체sigsetjmp/PG_exception_stack을 설치하고 잡힌ERROR에서false를 반환한다. 명시적 정리(LWLockReleaseAll,pgaio_error_cleanup,AtEOXact_Files, …)가 수행된다. -
restore_command실패는 WAL 끝을 의미한다. 확인:RestoreArchivedFile은 비신호 원인으로system()이 비영값을 반환하면false를 반환하고, SIGTERM은proc_exit(1)를 트리거한다. -
요약기는 PG17+ 기능이다. 확인:
walsummarizer.c가MyBackendType = B_WAL_SUMMARIZER로WalSummarizerMain을 정의하고,summarize_walGUC로 게이팅된다. PG17에서 도입되어 REL_18에 존재한다. -
요약 파일 명명과 내용. 확인:
SummarizeWAL이pg_wal/summaries/%08X%08X%08X%08X%08X.summary(TLI + start LSN 반쪽 2개 + end LSN 반쪽 2개)로WriteBlockRefTable후durable_rename한다. 블록 참조는BlockRefTableMarkBlockModified에서 오고FSM_FORKNUM은 건너뛴다. -
체크포인트 redo/셧다운에서 파일 경계;
wal_level=minimalfast-forward. 확인:SummarizeXlogRecord가XLOG_CHECKPOINT_REDO,XLOG_CHECKPOINT_SHUTDOWN,XLOG_PARAMETER_CHANGE,XLOG_END_OF_RECOVERY에서 true(여기서 자름)를 반환하고,*new_fast_forward = (record_wal_level == WAL_LEVEL_MINIMAL)을 설정한다. -
삭제/잘림/생성 — limit blocks. 확인:
SummarizeSmgrRecord,SummarizeDbaseRecord,SummarizeXactRecord의BlockRefTableSetLimitBlock호출로 확인. -
따라잡기 대기 감시 장치. 확인:
WaitForWalSummarization이deadcycles >= 6(~60초pending_lsn전진 없음) 후ereport(ERROR ... "WAL summarization is not progressing")한다.
인접 메커니즘은 의도적으로 형제 문서에 위임되어 여기서 재검증하지 않았다.
WAL 레코드 포맷, LSN 모델, 세그먼트 재활용(postgres-xlog-wal.md); 요약 파일의
소비자 측 증분 백업(postgres-incremental-backup.md); 베이스 백업 메커니즘
(postgres-backup-basebackup.md); 스탠바이를 공급하는 스트리밍 전송
(postgres-wal-sender-receiver.md).
범위 주석.
contrib/의 아카이브 모듈은 범위 밖이다. 코어 메커니즘 (ArchiveModuleCallbacksvtable, 내장shell_archive)만 분석한다. pgBackRest나 WAL-G 같은 서드파티 도구는 다음 절에서 비교 예시로만 언급하며 소스는 분석하지 않는다.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”PostgreSQL의 아카이빙과 요약은 보수적이고, 로그-도출형이며, 아웃오브밴드 설계다. 대안을 명확히 하면 그 선택이 무엇을 얻고 잃는지가 선명해진다.
-
외부 아카이버 vs. 코어 내장 데몬 — pgBackRest / WAL-G. 가장 흔한 프로덕션 설정은
archive_command를 헬퍼 도구로 교체한다. pgBackRest와 WAL-G는 여전히.ready/.done핸드셰이크와 세그먼트당 콜백을 이용한다. 더 정교한archive_file_cb페이로드일 뿐이다. 압축, 암호화, 오브젝트 스토리지로의 병렬 전송, 보존 정책을 추가한다. 코어 아카이버가 의도적으로 생략하는 기능들이다. 파일마다 fork하는archive_command는 단일 스트림 처리량을 제한한다. 그래서 도구가 상주하며 배치 처리할 수 있도록archive_library(이 문서에서 분석한 모듈 경로)가 추가됐다. -
인밴드 변경 블록 추적 — Oracle BCT, SQL Server DCM. Oracle의 Block Change Tracking 파일과 SQL Server의 differential changed-map(DCM) 비트맵은 쓰기 경로에서 동기적으로 변경 레코드를 갱신한다. 증분 백업은 거의 지연 없이 준비된 비트맵을 읽는다. PostgreSQL은 반대를 선택했다. WAL 요약기가 사후에 변경 집합을 도출해 쓰기 경로를 건드리지 않는다. 대신
pg_basebackup --incremental이 기다려야 하는 요약 지연(WaitForWalSummarization감시 장치)이 생긴다. 트레이드오프는 쓰기 증폭과 복구 정확성 결합(인밴드) 대 배경 CPU와 지연(아웃오브밴드)이다. WAL이 이미 모든 수정 블록의 이름을 담고 있으므로 두 번째 동기 구조는 핫 경로의 중복 작업이라는 것이 PostgreSQL의 판단이다. -
물리적 vs. 논리적 변경 추적.
BlockRefTable은 물리적 색인이다. (relfilenode, fork, blocknumber) 키를 사용하므로 블록 수준 PITR과 조합되지만 행에 대해서는 아무 말도 하지 않는다. 논리 복제 변경 스트림 (postgres-logical-decoding.md)은 행 수준의 대응물이다. “어떤 튜플이 변경됐나”를 답하지만 블록-증분 백업을 구동할 수 없다. 같은 WAL 위의 두 색인이 분리된 소비자를 섬기기 때문에 PostgreSQL이 통합하지 않고 둘 다 유지하는 것이다. -
연속 아카이빙 대안 — 스트리밍 전용 DR. 스트리밍 복제만으로 공급받는 스탠바이(
postgres-wal-sender-receiver.md)는 HA를 위해 아카이브가 전혀 필요 없다. 그래서 운영자가 아카이빙을 빼고 싶어지는 경우가 있다. 핵심 질문은 갭 복구다.wal_keep_size보다 뒤처진 스탠바이는 아카이브의restore_command로 폴백해야 한다. 아니면 복제 슬롯이 WAL을 고정해야 하는데, 이는 무한정pg_wal증가 위험이 있다. 아카이브와 슬롯은 “소비자가 아직 필요한 WAL을 재활용하지 말라”는 같은 문제에 대한 두 가지 답이다. 하나는 내구적·클러스터 외부, 다른 하나는 클러스터 내부이지만 디스크 압박에 취약하다. -
요약 파일 단위와 연구 프론티어. 체크포인트-redo 경계에서 요약을 자르면 증분 백업의 기준 LSN이 반드시 파일 경계에 떨어지지만, 요약 해상도가 체크포인트 빈도에 묶인다. 더 세밀하거나 겹치는 요약(체크포인트 중간에서 백업 시작 가능)은 대기를 줄이지만
BlockRefTable병합 로직을 복잡하게 만든다. 또한 요약기는 WAL을 직렬로 읽는 단일 프로세스다. WAL 비율이 매우 높은 클러스터에서 LSN 범위를 분할하는 병렬 요약기는 실현되지 않은 확장 축이다. 둘 다 현재 코드 주석이 암시하지만 추구하지는 않는 열린 설계 질문이다. -
ARIES 계보. 전체 구조는 ARIES(Mohan et al. 1992) 위에 서 있다. 미리-쓰기 로깅은 WAL을 기준으로 만들고, 멱등적 페이지-LSN redo는 임의로 오래된 베이스 백업 위의 롤포워드를 올바르게 하고, 체크포인트-부터-redo 규율이 정확히 요약 경계를 체크포인트 레코드에 강제한다. PostgreSQL의 기여는 복구 이론이 아니라 로그 위의 보조 색인(
BlockRefTable)이다. “모든 것을 재실행하라”를 “변경된 것만 복사하라”로 바꾸는 실용적·post-ARIES 공학 계층이다.
인-트리 소스 파일 (REL_18_STABLE, commit 273fe94)
섹션 제목: “인-트리 소스 파일 (REL_18_STABLE, commit 273fe94)”src/backend/postmaster/pgarch.c— 아카이버 프로세스:PgArchiverMain,pgarch_MainLoop,pgarch_ArchiverCopyLoop,pgarch_archiveXlog,pgarch_readyXlog,ready_file_comparator,PgArchForceDirScan,pgarch_archiveDone,LoadArchiveLibrary, SIGUSR2 최종-사이클 프로토콜, 64파일 max-heap, 고아.ready정리.src/backend/postmaster/walsummarizer.c— WAL 요약기:WalSummarizerMain,SummarizeWAL,SummarizeXlogRecord,SummarizeSmgrRecord/SummarizeDbaseRecord/SummarizeXactRecord,summarizer_read_local_xlog_page,summarizer_wait_for_wal,WaitForWalSummarization,GetOldestUnsummarizedLSN,MaybeRemoveOldWalSummaries,FSM_FORKNUM을 건너뛰는BlockRefTable채우기.src/backend/access/transam/xlogarchive.c— 복구/알림 측:RestoreArchivedFile,ExecuteRecoveryCommand,KeepFileRestoredFromArchive,XLogArchiveNotify,XLogArchiveForceDone,XLogArchiveCheckDone,XLogArchiveIsBusy,XLogArchiveCleanup—.ready/.done상태 프로토콜과restore_command실행.src/backend/archive/shell_archive.c—archive_command를 래핑하는 내장shell_archive_init모듈. 아카이버가 항상 하나의ArchiveModuleCallbacksvtable만 호출하게 한다.src/include/postmaster/pgarch.h,src/include/postmaster/walsummarizer.h,src/include/access/xlogarchive.h,src/include/archive/archive_module.h— 공개 시그니처,ArchiveModuleCallbacks구조체, GUC 훅(archive_mode,archive_command,archive_library,summarize_wal,wal_summary_keep_time).
논문 및 교재
섹션 제목: “논문 및 교재”- Mohan, C., Haderle, D., Lindsay, B., Pirahesh, H. & Schwarz, P. (1992).
“ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking
and Partial Rollbacks Using Write-Ahead Logging.” ACM TODS 17(1):94-162.
아카이브된 WAL을 베이스 백업 위에 올바르게 재실행하게 하는 미리-쓰기 로깅과
멱등적 redo 규율.
knowledge/research/dbms-papers/aries.md에 정리. - Database Internals(Petrov, 2019), 3장 (WAL / 로그-구조 저장소) 및 복구 챕터 —
아카이빙과 요약 모두가 기반하는 “WAL이 진실의 원천, 데이터 파일은 도출된
상태” 관점.
knowledge/research/dbms-general/아래 캡처. - Database System Concepts(Silberschatz, Korth, Sudarshan, 7e), 19장
“Recovery System” — 체크포인트, 로그-기반 복구, 요약 파일 경계를 고정하는
퍼지-체크포인트/redo-from-checkpoint 모델.
knowledge/research/dbms-general/아래 캡처.
형제 문서 (교차 참조 — 메커니즘은 해당 문서 소유, 여기서 중복 없음)
섹션 제목: “형제 문서 (교차 참조 — 메커니즘은 해당 문서 소유, 여기서 중복 없음)”postgres-xlog-wal.md— 아카이빙이 연장하고 요약기가 읽는 WAL 레코드 포맷, LSN 모델, 세그먼트 명명,pg_wal재활용 수평선.postgres-incremental-backup.md—.summary파일의 소비자 측:pg_basebackup --incremental이 기준 백업부터 현재까지BlockRefTable을 합집합해 변경된 블록만 복사하는 방법.postgres-backup-basebackup.md— PITR이 아카이브된 WAL을 롤포워드하는 대상인 베이스 백업 메커니즘(pg_backup_start/pg_backup_stop, 백업 레이블,pg_basebackup).postgres-checkpoint.md— 요약 파일 자름 지점과 PITR의 redo 포인터를 고정하는XLOG_CHECKPOINT_REDO/XLOG_CHECKPOINT_SHUTDOWN레코드.postgres-recovery-redo.md— PITR 중 아카이브된 세그먼트를 꺼내 오기 위해RestoreArchivedFile을 호출하는 복구 루프.postgres-wal-sender-receiver.md— 스탠바이를 공급하는 아카이브의 대안인 스트리밍 전송과restore_command로의 갭 복구 폴백.postgres-aux-processes.md— 포스트마스터의 보조 프로세스 명부에서 아카이버 (B_ARCHIVER)와 WAL 요약기(B_WAL_SUMMARIZER)의 위치.