콘텐츠로 이동

(KO) PostgreSQL pg_rewind — 타임라인 분기 감지와 데이터 디렉터리 재동기화

목차:

고가용성(HA, High Availability) 환경의 DBMS는 뒤처지거나 분기된 노드를 재합류시키는 능력에 의존한다. 문제는 같은 체크포인트에서 출발한 두 노드가 네트워크 파티션, 잘못 펜싱된 프라이머리, 또는 계획된 스위치오버 실패 이후 각자 쓰기를 받아들일 수 있다는 데 있다. 상황이 정리되면 한 노드는 권위 있는 히스토리를 보유하고, 다른 노드는 새 타임라인과 절대 합쳐질 수 없는 분기된 히스토리를 보유하게 된다. 분기된 히스토리는 반드시 버려야 한다.

PostgreSQL은 WAL(Write-Ahead Log) 스트림을 **타임라인(timeline)**의 선형 시퀀스로 표현한다. 프로모션이 일어날 때마다 타임라인 ID(TLI)가 증가하고, 포크 지점이 .history 파일에 기록된다. <TLI>.history는 각 선행 TLI와 그 타임라인이 끝난 LSN(Log Sequence Number)을 매핑한다. 이 파일을 파싱해 구성한 TimeLineHistoryEntry 배열이 “타임라인 A와 B는 어느 LSN에서 마지막으로 동일한 WAL 바이트를 공유했는가?”라는 질문에 답한다.

기본 관계는 다음과 같다:

target LSN stream: ... [common ancestor checkpoint C] ---> [diverged WAL D]
source LSN stream: ... [common ancestor checkpoint C] ---> [new WAL after promotion]

분기 지점 이후 두 스트림은 독립적이다. 재합류 작업은 다음 네 단계를 수행해야 한다:

  1. C를 식별한다 — 분기 이전의 마지막 공통 체크포인트.
  2. 타깃이 영역 D(분기된 WAL)에서 수정한 데이터 페이지 집합을 파악한다. 해당 페이지들을 소스에서 덮어써야 한다.
  3. 필요한 파일과 페이지를 소스에서 타깃으로 복사한다.
  4. 타깃이 WAL 리플레이를 어디서 시작해야 하는지 알려 주는 마커를 남긴다. 그래야 다음 시작 시 남은 간격이 메워진다.

이 작업은 크래시 복구(단일 노드의 자체 WAL을 리플레이)나 스트리밍 리플리케이션(타깃이 분기된 적 없다는 전제)과 다르다. pg_rewind는 그 둘 사이의 공백을 채운다. 분기됐지만 그 외에는 정상인 노드를, 전체 pg_basebackup보다 낮은 비용으로 최신 상태로 만든다.

pg_basebackup을 다시 쓰지 않는 이유

섹션 제목: “pg_basebackup을 다시 쓰지 않는 이유”

전체 베이스 백업은 데이터 디렉터리의 모든 바이트를 복사하는데, 보통 수십에서 수백 기가바이트에 달한다. pg_rewind는 타깃이 분기 이후에 더럽힌 페이지와 내용이 달라진 비관계 파일만 복사한다. 분기가 최근(수 분에서 수 시간)인 노드라면 전송량이 수 배 이상 줄어들고, 전 프라이머리는 전체 재시딩 비용 없이 스탠바이로 재합류할 수 있다.

트레이드오프도 있다. pg_rewind는 마지막 공통 체크포인트 이후의 WAL이 타깃에 존재하거나 아카이브에서 복구 가능해야 한다. 타깃이 체크섬 또는 wal_log_hints = on을 사용하는 것도 전제 조건이다. 이 조건 없이는 힌트 비트 갱신이 WAL에서 보이지 않아 수정된 페이지를 놓친다.

pg_rewind는 WAL 리플레이를 직접 수행하지 않는다. 체크포인트 C에서 시작한 베이스 백업처럼 보이는 상태를 타깃에 남긴다. 컨트롤 파일의 minRecoveryPoint를 소스의 현재 WAL 끝으로 설정하고, backup_label 파일이 리플레이 시작점으로 체크포인트 C를 가리킨다. 이후 일반적인 PostgreSQL 복구(startup 프로세스)가 C부터 소스의 WAL을 리플레이해 간격을 메운다. pg_basebackup --wal-method=stream과 동일한 모델이다. 백업 도구가 파일을 전송하고, 복구가 WAL 작업을 담당한다.

대부분의 리플리케이션 도구는 최소한 두 가지 데이터 소스를 다룬다. 실행 중인 라이브 서버와 로컬 디렉터리다. 보편적인 패턴은 그 차이를 감싸는 얇은 vtable(또는 인터페이스)이다. 호출자는 “파일 목록 조회”, “이 바이트 범위 가져오기”, “현재 WAL 위치 얻기” 같은 연산만 본다. 요청을 libpq를 통한 COPY 쿼리로 처리할지, 직접 파일 I/O로 처리할지는 백엔드가 결정한다. PostgreSQL은 rewind_source에서 이 패턴을 사용한다.

