콘텐츠로 이동

(KO) PostgreSQL pg_upgrade — 메이저 버전 현위치 업그레이드

메이저 버전 업그레이드는 서버 바이너리를 교체하며, 통상 시스템 카탈로그의 온디스크 포맷도 함께 바뀐다. 사용자 데이터를 이 경계 너머로 옮기는 전략은 두 가지다.

논리적 이행 (덤프-복구) 방식은 모든 릴레이션을 pg_dump로 SQL 텍스트로 직렬화한 뒤, 구 클러스터를 삭제하고 신 클러스터를 초기화하여 SQL을 재실행한다. 항상 정확하다 — 복구 코드 경로가 일반 복구와 동일하기 때문이다 — 그러나 비용은 데이터 전체 크기에 비례한다. 10 TB 데이터베이스라면 원래 적재에 걸린 시간만큼 복구에도 시간이 든다. 이 메커니즘은 postgres-pg-dump-restore.md에서 다룬다.

물리적 이행 (pg_upgrade) 방식은 힙과 인덱스 파일을 직접 옮긴다. 전제는 이렇다. 시스템 카탈로그는 메이저 버전 간에 바뀌지만, 사용자 테이블 힙 페이지와 인덱스 페이지는 대체로 호환된다. PostgreSQL의 페이지 레이아웃(postgres-page-layout.md), 튜플 와이어 포맷, B-트리 온디스크 표현은 8.x 이후 대부분의 메이저 버전 경계에서 안정적으로 유지되어 왔다. 데이터 파일을 그대로 옮길 수 있다면 업그레이드 시간은 파일 전송에 걸리는 시간으로 줄어든다. 하드링크나 디렉터리 교체(swap) 모드에서는 데이터 크기와 무관하게 사실상 0에 가깝다.

물리적 이행이 반드시 지켜야 할 핵심 불변 조건은 **OID 안정성(OID stability)**이다. 사용자 데이터에 저장되는 특정 카탈로그 컬럼에 한해 적용된다.

카탈로그 컬럼사용자 데이터에 저장되는 형태
pg_class.oid / pg_class.relfilenode힙 튜플 내 toast 포인터
pg_type.oid사용자 테이블의 복합 타입 값
pg_enum.oid사용자 테이블의 enum 값
pg_tablespace.oid디스크상 디렉터리 이름
pg_database.oid디스크상 디렉터리 이름
pg_authid.oid레거시 대형 객체 메타데이터

신 클러스터가 이 객체들에 다른 OID를 부여하면, 전송된 힙 파일은 댕글링 포인터를 참조하게 된다. pg_upgrade는 신 클러스터 스키마 복원 단계에서 OID 할당을 명시적으로 제어한다. initdbpg_restore를 특수한 binary upgrade 모드로 실행하여 OID가 구 클러스터 값과 일치하도록 강제한다.

물리 파일 계층에서는 두 가지 불변 조건이 추가로 적용된다.

  1. 블록 크기 동일성. 두 클러스터에서 페이지 크기가 blocksz 바이트로 같아야 한다. 불일치는 즉시 중단 사유다.
  2. WAL 세그먼트 호환성. WAL 레코드는 페이지 LSN을 참조한다. WAL 세그먼트 크기가 다르면 pg_resetwal이 신 WAL 디렉터리를 안전하게 다시 봉인할 수 없다.

두 조건 모두 사전 검사 단계에서 check_control_data가 확인한다.

관계형 데이터베이스의 물리적 이행 도구는 벤더에 무관하게 공통 구조를 공유한다.

파일에 손대기 전에, 두 클러스터가 호환되는지 확인한다. 검사 항목은 다음과 같다. 블록 크기, WAL 세그먼트 크기, 구 클러스터를 비정상 상태로 만들 수 있는 준비된 트랜잭션(prepared transaction) 유무, 신 클러스터에 없는 확장 라이브러리 유무, 두 버전 간 온디스크 표현이 바뀐 데이터 타입 유무, 불안정한 OID를 참조하는 컬럼 타입을 쓰는 사용자 테이블 유무. 각 검사는 독립적이며, 실제 업그레이드 없이 --check 모드로 단독 실행할 수 있다.

스키마 전용 덤프 + 바이너리 모드 복원

섹션 제목: “스키마 전용 덤프 + 바이너리 모드 복원”

initdb로 신 클러스터를 새로 초기화한다. 구 클러스터의 스키마 — 모든 데이터베이스 객체에 대한 DDL, 의존성 순서대로 — 를 논리적 백업 도구로 덤프하되 명시적 OID = 절을 함께 출력한다. 이 스키마를 신 클러스터에 복원할 때, 신 클러스터는 해당 명시적 OID를 받아들이고 강제하는 모드로 실행된다. 결과적으로 시스템 카탈로그 구조는 신버전이지만 OID 네임스페이스는 구 클러스터와 정확히 일치하는 신 클러스터가 만들어진다.

