(KO) PostgreSQL 베이스 백업 — BASE_BACKUP, 백업 레이블, 스트리밍 프로토콜
목차
- 학술적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 가이드
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
학술적 배경
섹션 제목: “학술적 배경”백업은 내구성 이야기의 후반부를 맡는다. WAL(write-ahead log, 미리-쓰기 로그)은 커밋된 트랜잭션을 크래시로부터 보호하지만, 그것은 어디까지나 데이터베이스의 기준선 이미지를 전제로 한다. 힙 파일을 담은 디스크가 날아가 버리면 WAL을 아무것도 없는 곳에 재생해 봐야 아무것도 재구성할 수 없다. 그 기준선 이미지가 백업이며, 시스템이 계속 실행되는 동안 그것을 찍는 기법이 Database System Concepts(Silberschatz, 7판)의 19장 “Recovery System”에서 **아카이벌 덤프(archival dump)**라는 이름으로 다뤄지는 주제다.
가장 단순한 형태의 덤프는 정지형(quiescent) 덤프다. 모든 트랜잭션을 멈추고, 버퍼를 전부 플러시한 뒤 파일을 복사하고, 다시 재개한다. DSC는 그 비용을 직설적으로 표현한다 — “the database must be brought to a halt(데이터베이스를 완전히 멈춰야 한다)”. 실제 가용성 요건이 있는 시스템에서 이 정지는 허용 불가다. 진짜 설계 문제는 퍼지 덤프(fuzzy dump)(DSC §19.x, “archival dump”)다. 트랜잭션이 계속 변경을 가하는 동안 데이터 파일을 복사한다. 결과 이미지가 내부적으로 일관성이 없다는 사실을 감수하되, 로그가 그것을 복구해 줄 것을 믿는 방식이다. 교과서는 이 덤프를 크래시를 처리하는 것과 동일한 복구 프레임워크 안의 한 시점으로 규정한다.
“An archival dump of the contents of the database is a copy of the database contents at some point in time. … A fuzzy dump allows transactions to be active while the dump is in progress.”
이것이 작동하는 데는 세 가지 이론적 성질이 필요하며, 모든 베이스 백업 구현은 이 성질들의 인스턴스다.
-
덤프가 일관적일 필요는 없다 — 다만 복구 가능해야 한다. 벽시계 시각 t₁에 복사한 파일과 t₂ > t₁에 복사한 파일은 서로 다른 커밋 경계를 반영한다. 하나의 8 KB 페이지조차 읽기와 쓰기가 겹치면 절반은 옛 내용, 절반은 새 내용인 torn 상태일 수 있다. 로그가 리두 포인트(덤프 시작 이전의 체크포인트)부터 덤프 종료 시점까지의 모든 변경을 담고 있고, 재생이 멱등(idempotent)하다면 이것은 문제가 되지 않는다.
-
Redo는 멱등하고 물리적이어야 한다. torn 페이지에 기록된 페이지 변경을 재생할 때 torn 사본이 무엇을 담고 있든 올바른 페이지를 만들어 내야 한다. 이것이 ARIES(Mohan et al. 1992)가 보장하는 physiological/physical redo이며, **전체 페이지 이미지(full-page images)**와 결합된다. 리두 포인트 이후 페이지의 첫 번째 수정은 전체 페이지를 로그에 기록하므로, 재생 시 torn 사본을 통째로 덮어쓸 수 있다.
-
복구 경계는 자기 기술적이어야 한다. 복원된 이미지는 “어디서부터 재생을 시작하고, 어디까지 가야 일관된 상태가 되는가”라는 물음에 대한 답을 어딘가에 가지고 있어야 한다. 크래시 복구는 이것을 컨트롤 파일에서 읽는다. 백업은 이에 상응하는 안내서를 삽입해야 한다. 복사본 안의 컨트롤 파일 자체가 퍼지 이미지의 일부이기 때문이다.
DSC는 백업의 소비처도 두 가지로 구분한다. 하나는 재해 복구(disaster recovery) — 덤프를 복원하고, 아카이브된 WAL을 재생하면 복구된다. 다른 하나는 시점 복구(point-in-time recovery, PITR) — 선택한 목표 시각/LSN까지만 재생하고 그 이후 트랜잭션은 버린다. 실수로 날린 대량 DELETE를 되돌리는 방법이 바로 이것이다. 둘은 동일한 산출물을 소비하며 재생 종료 조건만 다르다. 세 번째 소비처로 복제에 특유한 스트리밍 스탠바이 부트스트랩이 있다. 스탠바이는 베이스 백업에서 시작해 멈추지 않고 라이브 WAL 스트림에서 재생을 이어간다. PostgreSQL의 BASE_BACKUP이 세 가지 모두를 소화하는 이유는, 셋이 공유하는 하나의 산출물 — 퍼지 물리 이미지와 그 위에서 멱등 redo를 구동하는 메타데이터 — 을 만들어 내기 때문이다.
복구 챕터에서 하나 더 끌어올 만한 개념이 있다. 시작 연산이 단순히 현재 LSN을 기록하는 대신 체크포인트를 강제하는 이유가 바로 이것이다. ARIES 방식 시스템의 복구는 분석, redo, undo의 세 패스로 이루어지며, 체크포인트를 기점으로 삼는다. 체크포인트는 redo가 얼마나 뒤까지 스캔해야 하는지를 한정한다. 재생은 체크포인트의 리두 포인트에서 시작하고, 그 이전은 이미 디스크에 있다고 신뢰한다. 베이스 백업은 redo 구간을 최소화하고 출처를 명확히 하기 위해 시작 시점에 새 체크포인트를 강제하고 그 위치를 복구 기점으로 기록한다. 강제 체크포인트 없이는, 복원 시 복사된 컨트롤 파일에 적혀 있는 오래된 체크포인트 — 경우에 따라 훨씬 과거에 있을 수 있고, 운영자가 더 이상 보유하지 않는 WAL을 참조할 수도 있다 — 에서 redo를 시작해야 한다.
교과서가 암시만 하고 지나가지만 실제 구현에서 반드시 다뤄야 하는 미묘한 점도 있다. 덤프는 파일 수준뿐 아니라 단일 페이지 수준에서도 퍼지하다. 8 KB 페이지를 복사하는 리더와 같은 페이지를 플러시하는 라이터가 경합하면 절반은 옛 내용, 절반은 새 내용인 이미지가 만들어진다. 로그 기반 해결책(전체 페이지 이미지)은 페이지가 리두 포인트 이후 적어도 한 번 전체로 기록되는 경우에만 작동한다. (2)번 성질이 “체크포인트 강제”와 “전체 페이지 쓰기 강제”를 짝지어 요구하는 이유가 바로 이것이다. 체크포인트가 구간을 한정하고, 전체 페이지 쓰기가 그 구간 안에서 건드린 페이지를 통째로 재구성할 수 있게 보장한다.
이 문서의 나머지는 REL_18이 (1)–(3)번 성질을 어떻게 실현하는지를 추적한다. 리두 포인트를 고정하고 전체 페이지 쓰기를 강제하는 시작/종료 브래킷(2번 성질), 퍼지 이미지를 만들어 내는 파일 순회(1번 성질), 그리고 backup_label(3번 성질)이 그 세 축이다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”교과서는 모델을 제시한다 — 로그가 복구 가능하게 만드는 퍼지 아카이벌 덤프. 이 절은 프로덕션 시스템들이 퍼지 물리 백업을 안전하고, 온라인 방식으로, 스트리밍 가능하게 만들기 위해 수렴한 공학적 관행을 정리한다. 다음 절 ## PostgreSQL의 구현에 나오는 구체적 선택들은 이 공유된 공간 안의 다이얼 설정값이다.
물리 백업 vs. 논리 백업. 논리 백업(PostgreSQL의 pg_dump)은 SQL 문이나 로우 데이터를 재도출한다. 버전과 아키텍처 간 이식성이 있지만 느리고 바이너리 복제 스탠바이를 시드하는 데 쓸 수 없다. 물리 백업은 온디스크 파일 포맷을 그대로 복사한다. 빠르고 바이트 단위로 정확하며 재생 가능하지만 버전과 플랫폼에 종속된다. 베이스 백업은 물리 방식이다. 둘은 경쟁이 아닌 보완 관계다.
시작/종료 마커로 복사를 감싼다. 모든 온라인 물리 백업은 파일 복사를 *시작(start)*과 종료(stop) 연산 사이에 끼워 넣는다. 시작은 세 가지를 한다. (a) 보통 체크포인트를 강제해 리두 포인트를 결정하고 재생의 기원을 확정한다. (b) 복사 기간 동안 전체 페이지 쓰기를 강제해 torn 페이지를 자기 치유 가능하게 만든다. (c) 시작 LSN을 기록한다. 종료는 종료 LSN을 기록하고 [start, stop]에 걸쳐 있는 WAL이 내구적으로 보존(아카이브)됐음을 보장한다. 그 WAL은 복원에 필수이기 때문이다. 두 끝점과 그 사이의 WAL이 없으면 백업은 무용지물이다.
파일 단위로 복사하되, 휘발성 파일은 제외한다. 워커는 파일 단위로 데이터 디렉터리를 복사하지만, 실행 중인 클러스터에는 그대로 복사하면 안 되는 파일들이 있다. 임시 파일, 릴레이션-캐시 초기화 파일(시작 시 재구성됨), 백엔드별 통계, 복제 슬롯 상태, postmaster.pid 등이 그렇다. 각 엔진은 *제외 목록(exclusion list)*을 관리한다. WAL 디렉터리 자체는 특별하다. 그 내용은 복원 시 아카이브에서 재구성되므로 빈 디렉터리(권한 보존을 위한)만 복사된다.
스테이징 없이 스트리밍한다. 수 테라바이트 클러스터의 백업을 서버 로컬 임시 파일에 스테이징할 수는 없다. 바이트는 읽히는 즉시 소비처로 흘러간다. pg_basebackup으로 향하는 네트워크 소켓이 될 수도 있고, 서버 사이드 파일이 될 수도 있다. 백프레셔와 함께. **싱크 체인(chain of sinks)**은 이것을 깔끔하게 표현하는 방법이다. 버퍼를 읽고 싱크에 넘기면, 스택의 각 싱크가 변환(압축), 계량(스로틀, 진행상황), 또는 전송(소켓, 파일)을 수행한 뒤 다음으로 넘긴다. 이것은 다른 시스템의 백업 파이프라인이 사용하는 것과 동일한 데코레이터 패턴이다.
스트림에 프레임을 씌워 소비자가 파싱할 수 있게 한다. 하나의 바이트 스트림이 여러 논리적 객체를 운반해야 한다. 테이블스페이스별 tar 아카이브 여러 개, 매니페스트, 주기적 진행상황 보고가 그것이다. 프레이밍 프로토콜은 각 청크에 태그를 붙여 클라이언트가 역다중화할 수 있게 한다. PostgreSQL은 이것을 와이어 프로토콜의 COPY 서브프로토콜 위에 얹는다.
복원을 자기 기술적으로 만든다. 복사된 컨트롤 파일이 퍼지 이미지의 일부이므로 백업은 대역 외 안내서를 삽입한다. 시작 LSN, 체크포인트 리두 위치, 타임라인, 백업이 스탠바이에서 찍혔는지를 담은 작은 텍스트 파일이다. 복구는 컨트롤 파일을 믿는 대신 이 파일을 읽는다. PostgreSQL은 이것을 backup_label이라 부른다.
증분 백업에는 변경 오라클이 필요하다. 전체 물리 백업은 변경되지 않은 데이터도 다시 복사한다. 이전 백업 이후 변경된 것만 복사하려면 “릴레이션 R의 어느 블록이 마지막 백업 이후 변경됐는가?”라는 질문에 답하는 오라클이 필요하다. 흔한 오라클 두 가지는 더티 블록 비트맵과 블록 참조를 위한 WAL 스캔이다. PostgreSQL은 후자를 택했다. WAL을 **서머리(summaries)**로 구체화하고 백업 시 *블록 참조 테이블(block-reference table)*로 병합하는 방식이다.
복원은 안내된 크래시 복구다. 물리 백업이 퍼지할 수 있는 이유는 복원이 원상 복구가 아닌 복구이기 때문이다. 운영자는 파일을 내려놓고 삭제하지 않은 채 서버를 시작한다. 시작 프로세스가 복구 안내서를 발견하고, 아카이브된 WAL을 가져와(restore_command 또는 사전 스테이징된 아카이브에서), 리두 포인트부터 재생한다. redo가 멱등하고 전체 페이지 이미지를 사용하므로 파일 A가 파일 B보다 먼저 복사됐든, 페이지가 torn 상태였든 상관없다. 로그가 기존 내용을 덮어쓴다. 백업 종료 LSN까지 재생을 완료해야 클러스터가 일관된 상태가 된다. 그 시점에서야 서버는 (재해 복구의 경우) 커넥션을 받거나, (레플리카의 경우) 읽기 전용/핫 스탠바이로 열린다. 세 소비처를 하나로 묶는 단일 사실이 바로 이것이다. 재해 복구는 아카이브 끝까지 재생하고, PITR은 설정된 recovery_target까지 재생하고, 스탠바이는 끝까지 재생한 뒤 라이브 스트림을 이어가지만 — 셋 모두 동일한 퍼지 이미지와 동일한 안내서에서 출발한다. WAL을 건너뛰거나 backup_label을 불필요한 파일로 착각해 삭제하면 조용히 손상된 데이터베이스가 된다. 이 설계는 그런 실수를 우연히 저지르기 어렵게 만드는 쪽으로 기울어 있다.
flowchart TD
subgraph Bracket["온라인 백업 브래킷 (모든 엔진)"]
S["START<br/>리두 포인트 결정 (체크포인트)<br/>전체 페이지 쓰기 강제<br/>시작 LSN 기록"]
C["데이터 파일 복사<br/>퍼지, 온라인, 파일 단위<br/>휘발성 파일 제외"]
E["STOP<br/>종료 LSN 기록<br/>WAL [start,stop] 아카이브 보장"]
S --> C --> E
end
Bracket --> R["복원: 파일 배치<br/>리두 포인트부터 WAL 재생<br/>종료 LSN까지 -> 일관성"]
R --> U1["재해 복구<br/>아카이브 끝까지 재생"]
R --> U2["PITR<br/>선택한 목표까지 재생"]
R --> U3["스탠바이 부트스트랩<br/>재생 후 라이브 스트림 지속"]
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”PostgreSQL은 베이스 백업을 복제 프로토콜 명령으로 받는다. 클라이언트(보통 pg_basebackup이지만 pg_basebackup --incremental이나 복제 프로토콜을 사용하는 libpq 클라이언트도 가능하다)가 복제 모드로 접속해 BASE_BACKUP [ options ]를 발행한다. walsender 백엔드가 이것을 BaseBackupCmd로 파싱해 SendBaseBackup을 호출한다. 파일을 스트리밍하는 SQL-호출 가능 등가물은 없다. SQL 함수 pg_backup_start() / pg_backup_stop()은 브래킷(저수준 API)만 설정하고 파일 복사는 운영자(tar, rsync 등)에게 맡긴다. 두 경로 모두 xlog.c의 C 프리미티브 do_pg_backup_start와 do_pg_backup_stop을 공유한다.
브래킷: do_pg_backup_start. Start는 교과서의 세 가지 역할을 수행한다. 첫째로 WAL 삽입 락 아래 공유 카운터 runningBackups를 증가시킨다. 이것이 전역적으로 전체 페이지 쓰기를 강제하는 기제다 — torn 페이지 방어(2번 성질):
// do_pg_backup_start — src/backend/access/transam/xlog.cWALInsertLockAcquireExclusive();XLogCtl->Insert.runningBackups++;WALInsertLockRelease();소스의 주석은 그 이유를 정확히 설명한다. “백업 덤프가 데이터베이스 페이지를 동시 쓰기와 함께 읽으면 ‘torn’(부분적으로 쓰인) 사본을 얻을 가능성이 충분히 있다. WAL 시퀀스에서 그 페이지에 대한 첫 번째 쓰기가 전체 페이지 쓰기인 한 이것은 고칠 수 있다.” 그런 다음 WAL 스위치와 체크포인트를 강제해 리두 포인트를 고정하고, state->startpoint / state->starttli를 기록한다. 체크포인트는 CHECKPOINT { fast | spread } 옵션(기본값 spread)으로 fast(즉시) 또는 spread(checkpoint_completion_target에 걸쳐 분산)를 선택할 수 있다. REL_18은 fast/spread 철자를 사용하며 파서는 이것을 불리언 opt->fastcheckpoint로 변환한다.
브래킷: do_pg_backup_stop. Stop은 종료 포인트를 기록하고, 클라이언트가 대기하지 않도록 요청하지 않은 한 [start, stop]에 걸친 WAL이 아카이브될 때까지 블로킹한다 — 그 WAL이 복원에 필수이기 때문이다. runningBackups를 감소시켜 전체 페이지 쓰기 의무를 해제한다.
브래킷 주변의 정리 규율은 엄격하다. perform_base_backup은 start와 stop 사이의 모든 것을 PG_ENSURE_ERROR_CLEANUP으로 감싼다. 오류가 발생하면 do_pg_abort_backup이 runningBackups를 감소시킨다. 카운터가 “누출”되면 전체 페이지 쓰기가 영원히 강제된 상태로 남는다:
// perform_base_backup — src/backend/backup/basebackup.cdo_pg_backup_start(opt->label, opt->fastcheckpoint, &state.tablespaces, backup_state, tablespace_map);state.startptr = backup_state->startpoint;state.starttli = backup_state->starttli;PG_ENSURE_ERROR_CLEANUP(do_pg_abort_backup, BoolGetDatum(false));{ /* ... walk $PGDATA, stream every file ... */ do_pg_backup_stop(backup_state, !opt->nowait);}PG_END_ENSURE_ERROR_CLEANUP(do_pg_abort_backup, BoolGetDatum(false));backup_label: 자기 기술적 안내서 (3번 성질). base.tar에 기록되는 첫 번째 파일은 backup_label이다. BackupState로부터 build_backup_content가 만든다. 텍스트 파일이며 START WAL LOCATION, CHECKPOINT LOCATION(리두 포인트), 백업 방법, 시작 시각, 레이블 문자열, 그리고 백업이 스탠바이에서 찍혔는지를 담는다. 복원 시 시작 프로세스는 복사된 pg_control을 믿는 대신 backup_label을 읽어 redo가 어디서 시작해야 하는지를 파악한다. 파일이 존재하면 없이는 시작을 거부한다 — 운영자가 퍼지 이미지를 크래시됐지만 깨끗한 클러스터로 착각하는 일을 막는 장치다. backup_label은 먼저 전송되고, pg_control은 일관된 워크 후 상태를 반영하도록 마지막에 전송된다:
// perform_base_backup — src/backend/backup/basebackup.cbbsink_begin_archive(sink, "base.tar");/* In the main tar, include the backup_label first... */backup_label = build_backup_content(backup_state, false);sendFileWithContent(sink, BACKUP_LABEL_FILE, backup_label, -1, &manifest);/* Then the tablespace_map file, if required... */if (opt->sendtblspcmapfile) sendFileWithContent(sink, TABLESPACE_MAP, tablespace_map->data, -1, &manifest);/* Then the bulk of the files... */sendDir(sink, ".", 1, false, state.tablespaces, sendtblspclinks, &manifest, InvalidOid, ib);/* ... and pg_control after everything else. */sendFile(sink, XLOG_CONTROL_FILE, XLOG_CONTROL_FILE, &statbuf, ...);backup_label의 실제 텍스트를 보면 복원 계약이 명확해진다. build_backup_content는 BackupState에서 필드 단위로 포맷한다. 두 LSN 필드 — START WAL LOCATION(백업이 시작된 WAL 위치)과 CHECKPOINT LOCATION(강제된 체크포인트의 리두 포인트) — 가 핵심이다. BACKUP FROM은 복구에게 스탠바이 특이사항을 예상할지 여부를 알리고, INCREMENTAL FROM 줄은 증분 백업에서만 나타난다:
// build_backup_content — src/backend/access/transam/xlogbackup.cappendStringInfo(result, "START WAL LOCATION: %X/%X (file %s)\n", LSN_FORMAT_ARGS(state->startpoint), startxlogfile);appendStringInfo(result, "CHECKPOINT LOCATION: %X/%X\n", LSN_FORMAT_ARGS(state->checkpointloc));appendStringInfoString(result, "BACKUP METHOD: streamed\n");appendStringInfo(result, "BACKUP FROM: %s\n", state->started_in_recovery ? "standby" : "primary");appendStringInfo(result, "START TIME: %s\n", startstrbuf);appendStringInfo(result, "LABEL: %s\n", state->name);appendStringInfo(result, "START TIMELINE: %u\n", state->starttli);/* ... STOP fields here only when ishistoryfile ... */if (!XLogRecPtrIsInvalid(state->istartpoint)){ appendStringInfo(result, "INCREMENTAL FROM LSN: %X/%X\n", LSN_FORMAT_ARGS(state->istartpoint)); appendStringInfo(result, "INCREMENTAL FROM TLI: %u\n", state->istarttli);}복구가 시작 시 backup_label을 발견하면 CHECKPOINT LOCATION에서 redo를 시작한다(복사된 pg_control이 가리키는 리두 포인터가 아니라, 그것은 더 이른 체크포인트를 가리킬 수 있다). 백업 종료 LSN을 넘어설 때까지 클러스터를 인-백업-복구 상태로 취급한다. 그 지점에서야 이미지가 일관된 상태가 된다. 이것이 이론 절의 3번 성질을 구체적으로 실현하는 메커니즘이다. 안내서가 신뢰할 수 없는 컨트롤 파일을 덮어쓴다. STOP 필드가 추가된 동일한 내용(ishistoryfile = true)은 do_pg_backup_stop이 아카이벌 기록을 위해 pg_wal에 떨어뜨리는 <startwalfile>.<offset>.backup 이력 파일이 된다.
tablespace_map. $PGDATA 외부에 있는 테이블스페이스는 pg_tblspc/ 아래의 심볼릭 링크로 접근한다. 백업은 절대 경로 심볼릭 링크를 그대로 복사할 수 없다(복원 대상의 경로가 다르다). 대신 tablespace_map에 심볼릭 링크-경로 매핑을 기록하고 각 테이블스페이스를 <oid>.tar라는 별도의 tar 아카이브로 내보낸다. 메인 데이터 디렉터리는 ti->path == NULL인 아카이브이며, WAL 추가 단계가 붙을 자리를 확보하기 위해 항상 마지막에 전송된다.
싱크 체인. SendBaseBackup은 스트리밍 파이프라인을 bbsink 데코레이터 스택으로 조립한다. 베이스 싱크가 가장 안쪽이다:
// SendBaseBackup — src/backend/backup/basebackup.csink = bbsink_copystream_new(opt.send_to_client);if (opt.target_handle != NULL) sink = BaseBackupGetSink(opt.target_handle, sink);if (opt.maxrate > 0) sink = bbsink_throttle_new(sink, opt.maxrate);if (opt.compression == PG_COMPRESSION_GZIP) sink = bbsink_gzip_new(sink, &opt.compression_specification);/* ... lz4, zstd ... */sink = bbsink_progress_new(sink, opt.progress);가장 바깥쪽 싱크(bbsink_progress)가 basebackup.c가 호출하는 진입점이다. 각 싱크는 자신의 역할을 수행하고 감싸고 있는 싱크로 전달한다. bbsink_state 구조체는 공유 커서 — tablespaces, bytes_done, bytes_total, startptr, starttli — 를 담아 어느 싱크에서도 진행상황을 보고할 수 있게 한다.
와이어 프레이밍. 가장 안쪽의 bbsink_copystream은 바이트 스트림을 와이어 프로토콜 메시지로 변환한다. 먼저 결과 집합(시작 LSN + 테이블스페이스 목록)을 보내고, 단일 COPY OUT 스트림을 열고, 이후 모든 청크를 CopyData 메시지로 프레이밍한다. 첫 번째 페이로드 바이트가 타입 태그다. n = 새 아카이브, d = 아카이브/매니페스트 데이터, m = 매니페스트 시작, p = 진행상황 보고. 타입 바이트가 정렬된 데이터 버퍼 바로 앞에 위치하도록 하는 정렬 트릭 덕분에 전체 메시지를 추가 복사 없이 하나의 pq_putmessage로 전송할 수 있다:
// bbsink_copystream_begin_backup — src/backend/backup/basebackup_copy.cbuf = palloc(mysink->base.bbs_buffer_length + MAXIMUM_ALIGNOF);mysink->msgbuffer = buf + (MAXIMUM_ALIGNOF - 1);mysink->base.bbs_buffer = buf + MAXIMUM_ALIGNOF;mysink->msgbuffer[0] = 'd'; /* archive or manifest data */SendXlogRecPtrResult(state->startptr, state->starttli);SendTablespaceList(state->tablespaces);pq_puttextmessage(PqMsg_CommandComplete, "SELECT");SendCopyOutResponse();flowchart TD
Cmd["BASE_BACKUP 명령<br/>복제 프로토콜로"] --> SBB["SendBaseBackup<br/>옵션 파싱, 싱크 체인 구성"]
SBB --> PBB["perform_base_backup"]
PBB --> Start["do_pg_backup_start<br/>runningBackups++, 체크포인트<br/>= 리두 포인트 + 전체 페이지 쓰기"]
Start --> Label["sendFileWithContent backup_label<br/>다음 tablespace_map"]
Label --> Walk["sendDir가 PGDATA 순회<br/>tar 멤버당 sendFile"]
Walk --> Ctl["sendFile pg_control 마지막"]
Ctl --> Stop["do_pg_backup_stop<br/>종료 LSN, WAL 아카이브 대기"]
Stop --> WAL["WAL 포함 시: pg_wal 세그먼트<br/>start..end를 base.tar에 추가"]
WAL --> Man["SendBackupManifest"]
Man --> End["bbsink_end_backup<br/>CopyDone + 종료 LSN/tli"]
subgraph Sinks["bbsink 체인 (데코레이터)"]
direction LR
Prog["progress"] --> Comp["gzip/lz4/zstd"] --> Thr["throttle"] --> Tgt["target"] --> Cs["copystream -> 와이어"]
end
Walk -.bbsink_archive_contents.-> Sinks
소스 코드 가이드
섹션 제목: “소스 코드 가이드”이 절은 복제 명령에서 단일 tar 멤버에 이르는 호출 흐름을 서브시스템별로 묶어 추적한다. 심볼이 영속적 기준점이다. 줄 번호는 끝의 위치 힌트 표에만 있다.
1. 명령 디스패치와 파이프라인 조립
섹션 제목: “1. 명령 디스패치와 파이프라인 조립”walsender가 BASE_BACKUP을 받아 SendBaseBackup(basebackup.c)으로 디스패치한다. 같은 세션에서 진행 중인 백업을 거부하고(get_backup_status() == SESSION_BACKUP_RUNNING), parse_basebackup_options로 옵션을 파싱하고, walsender를 WALSNDSTATE_BACKUP으로 전환하고, 증분 사전 조건을 검증한 뒤 싱크 체인을 조립하고 perform_base_backup으로 넘긴다.
parse_basebackup_options는 DefElem 옵션 목록을 순회하며 옵션당 중복 감지 불리언으로 확인한다. 주목할 옵션들: LABEL, PROGRESS, CHECKPOINT { fast | spread }, WAIT, WAL(pg_wal 세그먼트 포함), INCREMENTAL, MAX_RATE, TABLESPACE_MAP, VERIFY_CHECKSUMS, MANIFEST { yes | no | force-encode }, MANIFEST_CHECKSUMS, TARGET / TARGET_DETAIL, COMPRESSION / COMPRESSION_DETAIL. WAL 요약화가 꺼져 있을 때 증분 백업을 거부하는 가드가 있다:
// parse_basebackup_options — src/backend/backup/basebackup.copt->incremental = defGetBoolean(defel);if (opt->incremental && !summarize_wal) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("incremental backups cannot be taken unless WAL summarization is enabled")));타겟 해석은 바이트가 어디로 가는지를 결정한다. TARGET 없음은 COPY로 클라이언트에게 스트리밍하고(send_to_client = true) 하위 호환성을 위해 use_copytblspc도 설정한다. TARGET 'client'는 명시적 클라이언트 스트리밍이다. 다른 타겟 이름은 BaseBackupGetTargetHandle을 거쳐 서버 사이드 싱크(예: server, blackhole)로 해석된다. 파서는 옵션별로 표현할 수 없는 교차 옵션 제약도 강제한다. MANIFEST_CHECKSUMS는 매니페스트를 요구하고, COMPRESSION_DETAIL은 COMPRESSION을 요구하고, TARGET_DETAIL은 TARGET을 요구한다. 매니페스트가 요청되지 않으면 체크섬 타입은 CHECKSUM_TYPE_NONE으로 강제된다. 압축 사양도 여기서 파싱되고 검증된다(parse_compress_specification / validate_compress_specification). 잘못된 zstd:level=99는 파일을 한 바이트도 읽기 전에 거부된다.
그 다음 SendBaseBackup은 증분 계약을 검사한다. 스트레이 업로드 매니페스트가 있는 전체 백업은 이를 무시하고(ib = NULL), 사전 UPLOAD_MANIFEST 없는 증분 백업은 오류다. WAL 요약화가 비활성화된 상태의 증분 요청은 옵션 파싱에서 미리 잡힌다. 이 검사들을 통과한 뒤에야 싱크 체인을 조립하고, PG_TRY/PG_FINALLY 안에서 perform_base_backup을 호출한다. 오류가 발생해도 bbsink_cleanup이 실행되도록 보장하는 장치다 — 백업 카운터 정리 규율의 싱크 수준 유사물이다.
2. 시작/종료 브래킷 (xlog.c, xlogbackup.c)
섹션 제목: “2. 시작/종료 브래킷 (xlog.c, xlogbackup.c)”perform_base_backup은 먼저 CurrentResourceOwner를 보조 프로세스 리소스 오너로 전환하고(증분 북키핑에 BufFile을 사용한다), 매니페스트를 초기화하고, BackupState를 할당하고, do_pg_backup_start를 호출한다. 이 함수는 wal_level >= replica를 강제하고, WAL 삽입 락 아래 XLogCtl->Insert.runningBackups를 증가시켜 전체 페이지 쓰기를 표시하고, XLOG 스위치와 체크포인트를 강제하고, BackupState->startpoint / starttli를 채운다. 거기에 기록된 리두 포인트가 재생이 시작될 LSN이다.
build_backup_content(xlogbackup.c)는 BackupState를 backup_label 텍스트로 직렬화한다. START WAL LOCATION, CHECKPOINT LOCATION, BACKUP METHOD, BACKUP FROM, START TIME, LABEL, START TIMELINE이 포함된다. ishistoryfile이 true이면 do_pg_backup_stop이 기록하는 .backup 이력 파일의 추가 STOP 필드도 포함된다.
do_pg_backup_stop은 stoppoint / stoptli를 기록하고, runningBackups를 감소시키고, waitforarchive인 경우 종료 LSN까지 WAL이 아카이브될 때까지 블로킹한다. WAL 삽입 락을 재취득해 카운터를 원자적으로 감소시키고, 락을 해제하기 전에 sessionBackupState를 지운다. 두 업데이트 사이에 CHECK_FOR_INTERRUPTS()가 끼어들면 불일치가 생겨 이후 do_pg_abort_backup이 깨질 수 있다는 소스 경고가 있다:
// do_pg_backup_stop — src/backend/access/transam/xlog.cWALInsertLockAcquireExclusive();Assert(XLogCtl->Insert.runningBackups > 0);XLogCtl->Insert.runningBackups--;sessionBackupState = SESSION_BACKUP_NONE;WALInsertLockRelease();스탠바이에서 찍은 백업의 경우 복사 도중 스탠바이가 프로모션되지 않았는지도 재확인한다. 백업 중간 프로모션은 타임라인이 그 아래에서 분기하므로 이미지를 무효화한다. do_pg_abort_backup은 PG_ENSURE_ERROR_CLEANUP으로 등록된 오류 경로 수축자로, 이 카운터 감소만 수행한다. start와 stop 사이의 어떤 ereport(ERROR)도 전체 페이지 쓰기 의무를 해제한다. 카운터가 “누출”되면 원인 없는 조용한 클러스터 전체 성능 저하가 발생하므로 정리 규율이 이토록 엄격하다.
3. 데이터 디렉터리 순회: sendDir / sendFile
섹션 제목: “3. 데이터 디렉터리 순회: sendDir / sendFile”sendDir은 디렉터리를 재귀 순회하며, 서브디렉터리 항목마다 _tarWriteHeader를, 일반 파일마다 sendFile을 호출하고, 누적 크기를 반환한다(sizeonly 패스는 동일한 코드로 진행률용 bytes_total을 사전 계산한다). 주요 역할:
- 릴레이션 디렉터리 인식.
./base나 테이블스페이스 버전 디렉터리, 또는./global아래에서 마지막 경로 구성 요소가 모두 숫자인 경로는 릴레이션 파일을 담는다.isRelationDir이parse_filename_for_nontemp_relation을 통한 파일당 relfilenode 파싱을 게이팅한다. - 제외 목록 적용.
excludeFiles(예:postmaster.pid, relcache 초기화 파일, 스트레이backup_label/tablespace_map/backup_manifest)와excludeDirContents(예:pg_stat_tmp,pg_replslot,pg_notify,pg_serial,pg_subtrans— 권한 보존을 위해 빈 디렉터리로 전송). - 휘발성 파일 건너뛰기. 임시 파일(
PG_TEMP_FILE_PREFIX), 언로그드 테이블의 비-init 포크(_init포크가 있을 때), 임시 릴레이션,pg_control(마지막에 전송).pg_wal은 빈 디렉터리와archive_status및summaries서브디렉터리만 내보낸다 — 그 내용은 복원 시 아카이브에서 온다. - 테이블스페이스 심볼릭 링크 처리.
./pg_tblspc아래의 심볼릭 링크는 링크 타겟과 함께 tar에 기록된다.tablespace_map이 매핑을 담을 때는 여기서 건너뛰고 거기에 기록된다. - 인터럽션/프로모션 감지.
CHECK_FOR_INTERRUPTS()와RecoveryInProgress() != backup_started_in_recovery검사로, 스탠바이가 복사 도중 프로모션되면 백업을 중단한다(이미지가 손상된다).
제외 목록의 각 항목은 임의적이지 않다. 각각 정확성이나 정결함에 관한 사실을 인코딩한다. pg_replslot은 복제 슬롯 상태가 노드 특정적이고 복원하면 복원된 사본이 원본 프라이머리를 대신해 WAL을 보류하게 될 수 있기 때문에 제외된다. pg_stat_tmp, pg_notify, pg_serial, pg_subtrans, pg_snapshots는 시작 시 재생성되거나 초기화되므로 복사해도 의미가 없다(파일 권한이 살아남도록 빈 디렉터리로 유지된다). 라이브 데이터 디렉터리에서 발견된 스트레이 backup_label, tablespace_map, backup_manifest는 다른 백업을 기술하며 복구를 오도할 수 있으므로 제외된다 — 백업이 자신의 올바른 버전을 주입하기 때문이다. postmaster.pid는 복원된 사본이 원본 postmaster가 아직 살아 있다고 착각하지 않도록 제외된다. 소스는 이 목록을 pg_rewind의 filemap과 동기화 상태를 유지해야 한다고 명시한다. 두 도구 모두 어떤 파일이 노드 로컬인지, 논리적 클러스터 이미지의 일부인지를 추론하기 때문이다.
동일한 sendDir/sendFile 코드는 PROGRESS가 요청되면 두 번 실행된다. 첫 번째는 sizeonly = true로 bytes_total을 합산하고(클라이언트가 퍼센티지를 표시할 수 있도록), 두 번째는 sizeonly = false로 실제 스트리밍한다. 동일한 순회를 두 번 실행하면 — 두 번째 디렉터리 스캔 비용을 치르는 대신 — 크기 추정치가 실제 전송량과 정확히 일치하도록 보장한다. 별도의 크기 계산 경로를 두면 드리프트가 생길 수 있다.
sendFile은 파일을 열고, _tarWriteHeader로 tar 헤더를 쓰고, read_file_data_into_buffer -> bbsink_archive_contents 경로로 bbs_buffer 크기의 청크로 내용을 스트리밍한다. 버퍼 크기(SINK_BUFFER_LENGTH = Max(32768, BLCKSZ))는 의도적으로 BLCKSZ의 배수다. 체크섬 검증이 전체 페이지 단위로 작동하기 때문이다. 데이터 체크섬이 켜져 있고 파일이 릴레이션 파일이면 sendFile은 각 페이지의 체크섬을 검증하고(verify_page_checksum) 실패를 total_checksum_failures에 누적한다. 백업 도중의 torn 읽기는 즉각적 치명 오류가 아니다. 체크섬 실패 페이지는 한 번 재읽기하고, 재읽기 후에도 문제가 있어야 실패로 카운트된다. 백업과 동시에 쓰이는 페이지는 예상되는 상황이며 전체 페이지 쓰기 재생으로 복구되기 때문이다. 백업 끝에서 0이 아닌 총계는 ERRCODE_DATA_CORRUPTED를 발생시킨다 — 백업에 조용히 전파될 뻔한 진짜 온디스크 손상을 드러내는 장치다. sendDir에서의 디스패치는 릴레이션 파일별로 파일 방법을 선택한다:
// sendDir — src/backend/backup/basebackup.cmethod = GetFileBackupMethod(ib, lookup_path, dboid, relspcoid, relfilenumber, relForkNum, segno, statbuf.st_size, &num_blocks_required, relative_block_numbers, &truncation_block_length);if (method == BACK_UP_FILE_INCREMENTALLY){ statbuf.st_size = GetIncrementalFileSize(num_blocks_required); snprintf(tarfilenamebuf, sizeof(tarfilenamebuf), "%s/INCREMENTAL.%s", path + basepathlen + 1, de->d_name); tarfilename = tarfilenamebuf;}3a. bbsink 추상화 (basebackup_sink.h)
섹션 제목: “3a. bbsink 추상화 (basebackup_sink.h)”워커는 바이트가 어디로 가는지 알지 못한다. 공유 버퍼(bbs_buffer, bbs_buffer_length 크기)에 쓰고 bbsink_* 인라인 래퍼 중 하나를 호출한다. 래퍼는 싱크의 bbs_ops vtable로 디스패치한다. 각 싱크는 자신이 감싸는 싱크를 가리키는 bbs_next 포인터를 갖는다. 호출은 로컬에서 처리하거나 체인 아래로 전달한다:
// struct bbsink — src/include/backup/basebackup_sink.hstruct bbsink{ const bbsink_ops *bbs_ops; char *bbs_buffer; size_t bbs_buffer_length; bbsink *bbs_next; bbsink_state *bbs_state;};라이프사이클은 래퍼의 어서션으로 강제되는 엄격한 호출 시퀀스다. 정확히 하나의 begin_backup, 아카이브당 하나의 begin_archive, 여러 archive_contents, 하나의 end_archive. 그 다음 선택적으로 하나의 begin_manifest / 여러 manifest_contents / 하나의 end_manifest. 그 다음 하나의 end_backup. 마지막으로 오류 경로에서도 호출되는 cleanup. 특정 콜백에서 할 일이 없는 싱크는 bbsink_forward_* 헬퍼로 곧장 통과한다. SendBaseBackup이 임의의 스택 — progress(compress(throttle(target(copystream)))) — 을 구성할 수 있는 이유가 이것이다. 어떤 싱크도 bbs_next 너머 이웃을 알지 못한다. 버퍼 채우기 계약(Assert(len > 0 && len <= bbs_buffer_length)과 bbs_buffer_length % BLCKSZ == 0)이 SINK_BUFFER_LENGTH를 블록 정렬로 유지하고 체크섬 검증이 전체 페이지를 가정할 수 있는 이유다.
4. 와이어 프레이밍 (basebackup_copy.c)
섹션 제목: “4. 와이어 프레이밍 (basebackup_copy.c)”bbsink_copystream은 싱크 콜백을 프로토콜 메시지로 변환하는 bbsink_ops vtable을 구현한다. begin_archive는 첫 번째 바이트가 'n'이고 아카이브 이름과 테이블스페이스 경로가 따르는 CopyData를 보낸다. archive_contents는 버퍼링된 청크를 'd' 메시지로 전송하고, PROGRESS_REPORT_BYTE_INTERVAL 바이트마다 시계를 확인해 'p' 진행상황 보고를 내보낼 수 있다. begin_manifest/manifest_contents는 'm'/'d'를 사용한다. 백업 종료 핸드셰이크는 COPY 스트림을 닫고 종료 LSN을 전송한다:
// bbsink_copystream_end_backup — src/backend/backup/basebackup_copy.cstatic voidbbsink_copystream_end_backup(bbsink *sink, XLogRecPtr endptr, TimeLineID endtli){ SendCopyDone(); SendXlogRecPtrResult(endptr, endtli);}SendXlogRecPtrResult와 SendTablespaceList는 DestRemoteSimple 리시버를 대상으로 begin_tup_output_tupdesc / do_tup_output으로 작은 결과 집합을 만든다. COPY 스트림이 시작되기 전에 클라이언트가 읽는 시작 LSN과 {spcoid, spclocation, size} 행들이다.
5. WAL 추가와 매니페스트
섹션 제목: “5. WAL 추가와 매니페스트”WAL이 요청된 경우(pg_basebackup -X fetch), 메인 데이터 디렉터리 아카이브는 파일 순회 후에도 열려 있다 — 소스 주석은 메인 데이터 디렉터리가 WAL을 그 아카이브에 추가할 자리를 확보하기 위해 “항상 마지막에 전송”된다고 명시한다. perform_base_backup은 pg_wal을 스캔해 타임라인과 무관하게 이름이 [firstoff, lastoff](startsegno와 endsegno에서 도출) 범위에 속하는 모든 세그먼트와 타임라인 이력 파일을 선택한다. 세그먼트가 전송 전에 재활용될 가능성을 줄이기 위해 compareWalFileNames로 가장 오래된 것부터 정렬한다(타임라인 접두사를 무시하고 로그/세그먼트 부분만 비교한다). 그 다음 연속성 정상성 검사를 수행한다. 첫 번째 세그먼트가 startptr을 커버해야 하고, 각 연속 세그먼트가 인접해야 하고, 마지막 세그먼트가 endptr을 커버해야 한다. 체크포인트가 복사 도중 필요한 세그먼트를 재활용했을 수 있으므로 CheckXLogRemoved를 각 단계에서 재확인한다. 각 세그먼트는 tar 멤버로 추가된 직후 합성 .done 아카이브-상태 파일(sendFileWithContent(..., "", ...))이 따른다. 이 백업에서 프로모션된 노드가 이미 가진 세그먼트를 중복 아카이브하지 않도록 — walreceiver가 완전한 세그먼트 후에 하는 것과 일치한다. 마지막으로 AddWALInfoToBackupManifest가 WAL 범위를 기록하고 SendBackupManifest가 매니페스트를 스트리밍한다('m'/'d' 메시지로). 그 뒤 bbsink_end_backup이 COPY 스트림을 닫고 종료 LSN/타임라인을 전송한다. 기본 pg_basebackup 모드(-X stream)는 다른 경로를 택한다 — 두 번째 복제 연결을 열고 WAL을 끝에 가져오는 대신 동시에 스트리밍한다. WAL이 의무가 아닌 옵션인 이유가 이것이다. 그 스트리밍 경로는 walsender에 있으며 postgres-wal-sender-receiver.md가 다룬다.
6. 증분 백업 (basebackup_incremental.c)
섹션 제목: “6. 증분 백업 (basebackup_incremental.c)”증분 백업의 경우 클라이언트는 먼저 이전 백업의 매니페스트와 함께 UPLOAD_MANIFEST를 보낸다. walsender가 이것을 CreateIncrementalBackupInfo / AppendIncrementalManifestData / FinalizeIncrementalManifest로 스트리밍해 매니페스트의 WAL 범위와 파일 목록을 IncrementalBackupInfo로 파싱한다. 백업 시점에 PrepareForIncrementalBackup은 매니페스트의 WAL 범위를 이 서버의 타임라인 이력과 대조하고, 이전 백업 이후의 LSN 구간을 결정하고, 관련 WAL 서머리를 로드해 인메모리 블록 참조 테이블(ib->brtab)로 병합한다 — 변경 오라클이다.
그 다음 릴레이션 파일별로 GetFileBackupMethod를 호출한다. 파일이 의심스럽거나(크기가 BLCKSZ의 배수가 아니거나 세그먼트보다 크거나), FSM 포크이거나(신뢰할 수 있게 WAL 기록되지 않는다), 이전 매니페스트에 없거나, 새로 생성된 데이터베이스/테이블스페이스에 속하거나, 어차피 블록의 ≥ 90%가 필요하면 BACK_UP_FILE_FULLY를 반환한다. 그렇지 않으면 변경된 블록 목록과 잘림 길이와 함께 BACK_UP_FILE_INCREMENTALLY를 반환한다:
// GetFileBackupMethod — src/backend/backup/basebackup_incremental.cif (nblocks * BLCKSZ > size * 0.9) return BACK_UP_FILE_FULLY;/* ... sort + relativize block numbers ... */*num_blocks_required = nblocks;*truncation_block_length = size / BLCKSZ;/* ... clamp truncation_block_length against limit_block and RELSEG_SIZE ... */return BACK_UP_FILE_INCREMENTALLY;GetFileBackupMethod의 결정 로직은 보수적으로 설계되어 있다. 모든 의심스러운 경우는 전체 복사로 폴백한다. 잘못된 증분 파일은 복원된 데이터베이스를 조용히 손상시키지만, 잘못된 전체 파일은 공간만 낭비하기 때문이다. 이른 반환들이 그 편향을 인코딩한다. 크기가 BLCKSZ의 깔끔한 배수가 아니거나 세그먼트를 초과하는 파일은 구조적으로 수상하다. FSM 포크는 신뢰할 수 있게 WAL 기록되지 않아 변경이 서머리에 나타나지 않는다. 이전 매니페스트에 없는 파일은 비교 대상이 없다. 이전 백업 이후 생성된 데이터베이스/테이블스페이스 OID는 블록 참조 테이블의 특수 relNumber = 0 항목으로 감지되며, 그 아래의 모든 파일이 새 것임을 의미한다. 90% 임계값은 순수 최적화다. 거의 모든 블록이 변경됐다면 증분 파일과 헤더의 크기가 전체 파일과 비슷하지만 재구성이 더 느리므로 전체 파일을 보내는 편이 낫다.
증분 파일은 sendFile이 헤더 — 매직(INCREMENTAL_MAGIC), 블록 수, 잘림 블록 길이, 상대 블록 번호들 — 와 그 블록들의 원시 8 KB 이미지를 이어 붙여 배치한다. 잘림 블록 길이가 핵심 재구성 힌트다. pg_combinebackup에게 최종 파일 길이를 알려준다. 그 아래의 블록으로 증분에 없는 것은 이전 백업에서 가져오고, 그 이상으로 없는 것은 잘려 나간 것으로 처리된다(필요하면 영 채움). GetIncrementalFileSize / GetIncrementalHeaderSize는 tar 위의 크기를 계산한다(헤더는 블록 데이터가 따를 때만 BLCKSZ 배수로 패딩된다 — 블록이 없는 증분을 작게 유지하기 위해). 전체 파일로 재구성하는 작업은 서버가 아닌 pg_combinebackup의 역할이다. 파일에 기록되는 블록 번호는 정렬 후 start_blkno를 빼서 세그먼트 시작 기준 상대값이다. 각 INCREMENTAL.<seg> 파일이 자기 완결적인 이유다. 오라클의 WAL-서머리 측과 전체 재구성 알고리즘은 교차 참조 문서 postgres-incremental-backup.md에서 다룬다.
flowchart TD UM["UPLOAD_MANIFEST<br/>이전 백업 매니페스트"] --> CIB["CreateIncrementalBackupInfo<br/>Append/Finalize로 매니페스트 파싱"] CIB --> Prep["PrepareForIncrementalBackup<br/>WAL 범위를 타임라인에 대조<br/>WAL 서머리 로드 -> brtab"] Prep --> GFBM["파일별 GetFileBackupMethod"] GFBM -->|"WAL 미기록/새 DB<br/>이전에 없음/>=90% 블록"| Full["BACK_UP_FILE_FULLY<br/>전체 파일 전송"] GFBM -->|"소수의 변경 블록"| Inc["BACK_UP_FILE_INCREMENTALLY<br/>INCREMENTAL.* 파일:<br/>헤더 + 변경 블록"] Full --> Tar["아카이브의 tar 멤버"] Inc --> Tar Tar --> PCB["pg_combinebackup<br/>복원 시 전체 파일 재구성"]
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
SendBaseBackup | src/backend/backup/basebackup.c | 990 |
perform_base_backup | src/backend/backup/basebackup.c | 234 |
parse_basebackup_options | src/backend/backup/basebackup.c | 698 |
sendDir | src/backend/backup/basebackup.c | 1189 |
sendFile | src/backend/backup/basebackup.c | 1574 |
sendFileWithContent | src/backend/backup/basebackup.c | 1075 |
excludeDirContents[] | src/backend/backup/basebackup.c | 151 |
excludeFiles[] | src/backend/backup/basebackup.c | 191 |
basebackup_options (구조체) | src/backend/backup/basebackup.c | 62 |
SINK_BUFFER_LENGTH | src/backend/backup/basebackup.c | 60 |
bbsink_copystream_new | src/backend/backup/basebackup_copy.c | 108 |
bbsink_copystream_begin_backup | src/backend/backup/basebackup_copy.c | 126 |
bbsink_copystream_begin_archive | src/backend/backup/basebackup_copy.c | 165 |
bbsink_copystream_archive_contents | src/backend/backup/basebackup_copy.c | 183 |
bbsink_copystream_end_backup | src/backend/backup/basebackup_copy.c | 297 |
SendXlogRecPtrResult | src/backend/backup/basebackup_copy.c | 341 |
SendTablespaceList | src/backend/backup/basebackup_copy.c | 378 |
bbsink_copystream_ops (vtable) | src/backend/backup/basebackup_copy.c | 92 |
GetFileBackupMethod | src/backend/backup/basebackup_incremental.c | 663 |
PrepareForIncrementalBackup | src/backend/backup/basebackup_incremental.c | 263 |
CreateIncrementalBackupInfo | src/backend/backup/basebackup_incremental.c | 152 |
AppendIncrementalManifestData | src/backend/backup/basebackup_incremental.c | 194 |
FinalizeIncrementalManifest | src/backend/backup/basebackup_incremental.c | 227 |
GetIncrementalFilePath | src/backend/backup/basebackup_incremental.c | 625 |
GetIncrementalFileSize | src/backend/backup/basebackup_incremental.c | 909 |
GetIncrementalHeaderSize | src/backend/backup/basebackup_incremental.c | 881 |
IncrementalBackupInfo (구조체) | src/backend/backup/basebackup_incremental.c | 74 |
do_pg_backup_start | src/backend/access/transam/xlog.c | 8842 |
do_pg_backup_stop | src/backend/access/transam/xlog.c | 9170 |
do_pg_abort_backup | src/backend/access/transam/xlog.c | 9444 |
build_backup_content | src/backend/access/transam/xlogbackup.c | 29 |
bbsink_state (구조체) | src/include/backup/basebackup_sink.h | 66 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”/data/hgryoo/references/postgres REL_18 작업 트리, 커밋 273fe94852b3a7e34fd171e8abdf1481beb302fa(PG 18.x) 기준으로 검증됐다.
- 커밋 / 브랜치.
git log -1은 2026-06-05 날짜의273fe94를 보고한다. 위의 모든 줄 번호는 이 트리에서 직접 읽었다. - 파일이 명시된 크기로 존재함.
basebackup.c는 2136줄,basebackup_copy.c는 422줄,basebackup_incremental.c는 1056줄이다. - 심볼 존재 확인.
SendBaseBackup,perform_base_backup,parse_basebackup_options,sendDir,sendFile,sendFileWithContent,GetFileBackupMethod,PrepareForIncrementalBackup,GetIncrementalFileSize,GetIncrementalHeaderSize,bbsink_copystream_new,SendXlogRecPtrResult,SendTablespaceList,bbsink_copystream_opsvtable을 모두grep으로 확인했다. - 브래킷 프리미티브.
do_pg_backup_start,do_pg_backup_stop,do_pg_abort_backup은xlog.c에,build_backup_content는xlogbackup.c에 있다. WAL 삽입 락 아래의runningBackups++와 전체 페이지 쓰기 근거를 현지에서 읽었다. - 와이어 프레이밍 태그. 타입 바이트
'n'(새 아카이브),'d'(데이터),'m'(매니페스트),'p'(진행상황)가basebackup_copy.c에서 설명과 정확히 일치하게 나타난다.PqMsg_CopyData/PqMsg_CopyOutResponse/PqMsg_CopyDone이 사용되는 메시지 상수다. - 옵션 철자. REL_18은
CHECKPOINT { fast | spread }와 불리언형MANIFEST { yes | no | force-encode }를 받는다.summarize_wal에 대한INCREMENTAL사전 조건은parse_basebackup_options에서 강제된다. - 증분 레이아웃.
INCREMENTAL_MAGIC,INCREMENTAL.<name>tar 멤버 명명 규칙, 90%-블록 전체 백업 임계값이basebackup_incremental.c/sendFile에서 확인됐다. 제거된 18 심볼에 의존하는 주장은 없다. 블록 참조 테이블 API(BlockRefTableGetEntry,BlockRefTableEntryGetBlocks)가 REL_18 인터페이스다. - 범위 경계. WAL 스트리밍/복제 전송, WAL 요약화,
pg_combinebackup재구성은 교차 참조 문서에 미루며 이음새 너머는 재단언하지 않는다. - 체크섬 재읽기. 실패 페이지를 한 번 재읽기한 뒤 실패로 카운트한다는 주장이
read_file_data_into_buffer에서 확인됐다. 이 함수는reread_cnt재읽기 경로를 가지며 두 번째 읽기가 실패해야만checksum_failures++를 계산한다. - backup_label 필드. 정확한
START WAL LOCATION/CHECKPOINT LOCATION/BACKUP METHOD: streamed/BACKUP FROM/START TIME/LABEL/START TIMELINE/ 선택적INCREMENTAL FROM LSN+INCREMENTAL FROM TLI필드 집합을build_backup_content에서 직접 읽었다..backup이력 파일 변형은ishistoryfile = true로 호출된 동일한 함수다.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”PostgreSQL의 베이스 백업은 넓은 설계 공간의 한 점이다. 대안을 열거하면 PG가 실제로 무엇을 선택했는지가 선명해진다.
스냅샷 기반 백업 (COW 파일시스템 / 볼륨). ZFS, LVM, 클라우드 블록 스토어 스냅샷은 전체 볼륨의 크래시 일관적 시점 이미지를 원자적으로 찍을 수 있다. 파일 단위 순회를 완전히 우회한다. 트레이드오프는 스냅샷이 블록 디바이스 레이어에서만 일관적이라는 점이다. PostgreSQL-일관적 백업으로 만들려면 여전히 WAL 브래킷이 필요하다 — 스냅샷 주위의 pg_backup_start / pg_backup_stop — 복원된 볼륨이 올바르게 재생되도록. PG의 BASE_BACKUP과 저수준 API는 여기서 보완 관계다. API가 존재하는 이유가 바로 스냅샷 도구가 PG 외부의 복사 메커니즘을 감쌀 수 있도록 하기 위해서다. 핵심 교훈은 복구 가능성 메타데이터(backup_label, WAL 구간)가 핵심 부분이고 바이트 복사 메커니즘은 교체 가능하다는 것이다.
증분을 위한 블록 변경 추적 vs. WAL 스캔. Oracle RMAN의 블록 변경 추적은 블록이 더럽혀질 때 전용 파일에 변경된 블록의 영속 비트맵을 유지한다. SQL Server의 차등 백업은 DCM(Differential Changed Map) 비트맵 페이지로 변경된 익스텐트를 추적한다. 둘 다 증분 오라클을 직접 조회로 만들기 위해 작은 쓰기 시점 비용을 지불한다. PostgreSQL은 대신 WAL을 서머리(WAL 요약기)로 스캔해 사후에 오라클을 도출하고 백업 시점에 블록 참조 테이블로 병합하는 방식을 택했다. 장점은 요약화가 켜져 있을 때 쓰기 경로에 오버헤드가 전혀 없고, 크래시에 걸쳐 일관성을 유지해야 하는 온디스크 비트맵도 없다는 점이다. 비용은 요약기가 따라잡아야 하고 GetFileBackupMethod가 백업마다 변경 집합을 재구성해야 한다는 점이다. 이것은 고전적인 이른-구체화 vs. 늦은-구체화 트레이드오프다 — 실행기 문서가 구체화 평가와 파이프라인 평가 사이에 그리는 것과 동일한 축을, 변경 추적에 적용한 것이다.
푸시 vs. 풀, 압축의 위치. PG는 풀 방식으로 백업을 스트리밍한다. 클라이언트가 드라이브하고 서버가 읽고 프레이밍한다. 서버 사이드 압축(COMPRESSION gzip|lz4|zstd)은 체인의 싱크이므로 CPU 비용이 프라이머리에 떨어진다. 클라이언트 사이드 압축(pg_basebackup에서)은 네트워크 바이트 비용을 치르는 대신 프라이머리에서 비용을 옮긴다. MySQL의 xtrabackup은 역사적으로 외부 도구로서 서버 명령을 통하지 않고 파일을 직접 읽는 방식으로 비슷한 브래킷-앤-카피 형태를 취했다. bbsink 데코레이터 스택은 “계량, 압축, 전송 각각의 관심사가 어디에 놓이는가”에 대한 PG의 깔끔한 답이다 — 각각은 조합 가능한 싱크 하나다.
전체 페이지 쓰기 없는 Torn 페이지. PG의 torn-page 방어는 백업 윈도우 동안의 전체 페이지 쓰기 WAL이다. 다른 엔진은 다른 방식으로 문제를 피한다. 일부는 이중 쓰기 버퍼(doublewrite buffer)(InnoDB)를 사용해 torn 페이지를 백업과 무관하게 항상 이중 쓰기 영역에서 복구할 수 있게 한다. 일부는 스토리지 레이어의 원자적 8 KB+ 쓰기에 의존한다. PG의 선택은 평상시에는 힙 쓰기 경로를 단순하게 유지하고, 백업(과 체크포인트 이후)에만 비용을 치른다 — 하지만 이것이 바쁜 클러스터의 베이스 백업이 일시적으로 WAL 볼륨을 부풀리는 이유다.
연구 프론티어. 증분-영원 전략과 합성 전체(synthetic full) 전략(프라이머리를 다시 읽지 않고 증분 체인을 개념적 전체로 병합)은 엔터프라이즈 백업 제품에서 잘 탐구됐고 pg_combinebackup으로 부분적으로 실현됐다. PG에 특정한 열린 질문으로는 매우 큰 클러스터를 위해 인메모리 블록 참조 테이블을 디스크로 스필하는 것(소스 자체가 메모리 우려를 표시한다), WAL 요약화, 아카이빙, 백업의 더 긴밀한 통합으로 변경 오라클이 항상 따뜻하게 유지되는 것 등이 있다. 퍼지 체크포인팅과 ARIES 계열 복구(Mohan 1992)에 대한 더 넓은 문헌이 이론적 백본으로 남아 있다. 베이스 백업은 외부 미디어로 찍는 퍼지 체크포인트일 뿐이며, 모든 개선은 브래킷의 오버헤드나 복사되는 바이트를 줄이면서 멱등 물리 redo를 보존하는 것에 관한 것이다.
마지막 설계 관찰 하나가 비교를 PostgreSQL의 특유한 취향으로 돌아오게 한다. 베이스 백업 서브시스템은 복구 기계 치고는 비정상적으로 조합 가능하다. 브래킷 프리미티브(do_pg_backup_start / do_pg_backup_stop)는 SQL 저수준 API, 스냅샷 도구, BASE_BACKUP이 그대로 재사용한다. 싱크 체인은 워커를 건드리지 않고 압축, 스로틀링, 진행상황 표시, 전송을 혼합하고 조합할 수 있게 한다. 증분 경로는 — 메서드 열거 하나와 오라클 조회 하나 — 얇은 추가로 동일한 순회 위에 얹혀 있다. 그 조합 가능성이 이 모듈의 진짜 교훈이다. 세 가지 백업 엔진(전체, 증분, 스냅샷 지원)을 만드는 대신, PostgreSQL은 모든 물리 백업이 공유하는 하나의 불변 — redo-고정 시작과 아카이브 보장 종료로 브래킷된 복구 가능한 퍼지 이미지 — 을 추출하고 나머지를 모두 그 위에 플러그 가능한 결정으로 만들었다. 교차 참조 문서들(postgres-incremental-backup.md, postgres-archiving-walsummary.md, postgres-wal-sender-receiver.md)이 각각 그 플러그 가능한 이음새 중 하나를 이어받는다.
- PostgreSQL REL_18 소스 (
/data/hgryoo/references/postgres, 커밋273fe94):src/backend/backup/basebackup.c—SendBaseBackup,perform_base_backup,parse_basebackup_options,sendDir,sendFile,sendFileWithContent, WAL-추가 루프, 제외 목록.src/backend/backup/basebackup_copy.c—bbsink_copystreamvtable, COPY 프레이밍,SendXlogRecPtrResult,SendTablespaceList.src/backend/backup/basebackup_incremental.c—GetFileBackupMethod,PrepareForIncrementalBackup, 매니페스트 파싱, 증분 파일 크기 계산.src/include/backup/basebackup_sink.h—bbsink,bbsink_state,bbsink_ops.src/backend/access/transam/xlog.c—do_pg_backup_start,do_pg_backup_stop,do_pg_abort_backup,runningBackups.src/backend/access/transam/xlogbackup.c—build_backup_content.
- 이론 기반. Database System Concepts, Silberschatz, Korth & Sudarshan, 7판, 19장 “Recovery System” — 아카이벌 덤프, 퍼지 덤프, 미디어 장애로부터의 복구 (
knowledge/research/dbms-general/database-system-concepts.md). - 복구 모델. Mohan et al., “ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging,” 1992 — 멱등 physiological redo와 전체 페이지 이미지, 퍼지 이미지를 복구하는 토대 (
knowledge/research/dbms-papers/aries.md). - 교차 참조 (이 트리):
postgres-wal-sender-receiver.md—BASE_BACKUP과 스탠바이가 따르는 라이브 WAL 스트림을 운반하는 복제 전송.postgres-archiving-walsummary.md— WAL 아카이빙과 증분 변경 오라클을 공급하는 WAL 요약기.postgres-incremental-backup.md— 블록 참조 테이블과pg_combinebackup재구성을 깊이 다룸.postgres-xlog-wal.md,postgres-checkpoint.md,postgres-recovery-redo.md— 브래킷과 복원이 의존하는 WAL, 체크포인트, redo 기계.postgres-overview-replication-ha.md— 이 문서가 속한 서브시스템 지도.