(KO) PostgreSQL pg_upgrade — 메이저 버전 현위치 업그레이드
- 이론적 배경
- 공통 DBMS 설계
- PostgreSQL의 접근 방식
- 소스 워크스루
- 소스 검증 (2026-06-06 기준)
- PostgreSQL 너머 — 비교 설계와 연구 최전선
- 참고 자료
이론적 배경
섹션 제목: “이론적 배경”메이저 버전 업그레이드는 서버 바이너리를 교체하며, 통상 시스템 카탈로그의 온디스크 포맷도 함께 바뀐다. 사용자 데이터를 이 경계 너머로 옮기는 전략은 두 가지다.
논리적 이행 (덤프-복구) 방식은 모든 릴레이션을 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 할당을 명시적으로 제어한다. initdb와 pg_restore를 특수한 binary upgrade 모드로 실행하여 OID가 구 클러스터 값과 일치하도록 강제한다.
물리 파일 계층에서는 두 가지 불변 조건이 추가로 적용된다.
- 블록 크기 동일성. 두 클러스터에서 페이지 크기가
blocksz바이트로 같아야 한다. 불일치는 즉시 중단 사유다. - WAL 세그먼트 호환성. WAL 레코드는 페이지 LSN을 참조한다. WAL 세그먼트 크기가 다르면
pg_resetwal이 신 WAL 디렉터리를 안전하게 다시 봉인할 수 없다.
두 조건 모두 사전 검사 단계에서 check_control_data가 확인한다.
공통 DBMS 설계
섹션 제목: “공통 DBMS 설계”관계형 데이터베이스의 물리적 이행 도구는 벤더에 무관하게 공통 구조를 공유한다.
사전 검사 배터리
섹션 제목: “사전 검사 배터리”파일에 손대기 전에, 두 클러스터가 호환되는지 확인한다. 검사 항목은 다음과 같다. 블록 크기, WAL 세그먼트 크기, 구 클러스터를 비정상 상태로 만들 수 있는 준비된 트랜잭션(prepared transaction) 유무, 신 클러스터에 없는 확장 라이브러리 유무, 두 버전 간 온디스크 표현이 바뀐 데이터 타입 유무, 불안정한 OID를 참조하는 컬럼 타입을 쓰는 사용자 테이블 유무. 각 검사는 독립적이며, 실제 업그레이드 없이 --check 모드로 단독 실행할 수 있다.
스키마 전용 덤프 + 바이너리 모드 복원
섹션 제목: “스키마 전용 덤프 + 바이너리 모드 복원”initdb로 신 클러스터를 새로 초기화한다. 구 클러스터의 스키마 — 모든 데이터베이스 객체에 대한 DDL, 의존성 순서대로 — 를 논리적 백업 도구로 덤프하되 명시적 OID = 절을 함께 출력한다. 이 스키마를 신 클러스터에 복원할 때, 신 클러스터는 해당 명시적 OID를 받아들이고 강제하는 모드로 실행된다. 결과적으로 시스템 카탈로그 구조는 신버전이지만 OID 네임스페이스는 구 클러스터와 정확히 일치하는 신 클러스터가 만들어진다.
릴레이션 파일 전송
섹션 제목: “릴레이션 파일 전송”스키마가 자리를 잡으면, 각 사용자 릴레이션의 힙·인덱스 파일을 구 데이터 디렉터리에서 신 디렉터리로 옮긴다. 다섯 가지 전송 전략이 속도·안전성·유연성의 트레이드오프를 각각 다른 방식으로 처리한다.
| 전략 | 메커니즘 | 데이터 크기 비용 | 롤백 안전성 |
|---|---|---|---|
| Copy | cp / read+write | 전체 복사 | 구 클러스터 온전히 유지 |
| Clone | ioctl FICLONE / reflink | Copy-on-write, 거의 0 | 구 클러스터 온전히 유지 |
| Copy-file-range | copy_file_range(2) | 커널 내 복사 | 구 클러스터 온전히 유지 |
| Hard-link | link(2) | 거의 0 | 아이노드 공유 — 신 클러스터 기동 후 구 클러스터 불안전 |
| Swap | 디렉터리 rename | 거의 0 | 구 클러스터 디렉터리가 교체됨 |
트랜잭션 카운터 이식
섹션 제목: “트랜잭션 카운터 이식”물리 파일 이전이 끝나면, 신 클러스터는 구 클러스터의 트랜잭션 카운터(nextXid, nextOid, nextMultiXact, WAL 위치)를 상속해야 한다. 그래야 미래 트랜잭션 ID가 기존 MVCC 가시성 레코드와 충돌하지 않는다. pg_resetwal로 전체 서버 사이클 없이 이 값들을 신 클러스터의 pg_control에 기록한다.
이론 ↔ PostgreSQL 매핑
섹션 제목: “이론 ↔ PostgreSQL 매핑”| 개념 | PostgreSQL 명칭 |
|---|---|
| 구 클러스터 디스크립터 | ClusterInfo old_cluster |
| 신 클러스터 디스크립터 | ClusterInfo new_cluster |
| 릴레이션별 파일 매핑 | FileNameMap |
| 전송 전략 열거형 | transferMode / TRANSFER_MODE_* |
| 서버로 전달하는 바이너리 업그레이드 플래그 | --binary-upgrade (pg_dump / pg_restore) |
| 트랜잭션 카운터 기록 | copy_xact_xlog_xid의 pg_resetwal 호출 |
PostgreSQL의 접근 방식
섹션 제목: “PostgreSQL의 접근 방식”전체 파이프라인
섹션 제목: “전체 파이프라인”pg_upgrade.c의 main()은 선형 파이프라인을 실행한다. “OLD”와 “NEW”로 주석 표시된 두 단계가 각각 포스트마스터를 기동하고 종료한다.
// main — src/bin/pg_upgrade/pg_upgrade.cparseCommandLine(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와 ControlData
섹션 제목: “ClusterInfo와 ControlData”구/신 클러스터의 모든 상태는 전역 ClusterInfo 인스턴스 두 개에 담긴다.
// ClusterInfo, ControlData — src/bin/pg_upgrade/pg_upgrade.htypedef 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_data는 blocksz, walseg, align, index, toast, large_object 등 여러 필드를 교차 검사한다. 불일치가 발견되면 즉시 중단된다.
사전 검사 배터리
섹션 제목: “사전 검사 배터리”check_and_dump_old_cluster와 check_new_cluster(둘 다 check.c에 있음)가 사전 검사 배터리를 실행한다. 주요 검사 항목은 다음과 같다.
// check_and_dump_old_cluster — src/bin/pg_upgrade/check.ccheck_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.htypedef 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 신규)는 다른 전송 모드와 구조적으로 다르다. 개별 파일을 복사하거나 링크하지 않고, 다음 세 단계로 동작한다.
- 구 클러스터의 데이터베이스 디렉터리 전체(
$PGDATA/base/$db_oid)를rename(2)으로 신 클러스터 슬롯으로 이동한다. - 이동된 디렉터리에서 pg_restore가 생성한 카탈로그 파일을 신 데이터베이스 디렉터리로 되돌린다.
- 남은 구 카탈로그 파일은 구 클러스터 하위
moved_for_upgrade/로 옮겨서 나중에delete_old_cluster.sh가 정리할 수 있게 한다.
// do_swap / swap_catalog_files — src/bin/pg_upgrade/relfilenumber.cstatic voidswap_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의 릴레이션 파일 전송 분기.
char 부호 처리 (PG18)
섹션 제목: “char 부호 처리 (PG18)”PG18에서 char 타입의 부호 여부(ControlData의 default_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.relfrozenxid와 pg_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_outputdirs—pg_upgrade_output.d/$timestamp/{dump,log}/를 생성한다.prepare_new_cluster— 신 클러스터에vacuumdb --all --analyze와vacuumdb --all --freeze를 순서대로 실행한다. 구 카운터가 이식되기 전에 pg_statistic을 동결 상태로 만든다.prepare_new_globals—set_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_frozenxids—pg_class와pg_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.c — 사전 검사
섹션 제목: “check.c — 사전 검사”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_compatibility—get_control_data+check_control_data호출. live-check 모드에서 포트 충돌 거부.check_for_data_types_usage—data_types_usage_checks[]를 순회하며UpgradeTask단계를 만들어 데이터베이스별로 실행한다. 실패를 보고하고 중단한다.DataTypesUsageChecks— 정적 배열. 각 항목은status,report_filename,base_query,report_text,threshold_version, 옵션으로version_hook을 가진다.check_for_not_null_inheritance— PG18 신규: 부모 NOT NULL 제약을 누락한 자식 테이블 거부 (스키마 복원 실패 방지).
relfilenumber.c — 파일 전송
섹션 제목: “relfilenumber.c — 파일 전송”transfer_all_new_tablespaces— 진입점. 모드별로 분기한다(jobs > 1이면 테이블스페이스별 병렬).transfer_all_new_dbs— 구/신 데이터베이스 쌍을 순회하며gen_db_file_maps후transfer_single_new_db를 호출한다.transfer_single_new_db—vm_must_add_frozenbit를 확인하고,do_swap또는 맵별transfer_relfile루프로 분기한다.transfer_relfile— 1 GB 세그먼트 파일 +_fsm/_vm포크를 순회한다. frozen 비트를 추가해야 할 때rewriteVisibilityMap을 호출하고, 모드에 따라cloneFile/copyFile/copyFileByRange/linkFile로 분기한다.do_swap— 맵을 relfilenumber 순으로 정렬하고, 테이블스페이스별로prepare_for_swap후swap_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로 동작하며, 가득 차거나 끝날 때 비운다.
pg_upgrade.h — 핵심 타입
섹션 제목: “pg_upgrade.h — 핵심 타입”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 기준)”| 심볼 | 파일 | 줄 번호 |
|---|---|---|
main | pg_upgrade.c | 88 |
make_outputdirs | pg_upgrade.c | 252 |
setup | pg_upgrade.c | 337 |
set_locale_and_encoding | pg_upgrade.c | 440 |
prepare_new_cluster | pg_upgrade.c | 519 |
prepare_new_globals | pg_upgrade.c | 549 |
create_new_objects | pg_upgrade.c | 571 |
copy_xact_xlog_xid | pg_upgrade.c | 749 |
set_frozenxids | pg_upgrade.c | 874 |
create_logical_replication_slots | pg_upgrade.c | 976 |
set_new_cluster_char_signedness | pg_upgrade.c | 404 |
output_check_banner | check.c | 570 |
check_and_dump_old_cluster | check.c | 588 |
check_new_cluster | check.c | 709 |
check_cluster_versions | check.c | 849 |
check_cluster_compatibility | check.c | 904 |
check_for_data_types_usage | check.c | 463 |
data_types_usage_checks[] | check.c | 98 |
check_for_not_null_inheritance | check.c | — (grep: “not_null_inheritance”) |
DataTypesUsageChecks (struct) | check.c | 42 |
transfer_all_new_tablespaces | relfilenumber.c | 107 |
transfer_all_new_dbs | relfilenumber.c | 170 |
prepare_for_swap | relfilenumber.c | 236 |
swap_catalog_files | relfilenumber.c | 362 |
do_swap | relfilenumber.c | 452 |
transfer_single_new_db | relfilenumber.c | 500 |
transfer_relfile | relfilenumber.c | 552 |
sync_queue_push | relfilenumber.c | 74 |
FileNameMap (struct) | pg_upgrade.h | 180 |
ClusterInfo (struct) | pg_upgrade.h | 287 |
ControlData (struct) | pg_upgrade.h | 229 |
UserOpts (struct) | pg_upgrade.h | 328 |
transferMode (enum) | pg_upgrade.h | 259 |
RESTORE_TRANSACTION_SIZE | pg_upgrade.c | 58 |
DEFAULT_CHAR_SIGNEDNESS_CAT_VER | pg_upgrade.h | 132 |
소스 검증 (2026-06-06 기준)
섹션 제목: “소스 검증 (2026-06-06 기준)”확인된 사실
섹션 제목: “확인된 사실”-
--swap전송 모드는 REL_18_STABLE에 존재한다.TRANSFER_MODE_SWAP이transferMode열거형(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.cmain,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_xid는pg_xact를 무조건 복사한다. pg_multixact 파일은 두 클러스터 모두cat_ver >= MULTIXACT_FORMATCHANGE_CAT_VER인 경우에만 복사된다. 그렇지 않으면 파일은 복사하지 않고 카운터 값만 재설정한다. -
논리 복제 슬롯 이행은
count_old_cluster_logical_slots() > 0일 때에만 수행된다. 구 클러스터에 슬롯이 있을 때만 포스트마스터를 세 번째로 기동한다. 논리 복제를 쓰지 않는 클러스터에서 추가 포스트마스터 사이클을 피하기 위해서다.
미해결 사항
섹션 제목: “미해결 사항”-
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. -
--do-statistics옵션 동작.UserOpts.do_statistics가 파싱되지만, 파이프라인에서의 효과(별도의 통계 전송 단계를 트리거하는지 여부)를 읽은 범위에서 추적하지 못했다. 조사 경로:grep -n do_statistics /data/hgryoo/references/postgres/src/bin/pg_upgrade/*.c. -
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 할당을 구 클러스터에 맞게 강제하는 방법)