스키마가 자리를 잡으면, 각 사용자 릴레이션의 힙·인덱스 파일을 구 데이터 디렉터리에서 신 디렉터리로 옮긴다. 다섯 가지 전송 전략이 속도·안전성·유연성의 트레이드오프를 각각 다른 방식으로 처리한다.

전략메커니즘데이터 크기 비용롤백 안전성
Copycp / read+write전체 복사구 클러스터 온전히 유지
Cloneioctl FICLONE / reflinkCopy-on-write, 거의 0구 클러스터 온전히 유지
Copy-file-rangecopy_file_range(2)커널 내 복사구 클러스터 온전히 유지
Hard-linklink(2)거의 0아이노드 공유 — 신 클러스터 기동 후 구 클러스터 불안전
Swap디렉터리 rename거의 0구 클러스터 디렉터리가 교체됨

물리 파일 이전이 끝나면, 신 클러스터는 구 클러스터의 트랜잭션 카운터(nextXid, nextOid, nextMultiXact, WAL 위치)를 상속해야 한다. 그래야 미래 트랜잭션 ID가 기존 MVCC 가시성 레코드와 충돌하지 않는다. pg_resetwal로 전체 서버 사이클 없이 이 값들을 신 클러스터의 pg_control에 기록한다.

개념PostgreSQL 명칭
구 클러스터 디스크립터ClusterInfo old_cluster
신 클러스터 디스크립터ClusterInfo new_cluster
릴레이션별 파일 매핑FileNameMap
전송 전략 열거형transferMode / TRANSFER_MODE_*
서버로 전달하는 바이너리 업그레이드 플래그--binary-upgrade (pg_dump / pg_restore)
트랜잭션 카운터 기록copy_xact_xlog_xidpg_resetwal 호출

pg_upgrade.cmain()은 선형 파이프라인을 실행한다. “OLD”와 “NEW”로 주석 표시된 두 단계가 각각 포스트마스터를 기동하고 종료한다.

// main — src/bin/pg_upgrade/pg_upgrade.c
parseCommandLine(argc, argv);
adjust_data_dir(&old_cluster);
adjust_data_dir(&new_cluster);
make_outputdirs(new_cluster.pgdata); /* pg_upgrade_output.d/$timestamp/ */
setup(argv[0]);
output_check_banner();
check_cluster_versions();
check_cluster_compatibility(); /* pg_control 교차 검사 */
check_and_dump_old_cluster(); /* OLD: 검사 + pg_dump 스키마 */
/* -- NEW -- */
start_postmaster(&new_cluster, true);
check_new_cluster();
report_clusters_compatible();
set_locale_and_encoding();
prepare_new_cluster(); /* vacuumdb --all --analyze + --freeze */
stop_postmaster(false);
copy_xact_xlog_xid(); /* pg_resetwal: xid/oid/multixact/WAL */
set_new_cluster_char_signedness();
/* -- NEW (두 번째) -- */
start_postmaster(&new_cluster, true);
prepare_new_globals(); /* set_frozenxids + globals 덤프 복원 */
create_new_objects(); /* pg_restore 데이터베이스별, 병렬 */
stop_postmaster(false);
transfer_all_new_tablespaces(...); /* move/link/clone/swap 릴레이션 파일 */
/* pg_resetwal -o (next OID) */
create_logical_replication_slots(); /* 슬롯이 있는 경우 */
issue_warnings_and_set_wal_level();

그림 1 — pg_upgrade 메인 파이프라인.

flowchart TD
    A[parseCommandLine] --> B[check_cluster_versions<br/>check_cluster_compatibility]
    B --> C[check_and_dump_old_cluster<br/>OLD 포스트마스터: 검사 + pg_dump 스키마]
    C --> D[신 포스트마스터 기동]
    D --> E[check_new_cluster<br/>report_clusters_compatible]
    E --> F[prepare_new_cluster<br/>vacuumdb analyze + freeze]
    F --> G[신 포스트마스터 종료]
    G --> H[copy_xact_xlog_xid<br/>pg_resetwal: 카운터 + WAL]
    H --> I[신 포스트마스터 기동]
    I --> J[prepare_new_globals<br/>set_frozenxids + globals 복원]
    J --> K[create_new_objects<br/>pg_restore 데이터베이스별 병렬]
    K --> L[신 포스트마스터 종료]
    L --> M[transfer_all_new_tablespaces<br/>copy / link / clone / swap]
    M --> N[pg_resetwal -o next OID]
    N --> O[create_logical_replication_slots]
    O --> P[issue_warnings_and_set_wal_level]
    P --> Q[업그레이드 완료]

그림 1 — pg_upgrade 메인 파이프라인 (REL_18_STABLE). 포스트마스터의 두 번의 기동/종료 사이클이 스키마 복원과 물리 파일 전송을 각각 감싼다.