WAL 스캔을 통한 더티 페이지 추적

섹션 제목: “WAL 스캔을 통한 더티 페이지 추적”

부분 재동기화를 가능하게 하는 핵심 통찰이다. 타깃 자체의 WAL 레코드가 분기 이후에 어떤 페이지를 건드렸는지 정확히 기록한다. C부터 타깃의 WAL 끝까지 읽으면 (relfilelocator, forknum, blockno) 튜플의 완전한 집합을 얻는다. 이 블록들이 덮어써야 할 정확한 대상이다. 전체 데이터 디렉터리의 블록 수준 체크섬 비교는 불필요하다. 그 방법은 양쪽의 모든 페이지를 읽는다.

재동기화 도구는 데이터 디렉터리의 각 경로마다 고정된 액션 집합 중 하나를 결정한다. 파일 전체 복사, 꼬리 확장 복사, 잘라내기, 제거, 그대로 두기다. 관계 데이터 파일과 비관계 파일은 다르게 처리된다. WAL 추적은 관계 파일의 메인 포크에만 사용 가능하므로, 그 외의 모든 것은 내용이 달라지면 전체를 복사한다.

파일 액션을 잘못된 순서로 적용하면 디렉터리가 복구 불가능한 상태가 될 수 있다. 보편적으로 안전한 순서는 다음과 같다. 디렉터리를 먼저 만들고 내용을 쓴다. 이전 것을 지우기 전에 파일을 복사한다. 부모 디렉터리 전에 자식 항목을 지운다. 액션 enum 값 자체에 순서를 인코딩해 두면, 값 기준 정렬이 올바른 실행 시퀀스를 만든다는 점에서 깔끔한 구현 기법이다.

pg_rewind는 독립적인 클라이언트 측 바이너리(src/bin/pg_rewind/)다. 서버 백엔드에 링크하지 않는다. 라이브 소스 경로에는 libpq를 사용하고, WAL 읽기와 타임라인 히스토리 파싱 같은 소수의 백엔드 함수는 순수 프론트엔드 코드로 재구현한다. 메인 드라이버는 pg_rewind.c이고, 네 개의 지원 모듈이 있다. parsexlog.c(WAL 스캔), filemap.c(파일 결정 테이블), file_ops.c(타깃 측 I/O), timeline.c(히스토리 파싱)이다.

// main — src/bin/pg_rewind/pg_rewind.c
main()
├── init_libpq_source / init_local_source ← source vtable
├── ensureCleanShutdown ← single-user postgres if dirty
├── sanityChecks ← system_identifier, checksums
├── getTimelineHistory × 2 ← source + target TLI arrays
├── findCommonAncestorTimeline ← → divergerec
├── findLastCheckpoint ← → chkptrec / chkptredo
├── filehash_init + traverse_files × 2 ← populate hash
├── extractPageMap ← WAL scan → dirty-page bitmaps
├── decide_file_actions → filemap_t ← per-file action enum
└── perform_rewind ← copy + backup_label + control file

1단계 — 사전 조건 및 타임라인 분기

섹션 제목: “1단계 — 사전 조건 및 타임라인 분기”

main()은 먼저 타깃이 깨끗하게 종료됐는지 확인한다. 컨트롤 파일에 state != DB_SHUTDOWNED가 표시되고 --no-ensure-shutdown이 설정되지 않은 경우, ensureCleanShutdownpostgres --single을 실행해 크래시 복구를 완료한 뒤 pg_rewind가 작업을 시작한다. 깨끗하지 않은 타깃은 비일관된 페이지 상태를 가질 수 있어 이후 WAL 스캔을 혼란스럽게 만든다.

sanityChecks는 세 가지 불변식을 검증한다:

  1. system_identifier 일치 — 소스와 타깃이 같은 initdb 클러스터에서 유래했다.
  2. pg_control_versioncatalog_version_no가 컴파일된 상수와 일치 — 같은 PostgreSQL 메이저 버전.
  3. 타깃이 체크섬 또는 wal_log_hints를 사용한다 — 없으면 힌트 비트 갱신이 WAL에서 보이지 않아 pg_rewind가 수정된 페이지를 놓친다.
// sanityChecks — src/bin/pg_rewind/pg_rewind.c
if (ControlFile_target.system_identifier != ControlFile_source.system_identifier)
pg_fatal("source and target clusters are from different systems");
if (ControlFile_target.data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
!ControlFile_target.wal_log_hints)
pg_fatal("target server needs to use either data checksums or "
"\"wal_log_hints = on\"");

타임라인 히스토리는 getTimelineHistory로 양쪽에서 가져온다. TLI 1(히스토리 파일 없음)은 단일 항목 배열을 합성해 반환한다. 더 높은 TLI는 해당 소스에서 .history 파일을 가져와 rewind_parseTimeLineHistoryTimeLineHistoryEntry[] 배열로 파싱한다.

findCommonAncestorTimeline은 두 배열을 병렬로 탐색해 첫 번째 tli 또는 begin 불일치에서 멈춘다. 그 인덱스에서 두 end 필드의 MinXLogRecPtrdivergerec다. 히스토리가 갈라지는 LSN이다.

