(KO) pg_basebackup — BASE_BACKUP 구동, 병렬 WAL 스트리밍, 출력 형식, pg_receivewal / pg_recvlogical 패밀리
- 이론적 배경
- DBMS 공통 설계
- PostgreSQL의 접근 방식
- 소스 워크스루
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 지평
- 참고 자료
이론적 배경
섹션 제목: “이론적 배경”**물리 베이스 백업(physical base backup)**은 실행 중인 데이터베이스 클러스터의 디스크 상태를 서버를 멈추지 않고 바이트 단위로 복사한 것으로, 새 복제본(replica)의 시작점이나 특정 시점 복구(PITR, point-in-time recovery) 복원의 기반으로 쓰인다. 이 도구가 해결해야 할 핵심 문제는 퍼지 스냅샷(fuzzy snapshot) 문제다. 살아 있는 클러스터의 파일은 복사하는 동안에도 계속 변경되므로, 단순한 cp -r로는 내부적으로 일관성이 깨진 이미지만 얻게 된다.
교과서적 해법(Database System Concepts, Silberschatz et al., 복구 장; Database Internals, Petrov, Part II)은 파일 복사를 백업 시작(backup-start) 마커와 백업 종료(backup-stop) 마커로 괄호처럼 묶고, 두 마커 사이에 생성된 모든 WAL을 복원 시 반드시 재생하도록 요구하는 것이다. 복사본 자체는 찢겨 있을 수 있지만, 백업 창(backup window) 동안 발생한 페이지 수정이 전부 로그에도 있기 때문에 WAL 재생이 이를 복원한다. 이는 체크포인트에서 재실행하는 크래시 복구 논리를 “임의로 찢긴 파일 이미지 + 동시 기록된 로그로부터 복구”로 일반화한 것이다.
백업을 수행하는 도구의 설계 공간은 세 가지 속성으로 정의된다.
-
데이터를 어디서 가져오는가? 공유 파일시스템에서 데이터 디렉터리를 직접 읽는 방식(파일시스템 레벨 백업)과, 서버에 네트워크 프로토콜로 바이트를 스트리밍하도록 요청하는 방식(스트리밍 백업)이 있다. 스트리밍 모델에서 백업 클라이언트는 서버 파일시스템에 전혀 접근하지 않아도 된다. TCP 연결과 복제 권한만 있으면 충분하며,
$PGDATA에 대한 로컬 디스크 접근은 필요 없다. -
백업 창의 WAL을 어떻게 포착하는가? 찢긴 이미지는 시작·종료 사이의 모든 WAL 레코드가 있을 때만 유용하다. 전략은 두 가지다. 파일 복사가 끝난 뒤 필요한 세그먼트를 한꺼번에 가져오는(fetch) 방법과, 파일 복사와 병행하여 WAL을 스트리밍하는 방법이다. 병렬 스트리밍은 프라이머리가 보존해야 할 WAL 양을 제한하고, 느리고 큰 백업이 끝나기 전에 프라이머리가 필요한 세그먼트를 재활용하는 상황을 방지한다.
-
출력의 디스크 형태와 내구성 계약은 무엇인가? 백업은 즉시 시작할 수 있는 디렉터리 트리로 쓰거나 하나 이상의 tar 아카이브로 쓸 수 있으며, 압축 여부도 선택할 수 있다. 도구는 압축 위치도 결정해야 한다. 서버에서 압축하면 네트워크 대역폭을 절약하지만 서버 CPU가 쓰이고, 클라이언트에서 압축하면 클라이언트 디스크를 절약하지만 전체 전송이 필요하다.
PostgreSQL의 pg_basebackup은 스트리밍 백업 클라이언트다. PostgreSQL 와이어 프로토콜의 **복제 서브-프로토콜(replication sub-protocol)**을 사용한다. 이 프로토콜은 일반 쿼리와 같은 타입 바이트 + 길이 접두사 프레이밍을 쓰되, replication=true / replication=database 시작 옵션으로 진입한다(postgres-wire-protocol.md 참고). 서버 측 대화 상대는 WAL 송신자(postgres-wal-sender-receiver.md)이고, 실제 스냅샷 구성은 백엔드의 basebackup.c(postgres-backup-basebackup.md)에 있다. 이 문서는 클라이언트 쪽을 다룬다. pg_basebackup이 BASE_BACKUP 명령을 어떻게 조립하고, 다중 결과·COPY 대화를 어떻게 구동하며, 두 번째 연결을 분기하여 WAL을 스트리밍하고, 바이트를 출력 파일로 변환하는지 설명한다.
DBMS 공통 설계
섹션 제목: “DBMS 공통 설계”모든 프로덕션 RDBMS는 핫 백업 유틸리티를 제공하며, 메커니즘이 달라도 같은 골격으로 수렴한다.
-
Oracle RMAN은
ALTER DATABASE BEGIN BACKUP/END BACKUP으로 데이터파일 복사를 괄호처럼 묶는다(최근에는 서버 프로세스와 직접 통신하여 퍼지-복사 창이 사용자에게 보이지 않는 방식을 더 많이 쓴다). RMAN은 카탈로그에서 백업을 추적하고 블록 변경 추적 파일을 참고하여 블록 단위 증분 백업을 수행할 수 있다. 이는 PostgreSQL 17의 증분 백업(postgres-incremental-backup.md)의 개념적 선조다. -
MySQL / Percona XtraBackup은 InnoDB 데이터파일을 복사하는 동안 InnoDB 리두 로그를 별도 파일에 테일링하고, 준비(prepare) 시점에 그 리두를 적용한다. 구조적으로 PostgreSQL의 “파일 복사 + WAL 병렬 스트리밍 + 복원 시 재생” 모델과 동일하다. 리두 테일은 MySQL에서 병렬 WAL 스트림의 역할을 한다.
-
SQL Server는 VDI/VSS 스냅샷과 백업-로그 체인을 사용한다.
BACKUP DATABASE ... WITH DIFFERENTIAL패밀리는 전체/증분 구분을 그대로 반영한다.
공유 설계 어휘를 정리하면 다음과 같다. (a) 로그에 기록되는 일관된 시작·종료 마커, (b) 찢길 수 있는 파일 이미지, (c) 백업 창에 걸친 로그 테일, (d) 디렉터리 대 아카이브·압축 여부를 포함하는 출력 컨테이너, (e) 베이스 백업과 독립적으로 존재하여 PITR이 백업 종료 시점 이후로 롤 포워드할 수 있게 하는 재개 가능·연속 WAL 아카이버다. PostgreSQL은 (e)를 백업 도구에 통합하지 않고 별도 프로그램 pg_receivewal로 분리한다. 이 분리 덕분에 베이스 백업을 가끔 수행하는 동안에도 WAL 아카이브를 지속적으로 쌓을 수 있다.
또 하나의 범횡단적 관심사는 압축 위치다. 네트워크 병목 배포 환경에서는 서버가 전송 전에 압축하길 원하고, CPU 병목 서버(또는 빠른 LAN으로 저렴한 클라이언트에 백업하는 경우)에서는 클라이언트가 압축하길 원한다. 잘 설계된 도구는 양쪽을 모두 노출하고 서버가 어느 방식을 지원하는지 협상한다. PostgreSQL 15에서 서버 측 압축 협상이 도입되었으므로, pg_basebackup은 런타임에 요청한 압축을 서버 측에서 실행할 수 있는지 또는 클라이언트 측으로 폴백해야 하는지 판단하고 그에 맞게 아카이브 파일 확장자를 조정해야 한다.
PostgreSQL의 접근 방식
섹션 제목: “PostgreSQL의 접근 방식”pg_basebackup은 libpq 클라이언트다. 서버와의 모든 상호작용은 하나(또는 두 개)의 복제 연결 위에서 일련의 복제 명령으로 이루어진다. 오케스트레이션은 BaseBackup()에 있고, 연결 배관과 소형 복제 명령 헬퍼는 형제 도구들과 streamutil.c에서 공유된다.
백업 하나 = 옵션을 하나씩 조립하는 BASE_BACKUP 명령 하나
섹션 제목: “백업 하나 = 옵션을 하나씩 조립하는 BASE_BACKUP 명령 하나”BaseBackup()은 먼저 서버 버전을 확인하고, PQExpBuffer에 옵션을 추가하는 방식으로 BASE_BACKUP 명령 문자열을 조립한다. PG15 이상 서버는 괄호형 옵션 문법(BASE_BACKUP (LABEL 'x', PROGRESS, ...))을 받아들이고, 이전 서버는 공백으로 구분하는 위치 기반 문법을 사용한다. use_new_option_syntax라는 단일 불리언이 둘 중 하나를 선택하고, streamutil.c의 Append*CommandOption 헬퍼 세 개가 올바른 구분자를 내보낸다.
// BaseBackup — src/bin/pg_basebackup/pg_basebackup.cAppendStringCommandOption(&buf, use_new_option_syntax, "LABEL", label);if (estimatesize) AppendPlainCommandOption(&buf, use_new_option_syntax, "PROGRESS");if (includewal == FETCH_WAL) AppendPlainCommandOption(&buf, use_new_option_syntax, "WAL");...if (compressloc == COMPRESS_LOCATION_SERVER){ if (!use_new_option_syntax) pg_fatal("server does not support server-side compression"); AppendStringCommandOption(&buf, use_new_option_syntax, "COMPRESSION", compression_algorithm); ...}...if (use_new_option_syntax && buf.len > 0) basebkp = psprintf("BASE_BACKUP (%s)", buf.data);else basebkp = psprintf("BASE_BACKUP %s", buf.data);Append*CommandOption 헬퍼는 순서와 무관하게 올바른 구두점을 보장한다. AppendPlainCommandOption은 버퍼의 마지막 바이트를 보고 선행 ,(새 문법) 또는 공백(구 문법)이 필요한지 판단하며, 문자열 값은 PQescapeStringConn으로 이스케이프된다.
// AppendStringCommandOption — src/bin/pg_basebackup/streamutil.cAppendPlainCommandOption(buf, use_new_option_syntax, option_name);if (option_value != NULL){ size_t length = strlen(option_value); char *escaped_value = palloc(1 + 2 * length);
PQescapeStringConn(conn, escaped_value, option_value, length, NULL); appendPQExpBuffer(buf, " '%s'", escaped_value); pfree(escaped_value);}다중 결과 대화
섹션 제목: “다중 결과 대화”PQsendQuery(conn, basebkp)를 실행하면 서버는 일련의 결과 집합으로 응답하고, BaseBackup()은 고정된 순서로 이를 소비한다.
- WAL 시작 LSN과 시작 타임라인을 담은 단일 행 결과. 이것이 백업 시작 마커다. 서버가 응답하기 전에 실행하는 체크포인트 때문에 non-fast 백업은 여기서 오래 멈출 수 있다.
- 테이블스페이스당 한 행씩 담긴 헤더 결과 (OID, 위치, 크기). 크기는 진행 추정에 쓰이고, 위치는
verify_dir_is_empty_or_create를 구동한다. - COPY 데이터로 전달되는 아카이브 페이로드.
- WAL 종료 LSN을 담은 단일 행 결과 (백업 종료 마커).
- 마지막
CommandComplete— 백업 도중 체크섬 검증이 실패했다면ERRCODE_DATA_CORRUPTED를 담을 수 있다.
// BaseBackup — src/bin/pg_basebackup/pg_basebackup.cif (PQsendQuery(conn, basebkp) == 0) pg_fatal("could not send replication command \"%s\": %s", "BASE_BACKUP", PQerrorMessage(conn));
/* Get the starting WAL location */res = PQgetResult(conn);if (PQresultStatus(res) != PGRES_TUPLES_OK) pg_fatal("could not initiate base backup: %s", PQerrorMessage(conn));...strlcpy(xlogstart, PQgetvalue(res, 0, 0), sizeof(xlogstart));...if (PQnfields(res) >= 2) starttli = atoi(PQgetvalue(res, 0, 1));PG15 프로토콜 변경은 헤더 이후 디스패치에서 나타난다. v15 이상 서버는 모든 아카이브(및 매니페스트)를 하나의 COPY 스트림으로 연속 전송하며 ReceiveArchiveStream이 디코딩한다. 이전 서버는 테이블스페이스별로 별도의 tar COPY를 보내며 ReceiveTarFile 루프가 처리한다.
// BaseBackup — src/bin/pg_basebackup/pg_basebackup.cif (serverMajor >= 1500){ /* Receive a single tar stream with everything. */ ReceiveArchiveStream(conn, client_compress);}else{ /* Receive a tar file for each tablespace in turn */ for (i = 0; i < PQntuples(res); i++) ReceiveTarFile(conn, archive_name, spclocation, i, client_compress); if (!writing_to_stdout && manifest) ReceiveBackupManifest(conn);}분기된 두 번째 연결을 통한 병렬 WAL 스트리밍
섹션 제목: “분기된 두 번째 연결을 통한 병렬 WAL 스트리밍”pg_basebackup -X stream(기본값)의 핵심 아키텍처 선택은 WAL을 파일 복사와 동시에 두 번째 복제 연결에서 분기된 자식 프로세스(Windows에서는 스레드)로 포착한다는 점이다. 분기는 시작 LSN을 알게 된 이후, 아카이브 페이로드를 읽기 이전에 일어난다. 잠재적으로 거대한 파일 복사가 진행되는 동안 WAL이 클라이언트에 쌓이도록 하기 위해서다.
// BaseBackup — src/bin/pg_basebackup/pg_basebackup.cif (includewal == STREAM_WAL){ ... StartLogStreamer(xlogstart, starttli, sysidentifier, wal_compress_algorithm, wal_compress_level);}StartLogStreamer는 GetConnection()으로 새 연결을 열고, 선택적으로 임시 복제 슬롯을 만들며, 시작 위치를 세그먼트 경계로 내림하고, 종료 신호 전달용 Unix 파이프를 만든 다음 분기한다.
// StartLogStreamer — src/bin/pg_basebackup/pg_basebackup.cparam->startptr -= XLogSegmentOffset(param->startptr, WalSegSz);...if (pipe(bgpipe) < 0) pg_fatal("could not create pipe for background process: %m");param->bgconn = GetConnection();...if (temp_replication_slot && !replication_slot) replication_slot = psprintf("pg_basebackup_%u", (unsigned int) PQbackendPID(param->bgconn));...bgchild = fork();if (bgchild == 0) exit(LogStreamerMain(param)); /* in child process */자식은 LogStreamerMain을 실행한다. StreamCtl을 채우고 공유 ReceiveXlogStream 엔진(receivelog.c, postgres-wal-sender-receiver.md 참고)을 호출한다. 핵심 배선은 **정지 술어(stop predicate)**와 정지 소켓이다. 부모는 백업의 종료 LSN을 알게 되면 파이프에 기록하고, 자식의 reached_end_position 콜백이 이를 읽어 ReceiveXlogStream에 정확히 그 위치에서 멈추도록 알린다.
// LogStreamerMain — src/bin/pg_basebackup/pg_basebackup.cstream.stream_stop = reached_end_position;#ifndef WIN32stream.stop_socket = bgpipe[0];#endifstream.mark_done = true;stream.do_sync = false; /* fsync happens at the end of pg_basebackup */if (format == 'p') stream.walmethod = CreateWalDirectoryMethod(param->xlog, PG_COMPRESSION_NONE, 0, stream.do_sync);else stream.walmethod = CreateWalTarMethod(param->xlog, param->wal_compress_algorithm, param->wal_compress_level, stream.do_sync);if (!ReceiveXlogStream(param->bgconn, &stream)) return 1;reached_end_position은 파이프에 대한 비차단(non-blocking) select()다. 부모가 종료 LSN을 보내기 전까지 콜백은 “계속 진행”을 반환하고, 종료 포인터를 갖게 되면 스트리밍된 세그먼트가 그 지점에 도달하는 순간 true를 반환한다.
// reached_end_position — src/bin/pg_basebackup/pg_basebackup.cr = select(bgpipe[0] + 1, &fds, NULL, NULL, &tv);if (r == 1){ r = read(bgpipe[0], xlogend, sizeof(xlogend) - 1); ... if (sscanf(xlogend, "%X/%X", &hi, &lo) != 2) pg_fatal("could not parse write-ahead log location \"%s\"", xlogend); xlogendptr = ((uint64) hi) << 32 | lo; has_xlogendptr = 1;}else return false; /* don't know the end yet */...if (segendpos >= xlogendptr) return true;return false;부모는 종료 LSN 결과를 소비한 뒤 자식에게 그 값을 전달하고 waitpid()로 기다린다.
// BaseBackup — src/bin/pg_basebackup/pg_basebackup.c (background reap)if (write(bgpipe[1], xlogend, strlen(xlogend)) != strlen(xlogend)) pg_fatal("could not send command to background pipe: %m");r = waitpid(bgchild, &status, 0);클러스터 전체 fsync를 뒤로 미루는 것(자식은 do_sync = false로 설정)은 의도된 설계다. WAL 세그먼트가 도착할 때마다 fsync하는 대신, 부모가 맨 마지막에 전체 basedir을 한 번에 플러시한다. plain 포맷은 sync_pgdata, tar 포맷은 sync_dir_recurse를 사용하며, 이 방식이 훨씬 저렴하다.
flowchart TD
A["main()"] --> B["BaseBackup()"]
B --> C["BASE_BACKUP 명령 조립<br/>(Append*CommandOption)"]
C --> D["PQsendQuery(BASE_BACKUP)"]
D --> E["결과 1: 시작 LSN + TLI"]
E --> F["결과 2: 테이블스페이스 헤더"]
F --> G{"includewal == STREAM_WAL?"}
G -->|yes| H["StartLogStreamer()<br/>2번째 연결 분기"]
H --> H2["자식: LogStreamerMain<br/>ReceiveXlogStream()"]
G -->|no| I
H --> I{"serverMajor >= 1500?"}
I -->|yes| J["ReceiveArchiveStream<br/>(단일 COPY 스트림)"]
I -->|no| K["ReceiveTarFile 루프<br/>+ ReceiveBackupManifest"]
J --> L["결과 4: 종료 LSN"]
K --> L
L --> M["종료 LSN을 bgpipe에 기록<br/>waitpid(bgchild)"]
M --> N["sync_pgdata / sync_dir_recurse<br/>durable_rename manifest"]
출력 변환: astreamer 파이프라인
섹션 제목: “출력 변환: astreamer 파이프라인”COPY 스트림 아카이브 바이트는 직접 기록되지 않는다. CreateBackupStreamer()가 astreamer 객체 체인을 구성하고, 각 객체는 content / finalize / free vtable을 가진 필터다. 체인은 아래에서 위로 조립된다. 즉 최종 쓰기 객체를 먼저 만들고 래퍼를 덧붙이므로, astreamer_*_new 호출 순서는 데이터 흐름의 역순이다. 포맷과 압축 결정은 체인 구성 방식에만 인코딩된다.
// CreateBackupStreamer — src/bin/pg_basebackup/pg_basebackup.cif (compress->algorithm == PG_COMPRESSION_NONE) streamer = astreamer_plain_writer_new(archive_filename, archive_file);else if (compress->algorithm == PG_COMPRESSION_GZIP){ strlcat(archive_filename, ".gz", sizeof(archive_filename)); streamer = astreamer_gzip_writer_new(archive_filename, archive_file, compress);}else if (compress->algorithm == PG_COMPRESSION_LZ4){ strlcat(archive_filename, ".lz4", sizeof(archive_filename)); streamer = astreamer_plain_writer_new(archive_filename, archive_file); streamer = astreamer_lz4_compressor_new(streamer, compress);}else if (compress->algorithm == PG_COMPRESSION_ZSTD){ strlcat(archive_filename, ".zst", sizeof(archive_filename)); streamer = astreamer_plain_writer_new(archive_filename, archive_file); streamer = astreamer_zstd_compressor_new(streamer, compress);}plain 포맷(-Fp)에서는 체인이 tar 스트림을 디렉터리 트리로 풀어내는 익스트랙터로 시작하며, 서버가 스트림을 압축했다면 클라이언트가 압축을 풀 수 있도록 디컴프레서를 앞에 붙인다.
// CreateBackupStreamer — src/bin/pg_basebackup/pg_basebackup.cif (format == 'p'){ ... streamer = astreamer_extractor_new(directory, get_tablespace_mapping, progress_update_filename);}.../* server-compressed archive, but client wants plain: decompress */if (format == 'p'){ if (is_tar_gz) streamer = astreamer_gzip_decompressor_new(streamer); else if (is_tar_lz4) streamer = astreamer_lz4_decompressor_new(streamer); else if (is_tar_zstd) streamer = astreamer_zstd_decompressor_new(streamer);}클라이언트가 tar 내용을 불투명하게 통과시키는 것이 아니라 이해해야 할 때, 두 개의 래퍼가 조건부로 삽입된다. -R을 위해 standby.signal / primary_conninfo를 기록하는 astreamer_recovery_injector_new와, 인젝터가 tar 구조에 파일을 끼워 넣을 수 있도록 파싱하는 astreamer_tar_parser_new다. must_parse_archive 불리언이 이 모든 것의 게이트 역할을 한다. 클라이언트가 불투명한 압축 tar를 디스크에 쓰는 것이 전부라면 파싱 기계는 전혀 구성되지 않으며, 바이트는 곧장 쓰기 객체로 흐른다.
체인은 코드에서 바깥쪽 필터가 마지막으로 구성되지만 데이터는 반대 방향으로 흐른다. CopyData 청크가 맨 앞에 들어와 각 content 콜백을 거쳐 꼬리의 쓰기 객체까지 전달된다. 아래 다이어그램은 두 가지 대표적인 체인을 보여준다. 클라이언트 측 zstd로 tar 파일에 쓰는 경우와, 서버 측 gzip 스트림을 plain 디렉터리로 풀어내는 경우다.
flowchart LR
subgraph TAR["-Ft --compress=client-zstd"]
T0["CopyData 청크"] --> T1["astreamer_tar_archiver<br/>(must_parse 시)"]
T1 --> T2["astreamer_zstd_compressor"]
T2 --> T3["astreamer_plain_writer<br/>base.tar.zst"]
end
subgraph PLAIN["-Fp (서버 gzip 스트림)"]
P0["CopyData 청크"] --> P1["astreamer_gzip_decompressor"]
P1 --> P2["astreamer_tar_parser"]
P2 --> P3["astreamer_recovery_injector<br/>(-R 시)"]
P3 --> P4["astreamer_extractor<br/>디렉터리 트리"]
end
복구 구성 생성
섹션 제목: “복구 구성 생성”-R이 전달되면 GenerateRecoveryConfig()(src/fe_utils/recovery_gen.c)가 살아 있는 연결의 파라미터에서 primary_conninfo 줄을 재구성한다. 이때 replication, dbname, fallback_application_name을 의도적으로 제거한다. libpqwalreceiver가 이 값들을 덮어 쓰기 때문이며, 생성된 스탠바이 구성이 일반 복제본으로 재연결할 수 있도록 하기 위해서다.
// GenerateRecoveryConfig — src/fe_utils/recovery_gen.cfor (PQconninfoOption *opt = connOptions; opt && opt->keyword; opt++){ if (strcmp(opt->keyword, "replication") == 0 || strcmp(opt->keyword, "dbname") == 0 || strcmp(opt->keyword, "fallback_application_name") == 0 || (opt->val == NULL) || (opt->val != NULL && opt->val[0] == '\0')) continue; ... appendPQExpBuffer(&conninfo_buf, "%s=", opt->keyword); appendConnStrVal(&conninfo_buf, opt->val);}공유 복제 연결 레이어 (streamutil.c)
섹션 제목: “공유 복제 연결 레이어 (streamutil.c)”세 도구 모두 하나의 함수 GetConnection()으로 복제 연결을 연다. 이 함수는 연결 문자열, 커맨드라인 host/user/port 옵션, 복제 모드 기본값을 병합한다. 가장 중요한 한 줄은 dbname / replication 기본값 설정인데, 이것이 물리적 복제 연결(pg_basebackup, pg_receivewal: dbname=replication, replication=true)과 논리적 연결(pg_recvlogical: replication=database)을 구분한다.
// GetConnection — src/bin/pg_basebackup/streamutil.ckeywords[i] = "replication";values[i] = (dbname == NULL) ? "true" : "database";RunIdentifySystem()은 IDENTIFY_SYSTEM을 발행하고 시스템 식별자, 현재 타임라인, (논리 연결의 경우) 현재 플러시 LSN을 파싱한다. 플러시 LSN은 다른 정보가 없을 때의 폴백 시작 위치다.
// RunIdentifySystem — src/bin/pg_basebackup/streamutil.cres = PQexec(conn, "IDENTIFY_SYSTEM");...if (starttli != NULL) *starttli = atoi(PQgetvalue(res, 0, 1));if (startpos != NULL){ if (sscanf(PQgetvalue(res, 0, 2), "%X/%X", &hi, &lo) != 2) ... *startpos = ((uint64) hi) << 32 | lo;}CreateReplicationSlot()은 세 도구 모두를 위한 CREATE_REPLICATION_SLOT 명령을 구성한다. 백업·receivewal에는 PHYSICAL(선택적으로 RESERVE_WAL 포함), recvlogical에는 LOGICAL <plugin>(선택적으로 TWO_PHASE, FAILOVER 포함)을 사용한다. BASE_BACKUP과 같은 신구 문법 전환이 동일하게 적용된다.
pg_receivewal — 재개 기능을 갖춘 지속적 물리 WAL 아카이빙
섹션 제목: “pg_receivewal — 재개 기능을 갖춘 지속적 물리 WAL 아카이빙”pg_receivewal은 사실상 pg_basebackup의 WAL 스트리밍 부분을 독립적·장기 실행 프로그램으로 만든 것이다. 파일 복사 없이 ReceiveXlogStream만 로컬 디렉터리로 무한히 실행한다. 고유 로직은 **디스크에서 재개(resume-from-disk)**다. FindStreamingStart()가 목적지 디렉터리에서 번호가 가장 높은 완전한 WAL 세그먼트를 찾고 그 끝에서 재개하므로, 재시작된 pg_receivewal은 중복 수신이나 갭 없이 이어받는다.
// FindStreamingStart — src/bin/pg_basebackup/pg_receivewal.cwhile (errno = 0, (dirent = readdir(dir)) != NULL){ ... if (!is_xlogfilename(dirent->d_name, &ispartial, &wal_compression_algorithm)) continue; XLogFromFileName(dirent->d_name, &tli, &segno, WalSegSz); ...}StreamLog()가 전체를 묶는다. 시스템을 식별하고 우선순위 순서로 시작 위치를 선택한다. (1) 로컬 디렉터리의 마지막 완전한 세그먼트, (2) 복제 슬롯의 restart_lsn(PG15+, GetSlotInformation 경유), (3) 서버의 현재 플러시 위치(최후 수단) 순이다. 시작 위치를 세그먼트 경계로 내리고, mark_done = false(이 세그먼트들은 아카이버에 전달되는 것이 아니라 그 자체가 아카이브)와 .partial 접미사로 진행 중인 세그먼트를 스트리밍한다.
// StreamLog — src/bin/pg_basebackup/pg_receivewal.cstream.startpos = FindStreamingStart(&stream.timeline);if (stream.startpos == InvalidXLogRecPtr){ if (replication_slot != NULL && PQserverVersion(conn) >= 150000) GetSlotInformation(conn, replication_slot, &stream.startpos, &stream.timeline); if (stream.startpos == InvalidXLogRecPtr) { stream.startpos = serverpos; stream.timeline = servertli; }}stream.startpos -= XLogSegmentOffset(stream.startpos, WalSegSz);...stream.stream_stop = stop_streaming;stream.mark_done = false;stream.partial_suffix = ".partial";ReceiveXlogStream(conn, &stream);stop_streaming 콜백은 --endpos와 SIGINT로 설정된 time_to_stop 플래그를 따르며, 타임라인 전환을 기록한다. 이 프로그램은 종료 신호를 받거나 요청한 종료 LSN에 도달할 때까지 실행되도록 설계되어 있다.
pg_recvlogical — 논리 디코딩 소비자
섹션 제목: “pg_recvlogical — 논리 디코딩 소비자”pg_recvlogical은 논리적 복제 모드로 연결하고 START_REPLICATION SLOT ... LOGICAL 명령을 실행하여 플러그인 출력(예: test_decoding, pgoutput)을 파일로 내보낸다. 파일 지향 엔진인 ReceiveXlogStream을 사용하지 않는다. StreamLogicalLog()에 자체 COPY-both 루프를 갖는다. 이 루프는 'w'(WAL 데이터)와 'k'(keepalive) CopyData 메시지를 디코딩하고, 주기적으로 output_written_lsn / output_fsync_lsn을 서버에 피드백으로 보고한다. 슬롯의 confirmed-flush가 전진하여 WAL이 재활용될 수 있도록 하기 위해서다.
// StreamLogicalLog — src/bin/pg_basebackup/pg_recvlogical.cappendPQExpBuffer(query, "START_REPLICATION SLOT \"%s\" LOGICAL %X/%X", replication_slot, LSN_FORMAT_ARGS(startpos));...res = PQexec(conn, query->data);if (PQresultStatus(res) != PGRES_COPY_BOTH){ pg_log_error("could not send replication command \"%s\": %s", query->data, PQresultErrorMessage(res)); ...}세 도구는 공통 기반 위에 계층화된 패밀리를 형성한다. streamutil.c는 연결, IDENTIFY_SYSTEM, 슬롯 헬퍼를 제공한다. receivelog.c는 pg_basebackup의 WAL 자식과 pg_receivewal 모두가 호출하는 물리 ReceiveXlogStream 엔진을 제공한다. pg_recvlogical은 연결·슬롯 레이어만 재사용하고 자체 디코딩 루프를 가진다. 논리 출력은 WAL 세그먼트가 아니라 바이트 스트림이기 때문이다.
소스 워크스루
섹션 제목: “소스 워크스루”클라이언트는 전부 src/bin/pg_basebackup/ 아래에 있고, 공유 헬퍼 하나가 src/fe_utils/에 있다. 역할별로 심볼을 묶으면 다음과 같다.
백업 오케스트레이션 (pg_basebackup.c):
main— 옵션 파싱, 압축 위치 결정,BaseBackup디스패치.BaseBackup— 전체 대화: 명령 조립, 전송, 시작/헤더/페이로드/종료 결과 시퀀스 소비, WAL 스트리머 분기, 최종 sync, 내구성 있는 매니페스트 rename.backup_parse_compress_options—--compress인자를 알고리즘 + 세부 정보로 분리.verify_dir_is_empty_or_create— 테이블스페이스 디렉터리 사전 점검 (plain 포맷 전용).
병렬 WAL 스트리밍 (pg_basebackup.c):
StartLogStreamer— 두 번째GetConnection, 선택적 슬롯 생성, 세그먼트 경계 내림,pipe(),fork().LogStreamerMain— 자식 진입점.StreamCtl을 채우고 walmethod를 선택(-Fp는 디렉터리,-Ft는 tar)한 뒤ReceiveXlogStream을 호출.reached_end_position— 자식 정지 술어. 종료 파이프에 대한 비차단select, 스트리밍된 LSN을 부모가 전달한 종료 LSN과 비교.logstreamer_param— 분기 경계를 넘어 전달되는 구조체.
출력 변환 (pg_basebackup.c):
CreateBackupStreamer— 포맷 + 압축 + 파싱 필요 여부로astreamer필터 체인 조립.ReceiveArchiveStream/ReceiveArchiveStreamChunk— PG15+ 단일-COPY-스트림 디코더 ('n'새 아카이브,'d'데이터,'m'매니페스트,'p'진행).ReceiveTarFile/ReceiveTarCopyChunk— PG15 이전 테이블스페이스별 tar 디코더.ReceiveCopyData— 청크당 콜백을 호출하는 범용 COPY-out 펌프.progress_report/progress_update_filename—--progress진행 추적.
공유 복제 레이어 (streamutil.c):
GetConnection— 연결 문자열 병합 + 복제 모드 기본값 설정.RunIdentifySystem—IDENTIFY_SYSTEM파싱 (sysid, TLI, 시작 LSN, db).GetSlotInformation— 재개 LSN/TLI를 위한READ_REPLICATION_SLOT(PG15+).CreateReplicationSlot/DropReplicationSlot— 세 도구 모두를 위한 슬롯 생명주기.AppendPlainCommandOption/AppendStringCommandOption/AppendIntegerCommandOption— 이스케이프를 포함하는 신구 옵션 문법 내보내기.CheckServerVersionForStreaming— 최소 버전 게이트.
복구 구성 (recovery_gen.c):
GenerateRecoveryConfig— 복제 전용 키워드를 제거하며 살아 있는 연결에서primary_conninfo+primary_slot_name구성.
WAL 아카이빙 형제 도구 (pg_receivewal.c):
main/StreamLog— 장기 실행 물리 WAL 수신 루프.FindStreamingStart— 재개 LSN을 위한 디렉터리 스캔.stop_streaming—--endpos/ 시그널 기반 정지 술어.get_destination_dir/close_destination_dir— 출력 디렉터리 핸들.
논리 디코딩 형제 도구 (pg_recvlogical.c):
main/StreamLogicalLog—START_REPLICATION ... LOGICALCOPY-both 루프.prepareToTerminate— 정상 종료 / 피드백 플러시.
물리 스트리밍 엔진 (receivelog.c, postgres-wal-sender-receiver.md에 상세 기술):
ReceiveXlogStream—pg_basebackup의 자식과pg_receivewal모두가 호출하는 공유 엔진.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 행 |
|---|---|---|
BaseBackup | src/bin/pg_basebackup/pg_basebackup.c | 1754 |
main | src/bin/pg_basebackup/pg_basebackup.c | 2356 |
StartLogStreamer | src/bin/pg_basebackup/pg_basebackup.c | 616 |
LogStreamerMain | src/bin/pg_basebackup/pg_basebackup.c | 545 |
reached_end_position | src/bin/pg_basebackup/pg_basebackup.c | 462 |
logstreamer_param | src/bin/pg_basebackup/pg_basebackup.c | 533 |
CreateBackupStreamer | src/bin/pg_basebackup/pg_basebackup.c | 1062 |
ReceiveArchiveStream | src/bin/pg_basebackup/pg_basebackup.c | 1285 |
ReceiveArchiveStreamChunk | src/bin/pg_basebackup/pg_basebackup.c | 1333 |
ReceiveTarFile | src/bin/pg_basebackup/pg_basebackup.c | 1600 |
ReceiveCopyData | src/bin/pg_basebackup/pg_basebackup.c | 1015 |
progress_report | src/bin/pg_basebackup/pg_basebackup.c | 817 |
backup_parse_compress_options | src/bin/pg_basebackup/pg_basebackup.c | 987 |
verify_dir_is_empty_or_create | src/bin/pg_basebackup/pg_basebackup.c | 748 |
GetConnection | src/bin/pg_basebackup/streamutil.c | 60 |
RunIdentifySystem | src/bin/pg_basebackup/streamutil.c | 409 |
GetSlotInformation | src/bin/pg_basebackup/streamutil.c | 490 |
CreateReplicationSlot | src/bin/pg_basebackup/streamutil.c | 584 |
AppendPlainCommandOption | src/bin/pg_basebackup/streamutil.c | 746 |
AppendStringCommandOption | src/bin/pg_basebackup/streamutil.c | 767 |
AppendIntegerCommandOption | src/bin/pg_basebackup/streamutil.c | 790 |
StreamLog | src/bin/pg_basebackup/pg_receivewal.c | 500 |
FindStreamingStart | src/bin/pg_basebackup/pg_receivewal.c | 268 |
stop_streaming | src/bin/pg_basebackup/pg_receivewal.c | 184 |
get_destination_dir | src/bin/pg_basebackup/pg_receivewal.c | 235 |
StreamLogicalLog | src/bin/pg_basebackup/pg_recvlogical.c | 215 |
prepareToTerminate | src/bin/pg_basebackup/pg_recvlogical.c | 1064 |
ReceiveXlogStream | src/bin/pg_basebackup/receivelog.c | 452 |
GenerateRecoveryConfig | src/fe_utils/recovery_gen.c | 28 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”커밋
273fe94소스에 관한 사실. 외부 자료 없이 소스만으로 확인 가능하다. 미결 문제는 이후에 정리한다.
검증된 사실
섹션 제목: “검증된 사실”-
WAL 스트리밍은 아카이브 페이로드를 읽기 전에, 별도로 분기된 연결에서 시작된다.
BaseBackup에서 검증:if (includewal == STREAM_WAL) StartLogStreamer(...)블록이 시작 LSN과 헤더 결과를 소비한 이후,ReceiveArchiveStream/ReceiveTarFile이전에 위치한다.StartLogStreamer는 Unix에서fork(), Windows에서_beginthreadex를 호출하고GetConnection으로 자체 연결을 얻는다. 이것이 병렬 포착 설계다. -
WAL 자식은 fsync를 부모에게 위임한다.
LogStreamerMain에서 검증:stream.do_sync = false, 주석은 “fsync happens at the end of pg_basebackup for all data.”다. 부모는 마지막에 한 번만 plain 포맷은sync_pgdata, tar 포맷은sync_dir_recurse로 fsync한다. WAL 시작 위치는StartLogStreamer에서 세그먼트 경계로 내림된다(param->startptr -= XLogSegmentOffset(...)). -
부모는 Unix에서 종료 LSN을 공유 메모리가 아닌 파이프로 자식에게 전달한다. 검증:
StartLogStreamer가pipe()로bgpipe를 생성한다.BaseBackup이xlogend를bgpipe[1]에 쓰고,reached_end_position이 비차단select로bgpipe[0]에서 읽는다. Windows에서는 스트리머가 스레드이므로 공유xlogendptr+InterlockedIncrement(&has_xlogendptr)를 사용한다. -
PG15에서 테이블스페이스별 tar COPY가 단일 COPY 스트림으로 바뀌었다.
BaseBackup에서 검증:if (serverMajor >= 1500) ReceiveArchiveStream(...)elseReceiveTarFile루프. 단일 스트림은 CopyData 메시지당 선행 타입 바이트로 다중화된다('n'새 아카이브,'d'데이터 등).ReceiveArchiveStreamChunk가 디코딩한다. -
압축 위치는 런타임에 결정되며 아카이브 파일 확장자를 바꾼다.
CreateBackupStreamer에서 검증: 클라이언트 측 gzip은.gz를 붙이고astreamer_gzip_writer_new를 사용한다. lz4/zstd는.lz4/.zst를 붙이고 plain 라이터 위에 컴프레서를 쌓는다. 서버 측 압축은BaseBackup의COMPRESSION옵션으로 요청하고,-Fp출력에서는 익스트랙션 전에astreamer_*_decompressor_new로 해제한다.BaseBackup은 PG15 이전 서버에 서버 측 압축을 요청하면pg_fatal("server does not support server-side compression")으로 거부한다. -
CreateReplicationSlot하나가 물리·논리 슬롯 모두를 처리한다.streamutil.c에서 검증:is_physical,two_phase,failover플래그를 가진 단일 함수다.FAILOVER는 PG17+ 게이트(PQserverVersion(conn) >= 170000),TWO_PHASE는 PG15+ 게이트, 괄호형 옵션 문법도 PG15+ 게이트다. 모두 런타임 버전 검사이므로 바이너리 하나로 다양한 서버 버전과 통신한다. -
pg_receivewal은 디스크 우선, 다음 슬롯, 그 다음 서버 위치 순으로 재개한다.StreamLog에서 검증:FindStreamingStart(디렉터리 스캔)를 먼저 시도하고,InvalidXLogRecPtr을 반환할 때만GetSlotInformation(PG15+)을 조회한 뒤IDENTIFY_SYSTEM플러시 위치로 폴백한다. 출력 세그먼트는mark_done = false와partial_suffix = ".partial"을 사용한다. -
pg_recvlogical은replication=database를, 나머지는replication=true를 사용한다.GetConnection에서 검증:values[i] = (dbname == NULL) ? "true" : "database";.pg_recvlogical만dbname을 설정하며,Assert(dbname == NULL || connection_string == NULL)이 상호 배제를 강제한다. 논리 디코딩은 데이터베이스에 연결된 walsender가 필요하므로 다른 모드를 쓴다.
미결 문제
섹션 제목: “미결 문제”-
WAL 자식과 파일 복사 부모 간 배압(backpressure). 자식은 부모가 익스트랙팅하는 동일한 basedir에 WAL을 스트리밍하지만, 둘은 별도 프로세스에 별도 연결을 가진다. 한쪽의 디스크 가득 참이나 느린 디스크가 다른 쪽에 어떤 영향을 미치는지(또는 부모의 최종
waitpid가 막힌 자식에 데드락될 수 있는지)는 여기서 추적하지 않는다. 조사 경로:kill_bgchild_atexit와 Windowsbgchild_exited플래그 처리. -
오류 시 astreamer 체인 해제 순서.
ReceiveArchiveStreamChunk는 새'n'아카이브가 시작될 때astreamer_finalize/astreamer_free를 호출하지만, 부분적으로 구성된 체인의 오류 경로 정리(예: 스트림 중간pg_fatal)는 프로세스 종료에 의존한다. 부분 출력 파일이 혼란스러운 상태로 남는지는 검토하지 않는다. 조사 경로:astreamer_*파일들(이 문서의 세 파일 READ 범위 밖). -
매니페스트 스트리밍과 별도 파일 경로의 상호작용. PG15+ 단일 스트림 경로에서 매니페스트는
'm'-태그 CopyData로 메모리 또는 파일에 버퍼링된 뒤 내구성 있게 rename된다. 표준 출력으로 쓰는 경우에는 tar에 주입된다. 매니페스트 버퍼의 정확한 크기 임계값(메모리 대 스필 파일)은 정량화하지 않는다. 조사 경로:ReceiveArchiveStreamChunk의'm'/'d'분기와ReceiveBackupManifest.
PostgreSQL 너머 — 비교 설계와 연구 지평
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 지평”포인터만 제시하고 분석은 생략한다. 각 항목은 후속 문서를 위한 시작 핸들이다.
-
pgBackRest / Barman / WAL-G. 대표적인 서드파티 PostgreSQL 백업 관리자들은
pg_basebackup이 의도적으로 제외한 기능을 추가한다. 백업 카탈로그, 보존 정책, 병렬 다중 스트림 파일 전송, 델타/증분 블록 백업, 오브젝트 스토리지 타겟(S3/GCS/Azure)이 그것이다. 이들은 보통pg_basebackup이나 동일한BASE_BACKUP프로토콜을 내부적으로 사용하거나 데이터 디렉터리를 직접 읽는다. 이들의 병렬성 모델과pg_basebackup의 단일 스트림 COPY를 비교하면 내장 도구의 처리량 상한을 정량화할 수 있다. -
서버 측 압축 알고리즘과 오프로드. PG15에서 서버 측 gzip/lz4/zstd가 추가되었다. 프런티어는 하드웨어 오프로드 압축(QAT/zstd 장거리)과 측정된 네트워크 대 CPU 여유를 기반으로 위치를 자동 선택하는 것이다.
astreamer체인 추상화는 새 코덱이 연결되는 바로 그 접합부다. -
증분 및 블록 레벨 백업. PG17의 증분 백업(
UPLOAD_MANIFEST+ WAL 요약,postgres-incremental-backup.md와postgres-archiving-walsummary.md참고)은 RMAN 스타일 블록 변경 추적에 대한 내장 답변이다.BaseBackup은 이미 이전 매니페스트를 업로드(UPLOAD_MANIFESTCOPY-in)하고INCREMENTAL옵션을 추가한다. 클라이언트 측은 작고, 흥미로운 기계장치는 서버 측 요약에 있다. -
논리 대 물리 백업 수렴.
pg_recvlogical은 논리 스트림을 소비하고,pg_dump는 논리 덤프를 생성한다(postgres-pg-dump-restore.md). 둘 다 PITR을 위한 물리 베이스 백업의 대체재는 아니지만, 논리 복제 슬롯 +pg_recvlogical로 지속적 논리 아카이빙이 가능하다. 연구 질문은 “변경 데이터 포착 + 물리 베이스라인”을 하나의 통합 도구로 대체할 수 있는지다. CockroachDB의 CDC + 전체 백업 모델이 하나의 존재 증명이다. -
백업 검증과
pg_verifybackup.pg_basebackup이 마지막에 내구성 있게 rename하는 백업 매니페스트(backup_manifest)는pg_verifybackup이 파일 존재와 체크섬 확인에 사용하고, 증분 기계장치가 이전 백업 참조로 사용한다. 매니페스트 형식과 검증 시맨틱은 별도 주제다.
참고 자료
섹션 제목: “참고 자료”PostgreSQL 문서
섹션 제목: “PostgreSQL 문서”- “pg_basebackup”, “pg_receivewal”, “pg_recvlogical” 레퍼런스 페이지 — 옵션 시맨틱, 포맷/압축 플래그, 슬롯 상호작용.
- “Streaming Replication Protocol” 장 —
IDENTIFY_SYSTEM,BASE_BACKUP,START_REPLICATION,CREATE_REPLICATION_SLOT,READ_REPLICATION_SLOT,UPLOAD_MANIFEST명령 문법. - “Continuous Archiving and Point-in-Time Recovery (PITR)” 장 — 백업 시작/종료 마커 + WAL 재생 모델.
PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)
섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)”src/bin/pg_basebackup/pg_basebackup.c—BaseBackup,main,StartLogStreamer,LogStreamerMain,reached_end_position,CreateBackupStreamer,ReceiveArchiveStream,ReceiveArchiveStreamChunk,ReceiveTarFile,ReceiveCopyData,progress_report,verify_dir_is_empty_or_create.src/bin/pg_basebackup/streamutil.c—GetConnection,RunIdentifySystem,GetSlotInformation,CreateReplicationSlot,Append{Plain,String,Integer}CommandOption,CheckServerVersionForStreaming.src/bin/pg_basebackup/pg_receivewal.c—StreamLog,FindStreamingStart,stop_streaming,get_destination_dir.src/bin/pg_basebackup/pg_recvlogical.c—StreamLogicalLog,prepareToTerminate.src/bin/pg_basebackup/receivelog.c—ReceiveXlogStream(공유 물리 스트리밍 엔진;postgres-wal-sender-receiver.md에 상세 기술).src/fe_utils/recovery_gen.c—GenerateRecoveryConfig.
교과서 장 (knowledge/research/dbms-general/ 수록)
섹션 제목: “교과서 장 (knowledge/research/dbms-general/ 수록)”- Database System Concepts (Silberschatz et al.), 복구 장 — 퍼지 스냅샷, 체크포인트에서 재실행, 백업 및 복원 모델.
- Database Internals (Petrov), Part II — 로그 구조 복구와 로그 테일이 백업 창에 걸치는 논리.
상호 참조 (형제 모듈 문서)
섹션 제목: “상호 참조 (형제 모듈 문서)”postgres-backup-basebackup.md— 서버 측:basebackup.c가 시작 체크포인트를 실행하고 tar/COPY 스트림을 구성하며 시작/종료 마커를 기록하는 방식. 서버 측 세부 사항은 전부 이 문서로 위임한다.postgres-wal-sender-receiver.md— WAL 송신자 프로토콜과 WAL 자식 및pg_receivewal이 재사용하는ReceiveXlogStream/receivelog.c클라이언트 엔진.postgres-wire-protocol.md— 세 도구 모두가 진입하는 FE/BE 프레이밍과 복제 시작 옵션.postgres-replication-slots.md—CreateReplicationSlot/GetSlotInformation뒤의 슬롯 생명주기.postgres-incremental-backup.md,postgres-archiving-walsummary.md—UPLOAD_MANIFEST/INCREMENTAL경로와 WAL 요약.postgres-logical-decoding.md,postgres-pgoutput.md—pg_recvlogical이 소비하는 것.postgres-pg-dump-restore.md— 물리 베이스 백업의 대안인 논리 덤프.