구/신 클러스터의 모든 상태는 전역 ClusterInfo 인스턴스 두 개에 담긴다.

// ClusterInfo, ControlData — src/bin/pg_upgrade/pg_upgrade.h
typedef struct
{
ControlData controldata; /* pg_control 스냅샷 */
DbLocaleInfo *template0; /* template0 로케일 / 인코딩 */
DbInfoArr dbarr; /* 데이터베이스별: 릴레이션 + 논리 슬롯 */
char *pgdata; /* $PGDATA 경로 */
char *bindir; /* bin/ 경로 (pg_dump, pg_restore 등) */
unsigned short port; /* 포스트마스터 리슨 포트 */
uint32 major_version;
const char *tablespace_suffix;
} ClusterInfo;
typedef struct
{
uint32 cat_ver; /* 카탈로그 버전 — 호환성 검사 기준 */
uint32 chkpnt_nxtxid; /* 이식할 next transaction ID */
uint32 chkpnt_nxtoid; /* 이식할 next OID */
uint32 chkpnt_nxtmulti; /* 이식할 next MultiXactId */
uint32 chkpnt_nxtmxoff; /* 이식할 next MultiXact offset */
uint32 chkpnt_oldstMulti; /* 가장 오래된 MultiXactId */
uint32 chkpnt_oldstxid; /* 가장 오래된 transaction ID */
uint32 blocksz; /* 두 클러스터 간 일치 필요 */
uint32 walseg; /* WAL 세그먼트 크기 — 일치 필요 */
bool default_char_signedness; /* PG18: char 부호 있음/없음 플래그 */
/* ... */
} ControlData;

check_control_datablocksz, walseg, align, index, toast, large_object 등 여러 필드를 교차 검사한다. 불일치가 발견되면 즉시 중단된다.

check_and_dump_old_clustercheck_new_cluster(둘 다 check.c에 있음)가 사전 검사 배터리를 실행한다. 주요 검사 항목은 다음과 같다.

// check_and_dump_old_cluster — src/bin/pg_upgrade/check.c
check_for_connection_status(&old_cluster);
get_db_rel_and_slot_infos(&old_cluster);
check_is_install_user(&old_cluster);
check_for_prepared_transactions(&old_cluster);
check_for_isn_and_int8_passing_mismatch(&old_cluster);
check_for_data_types_usage(&old_cluster); /* reg* 타입, line, jsonb, aclitem … */
check_for_unicode_update(&old_cluster);
/* 버전 조건부: 인코딩 변환, postfix 연산자, 다형 함수,
OID가 있는 테이블, NOT NULL 상속 (PG18 신규), pg_ 역할 접두사 */
generate_old_dump(); /* pg_dump --schema-only --binary-upgrade */

데이터 타입 검사는 DataTypesUsageChecks 테이블이 주도한다. 정적으로 초기화된 구조체 배열로, 각 항목에는 상태 문자열, 보고서 파일명, 문제가 되는 타입의 OID를 추출하는 SQL 쿼리, 사람이 읽을 수 있는 오류 텍스트, 그리고 구 클러스터 버전에 검사를 적용할지 결정하는 threshold_version이 담겨 있다.

// DataTypesUsageChecks data_types_usage_checks[] — src/bin/pg_upgrade/check.c
{
.status = "Checking for system-defined composite types in user tables",
.base_query = "SELECT t.oid FROM pg_catalog.pg_type t ... WHERE typtype = 'c'
AND (t.oid < 16384 OR nspname = 'information_schema')",
.threshold_version = ALL_VERSIONS
},
{
.status = "Checking for reg* data types in user tables",
.base_query = "SELECT oid FROM pg_catalog.pg_type t WHERE t.typname IN
('regcollation','regconfig','regdictionary','regnamespace',
'regoper','regoperator','regproc','regprocedure')",
.threshold_version = ALL_VERSIONS
},
/* ... aclitem (<=15), unknown (<=9.6), sql_identifier (<=11), jsonb (수동),
abstime/reltime/tinterval (<=11), line (<=9.3) ... */

각 검사는 구 클러스터의 모든 데이터베이스에 접속하여 쿼리를 실행하는 UpgradeTask로 처리된다. 검사에 실패하면 pg_upgrade_output.d/$timestamp/에 보고서 파일을 작성하고 중단된다.

PG18 신규 검사인 check_for_not_null_inheritance는 부모 컬럼이 요구하는 NOT NULL 제약을 자식 테이블이 누락한 경우를 거부한다. 해당 자식 테이블이 있으면 스키마 복원이 실패하기 때문이다.

copy_xact_xlog_xid는 일련의 pg_resetwal 호출로 구 클러스터의 카운터를 신 클러스터의 pg_control에 기록한다.