// findCommonAncestorTimeline — src/bin/pg_rewind/pg_rewind.c
for (i = 0; i < n; i++)
{
if (a_history[i].tli != b_history[i].tli ||
a_history[i].begin != b_history[i].begin)
break;
}
if (i > 0)
{
i--;
*recptr = MinXLogRecPtr(a_history[i].end, b_history[i].end);
*tliIndex = i;
return;
}

target_wal_endrec <= divergerec이면 타깃이 이미 소스의 선조다. 포크 지점을 넘어서 쓴 적이 없으므로 리와인드가 필요 없다. target_wal_endrec > divergerec이면 리와인드가 필요하다.

2단계 — 마지막 공통 체크포인트

섹션 제목: “2단계 — 마지막 공통 체크포인트”

findLastCheckpoint(parsexlog.c)는 divergerec에서 거꾸로 타깃의 WAL을 탐색해, 포크 이전의 가장 최근 XLOG_CHECKPOINT_SHUTDOWN 또는 XLOG_CHECKPOINT_ONLINE 레코드를 찾는다. 이것이 리와인드 시작점 chkptrec이고, 체크포인트의 redo 필드가 chkptredo를 준다.

역방향 탐색이 올바른 이유가 있다. pg_rewind는 진행 중인 트랜잭션에 관심이 없다. WAL 리플레이가 일관된 상태를 재구성할 수 있는 체크포인트만 있으면 된다. 역방향 탐색 중 체크포인트를 포함하는 WAL 세그먼트 파일도 추적한다. 파일 조정 단계에서 실수로 제거되지 않도록 keepwal 해시에 추가해 둔다.

// findLastCheckpoint — src/bin/pg_rewind/parsexlog.c
info = XLogRecGetInfo(xlogreader) & ~XLR_INFO_MASK;
if (searchptr < forkptr &&
XLogRecGetRmid(xlogreader) == RM_XLOG_ID &&
(info == XLOG_CHECKPOINT_SHUTDOWN ||
info == XLOG_CHECKPOINT_ONLINE))
{
memcpy(&checkPoint, XLogRecGetData(xlogreader), sizeof(CheckPoint));
*lastchkptrec = searchptr;
*lastchkpttli = checkPoint.ThisTimeLineID;
*lastchkptredo = checkPoint.redo;
break;
}
/* Walk backwards to previous record. */
searchptr = record->xl_prev;

3단계 — 파일 목록 및 WAL 페이지 맵

섹션 제목: “3단계 — 파일 목록 및 WAL 페이지 맵”

traverse_files는 소스를(rewind_source vtable 경유), traverse_datadir는 타깃을 대상으로 호출된다. 각각 process_source_fileprocess_target_file 콜백으로 연결되며, 두 콜백 모두 상대 경로를 키로 하는 filehash 해시 테이블에 file_entry_t 레코드를 삽입하거나 갱신한다.

// process_source_file — src/bin/pg_rewind/filemap.c
entry = insert_filehash_entry(path);
entry->source_exists = true;
entry->source_type = type;
entry->source_size = size;
entry->source_link_target = link_target ? pg_strdup(link_target) : NULL;

extractPageMapXLogReaderAllocate / XLogReadRecord를 사용해 chkptrec부터 target_wal_endrec까지 타깃의 WAL을 읽는다. 디코딩된 레코드마다 extractPageInfo를 호출하고, 이것이 레코드의 블록 참조를 순회하며 MAIN_FORKNUM 블록 각각에 process_target_wal_block_change를 호출한다.

// extractPageInfo — src/bin/pg_rewind/parsexlog.c
for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++)
{
if (!XLogRecGetBlockTagExtended(record, block_id,
&rlocator, &forknum, &blkno, NULL))
continue;
if (forknum != MAIN_FORKNUM)
continue;
process_target_wal_block_change(forknum, rlocator, blkno);
}

process_target_wal_block_change는 해당 데이터 세그먼트의 파일 항목을 조회하고, blkno_inseg를 항목의 target_pages_to_overwrite 비트맵에 추가한다. 단, 블록이 소스 파일의 범위 안에 있을 때만 추가한다(end_offset <= source_size). 소스 EOF를 넘어선 블록은 어차피 잘릴 것이므로 가져올 필요가 없다.

// process_target_wal_block_change — src/bin/pg_rewind/filemap.c
end_offset = (blkno_inseg + 1) * BLCKSZ;
if (end_offset <= entry->source_size && end_offset <= entry->target_size)
datapagemap_add(&entry->target_pages_to_overwrite, blkno_inseg);

datapagemap_t는 간결한 바이트 비트맵이다. 비트 N이 세그먼트 내 블록 N을 나타낸다. 비트맵은 작은 여유 공간을 두고 필요 시 동적으로 늘어나므로, 순차 블록 삽입 시 블록마다 재할당이 일어나지 않는다.

decide_file_actions()가 해시 테이블을 순회하며 각 항목마다 decide_file_action()을 호출한다. 결정 논리는 다음과 같다:

조건액션
경로가 excludeFiles 목록에 해당REMOVE (타깃 전용) 또는 NONE
소스에만 있음 (소스에 새로 생김)COPY 또는 CREATE (디렉터리/심링크)
타깃에만 있음 (소스에서 삭제됨)REMOVE (keepwal에 없으면)
양쪽 존재, 비관계 파일COPY
양쪽 존재, 관계 파일, 타깃이 더 큼TRUNCATE
양쪽 존재, 관계 파일, 타깃이 더 작음COPY_TAIL
양쪽 존재, 관계 파일, 크기 같음NONE (더티 페이지는 별도 처리)

특별 처리 경로로는 XLOG_CONTROL_FILE(perform_rewind 끝으로 연기), PG_VERSION(덮어쓰지 않음), .DS_Store, 양쪽에 존재하는 디렉터리/심링크(항목 자체에 액션 불필요)가 있다.

// decide_file_action — src/bin/pg_rewind/filemap.c
if (strcmp(path, XLOG_CONTROL_FILE) == 0)
return FILE_ACTION_NONE; /* handled separately at end */
if (!entry->target_exists && entry->source_exists)
{
switch (entry->source_type)
{
case FILE_TYPE_DIRECTORY: return FILE_ACTION_CREATE;
case FILE_TYPE_REGULAR: return FILE_ACTION_COPY;
// ... symlink → CREATE
}
}
else if (entry->target_exists && !entry->source_exists)
{
if (keepwal_entry_exists(path))
return FILE_ACTION_NONE; /* WAL needed for recovery */
return FILE_ACTION_REMOVE;
}

양쪽에 모두 존재하는 관계 데이터 파일은 두 세그먼트 크기를 비교해 액션을 결정한다. 타깃이 더 크면 소스가 잘라낸 블록이 추가로 있다는 뜻이므로 지금 잘라낸다. 타깃이 더 작으면 소스에 있는 꼬리 부분이 없다는 뜻이므로 꼬리를 복사한다. 크기가 같으면 파일 전체 작업은 필요 없다. 블록 수준 변경 사항은 이미 WAL 스캔에서 target_pages_to_overwrite 비트맵에 담겼기 때문이다.

// decide_file_action — src/bin/pg_rewind/filemap.c
case FILE_TYPE_REGULAR:
if (!entry->isrelfile)
return FILE_ACTION_COPY; /* non-data file: copy in toto */
else
{
if (entry->target_size < entry->source_size)
return FILE_ACTION_COPY_TAIL; /* fetch the missing tail */
else if (entry->target_size > entry->source_size)
return FILE_ACTION_TRUNCATE; /* drop the extra blocks */
else
return FILE_ACTION_NONE; /* dirty pages handled via bitmap */
}

file_action_t enum은 의도적으로 qsort 정렬이 안전한 실행 시퀀스를 만들도록 순서가 정해져 있다. CREATE 먼저(자식을 쓰기 전에 부모 디렉터리 존재 보장), 그 다음 COPY/COPY_TAIL, NONE, TRUNCATE, REMOVE 순이다. REMOVE 항목은 foo/barfoo보다 먼저 제거되도록 경로 역순으로 2차 정렬한다.

5단계 — perform_rewind: 복사 및 컨트롤 파일 업데이트

섹션 제목: “5단계 — perform_rewind: 복사 및 컨트롤 파일 업데이트”

perform_rewind는 정렬된 filemap_t를 순회한다. target_pages_to_overwrite 비트맵이 비어 있지 않은 항목은 비트맵을 순회하며 더티 블록마다 source->queue_fetch_range를 호출한다. 그런 다음 파일별 액션을 디스패치한다. COPY는 queue_fetch_file, COPY_TAIL은 꼬리 범위의 queue_fetch_range, TRUNCATE는 truncate_target_file, REMOVE는 remove_target, CREATE는 create_target이다.

// perform_rewind — src/bin/pg_rewind/pg_rewind.c
iter = datapagemap_iterate(&entry->target_pages_to_overwrite);
while (datapagemap_next(iter, &blkno))
{
offset = blkno * BLCKSZ;
source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
}
// ... then switch(entry->action) for whole-file operations
source->finish_fetch(source);

모든 파일 작업 후, pg_rewind는 소스의 컨트롤 파일을 다시 가져와 ControlFile_new를 구성한다:

  • stateDB_IN_ARCHIVE_RECOVERY로 설정한다.
  • minRecoveryPoint를 소스의 현재 WAL 삽입 LSN(라이브 프라이머리 소스) 또는 최신 체크포인트(로컬 디렉터리 소스)로 설정한다. 타깃이 이 지점까지 리플레이해야 연결을 수락할 수 있다.
  • minRecoveryPointTLI를 소스의 TLI로 설정한다.

backup_label 파일에 chkptredoSTART WAL LOCATION으로 기록한다. 시작 프로세스는 이 정보를 바탕으로 마지막 공통 체크포인트부터 WAL 리플레이를 시작한다. pg_rewind가 그 지점에서 시작한 베이스 백업인 것처럼 처리되는 방식이다.

