(KO) PostgreSQL Postmaster — 클러스터 감독자, 프로세스 생명주기, 장애 복구
목차
- 이론적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 안내
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
이론적 배경
섹션 제목: “이론적 배경”다중 사용자 데이터베이스 서버는 근본적인 질문 하나를 반드시 풀어야 한다. 서버 프로그램이 어떻게 여러 개의 동시 실행 단위가 되는지, 그리고 그 단위들을 누가 조율하는지다. 이 질문에 대한 답이 장애 격리, 메모리 가시성, 스케줄링 동작, 그리고 장애 복구 전체 설계를 결정한다.
고전적인 아키텍처 답은 두 가지다.
-
감독자 하나 + 클라이언트별 워커(프로세스 또는 스레드). 오래 살아있는 조정자가 새 연결을 받아 자식에게 위임한다. 조정자는 사용자 SQL을 직접 처리하지 않고 멤버십 관리, 상태 감시, 장애 재시작만 담당한다. 예시: Apache httpd prefork, PostgreSQL 프로세스 모델, 오라클 dedicated-server 모드.
-
단일 프로세스 + 스레드 풀. 하나의 프로세스가 스레드 풀로 모든 연결을 처리한다. 주소 공간을 공유하므로 통신 비용이 낮지만, 한 스레드의 잘못된 포인터가 다른 세션의 상태를 망가뜨릴 수 있다. 예시: MySQL InnoDB, SQL Server, 현대 오라클.
Architecture of a Database System (Hellerstein et al., 2007, §2)은 두 모델을 비교하며, 프로세스 모델은 연결당 오버헤드가 높지만 장애 격리가 강하고, 스레드 모델은 세션 생성이 저렴하지만 장애 안전성 확보가 어렵다고 설명한다. PostgreSQL의 창립 논문인 “The Design of POSTGRES” (Stonebraker & Rowe, 1986)는 견고함을 이유로 프로세스 모델을 명시적으로 선택했다. 한 백엔드의 버그가 다른 백엔드의 스택을 오염시킬 수 없고, 운영 체제가 주소 공간 분리를 응용 수준 비용 없이 강제하기 때문이다.
감독자 역할은 두 번째 설계 질문을 더한다. 감독자와 공유 상태의 관계는 무엇인가? PostgreSQL의 답은 명확하다. Postmaster는 시작 시 공유 메모리를 한 번 만들고 크기를 영구 고정한 뒤, 순수한 프로세스 관리자가 된다. 사용자 데이터는 직접 읽지 않는다. 모든 자식 프로세스가 같은 공유 세그먼트에 붙는다. 버퍼 풀, 락 테이블, procarray, sinval 링 같은 공유 구조가 그 자체로 클러스터의 런타임 상태다. Postmaster가 소유하는 것은 리슨 소켓의 파일 디스크립터와 자식 PID에서 역할로의 매핑뿐이다.
이 설계에서 장애 복구의 결론이 나온다. Postmaster는 트랜잭션에 참여하지 않고 공유 메모리 상태를 직접 보유하지 않는다. 자식 충돌을 SIGCHLD로 감지한 뒤 형제들에게 신호를 보내고, 모두 종료되면 공유 메모리를 처음부터 재생성할 수 있다. 클러스터의 내구적 상태는 WAL과 데이터 파일에 있다. Postmaster 프로세스에는 없다. 이 구조 덕분에 “재시작”이란 “인메모리 구조를 재구성한 뒤 동일한 온디스크 상태에 다시 붙는 것”이 된다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”Postmaster 패턴은 프로세스 모델 데이터베이스 서버에서 반복적으로 나타난다. 일반적인 관례를 이해하면 PostgreSQL의 구체적인 선택이 잘 그려진 설계 공간 위의 한 점으로 읽힌다.
감독자는 쿼리 작업을 하지 않는다
섹션 제목: “감독자는 쿼리 작업을 하지 않는다”프로세스 모델을 채택한 성숙한 서버에서 감독자는 얇은 루프다. 연결을 accept()하고, 워커를 fork()하고, 파일 디스크립터를 넘기고, 다시 잠든다. 감독자는 트랜잭션 상태도, 캐시된 플랜도, 열린 릴레이션도 보유하지 않는다. 이것이 장애 복구를 결정론적(deterministic)으로 만드는 핵심 불변 조건이다. 감독자의 메모리가 항상 깨끗하다면 어떤 자식 충돌도 그 자식으로 한정된다.
초기 구현 중에는 조정자가 일부 요청을 직접 처리한 경우가 있었다. 그 경우 한 요청에서 발생한 오염이 조정자의 힙을 망가뜨리고, 서버 전체가 재시작되어야 했다.
시작 시 크기를 고정하는 공유 메모리 세그먼트
섹션 제목: “시작 시 크기를 고정하는 공유 메모리 세그먼트”프로세스 간 데이터 공유는 명시적으로 매핑된 공유 메모리 영역이 필요하다. 이 영역은 어떤 자식이 실행되기 전에 크기가 정해져야 한다. 대부분의 플랫폼에서 SysV 공유 메모리 세그먼트는 연결된 상태에서 크기를 늘릴 수 없기 때문이다. 보편적인 패턴은 다음과 같다.
- 감독자가 설정(
max_connections,shared_buffers,max_locks_per_transaction등)으로부터 필요한 총 크기를 계산한다. - 감독자가 세그먼트를 할당한다(
shmget/mmap). - 각 자식은 세그먼트가 존재한 이후에
fork()되어 매핑을 상속한다. - 충돌 재시작 시, 감독자가 기존 세그먼트를 분리하고 같은 크기의 새 세그먼트를 만들어 재초기화한다.
4단계가 max_connections와 shared_buffers에 서버 재시작이 필요한 이유다. 이들은 세그먼트 크기를 결정하며, 크기는 Postmaster 수명 동안 고정된다.
PMChild 풀: 활성 자식들의 고정된 명단
섹션 제목: “PMChild 풀: 활성 자식들의 고정된 명단”감독자는 어떤 자식이 어떤 역할인지 추적해야 한다. 자식이 종료될 때 재시작 여부, 형제 신호 전송 여부, 셧다운 상태 전환 여부를 결정하기 때문이다. 보편적인 패턴은 고정 크기 배열(또는 풀)의 자식 디스크립터로, 허용 자식 수만큼 슬롯을 공유 메모리 레이아웃에서 할당하되 감독자만 관리한다. 각 슬롯은 PID, 역할(BackendType), 그리고 역할별 상태(백그라운드 워커 등록 포인터, 알림 플래그 등)를 기록한다.
데드 엔드(dead-end) 자식, 즉 죽기 전에 클라이언트에게 오류를 전달하기 위해서만 fork되는 프로세스는 예외다. 공유 메모리 자원을 소비하지 않으므로 풀에 포함되지 않는다.
상태 머신으로 구동하는 셧다운
섹션 제목: “상태 머신으로 구동하는 셧다운”다중 프로세스 서버를 깨끗하고 결정론적으로 종료하려면 순서가 필요하다. 먼저 클라이언트 백엔드를 중단하고, WAL 플러시를 기다리고, 셧다운 체크포인트를 잡고, 아카이버와 walsender를 멈추고, 나머지 인프라를 정리한다. 각 서브프로세스 유형을 독립적으로 처리하는 임시 코드는 상호작용을 놓치기 쉽다. 예를 들어 아카이버가 WAL을 회수하려 하는데 walsender가 복제 슬롯을 열어 두는 경우다. 올바른 설계는 감독자 안에 명시적인 유한 상태 머신을 두고, 잘 이름 붙은 상태와 하나의 중앙 함수가 어떤 신호를 언제 보낼지 결정하게 하는 것이다.
이론 ↔ PostgreSQL 매핑
섹션 제목: “이론 ↔ PostgreSQL 매핑”| 일반 개념 | PostgreSQL 이름 |
|---|---|
| 감독자 프로세스 | postmaster (postmaster.c) |
| 감독자 메인 루프 | ServerLoop |
| 연결당 accept | BackendStartup → postmaster_child_launch |
| 자식 역할 식별자 | BackendType 열거형 (miscadmin.h) |
| 고정 자식 슬롯 명단 | PMChild 풀 (pmchild.c) |
| 공유 메모리 크기 계산 및 할당 | CalculateShmemSize → CreateSharedMemoryAndSemaphores |
| 감독자 상태 머신 | PMState 열거형 + PostmasterStateMachine |
| 자식 충돌 핸들러 | HandleChildCrash → HandleFatalError |
| 데드 엔드 자식 (슬롯 없음) | AllocDeadEndChild / B_DEAD_END_BACKEND |
| 백그라운드 프로세스 명단 유지 | LaunchMissingBackgroundProcesses |
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”단일 바이너리, 다양한 역할
섹션 제목: “단일 바이너리, 다양한 역할”PostgreSQL은 하나의 바이너리(postgres)로 배포된다. 프로세스가 무엇을 하는지는 전역 변수 MyBackendType에 저장된 BackendType 값으로 결정된다. 이 값은 프로세스가 역할별 main_fn을 호출하기 전에 설정된다. REL_18_STABLE 기준 전체 열거형은 다음과 같다.
// BackendType — src/include/miscadmin.htypedef enum BackendType{ B_INVALID = 0,
/* Backend-like processes (call PostgresMain or a thin wrapper) */ B_BACKEND, /* regular client-serving backend */ B_DEAD_END_BACKEND, /* forked only to send an error to the client */ B_AUTOVAC_LAUNCHER, B_AUTOVAC_WORKER, B_BG_WORKER, B_WAL_SENDER, B_SLOTSYNC_WORKER, B_STANDALONE_BACKEND, /* postgres -s / single-user mode */
/* Auxiliary processes (no database binding, no heavyweight locks) */ B_ARCHIVER, B_BG_WRITER, B_CHECKPOINTER, B_IO_WORKER, /* PG18: async I/O worker */ B_STARTUP, B_WAL_RECEIVER, B_WAL_SUMMARIZER, /* PG18: WAL summarization for incremental backup */ B_WAL_WRITER,
B_LOGGER, /* syslogger — does not attach to shared memory */} BackendType;백엔드형(backend-like)과 보조형(auxiliary)의 구분은 아키텍처적이다. 백엔드형 프로세스는 InitPostgres를 호출하고 헤비웨이트 락을 보유할 수 있다. 보조 프로세스는 더 단순한 초기화 경로를 거치며, 어떤 클라이언트 데이터베이스가 열려 있는지와 무관하게 클러스터 인프라를 지원하기 위해 존재한다. B_IO_WORKER와 B_WAL_SUMMARIZER는 PG18에서 추가됐으며, PG17 이하를 설명할 때 이 타입들을 단언해서는 안 된다.
PostmasterMain: 클러스터 시작 시퀀스
섹션 제목: “PostmasterMain: 클러스터 시작 시퀀스”PostmasterMain은 바이너리가 서버로 실행될 때(postgres -D $PGDATA)의 진입점이다. ServerLoop에 들어가기 전 시작 시퀀스는 다음과 같다.
// PostmasterMain — src/backend/postmaster/postmaster.cPostmasterMain(int argc, char *argv[]){ InitProcessGlobals(); /* PID, latch, random seed */ PostmasterPid = MyProcPid; IsPostmasterEnvironment = true;
/* parse argv, read postgresql.conf, validate DataDir */ InitializeGUCOptions(); /* ... option parsing ... */ SelectConfigFiles(userDoption, progname); checkDataDir(); checkControlFile(); ChangeToDataDir();
/* install postmaster signal handlers */ pqsignal(SIGHUP, handle_pm_reload_request_signal); pqsignal(SIGTERM, handle_pm_shutdown_request_signal); pqsignal(SIGQUIT, handle_pm_shutdown_request_signal); pqsignal(SIGCHLD, handle_pm_child_exit_signal); pqsignal(SIGUSR1, handle_pm_pmsignal_signal);
/* *** fixed-size shared memory allocated here *** */ CreateSharedMemoryAndSemaphores();
InitPostmasterChildSlots(); /* allocate PMChild pool */
/* open listen sockets, write postmaster.pid */ /* ... condensed ... */
/* launch syslogger, startup process */ /* ... condensed: StartSysLogger(), StartChildProcess(B_STARTUP) ... */
ServerLoop(); /* never returns */}CreateSharedMemoryAndSemaphores가 바로 돌아올 수 없는 지점이다. 이 함수가 공유 세그먼트를 크기 산정하고 할당하며, 이후의 모든 ShmemInitStruct 호출이 이 슬랩에서 공간을 잘라 쓴다. max_connections 변경이 서버 재시작을 요구하는 이유가 여기 있다. 버퍼 풀, 락 테이블, procarray, sinval 링 크기가 모두 이 값에서 파생되기 때문이다.
PMChild 풀
섹션 제목: “PMChild 풀”InitPostmasterChildSlots(pmchild.c)는 PMChild 구조체 배열을 할당하고 이를 BackendType별 프리리스트로 나눈다.
// pmchild.c — pool structuretypedef struct PMChildPool{ int size; /* slots reserved for this BackendType */ int first_slotno; /* index into the flat array */ dlist_head freelist; /* currently unused PMChild entries */} PMChildPool;
static PMChildPool pmchild_pools[BACKEND_NUM_TYPES];NON_EXEC_STATIC int num_pmchild_slots = 0;dlist_head ActiveChildList; /* all live children including dead-ends */Postmaster가 새 백엔드를 fork할 때, AssignPostmasterChildSlot은 해당 타입의 프리리스트에서 슬롯을 꺼내 ActiveChildList에 연결한다. 자식이 종료되면 ReleasePostmasterChildSlot이 슬롯을 프리리스트로 돌려보낸다. 데드 엔드 자식(B_DEAD_END_BACKEND)은 예외다. AllocDeadEndChild는 풀 밖에서 힙 할당을 한다. 공유 자원을 소비하지 않으므로 수 제한이 없다.
ServerLoop: 이벤트 루프
섹션 제목: “ServerLoop: 이벤트 루프”시작 이후 Postmaster는 WaitEventSetWait를 감싼 for(;;) 루프인 ServerLoop에 진입한다.
// ServerLoop — src/backend/postmaster/postmaster.cstatic intServerLoop(void){ ConfigurePostmasterWaitSet(true); /* latch + all listen sockets */ for (;;) { nevents = WaitEventSetWait(pm_wait_set, DetermineSleepTime(), events, lengthof(events), 0); for (int i = 0; i < nevents; i++) { if (events[i].events & WL_LATCH_SET) ResetLatch(MyLatch);
/* process deferred signals in priority order */ if (pending_pm_shutdown_request) process_pm_shutdown_request(); if (pending_pm_reload_request) process_pm_reload_request(); if (pending_pm_child_exit) process_pm_child_exit(); if (pending_pm_pmsignal) process_pm_pmsignal();
if (events[i].events & WL_SOCKET_ACCEPT) { AcceptConnection(events[i].fd, &s); BackendStartup(&s); /* fork a new backend */ closesocket(s.sock); /* postmaster does not keep it */ } } LaunchMissingBackgroundProcesses(); /* ... periodic: recheck postmaster.pid, touch socket files ... */ }}시그널 핸들러(SIGHUP, SIGTERM, SIGCHLD, SIGUSR1)는 pending_pm_* 불리언 플래그를 세우고 latch를 설정하는 것 외에 아무것도 하지 않는다. 실제 작업은 모두 메인 루프에서 일어난다. 이 지연-시그널 규칙은 핸들러 내부에서 async-signal-unsafe 연산(malloc, 파일 I/O)이 일어나는 것을 막는다.
그림 1 — Postmaster 이벤트 루프: 시그널이 latch를 세우고, 메인 루프가 처리한다
flowchart TD
WES["WaitEventSetWait\n(latch + 리슨 소켓에서 블록)"]
SIG["시그널 도착\nSIGHUP / SIGTERM / SIGCHLD / SIGUSR1"]
LATCH["pending_pm_* 플래그 세팅\nSetLatch"]
RESET["ResetLatch"]
SDOWN["process_pm_shutdown_request"]
RELOAD["process_pm_reload_request"]
CEXIT["process_pm_child_exit"]
PMSIG["process_pm_pmsignal"]
ACCEPT["WL_SOCKET_ACCEPT:\nAcceptConnection → BackendStartup"]
LAUNCH["LaunchMissingBackgroundProcesses"]
WES -->|깨어남| RESET
SIG --> LATCH --> WES
RESET --> SDOWN
SDOWN --> RELOAD --> CEXIT --> PMSIG --> ACCEPT --> LAUNCH --> WES
그림 1 — Postmaster의 ServerLoop은 순수한 이벤트 디스패처다. 시그널 핸들러는 불리언 플래그만 뒤집는다. 모든 로직은 WaitEventSetWait가 반환된 이후 포그라운드 루프에서 실행된다.
BackendStartup: 클라이언트 백엔드 fork
섹션 제목: “BackendStartup: 클라이언트 백엔드 fork”WL_SOCKET_ACCEPT가 발생하면 BackendStartup이 fork를 조율한다.
// BackendStartup — src/backend/postmaster/postmaster.cstatic intBackendStartup(ClientSocket *client_sock){ cac = canAcceptConnections(B_BACKEND); if (cac == CAC_OK) { bn = AssignPostmasterChildSlot(B_BACKEND); if (!bn) cac = CAC_TOOMANY; /* pool exhausted → dead-end child */ } if (!bn) bn = AllocDeadEndChild(); /* heap-allocated, no slot */
startup_data.canAcceptConnections = cac; pid = postmaster_child_launch(bn->bkend_type, bn->child_slot, &startup_data, sizeof(startup_data), client_sock); if (pid < 0) { ReleasePostmasterChildSlot(bn); report_fork_failure_to_client(client_sock, save_errno); return STATUS_ERROR; } bn->pid = pid; return STATUS_OK;}canAcceptConnections는 pmState, connsAllowed, 연결 수 대비 한도를 확인하고 CAC_state 열거형을 반환한다. 연결 한도를 초과한 상태에서 fork된 백엔드는 cac = CAC_TOOMANY를 받는다. 이 백엔드는 “too many connections” 오류를 즉시 전송하고 종료하는 데드 엔드 백엔드다.
postmaster_child_launch(launch_backend.c)가 실제 fork()를 실행한다. Unix에서는 자식이 BackendType에 등록된 main_fn을 호출한다. Windows에서는 EXEC_BACKEND가 정의되어 자식이 공유 메모리에서 BackendParameters를 역직렬화한 뒤 SubPostmasterMain으로 재진입한다.
PMState 머신: 정상 운영에서 셧다운까지
섹션 제목: “PMState 머신: 정상 운영에서 셧다운까지”Postmaster는 전체 상태를 단일 PMState 변수로 추적한다.
// PMState enum — src/backend/postmaster/postmaster.ctypedef enum PMState{ PM_INIT, /* postmaster starting */ PM_STARTUP, /* waiting for startup subprocess */ PM_RECOVERY, /* in archive recovery mode */ PM_HOT_STANDBY, /* in hot standby mode */ PM_RUN, /* normal: accepting connections */ PM_STOP_BACKENDS, /* need to stop remaining backends (transient) */ PM_WAIT_BACKENDS, /* waiting for live backends to exit */ PM_WAIT_XLOG_SHUTDOWN,/* waiting for checkpointer shutdown ckpt */ PM_WAIT_XLOG_ARCHIVAL,/* waiting for archiver and walsenders to finish */ PM_WAIT_IO_WORKERS, /* waiting for io workers to exit */ PM_WAIT_CHECKPOINTER, /* waiting for checkpointer to shut down */ PM_WAIT_DEAD_END, /* waiting for dead-end children to exit */ PM_NO_CHILDREN, /* all important children have exited */} PMState;PostmasterStateMachine은 모든 중요한 이벤트(자식 종료, 시그널 수신) 이후에 호출되어 전환을 구동한다. 정상 운영 진입 경로는 다음과 같다.
PM_INIT → PM_STARTUP → PM_RECOVERY (WAL 복구가 필요한 경우) → PM_HOT_STANDBY (스탠바이인 경우) → PM_RUN셧다운 경로(스마트 셧다운이 일반적인 경우)는 다음과 같다.
PM_RUN → PM_STOP_BACKENDS (클라이언트 백엔드에 SIGTERM 전송) → PM_WAIT_BACKENDS (종료 대기) → PM_WAIT_XLOG_SHUTDOWN (checkpointer가 셧다운 체크포인트 쓸 때까지) → PM_WAIT_XLOG_ARCHIVAL (archiver + walsender 완료) → PM_WAIT_IO_WORKERS → PM_WAIT_CHECKPOINTER → PM_WAIT_DEAD_END → PM_NO_CHILDREN → ExitPostmaster(0)그림 2 — 스마트 셧다운 시 PMState 전환
stateDiagram-v2
[*] --> PM_INIT
PM_INIT --> PM_STARTUP : 공유 메모리 생성\nstartup 프로세스 실행
PM_STARTUP --> PM_RECOVERY : startup 프로세스 실행 중\n복구 필요
PM_STARTUP --> PM_RUN : startup 프로세스 0 종료\n복구 불필요
PM_RECOVERY --> PM_HOT_STANDBY : 복구 완료\nstandby 모드
PM_HOT_STANDBY --> PM_RUN : 프라이머리로 승격
PM_RECOVERY --> PM_RUN : 복구 완료\n프라이머리 모드
PM_RUN --> PM_STOP_BACKENDS : 스마트 셧다운\nconnsAllowed=false
PM_STOP_BACKENDS --> PM_WAIT_BACKENDS : 백엔드에 SIGTERM 전송
PM_WAIT_BACKENDS --> PM_WAIT_XLOG_SHUTDOWN : 모든 백엔드 종료
PM_WAIT_XLOG_SHUTDOWN --> PM_WAIT_XLOG_ARCHIVAL : 셧다운 체크포인트 완료
PM_WAIT_XLOG_ARCHIVAL --> PM_WAIT_IO_WORKERS : 아카이버와 walsender 완료
PM_WAIT_IO_WORKERS --> PM_WAIT_CHECKPOINTER : io 워커 완료
PM_WAIT_CHECKPOINTER --> PM_WAIT_DEAD_END : checkpointer 종료
PM_WAIT_DEAD_END --> PM_NO_CHILDREN : 데드 엔드 자식 종료
PM_NO_CHILDREN --> [*] : ExitPostmaster(0)
그림 2 — 정상(스마트) 셧다운 시 PMState 진행. 즉시 셧다운(immediate shutdown)과 충돌 복구는 백엔드-대기 단계들을 건너뜀으로써 중간 상태 여러 개를 합친다.
충돌 복구: HandleChildCrash와 FatalError
섹션 제목: “충돌 복구: HandleChildCrash와 FatalError”process_pm_child_exit가 중요한 자식의 비정상 종료를 발견하면 HandleChildCrash를 호출한다.
// HandleChildCrash — src/backend/postmaster/postmaster.cstatic voidHandleChildCrash(int pid, int exitstatus, const char *procname){ if (FatalError || Shutdown == ImmediateShutdown) return; /* already in crash-recovery path */
LogChildExit(LOG, procname, pid, exitstatus); ereport(LOG, (errmsg("terminating any other active server processes")));
/* Sets FatalError=true, sends SIGQUIT to siblings */ HandleFatalError(PMQUIT_FOR_CRASH, true);}FatalError = true는 정상 셧다운을 충돌 재시작으로 바꾸는 플래그다. 이 플래그가 세워지면 PostmasterStateMachine이 클러스터를 다음 순서로 몰아간다.
- 모든 자식에게
SIGQUIT를 보낸다. 각 자식 안의 우아한 셧다운 경로를 우회하여quickdie→_exit(2)가 호출된다. - 모든 자식이 종료될 때까지 기다린다(
PM_WAIT_BACKENDS→PM_WAIT_DEAD_END). PM_NO_CHILDREN: 공유 메모리를 재생성하고(CreateSharedMemoryAndSemaphores재호출, 3202번 줄), startup 프로세스를 다시 실행하여 WAL 복구를 거친 뒤PM_STARTUP으로 전환한다.
Postmaster 자신이 사용자 데이터를 보유하지 않기 때문에, 이 전체 사이클 — 신호 전송, 대기, 재생성, 재실행 — 이 수백 줄의 결정론적 C 코드로 구현된다.
회수 작업은 process_pm_child_exit에서 일어난다. 이 함수는 SIGCHLD가 지연된 방식으로 처리될 때 호출되며, 비블록킹 waitpid 루프로 종료된 모든 자식을 드레인한다.
// process_pm_child_exit — src/backend/postmaster/postmaster.cstatic voidprocess_pm_child_exit(void){ int pid; int exitstatus;
pending_pm_child_exit = false;
while ((pid = waitpid(-1, &exitstatus, WNOHANG)) > 0) { PMChild *pmchild;
/* Check if this child was a startup process. */ if (StartupPMChild && pid == StartupPMChild->pid) { ReleasePostmasterChildSlot(StartupPMChild); StartupPMChild = NULL;
if (Shutdown > NoShutdown && (EXIT_STATUS_0(exitstatus) || EXIT_STATUS_1(exitstatus))) { StartupStatus = STARTUP_NOT_RUNNING; UpdatePMState(PM_WAIT_BACKENDS); continue; /* PostmasterStateMachine does the rest */ } /* ... unexpected startup-process exit → HandleChildCrash ... */ } /* ... checkpointer, bgwriter, walwriter, autovac, archiver, ... */ /* otherwise it was a backend or bgworker: */ CleanupBackend(pmchild, exitstatus); }
/* After processing all exits, recompute the postmaster's state. */ PostmasterStateMachine();}waitpid(-1, …, WNOHANG)는 한 번의 핸들러 호출에서 현재 좀비 상태인 자식을 모두 회수한다. Unix는 여러 SIGCHLD를 하나의 pending 비트로 합칠 수 있으므로, 루프는 waitpid가 0을 반환할 때까지 계속 돌아야 한다. 이름 있는 보조 프로세스(startup, checkpointer, bgwriter 등)는 저장된 PMChild 포인터로 매칭되고, 나머지는 CleanupBackend로 넘어간다. CleanupBackend는 풀 슬롯을 반환하고 bgworker 재시작 정책을 적용한다. 마지막의 PostmasterStateMachine() 호출이 셧다운을 진행시키거나 충돌 재시작 경로를 발동한다.
충돌 재시작 재초기화는 PostmasterStateMachine의 끝 부분에 있다. FatalError가 세워지고 syslogger를 제외한 모든 자식이 회수되면(pmState == PM_NO_CHILDREN), postmaster가 전체 인메모리 클러스터를 처음부터 다시 구축한다.
// PostmasterStateMachine (reinit tail) — src/backend/postmaster/postmaster.cif (FatalError && pmState == PM_NO_CHILDREN){ ereport(LOG, (errmsg("all server processes terminated; reinitializing")));
if (remove_temp_files_after_crash) RemovePgTempFiles();
ResetBackgroundWorkerCrashTimes(); /* allow bgworkers to restart now */
shmem_exit(1); /* detach old shared segment */ LocalProcessControlFile(true); /* re-read control file */ CreateSharedMemoryAndSemaphores(); /* fresh shared memory */
UpdatePMState(PM_STARTUP); maybe_adjust_io_workers(); /* need I/O workers for recovery */ StartupPMChild = StartChildProcess(B_STARTUP); /* runs WAL recovery */ StartupStatus = STARTUP_RUNNING; AbortStartTime = 0;
ConfigurePostmasterWaitSet(true); /* accept connections again */}이 블록 바로 위에 두 가지 가드가 있다. StartupStatus == STARTUP_CRASHED이면 postmaster는 ExitPostmaster(1)을 호출하고, restart_after_crash GUC가 꺼져 있으면 로그를 남기고 종료한다. 두 가드가 모두 통과되어야 재초기화 블록에 진입한다. shmem_exit(1)이 이전 세그먼트를 분리한 뒤 CreateSharedMemoryAndSemaphores()가 동일한 크기의 새 세그먼트를 매핑한다. 이 덕분에 충돌 재시작 시 postgresql.conf를 다시 읽지 않아도 shared_buffers와 max_connections 크기가 유지된다.
그림 3 — 충돌 감지에서 재초기화까지
flowchart TD
CHILD["중요한 자식이 비정상 종료<br/>(SIGCHLD)"]
REAP["process_pm_child_exit<br/>waitpid(-1, WNOHANG) 루프"]
CRASH["HandleChildCrash<br/>FatalError = true<br/>모든 형제에 SIGQUIT"]
SM1["PostmasterStateMachine"]
WAIT["PM_WAIT_BACKENDS ... PM_WAIT_DEAD_END<br/>형제들 quickdie / _exit(2)"]
NOCHILD["PM_NO_CHILDREN<br/>syslogger 제외 모든 자식 회수"]
GUARD{"StartupStatus==CRASHED<br/>또는 restart_after_crash 꺼짐?"}
EXIT["ExitPostmaster(1)"]
REINIT["shmem_exit(1) 이전 세그먼트 분리<br/>CreateSharedMemoryAndSemaphores<br/>StartChildProcess(B_STARTUP)"]
STARTUP["PM_STARTUP<br/>WAL 복구 재실행"]
CHILD --> REAP --> CRASH --> SM1 --> WAIT --> NOCHILD --> GUARD
GUARD -->|예| EXIT
GUARD -->|아니오| REINIT --> STARTUP
그림 3 — 충돌 재시작 사이클. SIGQUIT로 모든 형제가 즉시 quickdie 경로를 거친다. 풀이 완전히 드레인된 뒤(PM_NO_CHILDREN) postmaster가 공유 메모리를 분리하고 동일한 크기로 재구성한 뒤 startup 프로세스를 다시 실행하여 WAL을 재생한다.
LaunchMissingBackgroundProcesses
섹션 제목: “LaunchMissingBackgroundProcesses”이벤트 루프의 매 반복이 끝날 때 LaunchMissingBackgroundProcesses는 pmState를 확인하고 실행되어야 하지만 실행되지 않은 백그라운드 프로세스를 채운다.
B_CHECKPOINTER와B_BG_WRITER는PM_STARTUP,PM_RECOVERY,PM_HOT_STANDBY,PM_RUN에서 필요하다.B_WAL_WRITER와B_AUTOVAC_LAUNCHER는PM_RUN에서만 필요하다.B_ARCHIVER는 아카이빙이 활성화된 경우PM_RUN에서 필요하다(archive_mode = always이면 항상 필요).B_IO_WORKER수는io_combine_limit과max_io_concurrencyGUC 값에 따라maybe_adjust_io_workers가 동적으로 조정한다(PG18).B_WAL_SUMMARIZER는summarize_wal = on인 경우PM_RUN에서 필요하다(PG18).
매 반복마다 확인하는 이 패턴 덕분에 Postmaster는 개별 백그라운드 프로세스마다 “재시작” 코드 경로를 따로 둘 필요가 없다. 백그라운드 프로세스가 정상 종료되면 다음 루프 반복이 재실행한다.
소스 코드 안내
섹션 제목: “소스 코드 안내”PostmasterMain과 시작
섹션 제목: “PostmasterMain과 시작”PostmasterMain(postmaster.c:494) — 클러스터 진입점. GUC 초기화, 설정 파일 읽기, 공유 메모리 생성, 자식 슬롯 초기화, 리슨 소켓 바인딩, syslogger + startup 프로세스 실행, 그 다음ServerLoop.InitProcessGlobals(postmaster.c:1933) — 자식 fork 전에MyProcPid,MyStartTimestamp,MyLatch, 난수 시드를 설정한다.CreateSharedMemoryAndSemaphores— 최초 시작 시 1004번 줄에서, 충돌 재시작 후 3202번 줄에서 호출된다. 공유 세그먼트를 크기 산정하고 할당하며, 이후 모든ShmemInitStruct호출이 이 슬랩을 사용한다.
PMChild 풀
섹션 제목: “PMChild 풀”InitPostmasterChildSlots(pmchild.c:86) — 시작 시 한 번 호출된다.PMChild배열을 할당하고 타입별 프리리스트로 나눈다.AssignPostmasterChildSlot(pmchild.c:162) — fork 전에 해당 프리리스트에서 슬롯을 꺼내고ActiveChildList에 연결한다.AllocDeadEndChild(pmchild.c:208) — 데드 엔드 백엔드를 위해 풀 밖에서 힙 할당한다.ReleasePostmasterChildSlot(pmchild.c:236) — 자식 종료 후 슬롯을 프리리스트로 돌려보낸다.CleanupBackend와 이름 있는 자식의process_pm_child_exit에서 호출된다.FindPostmasterChildByPid(pmchild.c:274) —ActiveChildList의 O(n) 스캔. 백엔드인지 bgworker인지 확인할 때 사용된다.
ServerLoop과 이벤트 디스패치
섹션 제목: “ServerLoop과 이벤트 디스패치”ServerLoop(postmaster.c:1653) —WaitEventSetWait를 감싼for(;;). 지연된 시그널 작업과 accept를 처리한다.ConfigurePostmasterWaitSet(postmaster.c:1630) — latch와 모든 리슨 소켓으로WaitEventSet을 구성한다. 셧다운 중에는accept_connections=false로 재호출하여 새 연결을 차단한다.BackendStartup(postmaster.c:3518) — 자식 슬롯을 확보하고,postmaster_child_launch를 호출하고, PID를 기록한다.CAC_TOOMANY→ 데드 엔드 경로를 처리한다.canAcceptConnections(postmaster.c:1812) —pmState,connsAllowed, 연결 수 대비 한도를 확인하고CAC_state열거형을 반환한다.LaunchMissingBackgroundProcesses(postmaster.c:3267) — 모든 백그라운드 타입을 순회하며 실행 중이어야 하는 것을 실행한다.ServerLoop매 반복의 마지막에 호출된다.
자식 실행 (launch_backend.c)
섹션 제목: “자식 실행 (launch_backend.c)”postmaster_child_launch(launch_backend.c:229) — Unix에서는fork(), Windows에서는internal_forkexec. 자식은InitPostmasterChild()를 호출하고, postmaster 포트를 닫고,child_process_kinds[type].main_fn을 호출한다.PostmasterChildName(launch_backend.c:211) —BackendType을 로그 메시지와ps표시용 문자열로 변환한다.
상태 머신과 충돌 복구
섹션 제목: “상태 머신과 충돌 복구”PostmasterStateMachine(postmaster.c:2865) — 모든 시그널 기반 작업 함수 이후 호출되는 단일 함수. PMState 전환을 결정하고 어떤 신호를 보낼지 정한다.HandleChildCrash(postmaster.c:2772) — 충돌을 기록하고,HandleFatalError(PMQUIT_FOR_CRASH, true)를 호출해FatalError를 세우고SIGQUIT를 브로드캐스트한다.CleanupBackend(postmaster.c:2550) — 백엔드 또는 bgworker 종료 후PMChild슬롯을 해제한다. bgworker 재시작 로직을 갱신하고PostmasterStateMachine을 호출한다.process_pm_child_exit(postmaster.c:2233) — SIGCHLD 기반 회수기.waitpid(-1, WNOHANG)루프. 타입별 핸들러 또는CleanupBackend로 분기한다. 끝에서PostmasterStateMachine을 호출한다.
위치 힌트 (2026-06-05 기준, 커밋 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, 커밋 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
BackendType 열거형 | src/include/miscadmin.h | 337 |
PMState 열거형 | src/backend/postmaster/postmaster.c | 336 |
PostmasterMain | src/backend/postmaster/postmaster.c | 494 |
CreateSharedMemoryAndSemaphores (최초 호출) | src/backend/postmaster/postmaster.c | 1004 |
InitPostmasterChildSlots 호출 | src/backend/postmaster/postmaster.c | 952 |
ServerLoop | src/backend/postmaster/postmaster.c | 1653 |
ConfigurePostmasterWaitSet | src/backend/postmaster/postmaster.c | 1630 |
canAcceptConnections | src/backend/postmaster/postmaster.c | 1812 |
InitProcessGlobals | src/backend/postmaster/postmaster.c | 1933 |
CleanupBackend | src/backend/postmaster/postmaster.c | 2550 |
process_pm_child_exit | src/backend/postmaster/postmaster.c | 2233 |
HandleChildCrash | src/backend/postmaster/postmaster.c | 2772 |
PostmasterStateMachine | src/backend/postmaster/postmaster.c | 2865 |
LaunchMissingBackgroundProcesses | src/backend/postmaster/postmaster.c | 3267 |
BackendStartup | src/backend/postmaster/postmaster.c | 3518 |
CreateSharedMemoryAndSemaphores (충돌 재시작 호출) | src/backend/postmaster/postmaster.c | 3202 |
postmaster_child_launch | src/backend/postmaster/launch_backend.c | 229 |
PostmasterChildName | src/backend/postmaster/launch_backend.c | 211 |
MaxLivePostmasterChildren | src/backend/postmaster/pmchild.c | 70 |
InitPostmasterChildSlots | src/backend/postmaster/pmchild.c | 86 |
AssignPostmasterChildSlot | src/backend/postmaster/pmchild.c | 162 |
AllocDeadEndChild | src/backend/postmaster/pmchild.c | 208 |
ReleasePostmasterChildSlot | src/backend/postmaster/pmchild.c | 236 |
FindPostmasterChildByPid | src/backend/postmaster/pmchild.c | 274 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”검증된 사실
섹션 제목: “검증된 사실”-
BackendType은 REL_18_STABLE에서 18개 멤버를 갖는다. PG18에서 추가된B_IO_WORKER와B_WAL_SUMMARIZER가 포함된다. 커밋 273fe94 기준src/include/miscadmin.h:337–375에서 확인.B_IO_WORKER는storage/aio/와 함께 추가된 비동기 I/O 워커다.InitPostgres를 호출하지 않고 헤비웨이트 락을 보유하지 않는다.B_WAL_SUMMARIZER는 증분 백업(PG18 기능)을 지원한다. PG17 이하를 설명할 때 두 타입 모두 단언해서는 안 된다. -
PMState는 12개 값을 가지며,PM_HOT_STANDBY는PM_RECOVERY와 구별된다.postmaster.c:336–352에서 확인. 이 구분은LaunchMissingBackgroundProcesses에서 중요하다. 아카이버는PM_HOT_STANDBY에서 시작되지만(archive_mode=always인 경우),PM_RECOVERY에서는 시작되지 않는다. -
시그널 핸들러는 불리언 플래그만 세운다. 핸들러 내부에서는 async-signal-unsafe 작업이 없다. 검증:
handle_pm_child_exit_signal은pending_pm_child_exit = true와SetLatch(MyLatch)만 수행한다(postmaster.c:2223–2231). 모든 회수 작업은 메인 루프의process_pm_child_exit에서 일어난다. -
BackendStartup은 fork 이후 postmaster에서 accept된 소켓을 닫는다.postmaster.c:1704–1712에서 확인:BackendStartup이 반환된 뒤 부모에서closesocket(s.sock)이 무조건 호출된다. 자식은fork()시점에 열린 파일 디스크립터를 그대로 상속한다. Postmaster는 그 소켓이 필요 없다. -
데드 엔드 자식은 풀이 아닌 힙에서 할당된다.
pmchild.c:208–234(AllocDeadEndChild)에서 확인:pmchild_pools가 아닌TopMemoryContext에서palloc을 사용한다. 주석은 데드 엔드 백엔드 수에 제한이 없다고 명시한다. -
CreateSharedMemoryAndSemaphores는 시작 시와 충돌 재시작 시 두 번 호출된다.postmaster.c:1004(시작)와postmaster.c:3202(충돌 재시작,PostmasterStateMachine의PM_NO_CHILDREN분기 내부)에서 확인. 충돌 재시작 시 새 세그먼트를 할당하기 전에 이전 세그먼트를 분리한다. -
LaunchMissingBackgroundProcesses는 자식 종료 이벤트뿐 아니라 ServerLoop의 매 반복에서 호출된다.postmaster.c:1718에서 확인:for (int i = 0; i < nevents; i++)루프 밖에서, 이벤트 디스패치 블록의 마지막 문장이다. WAL 아카이빙을 활성화하는 설정 재로드 후처럼 이벤트가 발생하지 않아도 백그라운드 프로세스가 재시작된다.
열린 질문
섹션 제목: “열린 질문”-
B_IO_WORKER생명주기 세부 사항.LaunchMissingBackgroundProcesses에서 호출되는maybe_adjust_io_workers가 GUC 값에 따라B_IO_WORKER프로세스 수를 동적으로 조정한다. 유지해야 할 워커 수를 결정하는 알고리즘과, 초과 워커에게 SIGTERM을 보내는지 아니면 자연 종료를 기다리는지는 이 문서에서 추적하지 않았다. 조사 경로:storage/aio/에서maybe_adjust_io_workers와io_worker_*함수 읽기. -
PM_WAIT_XLOG_ARCHIVAL전환 트리거.pmState를PM_WAIT_XLOG_SHUTDOWN에서PM_WAIT_XLOG_ARCHIVAL로 옮기는 조건(셧다운 체크포인트가 충분히 쓰여진 상태)은 checkpointer가 보내는pmsignal에 구동된다. 정확한PMSIGNAL_*값과 핸드셰이크는 여기서 자세히 다루지 않았다. 조사 경로:pmsignal.c와checkpointer.c에서PMSIGNAL_SHUTDOWN_COMPLETE검색. -
EXEC_BACKEND(Windows) 재진입 경로. Windows에서postmaster_child_launch는fork()대신internal_forkexec를 호출한다. 자식은SubPostmasterMain으로 재진입하여BackendParameters를 역직렬화한 뒤 적절한main_fn을 호출한다. 직렬화되는 매개변수들과 Windows 경로에서MyClientSocket과의 상호작용은 여기서 분석하지 않았다. 조사 경로:launch_backend.c의save_backend_variables/restore_backend_variables읽기.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”-
인스턴스당 감독자 vs. 스레드 풀 모델. Architecture of a Database System(Hellerstein et al., 2007, §2.2–2.3)은 프로세스-per-세션, 스레드-per-세션, 프로세스 풀 모델을 비교한다. PostgreSQL의 postmaster-요청시-fork는 프로세스-per-세션 방식이다. 비용은 새 연결당
fork()지연이다. PgBouncer, pgpool-II 같은 연결 풀러와 PG17+의 내장 풀러 프로토타입은 많은 클라이언트 연결을 더 적은 수의 백엔드 프로세스로 다중화하여 이 문제를 해결한다.PostmasterMain의 시작 비용을 이해하면 풀이 무엇을 상각하는지 알 수 있다. -
“The Design of POSTGRES” (Stonebraker & Rowe, 1986). 원본 설계 논문은 단순성과 격리를 이유로 프로세스 모델을 명시적으로 선택했다. 1986년의 postmaster 설명과 현재
PostmasterMain(현재 약 900줄 vs. 수 페이지 산문)을 비교하면 복잡성의 진화를 구체적으로 볼 수 있다. 원본 설계가 미뤘던 것들(WindowsEXEC_BACKEND, PG18 비동기 I/O 워커, WAL 요약기, 증분 백업, hot standby) 중 REL_18이 처리해야 하는 것이 무엇인지를 파악하는 연습이 된다. -
오라클의 프로세스 모델과 PMON. 오라클도 비슷한 감독자/워커 구조를 쓰지만, 조정자(PMON — Process MONitor)는 다른 프로세스들의 부모가 아니라 그 자체가 백그라운드 프로세스다. PMON은 실패한 세션을 감지하고 공유 풀에서 그 자원을 정리한다. PostgreSQL의 postmaster는 두 역할을 모두 담당한다. 부모 프로세스 감독자(SIGCHLD 경유)이자 충돌 복구 조정자(
HandleChildCrash경유)다. 폴링(PMON) 대 시그널 기반 회수(SIGCHLD)의 트레이드오프를 비교하면 흥미롭다. -
MySQL의 스레드-per-연결 모델. MySQL의 스레드-per-연결 서버는
fork()비용을 완전히 건너뛴다. 새 연결은pthread_create다. 대신 한 스레드의 스택 버그가 다른 세션의 할당을 오염시킬 수 있다. postmaster에 해당하는 것이 없으므로 MySQL의 충돌 복구는 프로세스 감독자 수준이 아닌 엔진 수준(재시작 시 InnoDB 복구)에서 동작한다. -
Greenplum / Citus: postmaster 군단의 조율. 분산 PostgreSQL 변형은 세그먼트당 postmaster 하나를 실행한다. 코디네이터 postmaster는 libpq 연결로 쿼리 조각을 세그먼트 postmaster에 전송한다. 이 설정에서 postmaster의 충돌 격리 특성이 핵심이 된다. 세그먼트 충돌을 코디네이터를 재시작하지 않고 격리하여 복구할 수 있기 때문이다.
소비한 원본 파일
섹션 제목: “소비한 원본 파일”- 없음 (REL_18_STABLE / 커밋 273fe94 소스 트리에서 직접 합성).
소스 코드 경로 (REL_18_STABLE / 커밋 273fe94)
섹션 제목: “소스 코드 경로 (REL_18_STABLE / 커밋 273fe94)”src/backend/postmaster/postmaster.c—PostmasterMain,ServerLoop,BackendStartup,canAcceptConnections,process_pm_child_exit,HandleChildCrash,PostmasterStateMachine,LaunchMissingBackgroundProcesses,CleanupBackend,PMState열거형src/backend/postmaster/launch_backend.c—postmaster_child_launch,PostmasterChildName,SubPostmasterMain(Windows 재진입),save_backend_variables/restore_backend_variablessrc/backend/postmaster/pmchild.c—PMChild풀,PMChildPool,InitPostmasterChildSlots,AssignPostmasterChildSlot,AllocDeadEndChild,ReleasePostmasterChildSlot,FindPostmasterChildByPidsrc/include/miscadmin.h—BackendType열거형,AmRegularBackendProcess
교과서 및 논문 참조
섹션 제목: “교과서 및 논문 참조”- Hellerstein, Stonebraker, Hamilton. Architecture of a Database System, Foundations and Trends in Databases, 2007. §2 (프로세스 모델).
- Stonebraker, M., and Rowe, L. A. “The Design of POSTGRES.” SIGMOD 1986. (프로세스 모델 근거와 원본 postmaster 개념.)