// copy_xact_xlog_xid — src/bin/pg_upgrade/pg_upgrade.c
/* pg_xact(커밋 로그)를 구에서 신으로 복사 */
copy_subdir_files("pg_xact", "pg_xact");
/* oldest와 next XID 이식 */
exec_prog(..., "\"%s/pg_resetwal\" -f -u %u \"%s\"",
new_cluster.bindir, old_cluster.controldata.chkpnt_oldstxid, ...);
exec_prog(..., "\"%s/pg_resetwal\" -f -x %u \"%s\"",
new_cluster.bindir, old_cluster.controldata.chkpnt_nxtxid, ...);
/* MultiXact 카운터 이식 (포맷 호환 시) */
copy_subdir_files("pg_multixact/offsets", "pg_multixact/offsets");
copy_subdir_files("pg_multixact/members", "pg_multixact/members");
exec_prog(..., "\"%s/pg_resetwal\" -O %u -m %u,%u \"%s\"",
new_cluster.bindir,
old_cluster.controldata.chkpnt_nxtmxoff,
old_cluster.controldata.chkpnt_nxtmulti,
old_cluster.controldata.chkpnt_oldstMulti, ...);
/* WAL 아카이브를 구 클러스터 LSN에 맞게 재설정 */
exec_prog(..., "\"%s/pg_resetwal\" -l 00000001%s \"%s\"",
new_cluster.bindir,
old_cluster.controldata.nextxlogfile + 8, ...);

pg_xact 디렉터리(커밋 로그)는 그대로 복사된다. 신 클러스터가 그 구 XID들의 커밋 여부를 알아야 하기 때문이다. 없으면 전송된 파일의 힙 튜플이 보이지 않는 커밋 상태가 된다.

transfer_all_new_tablespaces(relfilenumber.c)는 모든 데이터베이스와 릴레이션을 순회하며 구/신 파일 경로를 짝 지은 FileNameMap 배열을 만들고, transfer_single_new_db로 전달한다.

// FileNameMap — src/bin/pg_upgrade/pg_upgrade.h
typedef struct
{
const char *old_tablespace;
const char *new_tablespace;
const char *old_tablespace_suffix;
const char *new_tablespace_suffix;
Oid db_oid;
RelFileNumber relfilenumber;
char *nspname;
char *relname;
} FileNameMap;

transfer_relfile은 릴레이션 하나를 처리하며, 1 GB 세그먼트 파일(relfilenumber, relfilenumber.1, relfilenumber.2, …)과 그 포크(_fsm, _vm)를 순회한다. VISIBILITY_MAP_FROZEN_BIT_CAT_VER보다 이전 버전 클러스터에서 업그레이드할 때 VM 포크가 있으면, 그대로 복사하지 않고 frozen 비트를 추가하여 VM 파일을 다시 쓴다.

여기서 relfilenumber 보존(relfilenumber-preservation) 불변 조건이 구체화된다. 구/신 파일 경로는 모두 같은 map->relfilenumber 필드로 구성되므로, 힙/인덱스 파일은 전송 후에도 온디스크 이름을 유지한다. 그 이름은 binary-upgrade 스키마 복원 단계에서 고정된 것이기 때문에, 전송된 튜플 안의 toast 포인터가 여전히 올바르게 해소된다. 아래 루프에서 모드별 분기와 VM 재기록 단락을 확인할 수 있다.

// transfer_relfile — src/bin/pg_upgrade/relfilenumber.c
/* 양쪽에 동일한 relfilenumber — 이름은 재할당이 아니라 보존된다 */
snprintf(old_file, sizeof(old_file), "%s%s/%u/%u%s%s",
map->old_tablespace, map->old_tablespace_suffix,
map->db_oid, map->relfilenumber, type_suffix, extent_suffix);
snprintf(new_file, sizeof(new_file), "%s%s/%u/%u%s%s",
map->new_tablespace, map->new_tablespace_suffix,
map->db_oid, map->relfilenumber, type_suffix, extent_suffix);
unlink(new_file);
if (vm_must_add_frozenbit && strcmp(type_suffix, "_vm") == 0)
/* 그대로 복사하지 않고 페이지별 frozen 비트를 추가하여 VM 재기록 */
rewriteVisibilityMap(old_file, new_file, map->nspname, map->relname);
else
switch (user_opts.transfer_mode)
{
case TRANSFER_MODE_CLONE: /* ioctl FICLONE / reflink */
cloneFile(old_file, new_file, map->nspname, map->relname); break;
case TRANSFER_MODE_COPY: /* read + write */
copyFile(old_file, new_file, map->nspname, map->relname); break;
case TRANSFER_MODE_COPY_FILE_RANGE: /* copy_file_range(2) */
copyFileByRange(old_file, new_file, map->nspname, map->relname); break;
case TRANSFER_MODE_LINK: /* link(2) — shared inode */
linkFile(old_file, new_file, map->nspname, map->relname); break;
case TRANSFER_MODE_SWAP: /* do_swap에서 처리, 여기서는 아님 */
pg_fatal("should never happen"); break;
}