// createBackupLabel — src/bin/pg_rewind/pg_rewind.c
len = snprintf(buf, sizeof(buf),
"START WAL LOCATION: %X/%X (file %s)\n"
"CHECKPOINT LOCATION: %X/%X\n"
"BACKUP METHOD: pg_rewind\n"
"BACKUP FROM: standby\n"
"START TIME: %s\n",
LSN_FORMAT_ARGS(startpoint), xlogfilename,
LSN_FORMAT_ARGS(checkpointloc),
strfbuf);

rewind_source 구조체는 일곱 개의 함수 포인터를 가진 C vtable이다:

// rewind_source — src/bin/pg_rewind/rewind_source.h
typedef struct rewind_source
{
void (*traverse_files)(struct rewind_source *,
process_file_callback_t callback);
char *(*fetch_file)(struct rewind_source *, const char *path,
size_t *filesize);
void (*queue_fetch_range)(struct rewind_source *, const char *path,
off_t offset, size_t len);
void (*queue_fetch_file)(struct rewind_source *, const char *path,
size_t len);
void (*finish_fetch)(struct rewind_source *);
XLogRecPtr (*get_current_wal_insert_lsn)(struct rewind_source *);
void (*destroy)(struct rewind_source *);
} rewind_source;

init_libpq_source(libpq_source.c)는 라이브 libpq 연결을 통한 COPY 쿼리와 pg_read_binary_file() 호출로 vtable을 구현한다. 범위 페치 요청을 일괄 처리하고 finish_fetch에서 플러시한다. init_local_source(local_source.c)는 직접 파일 시스템 호출로 구현한다.

두 백엔드 모두 호출자에게 투명하다. 위의 모든 단계는 vtable 인터페이스만 사용한다.

filemap.c는 두 개의 제외 목록을 관리한다. excludeDirContents는 서버 시작 시 항상 재생성되는 디렉터리 내용을 열거한다(pg_stat_tmp, pg_replslot, pg_dynshmem, pg_notify, pg_serial, pg_snapshots, pg_subtrans). excludeFiles는 복사하지 않아야 할 특정 파일명을 열거한다(postmaster.pid, postmaster.opts, backup_label, backup_manifest, pg_internal.init 접두사, postgresql.auto.conf.tmp, current_logfiles.tmp). 두 목록은 basebackup.c와 동기화를 유지하도록 문서에 명시돼 있다.

// excludeDirContents — src/bin/pg_rewind/filemap.c
static const char *const excludeDirContents[] = {
"pg_stat_tmp", "pg_replslot", "pg_dynshmem",
"pg_notify", "pg_serial", "pg_snapshots", "pg_subtrans",
NULL
};
flowchart TD
    A["main()<br/>인수 파싱, 연결"] --> B["ensureCleanShutdown<br/>(더티이면 postgres --single)"]
    B --> C["sanityChecks<br/>system_identifier<br/>체크섬/wal_log_hints"]
    C --> D["getTimelineHistory<br/>소스 + 타깃"]
    D --> E["findCommonAncestorTimeline<br/>→ divergerec"]
    E --> F{리와인드<br/>필요?}
    F -- 아니요 --> Z["exit 0"]
    F -- 예 --> G["findLastCheckpoint<br/>→ chkptrec / chkptredo"]
    G --> H["traverse_files 소스<br/>+ traverse_datadir 타깃<br/>→ filehash 구성"]
    H --> I["extractPageMap<br/>WAL chkptrec → 분기 끝<br/>→ 더티 페이지 비트맵"]
    I --> J["decide_file_actions<br/>→ 정렬된 filemap_t"]
    J --> K["perform_rewind<br/>queue_fetch_range 더티 블록<br/>+ 파일 수준 액션"]
    K --> L["createBackupLabel<br/>update_controlfile<br/>minRecoveryPoint = 소스 WAL 끝"]
    L --> M["sync_target_dir<br/>완료"]

분기 감지 및 변경 블록 복사 흐름

섹션 제목: “분기 감지 및 변경 블록 복사 흐름”

첫 번째 다이어그램은 최상위 단계 시퀀스를 보여 준다. 이 두 번째 다이어그램은 분석 핵심부를 확대한다. 두 타임라인 히스토리가 어떻게 divergerec를 산출하는지, 역방향 체크포인트 탐색이 어떻게 chkptrec를 산출하는지, 그리고 chkptrec부터 시작하는 순방향 WAL 스캔이 수정된 MAIN_FORKNUM 블록 각각을 어떻게 큐에 올라갈 페치 또는 폐기로 처리하는지를 보여 준다. 블록은 소스 파일과 타깃 파일 범위 모두에 속할 때만 덮어쓰기 비트맵에 추가된다. 소스 EOF를 넘어선 블록은 블록 단위 페치가 아닌, 이후 TRUNCATE/COPY_TAIL 전체 파일 결정으로 처리된다.

