콘텐츠로 이동

(KO) PostgreSQL pg_ctl / pg_controldata — 서버 생명주기 제어와 클러스터 상태 검사

목차

운영 수준의 데이터베이스 시스템은 부트스트래핑 문제를 피할 수 없다. 서버를 시작하고 멈추는 도구가 서버 자신에 의존할 수 없다는 문제다. 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)는 반드시 감지할 수 있어야 한다. 손상된 기준점에서 복구를 진행하는 상황은 허용되지 않는다.

실행 중인 서버는 파일을 남겨야 한다. 보통 postmaster.pid 또는 mysql.pid라는 이름이며, PID, 시작 시간, 상태를 기록한다. 이 파일은 세 가지 역할을 동시에 수행한다.

  1. 상호 배제(Mutual exclusion). 두 번째 서버 기동 시도는 파일을 읽고 해당 PID가 여전히 살아있는지 확인한 뒤 기동을 거부한다. 같은 데이터 디렉터리를 두 서버가 공유하는 상황을 막는다.
  2. 신호 라우팅(Signal routing). 제어 도구는 하드코딩된 값이 아니라 파일에서 읽은 PID로 정지·재로드 신호를 보낸다. PID는 Unix에서 프로세스 간 안정적인 핸들의 유일한 형태다.
  3. 준비 완료 광고(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 동작
SmartSIGTERM새 연결 거부, 유휴 상태까지 대기
FastSIGINT백엔드 종료, 체크포인트, 종료
ImmediateSIGQUIT모든 자식 종료, 체크포인트 없이 즉시 종료

이 매핑을 이해하는 제어 도구는 서버가 관리 API를 노출할 필요가 없다. OS 신호 메커니즘 자체가 API다.

승격 — 스탠바이에서 프라이머리로

섹션 제목: “승격 — 스탠바이에서 프라이머리로”

스탠바이 승격(promote)은 Postmaster에 Unix 신호만 보내는 방식으로 안전하게 처리할 수 없는 상태 전환이다. Postmaster는 신호가 승격 요청인지 일반적인 깨우기인지 알아야 한다. 표준 설계 패턴은 $PGDATA 안의 센티넬 파일(sentinel file)이다. 제어 도구가 파일을 만들고 SIGUSR1을 보내면 Postmaster가 신호를 받아 파일 존재 여부를 확인한다. 파일 기반 접근은 POSIX 파일시스템에서 원자적이다. open(O_CREAT) 시스콜이 직렬화 지점이며, 파일 생성과 신호 전달 사이의 짧은 경쟁 조건에서도 살아남는다.

pg_ctl — 얇은 오케스트레이션 셸

섹션 제목: “pg_ctl — 얇은 오케스트레이션 셸”

pg_ctl은 독립적인 C 바이너리다(src/bin/pg_ctl/pg_ctl.c). PostgreSQL 백엔드 라이브러리에 링크되지 않는다. 유일한 백엔드 의존은 src/include/catalog/pg_control.h(ControlFileDataDBState용)와 src/include/utils/pidfile.h(postmaster.pid 라인 위치용)다. 프로세스 생성, 신호 전달, 대기 루프는 모두 POSIX 시스콜이다.

명령 집합은 CtlCommand 열거형으로 인코딩된다.

// CtlCommand — src/bin/pg_ctl/pg_ctl.c
typedef 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.c
pm_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.c
char *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.c
ShutdownMode Signal sent
SMART_MODE SIGTERM
FAST_MODE SIGINT (default)
IMMEDIATE_MODE SIGQUIT

shutdown_mode 기본값은 FAST_MODE다. 신호가 OS 핸들이며, pg_ctl은 서버의 내부 함수를 직접 호출하지 않는다. --mode 인자에서 전역 변수 sig로의 매핑은 set_mode()에서 집중 처리된다. set_mode()가 stop-mode → 신호 대응 관계의 단일 진실 출처다.

// set_mode — src/bin/pg_ctl/pg_ctl.c
if (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.c
if (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.c
for (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.c
static DBState
get_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(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.h
typedef 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.h
typedef 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_ctlDBState를 두 곳에서 활용한다. 승격 가드(반드시 DB_IN_ARCHIVE_RECOVERY여야 함)와 기동 대기 폴백(DB_SHUTDOWNED_IN_RECOVERY를 비오류 종료로 처리)이다.

checkPointCopy에 내장된 CheckPoint 구조체는 다음 필드를 담는다.

// CheckPoint — src/include/catalog/pg_control.h
typedef 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.c
retry:
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.c
ControlFile = 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: 승격 시간 초과"]
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"]
심볼역할
CtlCommand (enum)명령 판별자
ShutdownMode (enum)SMART_MODE, FAST_MODE, IMMEDIATE_MODE
WaitPMResult (enum)기동 대기 결과
main옵션 파싱, 파일 경로 구성, ctl_command 디스패치
do_initinitdb fork
do_startpostgres fork, wait_for_postmaster_start로 대기
do_stopSIGTERM/INT/QUIT 전송, wait_for_postmaster_stop로 대기
do_restartdo_stopdo_start
do_reloadSIGHUP 전송
do_promotepromote 센티넬 파일 생성, SIGUSR1 전송, 대기
do_logrotatelogrotate 센티넬 파일 생성, SIGUSR1 전송
do_statuspostmaster.pid에서 PID와 옵션 출력
do_kill지정 PID에 임의 신호 전송
start_postmasterfork + exec /bin/sh -c "exec postgres …"
wait_for_postmaster_startpostmaster.pid 8번째 줄을 10 Hz로 폴링
wait_for_postmaster_stoppostmaster.pid 부재를 10 Hz로 폴링
wait_for_postmaster_promoteget_control_dbstate()를 10 Hz로 폴링
get_pgpidpostmaster.pid 1번째 줄에서 PID 읽기
get_control_dbstateget_controlfile()pg_control 읽어 state 반환
read_post_optspostmaster.opts에서 저장된 옵션 읽기 (재시작 시 사용)
postmaster_is_alivekill(pid, 0) 생존 확인
trap_sigint_during_startup기동 대기 중 SIGINT를 Postmaster로 전달
set_mode--modeShutdownMode로 파싱; 전역 sig를 SIGTERM/SIGINT/SIGQUIT로 설정
set_sigkill -s 신호 이름을 전역 sig로 파싱
adjust_data_dir-D가 설정 전용 디렉터리를 가리킬 때 처리
심볼역할
main-D 파싱, get_controlfile() 호출, 모든 필드 출력
dbStateDBState 열거값을 사람이 읽을 수 있는 문자열로 변환
wal_level_strWalLevel 열거값을 문자열로 변환

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_controlfileCRC 재계산, 8192바이트까지 제로 패딩, 쓰기; do_syncfsync 여부를 제어
심볼헤더비고
ControlFileDatacatalog/pg_control.hpg_control 온디스크 레이아웃; 활성 페이로드 ≤ 512B
CheckPointcatalog/pg_control.hControlFileData.checkPointCopy에 내장
DBStatecatalog/pg_control.h7개 값의 생명주기 열거형
PG_CONTROL_VERSIONcatalog/pg_control.hREL_18_STABLE에서 1800
PG_CONTROL_MAX_SAFE_SIZEcatalog/pg_control.h512 — 단일 섹터 원자적 쓰기 한계
PG_CONTROL_FILE_SIZEcatalog/pg_control.h8192 — 물리적 파일 크기, 버전 불일치 탐지용
LOCK_FILE_LINE_PM_STATUSutils/pidfile.hpostmaster.pid의 8번째 줄
PM_STATUS_READYutils/pidfile.h"ready " — 준비 완료 센티넬
PM_STATUS_STANDBYutils/pidfile.h"standby " — 핫 스탠바이 준비 완료

REL_18_STABLE 커밋 273fe94 기준 위치 힌트. 심볼이 안정적인 앵커이며, 라인 번호는 트리가 변하면 달라진다.

심볼파일대략적 줄 번호
CtlCommand enumsrc/bin/pg_ctl/pg_ctl.c53
ShutdownMode enumsrc/bin/pg_ctl/pg_ctl.c37
WaitPMResult enumsrc/bin/pg_ctl/pg_ctl.c44
mainsrc/bin/pg_ctl/pg_ctl.c2202
do_startsrc/bin/pg_ctl/pg_ctl.c931
do_stopsrc/bin/pg_ctl/pg_ctl.c1027
do_restartsrc/bin/pg_ctl/pg_ctl.c1085
do_reloadsrc/bin/pg_ctl/pg_ctl.c1149
do_promotesrc/bin/pg_ctl/pg_ctl.c1186
do_logrotatesrc/bin/pg_ctl/pg_ctl.c1267
do_statussrc/bin/pg_ctl/pg_ctl.c1348
do_killsrc/bin/pg_ctl/pg_ctl.c1405
start_postmastersrc/bin/pg_ctl/pg_ctl.c439
wait_for_postmaster_startsrc/bin/pg_ctl/pg_ctl.c593
wait_for_postmaster_stopsrc/bin/pg_ctl/pg_ctl.c717
wait_for_postmaster_promotesrc/bin/pg_ctl/pg_ctl.c754
get_pgpidsrc/bin/pg_ctl/pg_ctl.c246
get_control_dbstatesrc/bin/pg_ctl/pg_ctl.c2183
postmaster_is_alivesrc/bin/pg_ctl/pg_ctl.c1324
trap_sigint_during_startupsrc/bin/pg_ctl/pg_ctl.c857
read_post_optssrc/bin/pg_ctl/pg_ctl.c802
set_modesrc/bin/pg_ctl/pg_ctl.c2047
set_sigsrc/bin/pg_ctl/pg_ctl.c2075
dbStatesrc/bin/pg_controldata/pg_controldata.c49
wal_level_strsrc/bin/pg_controldata/pg_controldata.c73
main (pg_controldata)src/bin/pg_controldata/pg_controldata.c88
get_controlfilesrc/common/controldata_utils.c52
get_controlfile_by_exact_pathsrc/common/controldata_utils.c68
update_controlfilesrc/common/controldata_utils.c189
ControlFileData structsrc/include/catalog/pg_control.h104
CheckPoint structsrc/include/catalog/pg_control.h35
DBState enumsrc/include/catalog/pg_control.h89
PG_CONTROL_VERSIONsrc/include/catalog/pg_control.h25
PG_CONTROL_MAX_SAFE_SIZEsrc/include/catalog/pg_control.h247
PG_CONTROL_FILE_SIZEsrc/include/catalog/pg_control.h256
LOCK_FILE_LINE_PM_STATUSsrc/include/utils/pidfile.h44
PM_STATUS_READYsrc/include/utils/pidfile.h53
PM_STATUS_STANDBYsrc/include/utils/pidfile.h54

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)“라 부르는 이 개념이 ControlFileDatacheckPointCopy.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.c
  • src/bin/pg_controldata/pg_controldata.c
  • src/include/catalog/pg_control.h
  • src/common/controldata_utils.c
  • src/include/utils/pidfile.h

이 KB 내 교차 참조:

  • postgres-postmaster.md — pg_ctl이 기동하는 Postmaster 프로세스
  • postgres-xlog-wal.md — WAL 메커니즘, 체크포인트 LSN 해석
  • postgres-checkpoint.mdpg_control을 갱신하는 체크포인트 쓰기
  • postgres-recovery-redo.md — REDO 시작 시 pg_control 읽기
  • postgres-backup-basebackup.mdbackupStartPoint / 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 — 원래의 프로세스 모델 설계 근거