TRANSFER_MODE_SWAP은 이 경로에서 명시적으로 금지된다. swap 모드는 do_swap에서 데이터베이스 디렉터리 전체를 rename하며, 세그먼트별 루프를 거치지 않기 때문이다.

—swap 모드 (PG18 신규)는 다른 전송 모드와 구조적으로 다르다. 개별 파일을 복사하거나 링크하지 않고, 다음 세 단계로 동작한다.

  1. 구 클러스터의 데이터베이스 디렉터리 전체($PGDATA/base/$db_oid)를 rename(2)으로 신 클러스터 슬롯으로 이동한다.
  2. 이동된 디렉터리에서 pg_restore가 생성한 카탈로그 파일을 신 데이터베이스 디렉터리로 되돌린다.
  3. 남은 구 카탈로그 파일은 구 클러스터 하위 moved_for_upgrade/로 옮겨서 나중에 delete_old_cluster.sh가 정리할 수 있게 한다.
// do_swap / swap_catalog_files — src/bin/pg_upgrade/relfilenumber.c
static void
swap_catalog_files(FileNameMap *maps, int size,
const char *old_catalog_dir,
const char *new_db_dir,
const char *moved_db_dir)
{
/* maps[]에 없는 구 카탈로그 파일 옆으로 이동(사용자 데이터 파일은
rename된 디렉터리에 이미 제자리에 있음) */
/* 신 pg_restore 생성 카탈로그 파일을 제자리로 이동 */
/* sync_queue로 모든 파일 fsync */
}

swap 모드의 fsync 전략은 배치 방식의 sync_queue(1024개 경로 고정 배열, 가득 차거나 끝날 때 fsync 일괄 수행)를 쓴다. pg_restore가 fsync=off로 기록한 카탈로그 파일을 파일별로 fsync하는 비용을 피하기 위해서다.

flowchart LR
    A["transfer_all_new_tablespaces"] --> B["transfer_all_new_dbs<br/>테이블스페이스별"]
    B --> C["gen_db_file_maps<br/>구+신 RelInfo 배열 → FileNameMap[]"]
    C --> D["transfer_single_new_db"]
    D --> E{transfer_mode}
    E -- SWAP --> F["do_swap<br/>디렉터리 rename + swap_catalog_files"]
    E -- LINK --> G["transfer_relfile<br/>주 + _fsm + _vm 세그먼트 link"]
    E -- COPY/CLONE/CFR --> H["transfer_relfile<br/>세그먼트별 copy/clone/cfr"]
    G --> I["vm_must_add_frozenbit?<br/>rewriteVisibilityMap"]
    H --> I

그림 2 — relfilenumber.c의 릴레이션 파일 전송 분기.

PG18에서 char 타입의 부호 여부(ControlDatadefault_char_signedness)를 클러스터 수준으로 설정하는 기능이 도입됐다. copy_xact_xlog_xid 이후, set_new_cluster_char_signedness가 구 클러스터 값(또는 사용자가 --set-char-signedness로 지정한 값)을 읽고, 신 클러스터의 기본값과 다를 때만 pg_resetwal --char-signedness signed|unsigned를 호출한다. 이 검사에는 버전 조건이 붙는다. PG18 이상 소스 클러스터에서 업그레이드할 때 --set-char-signedness를 지정하면 거부된다. 클러스터 수준 기본값이 도입되기 전 클러스터에만 의미 있는 옵션이기 때문이다.

Frozen-XID 부트스트랩과 스키마 복원

섹션 제목: “Frozen-XID 부트스트랩과 스키마 복원”

사용자 객체 복원 전에, set_frozenxids(false)가 initdb가 생성한 모든 테이블의 pg_class.relfrozenxidpg_database.datfrozenxid를 구 클러스터의 next-XID 값으로 설정한다. 이식된 XID 카운터를 기준으로 autovacuum이 카탈로그 테이블을 즉시 노화 처리하는 것을 막기 위해서다.

create_new_objects는 두 패스로 데이터베이스별 pg_restore를 실행한다. template1을 먼저 직렬로 처리하고(일시적으로 삭제하면 접속이 막히기 때문), 나머지 데이터베이스를 parallel_exec_prog로 병렬 처리한다. 각 pg_restore 호출은 --transaction-size=1000(RESTORE_TRANSACTION_SIZE 상수)으로 TOC 항목을 묶는다. 병렬 모드에서는 잠금 한도를 맞추기 위해 트랜잭션 크기를 잡 수로 나눈다.

구 클러스터에 논리 복제 슬롯이 있으면(PG17+), create_logical_replication_slots가 신 클러스터에서 슬롯을 복원한다. 슬롯마다 원래의 플러그인 이름, 2단계 디코드 플래그, 페일오버 플래그를 넘겨 pg_create_logical_replication_slot을 호출한다. 슬롯 생성이 LSN을 기록하고 WAL이 최종 상태여야 하므로, 이 단계는 pg_resetwal 이후에 실행된다.