flowchart TD
    A["소스 history[]<br/>타깃 history[]"] --> B["findCommonAncestorTimeline<br/>tli/begin 불일치까지 병렬 탐색"]
    B --> C["divergerec =<br/>MinXLogRecPtr(end_a, end_b)"]
    C --> D{"target_wal_endrec<br/>&gt; divergerec ?"}
    D -- 아니요 --> E["타깃이 선조<br/>리와인드 불필요"]
    D -- 예 --> F["findLastCheckpoint<br/>divergerec부터 WAL 역방향 탐색"]
    F --> G{"RM_XLOG_ID 레코드 &&<br/>SHUTDOWN 또는 ONLINE<br/>&& searchptr &lt; forkptr ?"}
    G -- 아니요 --> H["searchptr = record-&gt;xl_prev<br/>세그먼트에 keepwal_add_entry"]
    H --> F
    G -- 예 --> I["chkptrec = searchptr<br/>chkptredo = checkPoint.redo"]
    I --> J["extractPageMap<br/>XLogReadRecord chkptrec .. endpoint"]
    J --> K["extractPageInfo<br/>block_id 각각 순회"]
    K --> L{"forknum ==<br/>MAIN_FORKNUM ?"}
    L -- 아니요 --> M["건너뜀<br/>FSM/VM/init는 전체 복사"]
    L -- 예 --> N["process_target_wal_block_change"]
    N --> O{"end_offset &lt;= source_size<br/>&& &lt;= target_size ?"}
    O -- 아니요 --> P["무시<br/>이후 truncate/remove 처리"]
    O -- 예 --> Q["datapagemap_add(blkno_inseg)<br/>queue_fetch_range 큐에 추가"]

pg_rewind.c — 메인 오케스트레이터

섹션 제목: “pg_rewind.c — 메인 오케스트레이터”
심벌역할
main최상위: 인수 파싱, 소스 초기화, 단계 순서 제어
perform_rewindfilemap 액션 실행; backup_label과 컨트롤 파일 기록
sanityCheckssystem_identifier, 버전, 체크섬/wal_log_hints 검증
getTimelineHistory.history 파일 가져오기 및 rewind_parseTimeLineHistory 래핑
findCommonAncestorTimelineTimeLineHistoryEntry[] 배열을 병렬 탐색해 divergerec 찾기
ensureCleanShutdown더티 타깃에 크래시 복구 강제 수행을 위한 postgres --single 실행
createBackupLabelSTART WAL LOCATION / CHECKPOINT LOCATION 마커 기록
digestControlFileControlFileData 버퍼 읽기 및 CRC 검사
getRestoreCommandpostgres -C restore_command 호출로 아카이브 복원 명령 획득
progress_report초당 한 번 스로틀된 stderr 진행 출력
ControlFile_target전역: 시작 시 읽은 타깃의 pg_control
ControlFile_source전역: 시작 시 읽은 소스의 pg_control
ControlFile_source_after전역: 파일 복사 후 다시 읽은 소스의 pg_control (정상성 검사)
targetHistory / targetNentries전역: 타깃 타임라인 히스토리 배열 (parsexlog.c에서 사용)
심벌역할
extractPageMapchkptrec부터 끝점까지 XLogReaderAllocate / XLogReadRecord 루프 구동
extractPageInfo디코딩된 WAL 레코드 하나의 블록 참조 순회; process_target_wal_block_change 호출
findLastCheckpointdivergerec 이전의 가장 최근 체크포인트를 찾기 위한 역방향 WAL 스캔
readOneRecord주어진 LSN에서 레코드 하나 읽기; target_wal_endrec 탐색에 사용
SimpleXLogPageReadXLogReader 페이지 읽기 콜백; 세그먼트 전환 및 아카이브 복원 처리
XLogPageReadPrivate리더별 상태: tliIndexrestoreCommand
심벌역할
filehash상대 경로를 키로 하는 simplehash 기반 해시 테이블; file_entry_t 레코드 보유
file_entry_t경로별 레코드: 소스/타깃 크기, 타입, 링크 타깃, 더티 페이지 비트맵, 액션
filemap_t실행 준비가 된 file_entry_t *의 정렬된 최종 배열
file_action_tenum: UNDECIDED / CREATE / COPY / COPY_TAIL / NONE / TRUNCATE / REMOVE
filehash_init해시 테이블 할당
process_source_file콜백: filehash에 소스 측 파일 메타데이터 기록
process_target_file콜백: filehash에 타깃 측 파일 메타데이터 기록
process_target_wal_block_change콜백: 파일 항목의 target_pages_to_overwrite 비트맵에 블록 하나 추가
decide_file_actions각 항목에 decide_file_action 호출; filemap_t로 정렬
decide_file_action항목별 논리: 크기 비교, 제외 필터, keepwal 검사
keepwalWAL 세그먼트 파일을 제거로부터 보호하는 보조 해시 테이블
keepwal_init / keepwal_add_entrykeepwal 테이블 초기화 및 구성
check_file_excludedexcludeFilesexcludeDirContents 목록 검사
isRelDataFile경로 정규식: global/<oid>, base/<db>/<oid>, pg_tblspc/... 패턴 감지
calculate_totals진행 보고를 위한 fetch_sizetotal_size 합산
final_filemap_cmp정렬 비교자: 액션 enum 순서 후 경로; REMOVE 항목은 역순
심벌역할
datapagemap_t구조체: bitmap 바이트 배열 + bitmapsize
datapagemap_add블록 번호 하나의 비트 설정; 필요 시 배열 확장
datapagemap_iterate블록 0에서 이터레이터 할당
datapagemap_next이터레이터 전진; 다음으로 설정된 블록 번호 반환
심벌역할
open_target_file / close_target_file현재 열린 타깃 파일 디스크립터(dstfd) 관리
write_target_range바이트 범위 시크 및 쓰기; 진행을 위해 fetch_done 갱신
remove_target / create_target타입별 헬퍼(파일/디렉터리/심링크)로 디스패치
truncate_target_file타깃 경로에 ftruncate 호출
slurpFile파일 전체를 malloc 버퍼로 읽기 (pg_control, 히스토리 파일에 사용)
traverse_datadir재귀 디렉터리 탐색기; 각 항목에 process_file_callback_t 호출
sync_target_dir전체 데이터 디렉터리의 2패스 fsync를 위한 sync_pgdata 호출
심벌역할
rewind_parseTimeLineHistory.history 파일 버퍼를 TimeLineHistoryEntry[] 배열로 파싱; 타깃 TLI의 팁 항목 추가

