(KO) PostgreSQL pg_ctl / pg_controldata — 서버 생명주기 제어와 클러스터 상태 검사
목차
- 이론적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 안내
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
이론적 배경
섹션 제목: “이론적 배경”운영 수준의 데이터베이스 시스템은 부트스트래핑 문제를 피할 수 없다. 서버를 시작하고 멈추는 도구가 서버 자신에 의존할 수 없다는 문제다. Postmaster가 아직 실행 중이 아닌 시점에는 열 수 있는 SQL 연결도, 조회할 카탈로그도, 명령을 받을 세션도 없다. 제어 인터페이스는 반드시 대역 외(out-of-band) 경로여야 한다. 파일시스템과 OS 신호만으로 동작해야 한다는 뜻이다.
이런 인터페이스 설계는 두 가지 보완적 요구에서 출발한다.
생명주기 제어. 운영자는 서버 안에 내장된 지식(바이너리 경로, 허용 옵션, 선택한 PID)을 셸 스크립트로 직접 복제하지 않고도 서버를 시작·정지·재시작·재설정할 수 있어야 한다. 제어 도구가 그 상태를 자율적으로 파악하고 적절한 OS 신호를 적절한 시점에 전달해야 한다. 진지한 구현이라면 정지 모드를 세 단계로 구분한다.
- Smart(스마트): 새 연결을 거부하고 기존 세션이 자연스럽게 종료되기를 기다린 뒤 종료한다.
- Fast(패스트): 활성 세션을 즉시 끊고, WAL을 플러시하고, 셧다운 체크포인트를 쓴 뒤 종료한다.
- Immediate(즉각): 체크포인트 없이 즉시 종료한다. 전원을 뽑는 것과 동등하다. 다음 기동 시 충돌 복구가 뒤따른다.
활성 연결 없는 상태 검사. 충돌, 업그레이드 실패, 설정 오류는 서버가 연결을 받을 수 없는 상태를 남긴다. 관리자는 DBState, 체크포인트 LSN, 타임라인을 디스크 위의 파일에서 읽을 수 있어야 한다. 이를 위해서는 충돌에서 살아남고 독립적인 도구로 해독할 수 있는, 안정적이고 체크섬으로 보호된 온디스크 구조가 필요하다.
제어 파일(control file)은 두 번째 요구에 대한 표준 답변이다. 서버급 데이터베이스에 보편적으로 존재하며 pg_control, CURRENT, control01.ctl 같은 이름을 쓴다. Database Internals (Petrov, ch. 2 “B-Tree Basics”와 WAL 챕터)은 제어 파일이 충돌 복구 경로에서 가장 먼저 읽히는 파일이라고 설명한다. REDO 시작점, 타임라인, 이전 셧다운이 클린했는지의 확인이 여기에 담긴다. 하나의 디스크 섹터 안에 들어가는 원자적 쓰기(CRC로 보호)가 정확성 불변 조건이다. 찢긴 쓰기(torn write)는 반드시 감지할 수 있어야 한다. 손상된 기준점에서 복구를 진행하는 상황은 허용되지 않는다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”PID 파일 — 서버의 명함
섹션 제목: “PID 파일 — 서버의 명함”실행 중인 서버는 파일을 남겨야 한다. 보통 postmaster.pid 또는 mysql.pid라는 이름이며, PID, 시작 시간, 상태를 기록한다. 이 파일은 세 가지 역할을 동시에 수행한다.
- 상호 배제(Mutual exclusion). 두 번째 서버 기동 시도는 파일을 읽고 해당 PID가 여전히 살아있는지 확인한 뒤 기동을 거부한다. 같은 데이터 디렉터리를 두 서버가 공유하는 상황을 막는다.
- 신호 라우팅(Signal routing). 제어 도구는 하드코딩된 값이 아니라 파일에서 읽은 PID로 정지·재로드 신호를 보낸다. PID는 Unix에서 프로세스 간 안정적인 핸들의 유일한 형태다.
- 준비 완료 광고(Readiness advertisement). 알려진 라인 위치에 상태 필드가 있는 구조화된 PID 파일은 별도의 헬스체크 프로토콜 없이 제어 도구가 기동 완료를 폴링할 수 있게 한다.
제어 파일 — 원자적 쓰기 상태 레코드
섹션 제목: “제어 파일 — 원자적 쓰기 상태 레코드”충돌 복구가 있는 모든 DBMS는 복구 진입점을 담는 작은 레코드(한 섹터, ≤ 512바이트)가 필요하다. 표준 필드 구성은 다음과 같다.
| 필드 그룹 | 목적 |
|---|---|
| 버전 식별자 | 카탈로그를 읽기 전에 버전 또는 아키텍처 불일치를 감지 |
| 시스템 식별자 | 이 클러스터 인스턴스를 고유하게 식별하고 다른 클러스터의 WAL을 거부 |
| DBState / 생명주기 플래그 | 클린 셧다운, 복구 중, 충돌 상태를 구분 |
| 체크포인트 LSN + 복사본 | WAL 재생의 REDO 시작점 제공 |
| 타임라인 ID | 타임라인 전환(스탠바이 승격, PITR) 감지 및 추적 |
| WAL 레벨 파라미터 | 아카이빙·복제 설정이 실제 쓴 WAL과 일치하는지 확인 |
| 컴파일 타임 상수 | 첫 페이지 읽기 전 블록 크기·정렬 불일치 감지 |
원자적 쓰기 제약 — 활성 페이로드가 하나의 디스크 섹터 안에 들어와야 한다 — 은 PostgreSQL(PG_CONTROL_MAX_SAFE_SIZE = 512), Oracle(제어 파일 헤더 블록), MySQL/InnoDB(ibdata1 시스템 테이블스페이스 헤더)가 공유하는 설계 원칙이다. 이 제약을 위반하면 부분 쓰기가 일관성 없는 복구 기준점을 남길 수 있는 창이 열린다.
정지 모드별 신호 의미
섹션 제목: “정지 모드별 신호 의미”세 가지 정지 모드는 PostgreSQL에서 세 가지 Unix 신호에 대응한다. 유사한 매핑이 다른 서버에도 존재한다.
| 모드 | 신호 | Postmaster 동작 |
|---|---|---|
| Smart | SIGTERM | 새 연결 거부, 유휴 상태까지 대기 |
| Fast | SIGINT | 백엔드 종료, 체크포인트, 종료 |
| Immediate | SIGQUIT | 모든 자식 종료, 체크포인트 없이 즉시 종료 |
이 매핑을 이해하는 제어 도구는 서버가 관리 API를 노출할 필요가 없다. OS 신호 메커니즘 자체가 API다.
승격 — 스탠바이에서 프라이머리로
섹션 제목: “승격 — 스탠바이에서 프라이머리로”스탠바이 승격(promote)은 Postmaster에 Unix 신호만 보내는 방식으로 안전하게 처리할 수 없는 상태 전환이다. Postmaster는 신호가 승격 요청인지 일반적인 깨우기인지 알아야 한다. 표준 설계 패턴은 $PGDATA 안의 센티넬 파일(sentinel file)이다. 제어 도구가 파일을 만들고 SIGUSR1을 보내면 Postmaster가 신호를 받아 파일 존재 여부를 확인한다. 파일 기반 접근은 POSIX 파일시스템에서 원자적이다. open(O_CREAT) 시스콜이 직렬화 지점이며, 파일 생성과 신호 전달 사이의 짧은 경쟁 조건에서도 살아남는다.
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”pg_ctl — 얇은 오케스트레이션 셸
섹션 제목: “pg_ctl — 얇은 오케스트레이션 셸”pg_ctl은 독립적인 C 바이너리다(src/bin/pg_ctl/pg_ctl.c). PostgreSQL 백엔드 라이브러리에 링크되지 않는다. 유일한 백엔드 의존은 src/include/catalog/pg_control.h(ControlFileData와 DBState용)와 src/include/utils/pidfile.h(postmaster.pid 라인 위치용)다. 프로세스 생성, 신호 전달, 대기 루프는 모두 POSIX 시스콜이다.
명령 집합은 CtlCommand 열거형으로 인코딩된다.
// CtlCommand — src/bin/pg_ctl/pg_ctl.ctypedef enum{ NO_COMMAND = 0, INIT_COMMAND, START_COMMAND, STOP_COMMAND, RESTART_COMMAND, RELOAD_COMMAND, STATUS_COMMAND, PROMOTE_COMMAND, LOGROTATE_COMMAND, KILL_COMMAND, REGISTER_COMMAND, /* Windows service only */ UNREGISTER_COMMAND, /* Windows service only */ RUN_AS_SERVICE_COMMAND,} CtlCommand;서버 시작. do_start()는 start_postmaster()를 호출한다. 이 함수는 자식을 fork하고, /bin/sh -c "exec postgres -D … < /dev/null" 형태로 자식에서 실행한 뒤, 셸의 PID를 부모에게 돌려준다. 부모는 wait_for_postmaster_start()를 호출해 postmaster.pid를 최대 wait_seconds(기본 60초) 동안 10 Hz로 폴링한다.
// start_postmaster — src/bin/pg_ctl/pg_ctl.cpm_pid = fork();if (pm_pid == 0) { /* child: detach session, exec postgres via shell */ setsid(); cmd = psprintf("exec \"%s\" %s%s < \"%s\" 2>&1", exec_path, pgdata_opt, post_opts, DEVNULL); execl("/bin/sh", "/bin/sh", "-c", cmd, (char *) NULL); exit(1); /* exec failed */}return pm_pid; /* parent returns shell PID */대기 루프는 postmaster.pid의 8번째 줄(LOCK_FILE_LINE_PM_STATUS = 8)을 읽고 PM_STATUS_READY 또는 PM_STATUS_STANDBY를 확인한다.
// wait_for_postmaster_start — src/bin/pg_ctl/pg_ctl.cchar *pmstatus = optlines[LOCK_FILE_LINE_PM_STATUS - 1];if (strcmp(pmstatus, PM_STATUS_READY) == 0 || strcmp(pmstatus, PM_STATUS_STANDBY) == 0) return POSTMASTER_READY;Postmaster가 준비 완료 상태를 쓰기 전에 죽으면, pg_ctl은 get_control_dbstate()를 호출해 pg_control을 직접 읽는다. DB_SHUTDOWNED_IN_RECOVERY와 실제 기동 실패를 구별하기 위해서다.
서버 정지. do_stop()은 postmaster.pid에서 PID를 읽고, 모드에 맞는 신호(SIGTERM / SIGINT / SIGQUIT)를 보낸 뒤, PID 파일이 사라질 때까지 폴링한다.
// do_stop — src/bin/pg_ctl/pg_ctl.cShutdownMode Signal sentSMART_MODE SIGTERMFAST_MODE SIGINT (default)IMMEDIATE_MODE SIGQUITshutdown_mode 기본값은 FAST_MODE다. 신호가 OS 핸들이며, pg_ctl은 서버의 내부 함수를 직접 호출하지 않는다. --mode 인자에서 전역 변수 sig로의 매핑은 set_mode()에서 집중 처리된다. set_mode()가 stop-mode → 신호 대응 관계의 단일 진실 출처다.
// set_mode — src/bin/pg_ctl/pg_ctl.cif (strcmp(modeopt, "s") == 0 || strcmp(modeopt, "smart") == 0){ shutdown_mode = SMART_MODE; sig = SIGTERM;}else if (strcmp(modeopt, "f") == 0 || strcmp(modeopt, "fast") == 0){ shutdown_mode = FAST_MODE; sig = SIGINT;}else if (strcmp(modeopt, "i") == 0 || strcmp(modeopt, "immediate") == 0){ shutdown_mode = IMMEDIATE_MODE; sig = SIGQUIT;}sig는 파일 스코프 전역 변수로 SIGINT(fast 모드 기본값)로 초기화된다. 따라서 --mode 플래그 없이 pg_ctl stop을 실행하면 set_mode()에 진입하지 않고도 FAST_MODE 의미론이 이미 적용된 상태로 실행된다. kill 명령은 병렬적인 set_sig() 파서가 동일한 sig 전역 변수를 채운다. 그 덕분에 do_kill()은 모드 로직 없이 임의의 신호를 전달할 수 있다.
스탠바이 승격. do_promote()는 먼저 get_control_dbstate()를 호출해 DB_IN_ARCHIVE_RECOVERY 상태임을 확인한다. 스탠바이가 아닌 서버를 잘못 승격하는 상황을 막기 위해서다. 확인 후 빈 $PGDATA/promote 파일을 만들고 SIGUSR1을 보낸다.
// do_promote — src/bin/pg_ctl/pg_ctl.cif (get_control_dbstate() != DB_IN_ARCHIVE_RECOVERY){ write_stderr(_("%s: cannot promote server; " "server is not in standby mode\n"), progname); exit(1);}snprintf(promote_file, MAXPGPATH, "%s/promote", pg_data);if ((prmfile = fopen(promote_file, "w")) == NULL) { ... exit(1); }if (fclose(prmfile)) { ... exit(1); }sig = SIGUSR1;if (kill(pid, sig) != 0){ write_stderr(_("%s: could not send promote signal (PID: %d): %m\n"), progname, (int) pid); if (unlink(promote_file) != 0) /* best-effort cleanup on failure */ write_stderr(...); exit(1);}정확성 측면에서 두 가지가 눈에 띈다. 첫째, 단일 creat 대신 fopen/fclose 쌍을 쓴다. 빈 파일을 플러시하는 데 실패한 경우와 생성에 실패한 경우를 별개의 오류로 잡을 수 있도록 하기 위해서다. 둘째, 파일이 이미 생긴 뒤에 kill()이 실패하면 do_promote()는 센티넬 파일을 삭제(unlink)한다. 이후 재시작 때 오래된 promote 파일로 인해 묵시적으로 자동 승격이 일어나는 상황을 막기 위해서다. 신호가 전달된 뒤 wait_for_postmaster_promote()는 get_control_dbstate()를 폴링해 DB_IN_PRODUCTION을 관찰할 때까지 기다린다. PID 파일이 사라지거나 Postmaster가 승격 도중에 죽으면 일찍 빠져나온다.
// wait_for_postmaster_promote — src/bin/pg_ctl/pg_ctl.cfor (cnt = 0; cnt < wait_seconds * WAITS_PER_SEC; cnt++){ if ((pid = get_pgpid(false)) == 0) return false; /* pid file is gone */ if (kill(pid, 0) != 0) return false; /* postmaster died */
state = get_control_dbstate(); if (state == DB_IN_PRODUCTION) return true; /* successful promotion */
if (cnt % WAITS_PER_SEC == 0) print_msg("."); pg_usleep(USEC_PER_SEC / WAITS_PER_SEC);}return false; /* timeout reached */기동 및 정지 대기 루프와 같은 10 Hz 폴링 주기(WAITS_PER_SEC)를 쓰지만, 준비 완료 신호는 postmaster.pid의 라인이 아니라 pg_control에서 읽은 DBState다. 승격에는 전용 PID 파일 상태 라인이 없기 때문에, 프라이머리 전환이 완료됐음을 증명하는 권위 있는 증인은 제어 파일뿐이다.
재로드. do_reload()는 SIGHUP을 보낸다. Postmaster는 이를 모든 백엔드에 전달하며, 백엔드들은 postgresql.conf를 다시 읽는다. SIGHUP 처리는 비동기적이고 비파괴적이므로 별도의 대기 루프가 필요 없다.
get_control_dbstate — pg_ctl과 pg_control을 잇는 다리. 대기 루프와 승격 가드에서 호출되는 이 정적 헬퍼는 공유 유틸리티인 get_controlfile()로 pg_control을 읽는다.
// get_control_dbstate — src/bin/pg_ctl/pg_ctl.cstatic DBStateget_control_dbstate(void){ bool crc_ok; ControlFileData *ctl = get_controlfile(pg_data, &crc_ok); if (!crc_ok) { write_stderr("control file appears to be corrupt\n"); exit(1); } DBState ret = ctl->state; pfree(ctl); return ret;}ControlFileData와 pg_control 형식
섹션 제목: “ControlFileData와 pg_control 형식”ControlFileData(src/include/catalog/pg_control.h)는 $PGDATA/global/pg_control의 온디스크 레이아웃이다. REL_18_STABLE에서 PG_CONTROL_VERSION = 1800이다. 이 구조체는 의도적으로 PG_CONTROL_MAX_SAFE_SIZE = 512바이트 안에 들어오도록 설계됐다. 하나의 디스크 섹터로 모든 쓰기가 원자적임을 보장하기 위해서다.
// ControlFileData — src/include/catalog/pg_control.htypedef struct ControlFileData{ uint64 system_identifier; /* unique cluster ID (set at initdb) */ uint32 pg_control_version; /* PG_CONTROL_VERSION = 1800 in PG18 */ uint32 catalog_version_no; /* catversion.h; changes on catalog changes */ DBState state; /* current lifecycle state */ pg_time_t time; /* timestamp of last pg_control update */ XLogRecPtr checkPoint; /* LSN of last checkpoint record */ CheckPoint checkPointCopy; /* full body of that checkpoint record */ XLogRecPtr unloggedLSN; /* fake LSN counter for unlogged relations */ XLogRecPtr minRecoveryPoint; /* must replay at least to here */ TimeLineID minRecoveryPointTLI; XLogRecPtr backupStartPoint; /* set during online backup */ XLogRecPtr backupEndPoint; bool backupEndRequired; int wal_level; bool wal_log_hints; int MaxConnections; int max_worker_processes; int max_wal_senders; int max_prepared_xacts; int max_locks_per_xact; bool track_commit_timestamp; uint32 maxAlign; double floatFormat; /* = 1234567.0; architecture check */ uint32 blcksz; /* data block size */ uint32 relseg_size; /* blocks per large-relation segment */ uint32 xlog_blcksz; /* WAL block size */ uint32 xlog_seg_size; /* WAL segment size */ uint32 nameDataLen; /* NAMEDATALEN */ uint32 indexMaxKeys; uint32 toast_max_chunk_size; uint32 loblksize; bool float8ByVal; uint32 data_checksum_version; bool default_char_signedness; /* new in PG18 */ char mock_authentication_nonce[MOCK_AUTH_NONCE_LEN]; pg_crc32c crc; /* MUST BE LAST */} ControlFileData;DBState는 클러스터의 생명주기 상태 머신이다.
// DBState — src/include/catalog/pg_control.htypedef enum DBState{ DB_STARTUP = 0, DB_SHUTDOWNED, DB_SHUTDOWNED_IN_RECOVERY, DB_SHUTDOWNING, DB_IN_CRASH_RECOVERY, DB_IN_ARCHIVE_RECOVERY, DB_IN_PRODUCTION,} DBState;DB_SHUTDOWNED만이 복구 없이 클린하게 기동되는 상태다. 다른 모든 상태는 다음 기동 시 WAL 재생을 유발한다. pg_ctl은 DBState를 두 곳에서 활용한다. 승격 가드(반드시 DB_IN_ARCHIVE_RECOVERY여야 함)와 기동 대기 폴백(DB_SHUTDOWNED_IN_RECOVERY를 비오류 종료로 처리)이다.
checkPointCopy에 내장된 CheckPoint 구조체는 다음 필드를 담는다.
// CheckPoint — src/include/catalog/pg_control.htypedef struct CheckPoint{ XLogRecPtr redo; /* REDO start LSN */ TimeLineID ThisTimeLineID; TimeLineID PrevTimeLineID; /* non-zero if this record begins a new TL */ bool fullPageWrites; int wal_level; FullTransactionId nextXid; Oid nextOid; MultiXactId nextMulti; MultiXactOffset nextMultiOffset; TransactionId oldestXid; Oid oldestXidDB; MultiXactId oldestMulti; Oid oldestMultiDB; pg_time_t time; TransactionId oldestCommitTsXid; TransactionId newestCommitTsXid; TransactionId oldestActiveXid;} CheckPoint;controldata_utils.c를 통한 읽기/쓰기 경로
섹션 제목: “controldata_utils.c를 통한 읽기/쓰기 경로”백엔드 코드와 프론트엔드 코드 모두 src/common/controldata_utils.c를 공유한다. get_controlfile()은 $PGDATA/global/pg_control 경로를 구성하고 O_RDONLY로 열어 정확히 sizeof(ControlFileData) 바이트를 읽은 뒤 CRC32c를 검증한다. 프론트엔드(도구) 모드에서는 CRC 불일치 시 10ms 슬립을 두며 최대 10회 재시도한다. 실행 중인 서버가 부분 쓰기를 진행하는 도중에 읽는 상황을 방어하기 위해서다.
// get_controlfile_by_exact_path — src/common/controldata_utils.cretry: fd = open(ControlFilePath, O_RDONLY | PG_BINARY, 0); r = read(fd, ControlFile, sizeof(ControlFileData)); close(fd); INIT_CRC32C(crc); COMP_CRC32C(crc, ControlFile, offsetof(ControlFileData, crc)); FIN_CRC32C(crc); *crc_ok_p = EQ_CRC32C(crc, ControlFile->crc); if (!*crc_ok_p && retries < 10) { retries++; pg_usleep(10000); goto retry; }update_controlfile()은 버퍼를 PG_CONTROL_FILE_SIZE(8192바이트)까지 제로 패딩하고, CRC를 재계산하며, O_WRONLY로 쓴다. 물리적 파일 크기를 형식 변경과 무관하게 일정하게 유지하는 이유가 있다. 구버전 바이너리가 짧은 읽기가 아닌 버전 불일치 오류로 감지하게 하기 위해서다. 백엔드 모드에서는 호출자가 이 함수를 호출하기 전에 ControlFileLock을 잡아야 한다.
pg_controldata — 읽기 전용 필드 출력기
섹션 제목: “pg_controldata — 읽기 전용 필드 출력기”pg_controldata(src/bin/pg_controldata/pg_controldata.c)는 get_controlfile()을 호출해 모든 필드를 printf()로 출력하는 최소한의 프로그램이다. 필드 포매팅 외에는 어떤 로직도 없다. 주목할 점 몇 가지가 있다.
#define FRONTEND 1을 정의하지만postgres_fe.h가 아닌postgres.h를#include한다. WAL 내부 타입(xlog_internal.h,transam.h)이 백엔드 헤더에만 있기 때문이다.- PG18에서 새로 추가된
default_char_signedness필드는signed/unsigned로 출력된다. 이 값은initdb시점 플랫폼의 기본char부호를 인코딩한다. ARM(unsigned)과 x86(signed) 사이 크로스 컴파일이나 마이그레이션 시 관련성이 생긴다. data_checksum_version은 페이지 체크섬이 비활성화됐을 때 0이며, 0이 아닌 값은 사용 중인 체크섬 알고리즘 버전을 나타낸다.
// main (pg_controldata) — src/bin/pg_controldata/pg_controldata.cControlFile = get_controlfile(DataDir, &crc_ok);if (!crc_ok) pg_log_warning("calculated CRC checksum does not match value stored in control file");
printf("pg_control version number: %u\n", ControlFile->pg_control_version);printf("Database cluster state: %s\n", dbState(ControlFile->state));printf("Latest checkpoint location: %X/%X\n", LSN_FORMAT_ARGS(ControlFile->checkPoint));// ... (all ~40 fields)printf("Default char data signedness: %s\n", ControlFile->default_char_signedness ? "signed" : "unsigned");printf("Mock authentication nonce: %s\n", mock_auth_nonce_str);생명주기 흐름 다이어그램
섹션 제목: “생명주기 흐름 다이어그램”flowchart TD
A["pg_ctl start<br/>do_start()"] --> B["start_postmaster()<br/>fork + exec postgres"]
B --> C["postmaster.pid 폴링<br/>wait_for_postmaster_start()"]
C --> D{"LOCK_FILE_LINE_PM_STATUS<br/>== PM_STATUS_READY?"}
D -- yes --> E["exit 0: 서버 시작 완료"]
D -- postmaster 종료 --> F["get_control_dbstate()<br/>pg_control 직접 읽기"]
F --> G{"DBState?"}
G -- DB_SHUTDOWNED_IN_RECOVERY --> H["exit 0: 복구 중 셧다운"]
G -- other --> I["exit 1: 기동 실패"]
D -- timeout --> J["exit 1: 제한 시간 내 기동 실패"]
K["pg_ctl stop<br/>do_stop()"] --> L["get_pgpid()<br/>postmaster.pid 읽기"]
L --> M["kill pid sig<br/>SIGTERM/SIGINT/SIGQUIT"]
M --> N["폴링: postmaster.pid 삭제됐나?<br/>wait_for_postmaster_stop()"]
N -- gone --> O["exit 0: 서버 정지 완료"]
N -- timeout --> P["exit 1: 서버가 종료되지 않음"]
Q["pg_ctl promote<br/>do_promote()"] --> R["get_control_dbstate()"]
R --> S{"DB_IN_ARCHIVE_RECOVERY?"}
S -- no --> T["exit 1: 스탠바이가 아님"]
S -- yes --> U["promote 파일 생성<br/>SIGUSR1 전송"]
U --> V["get_control_dbstate() 폴링<br/>wait_for_postmaster_promote()"]
V --> W{"DB_IN_PRODUCTION?"}
W -- yes --> X["exit 0: 승격 완료"]
W -- timeout --> Y["exit 1: 승격 시간 초과"]
DBState 전환 다이어그램
섹션 제목: “DBState 전환 다이어그램”flowchart LR
S0["DB_STARTUP"] --> S6["DB_IN_PRODUCTION"]
S0 --> S4["DB_IN_CRASH_RECOVERY"]
S0 --> S5["DB_IN_ARCHIVE_RECOVERY"]
S6 --> S3["DB_SHUTDOWNING"]
S3 --> S1["DB_SHUTDOWNED"]
S5 --> S6
S4 --> S1
S5 --> S2["DB_SHUTDOWNED_IN_RECOVERY"]
소스 코드 안내
섹션 제목: “소스 코드 안내”pg_ctl.c — 함수 목록
섹션 제목: “pg_ctl.c — 함수 목록”| 심볼 | 역할 |
|---|---|
CtlCommand (enum) | 명령 판별자 |
ShutdownMode (enum) | SMART_MODE, FAST_MODE, IMMEDIATE_MODE |
WaitPMResult (enum) | 기동 대기 결과 |
main | 옵션 파싱, 파일 경로 구성, ctl_command 디스패치 |
do_init | initdb fork |
do_start | postgres fork, wait_for_postmaster_start로 대기 |
do_stop | SIGTERM/INT/QUIT 전송, wait_for_postmaster_stop로 대기 |
do_restart | do_stop 후 do_start |
do_reload | SIGHUP 전송 |
do_promote | promote 센티넬 파일 생성, SIGUSR1 전송, 대기 |
do_logrotate | logrotate 센티넬 파일 생성, SIGUSR1 전송 |
do_status | postmaster.pid에서 PID와 옵션 출력 |
do_kill | 지정 PID에 임의 신호 전송 |
start_postmaster | fork + exec /bin/sh -c "exec postgres …" |
wait_for_postmaster_start | postmaster.pid 8번째 줄을 10 Hz로 폴링 |
wait_for_postmaster_stop | postmaster.pid 부재를 10 Hz로 폴링 |
wait_for_postmaster_promote | get_control_dbstate()를 10 Hz로 폴링 |
get_pgpid | postmaster.pid 1번째 줄에서 PID 읽기 |
get_control_dbstate | get_controlfile()로 pg_control 읽어 state 반환 |
read_post_opts | postmaster.opts에서 저장된 옵션 읽기 (재시작 시 사용) |
postmaster_is_alive | kill(pid, 0) 생존 확인 |
trap_sigint_during_startup | 기동 대기 중 SIGINT를 Postmaster로 전달 |
set_mode | --mode를 ShutdownMode로 파싱; 전역 sig를 SIGTERM/SIGINT/SIGQUIT로 설정 |
set_sig | kill -s 신호 이름을 전역 sig로 파싱 |
adjust_data_dir | -D가 설정 전용 디렉터리를 가리킬 때 처리 |
pg_controldata.c — 함수 목록
섹션 제목: “pg_controldata.c — 함수 목록”| 심볼 | 역할 |
|---|---|
main | -D 파싱, get_controlfile() 호출, 모든 필드 출력 |
dbState | DBState 열거값을 사람이 읽을 수 있는 문자열로 변환 |
wal_level_str | WalLevel 열거값을 문자열로 변환 |
controldata_utils.c — 공유 읽기/쓰기 경로
섹션 제목: “controldata_utils.c — 공유 읽기/쓰기 경로”| 심볼 | 역할 |
|---|---|
get_controlfile | $PGDATA/global/pg_control 경로 구성 후 get_controlfile_by_exact_path에 위임 |
get_controlfile_by_exact_path | 열기, 읽기, CRC 검증; 프론트엔드 모드에서 최대 10회 재시도 |
update_controlfile | CRC 재계산, 8192바이트까지 제로 패딩, 쓰기; do_sync가 fsync 여부를 제어 |
주요 구조체와 상수
섹션 제목: “주요 구조체와 상수”| 심볼 | 헤더 | 비고 |
|---|---|---|
ControlFileData | catalog/pg_control.h | pg_control 온디스크 레이아웃; 활성 페이로드 ≤ 512B |
CheckPoint | catalog/pg_control.h | ControlFileData.checkPointCopy에 내장 |
DBState | catalog/pg_control.h | 7개 값의 생명주기 열거형 |
PG_CONTROL_VERSION | catalog/pg_control.h | REL_18_STABLE에서 1800 |
PG_CONTROL_MAX_SAFE_SIZE | catalog/pg_control.h | 512 — 단일 섹터 원자적 쓰기 한계 |
PG_CONTROL_FILE_SIZE | catalog/pg_control.h | 8192 — 물리적 파일 크기, 버전 불일치 탐지용 |
LOCK_FILE_LINE_PM_STATUS | utils/pidfile.h | postmaster.pid의 8번째 줄 |
PM_STATUS_READY | utils/pidfile.h | "ready " — 준비 완료 센티넬 |
PM_STATUS_STANDBY | utils/pidfile.h | "standby " — 핫 스탠바이 준비 완료 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”REL_18_STABLE 커밋 273fe94 기준 위치 힌트. 심볼이 안정적인 앵커이며, 라인 번호는 트리가 변하면 달라진다.
| 심볼 | 파일 | 대략적 줄 번호 |
|---|---|---|
CtlCommand enum | src/bin/pg_ctl/pg_ctl.c | 53 |
ShutdownMode enum | src/bin/pg_ctl/pg_ctl.c | 37 |
WaitPMResult enum | src/bin/pg_ctl/pg_ctl.c | 44 |
main | src/bin/pg_ctl/pg_ctl.c | 2202 |
do_start | src/bin/pg_ctl/pg_ctl.c | 931 |
do_stop | src/bin/pg_ctl/pg_ctl.c | 1027 |
do_restart | src/bin/pg_ctl/pg_ctl.c | 1085 |
do_reload | src/bin/pg_ctl/pg_ctl.c | 1149 |
do_promote | src/bin/pg_ctl/pg_ctl.c | 1186 |
do_logrotate | src/bin/pg_ctl/pg_ctl.c | 1267 |
do_status | src/bin/pg_ctl/pg_ctl.c | 1348 |
do_kill | src/bin/pg_ctl/pg_ctl.c | 1405 |
start_postmaster | src/bin/pg_ctl/pg_ctl.c | 439 |
wait_for_postmaster_start | src/bin/pg_ctl/pg_ctl.c | 593 |
wait_for_postmaster_stop | src/bin/pg_ctl/pg_ctl.c | 717 |
wait_for_postmaster_promote | src/bin/pg_ctl/pg_ctl.c | 754 |
get_pgpid | src/bin/pg_ctl/pg_ctl.c | 246 |
get_control_dbstate | src/bin/pg_ctl/pg_ctl.c | 2183 |
postmaster_is_alive | src/bin/pg_ctl/pg_ctl.c | 1324 |
trap_sigint_during_startup | src/bin/pg_ctl/pg_ctl.c | 857 |
read_post_opts | src/bin/pg_ctl/pg_ctl.c | 802 |
set_mode | src/bin/pg_ctl/pg_ctl.c | 2047 |
set_sig | src/bin/pg_ctl/pg_ctl.c | 2075 |
dbState | src/bin/pg_controldata/pg_controldata.c | 49 |
wal_level_str | src/bin/pg_controldata/pg_controldata.c | 73 |
main (pg_controldata) | src/bin/pg_controldata/pg_controldata.c | 88 |
get_controlfile | src/common/controldata_utils.c | 52 |
get_controlfile_by_exact_path | src/common/controldata_utils.c | 68 |
update_controlfile | src/common/controldata_utils.c | 189 |
ControlFileData struct | src/include/catalog/pg_control.h | 104 |
CheckPoint struct | src/include/catalog/pg_control.h | 35 |
DBState enum | src/include/catalog/pg_control.h | 89 |
PG_CONTROL_VERSION | src/include/catalog/pg_control.h | 25 |
PG_CONTROL_MAX_SAFE_SIZE | src/include/catalog/pg_control.h | 247 |
PG_CONTROL_FILE_SIZE | src/include/catalog/pg_control.h | 256 |
LOCK_FILE_LINE_PM_STATUS | src/include/utils/pidfile.h | 44 |
PM_STATUS_READY | src/include/utils/pidfile.h | 53 |
PM_STATUS_STANDBY | src/include/utils/pidfile.h | 54 |
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”다른 데이터베이스의 제어 유틸리티
섹션 제목: “다른 데이터베이스의 제어 유틸리티”MySQL / MariaDB는 유사한 역할로 mysqladmin / mysqld_safe를 사용한다. mysqld를 fork하고 PID 파일을 감시하며 셧다운 시 SIGTERM을 보내는 래퍼 스크립트다. InnoDB 시스템 테이블스페이스 헤더(ibdata1, 페이지 0)가 제어 파일 역할을 하며, 마지막 체크포인트 LSN과 테이블스페이스 ID를 페이지 체크섬으로 보호해 저장한다. PostgreSQL과 달리 MySQL은 이 정보를 별도 파일이 아닌 테이블스페이스 안에 저장한다. 제어 레코드와 스토리지 엔진이 결합되는 설계 선택이다.
Oracle은 두 개 이상의 제어 파일을 별도 마운트 포인트로 미러링한다. RMAN 백업 카탈로그와 아카이브 로그 이력도 함께 저장하기 때문에 Oracle 제어 파일은 512바이트가 아닌 메가바이트 단위로 훨씬 크다. 미러링은 고가용성 조치이며 PostgreSQL에는 없다. PostgreSQL은 단일 pg_control이 동일 파일시스템에서 살아남는 것에 의존한다.
SQLite는 데이터베이스 파일 오프셋 0의 100바이트 헤더에 데이터베이스 상태를 저장한다. 가장 최소한의 제어 레코드 설계다. 오프셋 24의 “change counter”가 PostgreSQL CRC의 역할을 한다. 카운터가 변한 것을 감지한 리더는 공유 캐시를 다시 읽어야 함을 안다. 별도 제어 파일은 존재하지 않는다. 데이터베이스 파일 자체가 제어 파일이다.
연구 맥락
섹션 제목: “연구 맥락”pg_control 설계는 두 가지 고전적인 충돌 복구 통찰을 반영한다.
첫째, ARIES(Mohan et al., 1992)는 복구 관리자가 충돌에서 살아남는 구조에서 REDO 시작점을 찾을 수 있어야 한다는 원칙을 정립했다. ARIES 용어로 “마스터 레코드(master record)“라 부르는 이 개념이 ControlFileData의 checkPointCopy.redo에 직접 대응한다.
둘째, 제어 파일 쓰기의 원자성은 선행 기록 로그(write-ahead logging) 불변 조건의 특수한 경우다. 데이터 변경이 내구적으로 간주되기 전에, 그 위치를 지명하는 메타데이터 레코드가 반드시 먼저 내구적이어야 한다. CRC를 갖고 한 섹터 안에 들어오는 pg_control 쓰기가 두 번째 WAL 레코드 없이 이 보장을 달성하는 방법이다.
system_identifier 필드(initdb 시 설정된 64비트 난수값)는 HA 클러스터의 스플릿 브레인(split-brain) 문제에 대한 PostgreSQL의 답변이다. 프라이머리로 승격된 스탠바이는 이전 프라이머리의 WAL을 적용하지 않는다. WAL이 다른 system_identifier를 담고 있기 때문이다. 이 단순한 확인이 강등된 프라이머리가 예전 WAL 스트림에 다시 붙는 파국적 상황을 막는다.
mock_authentication_nonce(32바이트 난수값)는 PG10에서 추가됐다. 사용자가 존재하지 않아도 클러스터 고유 값으로 진행되는 SASL 인증 교환의 타이밍 사이드채널을 막기 위해서다. 서버 재시작에서 살아남고 카탈로그 접근 이전에 사용 가능해야 하기 때문에 pg_control에 저장된다. pg_control이 담도록 설계된 안정적인 사전 카탈로그 상태의 전형적인 사례다.
주요 소스 파일 (REL_18_STABLE, 커밋 273fe94):
src/bin/pg_ctl/pg_ctl.csrc/bin/pg_controldata/pg_controldata.csrc/include/catalog/pg_control.hsrc/common/controldata_utils.csrc/include/utils/pidfile.h
이 KB 내 교차 참조:
postgres-postmaster.md— pg_ctl이 기동하는 Postmaster 프로세스postgres-xlog-wal.md— WAL 메커니즘, 체크포인트 LSN 해석postgres-checkpoint.md—pg_control을 갱신하는 체크포인트 쓰기postgres-recovery-redo.md— REDO 시작 시pg_control읽기postgres-backup-basebackup.md—backupStartPoint/backupEndRequired필드postgres-pg-dump-restore.md— pg_control 의존 없는 논리 백업 대응
연구 자료 및 교재:
- Mohan et al., “ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging,” ACM TODS 17(1), 1992 — REDO 시작 “마스터 레코드” 기원
- Petrov, Database Internals (O’Reilly, 2019), ch. 7 “Log-Structured Storage” — 제어 파일과 WAL 부트스트랩
- Stonebraker & Rowe, “The Design of POSTGRES,” ACM SIGMOD 1986 — 원래의 프로세스 모델 설계 근거