pg_upgrade.c — 파이프라인과 헬퍼

섹션 제목: “pg_upgrade.c — 파이프라인과 헬퍼”
  • main — 최상위 파이프라인. 포스트마스터의 두 번의 기동/종료 사이클을 소유한다.
  • setup — 남은 포스트마스터 PID 파일이 없는지 확인한다. 발견되면 기동/종료를 시도한다.
  • make_outputdirspg_upgrade_output.d/$timestamp/{dump,log}/를 생성한다.
  • prepare_new_cluster — 신 클러스터에 vacuumdb --all --analyzevacuumdb --all --freeze를 순서대로 실행한다. 구 카운터가 이식되기 전에 pg_statistic을 동결 상태로 만든다.
  • prepare_new_globalsset_frozenxids(false)를 호출한 뒤 globals.dump(역할, 테이블스페이스)를 복원한다.
  • create_new_objects — 데이터베이스별 pg_restore. template1을 먼저 처리하고 나머지를 병렬로 처리한다. 완료 후 신 클러스터에서 get_db_rel_and_slot_infos를 호출한다.
  • copy_xact_xlog_xid — pg_xact / pg_multixact를 복사하고, XID·에포크·OID·multixact 카운터·WAL 위치를 pg_resetwal로 기록한다.
  • set_frozenxidspg_classpg_database에 UPDATE를 실행하여 동결 XID 마커를 구 클러스터의 XID 카운터에 맞춘다.
  • set_new_cluster_char_signedness — PG18 char 부호 정렬.
  • set_locale_and_encoding — 신 클러스터 template0의 datcollate, datctype, datlocprovider, datlocale을 구 클러스터에 맞게 UPDATE한다.
  • create_logical_replication_slots — 데이터베이스별 루프로 pg_create_logical_replication_slot을 호출한다.
  • check_and_dump_old_cluster — 구 클러스터 검사를 조율하고 마지막에 generate_old_dump를 호출한다.
  • check_new_cluster — 신 클러스터 검사: 빈 클러스터 확인, 로드 가능한 라이브러리 확인, 전송 모드별 검사(clone / copy_file_range / link / swap), 논리 슬롯 및 구독 상태 확인.
  • check_cluster_versions — 최소 소스 버전(9.2) 강제, 타겟은 현재 PG여야 함, 다운그레이드 불가, 바이너리와 데이터 디렉터리 일치 확인. PG18: 소스 >= 18이면 --set-char-signedness 거부.
  • check_cluster_compatibilityget_control_data + check_control_data 호출. live-check 모드에서 포트 충돌 거부.
  • check_for_data_types_usagedata_types_usage_checks[]를 순회하며 UpgradeTask 단계를 만들어 데이터베이스별로 실행한다. 실패를 보고하고 중단한다.
  • DataTypesUsageChecks — 정적 배열. 각 항목은 status, report_filename, base_query, report_text, threshold_version, 옵션으로 version_hook을 가진다.
  • check_for_not_null_inheritance — PG18 신규: 부모 NOT NULL 제약을 누락한 자식 테이블 거부 (스키마 복원 실패 방지).
  • transfer_all_new_tablespaces — 진입점. 모드별로 분기한다(jobs > 1이면 테이블스페이스별 병렬).
  • transfer_all_new_dbs — 구/신 데이터베이스 쌍을 순회하며 gen_db_file_mapstransfer_single_new_db를 호출한다.
  • transfer_single_new_dbvm_must_add_frozenbit를 확인하고, do_swap 또는 맵별 transfer_relfile 루프로 분기한다.
  • transfer_relfile — 1 GB 세그먼트 파일 + _fsm / _vm 포크를 순회한다. frozen 비트를 추가해야 할 때 rewriteVisibilityMap을 호출하고, 모드에 따라 cloneFile / copyFile / copyFileByRange / linkFile로 분기한다.
  • do_swap — 맵을 relfilenumber 순으로 정렬하고, 테이블스페이스별로 prepare_for_swapswap_catalog_files를 호출한다.
  • prepare_for_swap — 구 db 디렉터리를 신 클러스터 슬롯으로 rename하고 moved_for_upgrade/ 스테이징 영역을 생성한다.
  • swap_catalog_files — 구 카탈로그 파일을 옆으로 이동하고, pg_restore가 생성한 카탈로그 파일을 제자리로 이동하고, sync_queue로 fsync한다.
  • sync_queue_* — 1024개 경로의 고정 크기 큐. pre_sync_fname + 배치 fsync로 동작하며, 가득 차거나 끝날 때 비운다.
  • ClusterInfo — 클러스터별 상태: controldata, dbarr, pgdata, bindir, port, major_version, tablespace_suffix.
  • ControlData — pg_control 스냅샷: cat_ver, chkpnt_nxtxid, chkpnt_nxtoid, chkpnt_nxtmulti, blocksz, walseg, default_char_signedness (PG18).
  • FileNameMap — 릴레이션별 전송 매핑: 구/신 테이블스페이스 경로 + 접미사, db_oid, relfilenumber.
  • DbInfo / DbInfoArr — 데이터베이스별: db_oid, db_name, rel_arr, slot_arr.
  • RelInfo — 릴레이션별: nspname, relname, reloid, relfilenumber, tablespace.
  • LogicalSlotInfo / LogicalSlotInfoArr — 슬롯별: slotname, plugin, two_phase, failover.
  • UserOpts — 파싱된 CLI 옵션: check, live_check, transfer_mode, jobs, char_signedness (PG18), do_statistics.
  • transferMode 열거형 — TRANSFER_MODE_{CLONE,COPY,COPY_FILE_RANGE,LINK,SWAP}.