REL_18_STABLE 커밋 273fe94 기준 위치 힌트. 심벌이 안정적인 기준점이고 줄 번호는 점차 달라지는 참고값이다.

심벌파일대략적 줄
mainsrc/bin/pg_rewind/pg_rewind.c120
perform_rewindsrc/bin/pg_rewind/pg_rewind.c554
sanityCheckssrc/bin/pg_rewind/pg_rewind.c734
findCommonAncestorTimelinesrc/bin/pg_rewind/pg_rewind.c921
createBackupLabelsrc/bin/pg_rewind/pg_rewind.c963
ensureCleanShutdownsrc/bin/pg_rewind/pg_rewind.c1130
getRestoreCommandsrc/bin/pg_rewind/pg_rewind.c1057
digestControlFilesrc/bin/pg_rewind/pg_rewind.c1024
extractPageMapsrc/bin/pg_rewind/parsexlog.c65
extractPageInfosrc/bin/pg_rewind/parsexlog.c388
findLastCheckpointsrc/bin/pg_rewind/parsexlog.c167
readOneRecordsrc/bin/pg_rewind/parsexlog.c123
SimpleXLogPageReadsrc/bin/pg_rewind/parsexlog.c275
filehash_initsrc/bin/pg_rewind/filemap.c196
process_source_filesrc/bin/pg_rewind/filemap.c279
process_target_filesrc/bin/pg_rewind/filemap.c315
process_target_wal_block_changesrc/bin/pg_rewind/filemap.c353
decide_file_actionssrc/bin/pg_rewind/filemap.c860
decide_file_actionsrc/bin/pg_rewind/filemap.c699
final_filemap_cmpsrc/bin/pg_rewind/filemap.c679
isRelDataFilesrc/bin/pg_rewind/filemap.c570
keepwal_initsrc/bin/pg_rewind/filemap.c242
datapagemap_addsrc/bin/pg_rewind/datapagemap.c31
datapagemap_iteratesrc/bin/pg_rewind/datapagemap.c74
datapagemap_nextsrc/bin/pg_rewind/datapagemap.c87
traverse_datadirsrc/bin/pg_rewind/file_ops.c384
sync_target_dirsrc/bin/pg_rewind/file_ops.c318
slurpFilesrc/bin/pg_rewind/file_ops.c337
rewind_parseTimeLineHistorysrc/bin/pg_rewind/timeline.c28
rewind_source (struct)src/bin/pg_rewind/rewind_source.h23
file_entry_t (struct)src/bin/pg_rewind/filemap.h49
filemap_t (struct)src/bin/pg_rewind/filemap.h89
file_action_t (enum)src/bin/pg_rewind/filemap.h16
datapagemap_t (struct)src/bin/pg_rewind/datapagemap.h

PostgreSQL 너머 — 비교 설계와 연구 최전선

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 최전선”

MySQL의 고가용성 도구는 완전한 재시딩에 mysqldump 또는 Percona XtraBackup을 사용한다. GTID 기반 리플리케이션에서 분기된 노드를 전체 백업 없이 재합류시키려면, 누락된 트랜잭션 전부가 소스의 바이너리 로그에 남아 있어야 한다. pg_rewind의 WAL 스캔 기반 부분 페이지 재동기화에 해당하는 기능은 없다.

가장 널리 배포된 PostgreSQL HA 스택인 Patroni는 페일오버 후 전 프라이머리가 분기된 것으로 감지되면 자동으로 pg_rewind를 호출한다. 호출 전에 전 프라이머리가 깨끗하게 종료됐는지(ensureCleanShutdown에 대응) 확인하고, 새 프라이머리가 wal_log_hints = on 또는 체크섬을 사용하는지도 확인한다. 대규모 클러스터에서 전체 pg_basebackup 재시딩이 너무 느렸기 때문에 use_pg_rewind 설정 옵션이 추가됐다.