위치 힌트 (2026-06-06 / 커밋 273fe94 기준)

섹션 제목: “위치 힌트 (2026-06-06 / 커밋 273fe94 기준)”
심볼파일줄 번호
mainpg_upgrade.c88
make_outputdirspg_upgrade.c252
setuppg_upgrade.c337
set_locale_and_encodingpg_upgrade.c440
prepare_new_clusterpg_upgrade.c519
prepare_new_globalspg_upgrade.c549
create_new_objectspg_upgrade.c571
copy_xact_xlog_xidpg_upgrade.c749
set_frozenxidspg_upgrade.c874
create_logical_replication_slotspg_upgrade.c976
set_new_cluster_char_signednesspg_upgrade.c404
output_check_bannercheck.c570
check_and_dump_old_clustercheck.c588
check_new_clustercheck.c709
check_cluster_versionscheck.c849
check_cluster_compatibilitycheck.c904
check_for_data_types_usagecheck.c463
data_types_usage_checks[]check.c98
check_for_not_null_inheritancecheck.c— (grep: “not_null_inheritance”)
DataTypesUsageChecks (struct)check.c42
transfer_all_new_tablespacesrelfilenumber.c107
transfer_all_new_dbsrelfilenumber.c170
prepare_for_swaprelfilenumber.c236
swap_catalog_filesrelfilenumber.c362
do_swaprelfilenumber.c452
transfer_single_new_dbrelfilenumber.c500
transfer_relfilerelfilenumber.c552
sync_queue_pushrelfilenumber.c74
FileNameMap (struct)pg_upgrade.h180
ClusterInfo (struct)pg_upgrade.h287
ControlData (struct)pg_upgrade.h229
UserOpts (struct)pg_upgrade.h328
transferMode (enum)pg_upgrade.h259
RESTORE_TRANSACTION_SIZEpg_upgrade.c58
DEFAULT_CHAR_SIGNEDNESS_CAT_VERpg_upgrade.h132
  • --swap 전송 모드는 REL_18_STABLE에 존재한다. TRANSFER_MODE_SWAPtransferMode 열거형(pg_upgrade.h:265)에 있고 transfer_single_new_db에서 분기된다. swap 전용 헬퍼(prepare_for_swap, swap_catalog_files, do_swap, sync_queue_*)는 모두 relfilenumber.c에 있다. swap 모드는 소스 클러스터가 PG10 이상인 경우에만 허용된다(pg_upgrade.c main, check.c:753).

  • --set-char-signedness 옵션은 PG18 신규이며 소스 >= 18에서 거부된다. UserOpts.char_signedness는 기본값 -1(미설정). CLI에서 받아들인다. check_cluster_versions(check.c:895)는 소스 클러스터 메이저 버전 >= 18이고 옵션이 지정되면 명확한 메시지와 함께 중단한다. set_new_cluster_char_signedness(pg_upgrade.c:404)는 신 클러스터의 값이 목표값과 다를 때만 pg_resetwal --char-signedness를 호출한다.

  • RESTORE_TRANSACTION_SIZE는 하드코딩된 1000이다. pg_upgrade.c:58에 정의되어 있다. 주석에 사용자 제어 가능하게 할 수 있다고 나와 있지만, GUC가 아니다. 병렬 모드(jobs > 1)에서 잡별 트랜잭션 크기는 Max(1000 / jobs, 10)이다.

  • check_for_not_null_inheritance는 PG18 신규 검사다. check_and_dump_old_cluster의 버전 게이트는 <= 1800이며, 현재 타겟 기준으로 모든 소스 버전에서 무조건 실행된다. PG18 이전 스키마에서 자식 테이블이 부모 NOT NULL 제약을 누락할 수 있었던 경우를 잡기 위한 것이다.

  • pg_xact는 그대로 복사되고, pg_multixact 복사는 버전 조건부다. copy_xact_xlog_xidpg_xact를 무조건 복사한다. pg_multixact 파일은 두 클러스터 모두 cat_ver >= MULTIXACT_FORMATCHANGE_CAT_VER인 경우에만 복사된다. 그렇지 않으면 파일은 복사하지 않고 카운터 값만 재설정한다.

  • 논리 복제 슬롯 이행은 count_old_cluster_logical_slots() > 0일 때에만 수행된다. 구 클러스터에 슬롯이 있을 때만 포스트마스터를 세 번째로 기동한다. 논리 복제를 쓰지 않는 클러스터에서 추가 포스트마스터 사이클을 피하기 위해서다.

  1. check_for_not_null_inheritance 줄 번호. 함수가 check.c:28에 정적으로 선언되어 있으나, 읽은 범위(check.c는 2376줄)에서 정의를 확인하지 못했다. 조사 경로: grep -n check_for_not_null_inheritance /data/hgryoo/references/postgres/src/bin/pg_upgrade/check.c.

  2. --do-statistics 옵션 동작. UserOpts.do_statistics가 파싱되지만, 파이프라인에서의 효과(별도의 통계 전송 단계를 트리거하는지 여부)를 읽은 범위에서 추적하지 못했다. 조사 경로: grep -n do_statistics /data/hgryoo/references/postgres/src/bin/pg_upgrade/*.c.

  3. swap 모드 테이블스페이스 제한. prepare_for_swap에 주석(“XXX: The below line is a hack”)이 있어, 신 테이블스페이스 경로가 구 경로와 같다고 가정하며 swap 모드에서 in-place 테이블스페이스를 지원하지 못한다고 나와 있다. 이것이 알려진 제한으로 관리되는지 수정 예정인지는 소스에서 확인하지 못했다.

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

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 최전선”
  • Oracle Database 업그레이드 (dbupgrade / DBUA) — Oracle은 실행 중인 데이터베이스에 업그레이드 스크립트를 실행하는 in-place 카탈로그 업그레이드 방식을 쓴다. 힙 포맷이 안정적이라 물리 파일 전송이 불필요하다. 카탈로그 DDL은 별도로 버전 관리된다. PostgreSQL의 스키마 덤프 + OID 고정 방식과 Oracle의 in-place 카탈로그 변경 방식 중 어느 쪽이 정확성 리스크가 낮은지는 비교 분석이 필요하다.

  • MySQL / InnoDB 업그레이드 — InnoDB는 데이터 딕셔너리 테이블에 포맷 버전을 표시하고, 서버가 최초 기동 시 DDL 업그레이드를 실행한다. pg_upgrade의 명시적 OID 제어에 해당하는 메커니즘이 없다. InnoDB의 클러스터드 인덱스 설계상 PostgreSQL에서 OID 안정성을 필수로 만드는 toast 포인터 OID 문제가 없기 때문이다.

  • pg_upgrade + 논리 복제를 결합한 무중단 경로 — pg_upgrade(물리 복사)와 논리 복제(복사 기간 중 in-flight 쓰기 재생)를 결합하는 것은 알려진 운영 패턴이다. --swap 모드(PG18)는 대용량 데이터베이스에서도 물리 복사 윈도우를 사실상 0에 가깝게 줄여 이 조합을 더 실용적으로 만든다.

  • Reflink / copy-on-write 업그레이드TRANSFER_MODE_CLONE은 btrfs와 reflink가 활성화된 XFS에서 쓸 수 있는 ioctl(FICLONE) 또는 reflink 기반 copy_file_range를 사용한다. 전송이 즉각 완료되면서 구 파일이 그대로 유지된다(어느 쪽에 첫 번째 쓰기가 발생할 때 copy-on-write). 업그레이드 후 WAL 집약적 워크로드에서 성능 영향은 별도 측정이 필요하다.

(없음 — 소스 트리에서 직접 합성)

소스 코드 (REL_18_STABLE, 커밋 273fe94)

섹션 제목: “소스 코드 (REL_18_STABLE, 커밋 273fe94)”
  • src/bin/pg_upgrade/pg_upgrade.c — 메인 파이프라인, 헬퍼
  • src/bin/pg_upgrade/pg_upgrade.h — 모든 핵심 타입
  • src/bin/pg_upgrade/check.c — 사전 검사
  • src/bin/pg_upgrade/relfilenumber.c — 릴레이션 파일 전송
  • knowledge/code-analysis/postgres/postgres-pg-dump-restore.md — pg_dump / pg_restore 메커니즘 (pg_upgrade가 호출하는 스키마 덤프 및 복원 단계)
  • knowledge/code-analysis/postgres/postgres-page-layout.md — 힙 페이지 포맷 (버전 간 페이지 전송이 가능한 이유)
  • knowledge/code-analysis/postgres/postgres-mvcc-snapshots.md — XID 가시성 (커밋 로그 이식이 필요한 이유)
  • knowledge/code-analysis/postgres/postgres-initdb-bootstrap-genbki.md — initdb / binary-upgrade 모드 (신 클러스터의 OID 할당을 구 클러스터에 맞게 강제하는 방법)