pg_rewind의 알고리즘적 핵심인 두 타임라인 히스토리 배열을 비교해 분기 LSN을 찾는 작업은, 분산 시스템에서 연구된 더 일반적인 문제의 특수 사례다. 분기하는 두 로그 시퀀스에서 마지막 공통 접두사를 찾는 문제다. Raft와 Paxos 기반 리플리케이션(Ongaro & Ousterhout, In Search of an Understandable Consensus Algorithm, USENIX ATC 2014 참조)에서는 nextIndex / matchIndex 프로토콜로 해결한다. PostgreSQL의 접근 방식이 더 단순한 이유는 두 가지다. 타임라인은 엄격한 트리 구조여서 동시 포크가 없다. 분기 지점이 논리적 term+index 튜플이 아닌 물리적 바이트 오프셋이다.

PostgreSQL 17은 WAL 요약화(pg_wal_summarize)와 pg_basebackup --incremental 모드를 도입했다. 두 기능 모두 마지막 백업 이후 어떤 페이지가 변경됐는지 파악하기 위해 블록 수준 변경 추적 파일(.walsumm 파일, pg_wal/summaries/)을 사용한다. pg_rewind가 WAL 스캔으로 해결하는 것과 정확히 같은 문제다. 증분 백업 방식은 백업 시점의 WAL 스캔 지연을 피할 수 있지만, 마지막 전체 백업 이후 WAL 요약화가 계속 실행되고 있어야 한다. pg_rewind의 WAL 스캔 방식은 사전 설정 없이 작동하되, 재동기화 시점에 타깃의 분기된 WAL을 스캔하는 비용이 있다. 두 기능 모두 REL_18 범위에 있다. WAL 요약화는 PG17에 도입됐고 REL_18_STABLE에 존재한다.

  1. 소스 동시 수정. libpq 소스 경로는 복사 도중 소스가 수정될 수 있다는 점을 명시적으로 처리한다. 끝에서 컨트롤 파일을 다시 읽고 minRecoveryPoint를 현재 WAL 삽입 LSN으로 설정한다. 로컬 소스 경로는 소스가 변경되지 않았다고 단언한다. 두 번의 컨트롤 파일 읽기를 memcmp하고 불일치 시 fatal 오류를 낸다. 로컬 경로의 단언이 엄격히 필요한지, 아니면 “minRecoveryPoint를 최신 체크포인트로 설정”하는 동일한 논리가 안전한지는 perform_rewindXXX 주석에 미해결로 남아 있다.

  2. 메인 포크 외 추적. extractPageInfoMAIN_FORKNUM 외의 모든 포크를 명시적으로 건너뛰고 FSM, VM, init 포크는 전체 복사한다. 보수적이고 올바른 방식이지만, 메인 포크에서 페이지 하나만 변경됐더라도 큰 free-space map을 가진 테이블은 FSM 전체가 복사된다.

  3. excludeFiles / basebackup.c 동기화. filemap.c의 주석에 excludeDirContentsbasebackup.c와 동기화 유지하도록 명시돼 있다. 이 불변식의 자동화된 강제 수단은 없다.

소스 파일 (REL_18_STABLE, 커밋 273fe94)

섹션 제목: “소스 파일 (REL_18_STABLE, 커밋 273fe94)”
  • src/bin/pg_rewind/pg_rewind.c — 메인 오케스트레이터
  • src/bin/pg_rewind/parsexlog.c — WAL 리더 (extractPageMap, findLastCheckpoint)
  • src/bin/pg_rewind/filemap.c — 파일 결정 테이블 (filehash, decide_file_actions)
  • src/bin/pg_rewind/rewind_source.h — 소스 vtable 정의
  • src/bin/pg_rewind/file_ops.c — 타깃 측 I/O (traverse_datadir, sync_target_dir)
  • src/bin/pg_rewind/timeline.c — 타임라인 히스토리 파서
  • src/bin/pg_rewind/datapagemap.c — 블록 비트맵
  • src/bin/pg_rewind/filemap.h — file_entry_t, filemap_t, file_action_t
  • knowledge/code-analysis/postgres/postgres-recovery-redo.md — 리와인드 후 타깃 시작 시의 WAL 리플레이
  • knowledge/code-analysis/postgres/postgres-xlog-wal.md — WAL 삽입, LSN 모델, 전체 페이지 쓰기
  • knowledge/code-analysis/postgres/postgres-checkpoint.md — 체크포인트 메커니즘; 리와인드가 사용하는 chkptredo 기준점
  • knowledge/code-analysis/postgres/postgres-wal-records-rmgr.md — rmgr 디스패치; parsexlog.c가 디코딩하는 WAL 레코드 구조
  • knowledge/code-analysis/postgres/postgres-incremental-backup.md — WAL 요약화; 대안적 변경 추적 방식