(KO) PostgreSQL 백엔드 생애주기 — 포크, 초기화, 명령 루프, 종료
목차
이론적 배경
섹션 제목: “이론적 배경”동시 클라이언트 세션을 받아들이는 데이터베이스 서버는 근본적인 격리 문제를 풀어야 한다. 각 세션은 자신만의 실행 컨텍스트 — 자신만의 스택, 메모리, 시스템 상태 뷰 — 를 가져야 한다. 동시에 모든 세션이 함께 건드리는 자료구조(버퍼 풀, 락 테이블, 트랜잭션 카탈로그)는 공유해야 한다. 이 문제를 푸는 지배적 아키텍처 패턴은 두 가지다.
-
세션당 스레드. 연결된 클라이언트마다 OS 스레드 하나를 배정하고, 단일 주소 공간을 공유한다. 상태는 스레드 지역 변수로 격리하고, 공유 상태는 뮤텍스로 보호한다. MySQL(InnoDB 포어그라운드 스레드), Oracle(스레드 모드 전용 서버), SQL Server가 이 방식을 쓴다. 포크 오버헤드가 낮고 메모리 공유가 밀접하다는 장점이 있다. 단점은 한 세션의 코드 버그가 공유 메모리를 오염시켜 서버 전체를 내릴 수 있다는 점이다.
-
세션당 프로세스. 연결된 클라이언트마다 OS 프로세스 하나를 배정한다. 각 프로세스는 자신만의 주소 공간을 가진다. 한 백엔드 크래시가 서버를 살려둔다. 공유 상태는 시작 시점에 모든 프로세스가 연결하는 명시적인 공유 메모리 세그먼트에 산다. 원래 PostgreSQL과 Oracle 전용 서버 모드가 이 방식을 쓴다.
PostgreSQL은 세션당 프로세스 방식을 선택한다. 이 선택은 1986년 “Design of POSTGRES” 논문(Stonebraker & Rowe)까지 거슬러 올라가며 이후에도 의도적으로 유지됐다. 프로세스 격리란 백엔드 크래시가 해당 세션에만 국한된다는 뜻이다. 또한 대부분의 전역 C 변수가 기본적으로 프로세스별이므로 코드를 추론하기 쉽다. OS가 주소 공간 분리를 강제하기 때문에, 난폭한 백엔드가 다른 세션의 버퍼 풀을 오염시킬 위험이 없다는 점이다.
대신 포크 오버헤드가 발생한다. 이는 PgBouncer 같은 연결 풀러나 PG17 이후 내장 연결 풀 프로토타입으로 완화한다. 또한 명시적인 공유 메모리 설계가 필요하다. Architecture of a Database System(Hellerstein et al., 2007, §2)은 두 모델을 균형 있게 비교하며, “대부분의 현대 데이터베이스는 멀티스레드”이지만 “멀티프로세스가 올바르게 구현하기 더 단순하다”고 지적한다. PostgreSQL의 선택은 단순성 배당이 결정적이었던 시절의 판단이고, 전역 변수 사용 전체를 재작성해야 한다는 변경 비용 때문에 그 이후로도 유지됐다.
세션당 프로세스 방식은 스레드 방식이 회피하는 세 가지 추가 문제를 풀어야 한다.
- 새 프로세스가 공유 상태에서 어떻게 신원을 획득하는가? 부모로부터 세션별 구조체를 상속받을 수 없다. 그것들은 부모의 사설 힙에 있기 때문이다. 따라서 공유 메모리에 직접 등록해야 한다.
- 카탈로그 데이터에 어떻게 접근하는가?
pg_class,pg_authid등 수십 개 카탈로그를 읽으려면 트랜잭션 컨텍스트, 릴레이션 캐시, 시스템 카탈로그 캐시, 데이터베이스 디렉터리에 대한 열린 릴레이션이 필요하다. 새로 포크된 프로세스는 이 중 어느 것도 갖고 있지 않다. - 프로세스를 종료하지 않고 문장 오류로부터 어떻게 회복하는가? 클라이언트가 보낸
kill(backend, SIGTERM)이 트랜잭션 중간에 프로세스를 내리도록 허용해선 안 된다. 백엔드는 문장을 롤백하고 유휴 루프로 돌아가야 한다.
이 세 가지 질문이 §“PostgreSQL의 접근법”에서 설명하는 초기화와 명령 루프 설계 전체를 형성한다.
DBMS 공통 설계
섹션 제목: “DBMS 공통 설계”세션당 프로세스 모델을 채택한 거의 모든 데이터베이스 엔진은 브랜드에 상관없이 동일한 네 단계 흐름을 거친다. 일반적인 패턴을 이해하면 PostgreSQL의 선택이 잘 알려진 관례의 구체적인 인스턴스로 읽힌다.
1단계 — 프로세스 탄생과 환경 설정
섹션 제목: “1단계 — 프로세스 탄생과 환경 설정”postmaster가 fork()로 자식을 생성한다(Windows에서는 EXEC_BACKEND로 새 프로세스를 만든다). 자식은 postmaster의 열린 파일 디스크립터와 GUC 상태 복사본을 상속한다. 반면 세션별 구조체는 상속하지 않는다. 그것들은 아직 존재하지 않기 때문이다. 첫 번째 동작은 백엔드에 적합한 시그널 핸들러(postmaster의 것이 아닌)를 설치하고, 부모에서만 유효한 상속 상태를 초기화하며, 백엔드 자신의 PID를 기록하는 것이다.
2단계 — 공유 메모리 등록
섹션 제목: “2단계 — 공유 메모리 등록”백엔드가 공유 서브시스템(락 매니저, 버퍼 매니저, procarray)을 사용하기 전에 반드시 자신을 등록해야 한다. 등록이란 공유 PGPROC 배열에서 슬롯을 할당하고, 다른 백엔드가 찾을 수 있도록 PID와 데이터베이스 OID를 게시하며, 공유 무효화 배열과 시그널 배열에서 백엔드별 항목을 초기화하는 것이다. 등록 이후에야 백엔드는 클러스터의 나머지 구성원에게 보인다.
3단계 — 세션별 카탈로그 초기화
섹션 제목: “3단계 — 세션별 카탈로그 초기화”백엔드는 대상 데이터베이스를 열고, 존재 여부와 접근 가능 여부를 확인하고, 연결 사용자를 인증하고, 로컬 캐시를 부트스트랩해야 한다. “부트스트랩”이란 릴레이션 캐시를 워밍업하고(최소한 모든 쿼리에 필요한 시스템 카탈로그 항목), 시스템 카탈로그 캐시를 워밍업하고, 포털 매니저를 초기화하고, 검색 경로와 클라이언트 인코딩을 설정하는 것이다. 이 과정은 초기화가 완료되면 커밋되는 시작 트랜잭션 안에서 일어난다.
4단계 — 명령 루프
섹션 제목: “4단계 — 명령 루프”일반적인 루프 형태는 다음과 같다. 클라이언트에게 ReadyForQuery를 보내고, 읽기에서 블록하고, 수신한 메시지를 적절한 핸들러로 디스패치하고, 트랜잭션 컨텍스트로 실행을 감싸고(아직 트랜잭션이 없으면 시작하고, 끝에서 커밋하거나 롤백하고), 루프 상단으로 돌아간다. 오류 복구는 루프 전에 한 번 설치하는 setjmp/longjmp 점프 포인트를 사용한다. 어떤 ereport(ERROR)든 그 점프 포인트까지 풀리고, 현재 트랜잭션을 중단하고, 루프 상단에서 재개한다.
종료 변형
섹션 제목: “종료 변형”대부분의 시스템은 최소 두 가지 종료 경로를 구분한다.
- 정상 종료 — 현재 명령을 마치고, 커밋 또는 롤백하고, 정리 콜백을 실행하고, 코드 0으로 종료한다.
- 크래시 종료 — postmaster가 공유 메모리 손상이나 SIGQUIT를 감지했을 때 발생한다. 백엔드는 손상된 상태를 건드릴 수 있는 콜백을 건너뛰고
_exit()를 직접 호출한다.
이론 ↔ PostgreSQL 대응표
섹션 제목: “이론 ↔ PostgreSQL 대응표”| 일반 개념 | PostgreSQL 이름 |
|---|---|
| 프로세스 탄생 진입점 | postmaster_child_launch → PostgresMain |
| PGPROC 슬롯 할당 | InitProcess (PostgresMain 이전) |
| 공유 메모리 등록 | InitProcessPhase2 (InitPostgres 내부) |
| 세션별 카탈로그 부트스트랩 | postinit.c의 InitPostgres |
| 릴레이션 캐시 워밍업 | RelationCacheInitializePhase2/3 |
| 시스템 카탈로그 캐시 | InitCatalogCache |
| 오류 복구 점프 포인트 | PostgresMain의 sigsetjmp(local_sigjmp_buf, 1) |
| 명령 읽기 | ReadCommand → SocketBackend / InteractiveBackend |
| 단순 쿼리 디스패치 | exec_simple_query |
| 확장 쿼리 디스패치 | exec_parse_message / exec_bind_message / exec_execute_message |
| 트랜잭션 감싸기 | start_xact_command / finish_xact_command |
| 정상 종료 | die 핸들러 → proc_exit 콜백 |
| 크래시 종료 | quickdie → _exit(2) |
PostgreSQL의 접근법
섹션 제목: “PostgreSQL의 접근법”BackendType 분류 체계
섹션 제목: “BackendType 분류 체계”PostgreSQL 클러스터의 모든 자식 프로세스는 자신의 역할을 식별하는 MyBackendType 변수(타입 BackendType, src/include/miscadmin.h에 선언)를 갖는다. REL_18_STABLE 기준 전체 열거값은 다음과 같다.
// BackendType — src/include/miscadmin.htypedef enum BackendType{ B_INVALID = 0,
/* Backends and other backend-like processes */ B_BACKEND, /* regular client backend */ B_DEAD_END_BACKEND, /* rejected before auth, drains the connection */ 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, /* not connected to shared memory */} BackendType;AmRegularBackendProcess()는 MyBackendType == B_BACKEND를 테스트하는 매크로다. 이 문서에서 설명하는 생애주기는 PostgresMain을 호출하는 “백엔드 유사” 그룹인 B_BACKEND, B_WAL_SENDER, B_BG_WORKER, B_AUTOVAC_WORKER가 따르는 경로다. 보조 프로세스(B_CHECKPOINTER, B_WAL_WRITER 등)는 별도의 진입점을 가지며 postgres-aux-processes.md에서 다룬다.
1단계: postmaster_child_launch → PostgresMain
섹션 제목: “1단계: postmaster_child_launch → PostgresMain”postmaster는 launch_backend.c의 postmaster_child_launch를 호출해 새 자식 프로세스를 생성한다. Unix에서는 fork()로, Windows에서는 EXEC_BACKEND가 정의되어 있어 internal_forkexec를 사용한다. Windows 경로에서 자식은 fork()로 상속하는 대신 공유 메모리 세그먼트에서 직렬화된 BackendParameters를 다시 읽는다.
자식의 첫 번째 유저랜드 호출은 PostgresMain(dbname, username)이다(postmaster 하에 생성된 백엔드의 경우). 독립 실행형 경로(postgres -s)에서는 PostgresSingleUserMain이 먼저 호출되고, 이것이 PostgresMain에 위임한다.
PostgresMain은 시그널 핸들러 설치로 시작한다.
// PostgresMain — src/backend/tcop/postgres.cpqsignal(SIGHUP, SignalHandlerForConfigReload);pqsignal(SIGINT, StatementCancelHandler); /* cancel current query */pqsignal(SIGTERM, die); /* graceful shutdown */pqsignal(SIGQUIT, quickdie); /* hard crash exit */InitializeTimeouts(); /* installs SIGALRM handler */// ... condensed ...pqsignal(SIGUSR1, procsignal_sigusr1_handler);여기서 설치하는 시그널 세트는 postmaster의 것과 다르다. SIGTERM은 즉시 종료가 아니라 die(ProcDiePending을 설정하고 명령 루프로 반환)에 매핑된다. SIGQUIT는 quickdie에 매핑된다. quickdie는 클라이언트에게 간략한 알림을 시도한 뒤 모든 정리 콜백을 우회하고 _exit(2)를 호출한다. 공유 메모리가 손상됐을 수 있기 때문이다.
2단계: BaseInit — 로컬 서브시스템 등록
섹션 제목: “2단계: BaseInit — 로컬 서브시스템 등록”시그널 설정 이후 PostgresMain은 BaseInit을 호출한다.
// BaseInit — src/backend/utils/init/postinit.cBaseInit(void){ Assert(MyProc != NULL); /* InitProcess() already ran */ DebugFileOpen(); InitFileAccess(); pgstat_initialize(); /* cumulative stats, early so shutdown hooks run last */ pgaio_init_backend(); /* PG18: async I/O per-backend state */ InitSync(); smgrinit(); InitBufferManagerAccess(); /* per-backend local buffer structures */ InitTemporaryFileAccess(); InitXLogInsert(); /* WAL record construction buffers */ InitLockManagerAccess(); /* local lock manager structures */ ReplicationSlotInitialize();}BaseInit은 공유 서브시스템의 로컬(프로세스별) 쪽을 초기화한다. InitBufferManagerAccess는 백엔드별 핀 카운트 배열(PrivateRefCountArray)을 할당한다. InitLockManagerAccess는 로컬 락 해시를 할당한다. 이 호출들 중 어느 것도 아직 공유 메모리 구조체를 건드리지 않는다. 그것은 InitPostgres에서 일어난다.
3단계: InitPostgres — 공유 등록과 카탈로그 부트스트랩
섹션 제목: “3단계: InitPostgres — 공유 등록과 카탈로그 부트스트랩”InitPostgres는 중심적인 초기화 함수다. postinit.c에서 약 525줄에 달하며 의도적으로 순차적이다. 710번째 줄 주석이 경고하듯 “InitPostgres 함수에서의 호출 순서에 매우 주의하라”는 점이 핵심이다. 호출 순서 자체가 정확성의 일부이기 때문이다.
뼈대만 남긴 시퀀스는 다음과 같다.
// InitPostgres — src/backend/utils/init/postinit.cvoidInitPostgres(const char *in_dbname, Oid dboid, const char *username, Oid useroid, bits32 flags, char *out_dbname){ /* 1. ProcArray에 등록 — 이제 다른 백엔드에게 보인다 */ InitProcessPhase2();
/* 2. 백엔드 상태 배열 항목 (pg_stat_activity용) */ pgstat_beinit(); pgstat_bestart_initial();
/* 3. 공유 무효화 배열 항목 */ SharedInvalBackendInit(false);
/* 4. ProcSignal 슬롯 (취소 키, SIGUSR1 라우팅) */ ProcSignalInit(MyCancelKey, MyCancelKeyLength);
/* 5. 타임아웃 핸들러 등록 */ RegisterTimeout(DEADLOCK_TIMEOUT, CheckDeadLockAlert); RegisterTimeout(STATEMENT_TIMEOUT, StatementTimeoutHandler); // ... condensed ...
/* 6. 로컬 카탈로그 비계 초기화 (카탈로그 읽기 없음) */ RelationCacheInitialize(); InitCatalogCache(); InitPlanCache(); EnablePortalManager();
/* 7. 공유 카탈로그 릴케이시 항목 로드 (pg_database, pg_authid, …) */ RelationCacheInitializePhase2();
/* 8. 종료 콜백 등록 */ before_shmem_exit(ShutdownPostgres, 0);
/* 9. 시작 트랜잭션 열기, 인증, 연결 한도 확인 */ StartTransactionCommand(); PerformAuthentication(MyProcPort); /* 정규 백엔드만 */ InitializeSessionUserId(username, useroid, false); // ... condensed: 연결 한도 확인, walsender 단축 경로 ...
/* 10. 데이터베이스 OID 잠금, MyDatabaseId 설정, 디렉터리 검증 */ LockSharedObject(DatabaseRelationId, dboid, 0, RowExclusiveLock); MyDatabaseId = dboid; MyProc->databaseId = MyDatabaseId;
/* 11. 실제 데이터베이스를 열고 릴레이션 캐시 전체 워밍업 */ RelationCacheInitializePhase3(); initialize_acl(); CheckMyDatabase(dbname, am_superuser, ...);
/* 12. 시작 옵션과 역할/DB별 GUC 설정 */ process_startup_options(MyProcPort, am_superuser); process_settings(MyDatabaseId, GetSessionUserId());
/* 13. 마무리: 검색 경로, 클라이언트 인코딩, 세션 상태, 프리로드 라이브러리 */ InitializeSearchPath(); InitializeClientEncoding(); InitializeSession(); process_session_preload_libraries(); /* INIT_PG_LOAD_SESSION_LIBS인 경우 */
/* 14. pg_stat_activity 항목 완성, 시작 트랜잭션 커밋 */ pgstat_bestart_final(); CommitTransactionCommand();}10단계에서 LockSharedObject(DatabaseRelationId, dboid, 0, RowExclusiveLock)은 동시에 실행 중인 DROP DATABASE와 직렬화하기 위해 데이터베이스 OID에 짧은 쓰기 락을 건다. 이 락은 시작 트랜잭션이 끝나는 14단계까지만 유지된다. 그러나 그 시점에는 MyProc->databaseId가 이미 설정되어 있어, 이 데이터베이스를 제거하려는 DROP DATABASE가 procarray에서 이 백엔드를 발견하고 기다리게 된다.
3단계 SharedInvalBackendInit는 공유 무효화 링(sinval ring)의 진입점이다. 이 호출 이후부터 백엔드는 시스템 카탈로그를 수정하는 다른 백엔드로부터 캐시 무효화 메시지를 받는다. 이 시점부터 로컬 릴레이션 캐시와 catcache는 다른 백엔드의 카탈로그 변경에 맞춰 일관성을 유지한다.
그림 1 — 백엔드 초기화 시퀀스: 포크에서 ReadyForQuery까지
sequenceDiagram
participant PM as postmaster
participant BE as 백엔드 (신규)
participant SHM as 공유 메모리
PM->>BE: fork() / EXEC_BACKEND
BE->>BE: 시그널 핸들러 설치 (PostgresMain)
BE->>BE: InitProcess() — PGPROC 슬롯 할당
BE->>BE: BaseInit() — 로컬 서브시스템 초기화
BE->>SHM: InitProcessPhase2() — ProcArray 합류
BE->>SHM: SharedInvalBackendInit() — sinval 링 합류
BE->>BE: RelationCacheInitialize() — 빈 해시 테이블
BE->>SHM: RelationCacheInitializePhase2() — 공유 카탈로그 항목
BE->>BE: StartTransactionCommand() — 시작 트랜잭션
BE->>SHM: LockSharedObject(DatabaseOID) — DROP DATABASE 직렬화
BE->>BE: PerformAuthentication()
BE->>SHM: MyDatabaseId = dboid; MyProc->databaseId 설정
BE->>BE: RelationCacheInitializePhase3() — 릴레이션 캐시 전체 워밍업
BE->>BE: CheckMyDatabase(), process_settings()
BE->>BE: CommitTransactionCommand() — 시작 트랜잭션 + DB 락 해제
BE->>PM: 클라이언트에게 ReadyForQuery 전송
그림 1 — 백엔드 시작의 순차적 단계. InitProcessPhase2까지의 단계는 공유 메모리에서 백엔드를 보이게 만든다. LockSharedObject는 DROP DATABASE와 직렬화한다. 시작 트랜잭션은 9~14단계를 포괄한다.
4단계: 명령 루프
섹션 제목: “4단계: 명령 루프”InitPostgres가 반환되면 PostgresMain은 PostmasterContext를 해제하고(postmaster로부터 상속받은 GUC 문자열은 더 이상 필요 없다), NormalProcessing 모드로 전환하고, 명령별 할당을 위한 MessageContext를 생성하고, 로그인 이벤트 트리거를 실행하고, 오류 복구 점프 포인트를 설치한다.
// PostgresMain (명령 루프 설정) — src/backend/tcop/postgres.cif (sigsetjmp(local_sigjmp_buf, 1) != 0){ /* ereport(ERROR) 또는 시그널로 이곳에 도달 */ error_context_stack = NULL; HOLD_INTERRUPTS(); disable_all_timeouts(false); QueryCancelPending = false; pq_comm_reset(); EmitErrorReport(); AbortCurrentTransaction(); MemoryContextSwitchTo(MessageContext); FlushErrorState(); xact_started = false; /* 루프 상단에서 재개 */}PG_exception_stack = &local_sigjmp_buf;이것은 백엔드에서 가장 바깥쪽에 있는 setjmp다. 오류 복구 중에도 유일하게 활성화된 예외 핸들러다. 이는 의도적이다. PG_TRY로 감쌌다면 CATCH 부분 동안 활성 예외 핸들러가 전혀 없게 된다. 가장 바깥쪽 setjmp를 항상 활성화해두면 오류 복구 중 오류가 발생해도 최소한 이 핸들러로 돌아올 수 있다. 무한 루프에 빠지더라도 elog의 내부 상태 스택이 넘쳐 결국 정리되며 종료한다.
이어지는 for(;;) 루프는 다음과 같다.
// PostgresMain 명령 루프 — src/backend/tcop/postgres.cfor (;;){ MemoryContextSwitchTo(MessageContext); MemoryContextReset(MessageContext); /* 이전 명령 할당 해제 */ InvalidateCatalogSnapshotConditionally();
if (send_ready_for_query) { /* 통계 보고, notify 플러시, ps_display 설정, 유휴 타이머 시작 */ pgstat_report_stat(false); ReadyForQuery(whereToSendOutput); /* 클라이언트에게 'Z' 메시지 전송 */ send_ready_for_query = false; }
DoingCommandRead = true; firstchar = ReadCommand(&input_message); /* 여기서 블록 */ DoingCommandRead = false; CHECK_FOR_INTERRUPTS();
/* SIGHUP이 왔으면 설정 재로드 */ if (ConfigReloadPending) ProcessConfigFile(PGC_SIGHUP);
switch (firstchar) { case PqMsg_Query: exec_simple_query(query_string); break; case PqMsg_Parse: exec_parse_message(...); break; case PqMsg_Bind: exec_bind_message(...); break; case PqMsg_Execute: exec_execute_message(...); break; case PqMsg_Describe: exec_describe_statement_message / exec_describe_portal_message; break; case PqMsg_Terminate: proc_exit(0); /* 정상 종료 */ break; case EOF: proc_exit(0); break; // ... condensed ... } send_ready_for_query = true;}ReadCommand는 SocketBackend(와이어 프로토콜)나 InteractiveBackend(독립 실행형 모드)로 디스패치한다. SocketBackend는 1바이트 메시지 타입을 읽고, 알려진 집합과 대조해 검증한 뒤, 길이 접두사가 있는 본문을 읽는다.
트랜잭션 감싸기: start_xact_command / finish_xact_command
섹션 제목: “트랜잭션 감싸기: start_xact_command / finish_xact_command”데이터를 수정하거나 읽는 각 명령은 트랜잭션 컨텍스트로 감싼다. exec_simple_query는 파서/플래너/실행기를 호출하기 전에 start_xact_command를, 이후에 finish_xact_command를 호출한다.
// start_xact_command — src/backend/tcop/postgres.cstart_xact_command(void){ if (!xact_started) { StartTransactionCommand(); xact_started = true; } else if (MyXactFlags & XACT_FLAGS_PIPELINING) BeginImplicitTransactionBlock(); enable_statement_timeout(); /* ... condensed: 클라이언트 연결 확인 타임아웃 ... */}
// finish_xact_command — src/backend/tcop/postgres.cfinish_xact_command(void){ disable_statement_timeout(); if (xact_started) { CommitTransactionCommand(); xact_started = false; }}xact_started 플래그는 파이프라인으로 여러 Parse/Bind/Execute 메시지가 도착할 때 트랜잭션이 이중 시작되는 것을 막는다. CommitTransactionCommand는 xact.c로 내려간다(postgres-xact.md에서 문서화). 이 문서에서 관심사는 트랜잭션 내부가 아니라 생애주기 감싸기다.
그림 2 — 명령 루프: ReadyForQuery → ReadCommand → 디스패치 → 트랜잭션 감싸기 → 루프
stateDiagram-v2
[*] --> 유휴 : InitPostgres 완료
유휴 --> 읽기 : ReadyForQuery 전송\nDoingCommandRead=true
읽기 --> 디스패치 : ReadCommand 반환
디스패치 --> 실행 : start_xact_command
실행 --> 유휴 : finish_xact_command\nsend_ready_for_query=true
실행 --> 오류복구 : ereport(ERROR)\nlongjmp to sigsetjmp
오류복구 --> 유휴 : AbortCurrentTransaction\nFlushErrorState
읽기 --> 종료 : PqMsg_Terminate 또는 EOF
종료 --> [*] : proc_exit(0)
유휴 --> 크래시종료 : SIGQUIT
크래시종료 --> [*] : quickdie _exit(2)
그림 2 — 명령 루프의 상태 기계. sigsetjmp 오류 복구 경로는 외부 루프 구조를 건드리지 않고 실행에서 유휴로 연결된다.
종료: 정상 종료 vs 크래시 종료
섹션 제목: “종료: 정상 종료 vs 크래시 종료”정상 종료(die 핸들러, postmaster의 SIGTERM이나 클라이언트의 PqMsg_Terminate로 트리거): ProcDiePending = true와 InterruptPending = true를 설정한다. 메인 루프의 다음 CHECK_FOR_INTERRUPTS() 호출이 ProcDiePending을 보고 FATAL을 일으킨다. 이것은 elog 경로로 proc_exit까지 언와인드된다. proc_exit는 등록된 before_shmem_exit와 on_proc_exit 콜백을 역순으로 실행한다 — ShutdownPostgres → 리소스 오너 정리 → relcache 종료 → smgr 종료 — 그리고 _exit(0)을 호출한다.
크래시 종료(quickdie, SIGQUIT로 트리거): postmaster가 공유 메모리를 손상시켰을 수 있는 형제 크래시를 감지하면 모든 백엔드에 SIGQUIT을 보낸다. quickdie는 클라이언트에게 간략한 WARNING을 시도하고(최선 노력), 콜백을 전혀 실행하지 않고 _exit(2)를 호출한다. _exit(0)이 아니라 _exit(2)를 쓰는 것은 의도적이다. 누군가가 임의 백엔드에 수동으로 SIGQUIT을 보내면, 0이 아닌 종료 코드가 postmaster의 크래시-재시작 로직을 트리거해 실제 크래시 없이도 공유 메모리가 재구축된다는 점이다.
// quickdie — src/backend/tcop/postgres.cquickdie(SIGNAL_ARGS){ sigaddset(&BlockSig, SIGQUIT); /* 재귀적 quickdie 방지 */ sigprocmask(SIG_SETMASK, &BlockSig, NULL); HOLD_INTERRUPTS(); /* 최선 노력: WARNING_CLIENT_ONLY로 클라이언트에게 알림 */ error_context_stack = NULL; ereport(WARNING_CLIENT_ONLY, ...); /* proc_exit 콜백 실행 금지 — 공유 메모리가 손상됐을 수 있다 */ _exit(2);}소스 탐방
섹션 제목: “소스 탐방”초기화 서브시스템
섹션 제목: “초기화 서브시스템”postmaster_child_launch(launch_backend.c) — 진입점. Unix에서는 자식을 포크하고, Windows에서는internal_forkexec를 호출한다.MyBackendType을 설정한다.PostgresSingleUserMain(postgres.c) — 독립 실행형 모드 진입점. 최소한의 환경을 설정하고PostgresMain에 위임한다.PostgresMain(postgres.c) — 시그널 설정 →BaseInit→InitPostgres→ 명령 루프. 모든 백엔드 유사 프로세스의 최상위 함수다.BaseInit(postinit.c) — 파일, 통계, AIO, smgr, 버퍼, WAL-삽입, 락, 복제 슬롯 서브시스템의 로컬 측 초기화.InitPostgres(postinit.c) — 14단계 공유 등록 및 카탈로그 부트스트랩 시퀀스. 가장 안전에 민감한 초기화 경로다.InitProcess(proc.c) —PostgresMain이 호출되기 전에ProcGlobal에서PGPROC슬롯을 할당한다.InitProcessPhase2(proc.c) —MyProc를ProcArray에 연결한다. 백엔드가 클러스터 전체에 보이게 되는 순간이다.SharedInvalBackendInit(sinvaladt.c) — 공유 무효화 링에 백엔드를 등록한다. 이 호출 이후부터 sinval 메시지를 받는다.RelationCacheInitialize/RelationCacheInitializePhase2/RelationCacheInitializePhase3(relcache.c) — 3단계 릴레이션 캐시 워밍업: 해시 할당 → 공유 카탈로그 항목 로드 → 데이터베이스 로컬 항목 로드.InitCatalogCache/InitPlanCache(catcache.c,plancache.c) — syscache와 플랜 캐시 해시 테이블 할당.PerformAuthentication(postinit.c) —auth.c의ClientAuthentication을 호출한다. HBA/GSSAPI/SCRAM/md5/trust 디스패치 포인트다.LockSharedObject(InitPostgres에서 호출) —DROP DATABASE에 대한 직렬화를 위해 데이터베이스 OID에 쓰기 락을 건다.
명령 루프 서브시스템
섹션 제목: “명령 루프 서브시스템”ReadCommand(postgres.c) —whereToSendOutput에 따라SocketBackend나InteractiveBackend로 디스패치한다.SocketBackend(postgres.c) — 1바이트 메시지 타입을 읽고, 검증하고, 길이 접두사가 있는 본문을inBuf로 읽는다.exec_simple_query(postgres.c) — 단순 쿼리 프로토콜 핸들러.start_xact_command를 호출하고,pg_parse_query→pg_analyze_and_rewrite→pg_plan_queries→PortalRun을 거쳐,finish_xact_command를 호출한다.exec_parse_message/exec_bind_message/exec_execute_message(postgres.c) — 확장 쿼리 프로토콜 핸들러.start_xact_command/finish_xact_command(postgres.c) — 트랜잭션 생애주기 감싸기.finish_xact_command는xact.c의CommitTransactionCommand를 호출한다.ReadyForQuery(tcopprot.h/postgres.c) —Z와이어 메시지를 전송한다. 명령 사이의 플러시 포인트다.ProcessCatchupInterrupt(sinval.c) — 백엔드가 바쁜 동안 도착한 공유 무효화 catchup 메시지를 처리한다.
종료 서브시스템
섹션 제목: “종료 서브시스템”die(postgres.c) —SIGTERM핸들러.ProcDiePending을 설정하고, 다음CHECK_FOR_INTERRUPTS까지 지연한다.quickdie(postgres.c) —SIGQUIT핸들러. 콜백 없이_exit(2).ShutdownPostgres(postinit.c) —before_shmem_exit콜백. 공유 서브시스템이 종료되기 전에 포털, 커서를 정리하고 열린 트랜잭션을 중단한다.proc_exit(ipc.c) — 등록된before_shmem_exit와on_proc_exit콜백을 역순으로 실행하고,_exit(code)를 호출한다.
위치 힌트 (2026-06-05 기준, 커밋 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, 커밋 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
BackendType 열거형 | src/include/miscadmin.h | 337 |
AmRegularBackendProcess 매크로 | src/include/miscadmin.h | 381 |
postmaster_child_launch | src/backend/postmaster/launch_backend.c | 229 |
PostgresSingleUserMain | src/backend/tcop/postgres.c | 4059 |
PostgresMain | src/backend/tcop/postgres.c | 4188 |
sigsetjmp 오류 복구 블록 | src/backend/tcop/postgres.c | 4397 |
for(;;) 명령 루프 | src/backend/tcop/postgres.c | 4520 |
ReadCommand | src/backend/tcop/postgres.c | 481 |
SocketBackend | src/backend/tcop/postgres.c | 353 |
start_xact_command | src/backend/tcop/postgres.c | 2787 |
finish_xact_command | src/backend/tcop/postgres.c | 2826 |
quickdie | src/backend/tcop/postgres.c | 2930 |
die | src/backend/tcop/postgres.c | 3027 |
BaseInit | src/backend/utils/init/postinit.c | 612 |
InitPostgres | src/backend/utils/init/postinit.c | 712 |
PerformAuthentication | src/backend/utils/init/postinit.c | 194 |
LockSharedObject (DB 락) | src/backend/utils/init/postinit.c | 1058 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”검증된 사실
섹션 제목: “검증된 사실”-
REL_18_STABLE의
BackendType은 18개 멤버를 갖는다.B_IO_WORKER(PG18 비동기 I/O 워커)와B_WAL_SUMMARIZER(PG18 WAL 요약)가 포함된다. 커밋 273fe94의src/include/miscadmin.h337~375번째 줄을 읽어 검증했다.B_IO_WORKER와B_WAL_SUMMARIZER는 PG18에서 새로 추가됐다. PG17 대상 문서에서 이 타입들을 주장해선 안 된다. -
BaseInit은pgaio_init_backend()를 호출한다(PG18 비동기 I/O).postinit.c:639에서 검증했다. 이 호출은 PG17에 존재하지 않는다. PG18 전용 비동기 I/O 서브시스템은postgres-aio.md(계획 중)에서 다룬다. -
InitPostgres는 데이터베이스 OID에RowExclusiveLock(공유 락이 아님)을 건다.postinit.c:1058에서 검증했다. 주석에서 이유를 설명한다. 세션은 데이터베이스의 동시 쓰기자로 간주되므로 쓰기 락이 적절하다. 락은InitPostgres끝의CommitTransactionCommand까지만 유지된다. -
quickdie는_exit(0)이 아니라_exit(2)를 호출한다.postgres.c:3019에서 검증했다. 종료 코드 2는 의도적이다.pmsignal.c의 “dead man switch” 메커니즘이 postmaster의 크래시-재시작 로직을 트리거하며, 임의 백엔드에 수동으로SIGQUIT를 보낼 때도 동작한다. -
sigsetjmp(local_sigjmp_buf, 1)— 두 번째 인수는 1(시그널 마스크 저장).postgres.c:4397에서 검증했다. 주석에서 설명한다. 이는 longjmp 시UnBlockSig(백엔드의 기본 시그널 마스크)를 복원해, 시그널 핸들러가 longjmp로 언와인드된 후 시그널이 블록된 채 남지 않도록 한다. -
MessageContext는 매 명령 루프 반복 상단에서 삭제 후 재생성이 아니라 리셋된다.postgres.c:4542~4545에서 검증했다.MemoryContextReset은 모든 자식을 해제하고 컨텍스트 자체의 할당을 리셋하되, 컨텍스트 헤더 자체를 해제하지 않는다. 이는 매 사이클마다MemoryContextDelete+AllocSetContextCreate를 호출하는 것보다 효율적이다. -
ReadyForQuery는Z메시지를 전송하기 전에pgstat_report_stat(누적 통계 플러시)와ProcessNotifyInterrupt(LISTEN/NOTIFY 전달)를 호출한다.postgres.c:4610~4641에서 검증했다. 통계는 매 명령이 아니라 유휴 시점에만 플러시된다. 미커밋 상태를 autovacuum에 보고하는 것을 피하기 위해서다.
미해결 질문
섹션 제목: “미해결 질문”-
B_DEAD_END_BACKEND생애주기.B_DEAD_END_BACKEND는 postmaster에 사용 가능한PGPROC슬롯이 없거나(연결 한도 초과) 종료 전에 알리고 싶은 인증 오류를 만날 때 포크된다. 이 타입에서BaseInit이 호출되는지, 아니면 프로세스가 단순히 오류 메시지를 보내고 바로 종료하는지 불명확하다. 조사 경로:launch_backend.c에서child_type == B_DEAD_END_BACKEND로postmaster_child_launch를 추적하고 해당 진입점을 찾는다. -
EventTriggerOnLogin(PG16 기능).PostgresMain은InitPostgres완료 후EventTriggerOnLogin()을 호출한다(postgres.c:4373). 로그인 이벤트 트리거 실패가 명령 루프 복구 경로와 어떻게 상호작용하는지(즉,ReadyForQuery를 막는지 여부)는 이 문서에서 분석하지 않았다. 조사 경로:event_trigger.c에서EventTriggerOnLogin을 추적하고 오류 처리를 살펴본다. -
start_xact_command의XACT_FLAGS_PIPELINING동작. 파이프라이닝이 활성화되어 있고 트랜잭션이 이미 시작된 경우BeginImplicitTransactionBlock이 호출된다. 파이프라인을 보통 종료하는 확장 쿼리Sync메시지와ignore_till_sync플래그와의 상호작용은postgres-portals-prepared.md에서 별도 탐구가 필요하다.
PostgreSQL 너머 — 비교 설계와 연구 프런티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프런티어”-
세션당 스레드(MySQL/InnoDB, SQL Server) — 지배적인 대안이다. Architecture of a Database System(Hellerstein et al. 2007, §2.2)은 균형 잡힌 비교를 제공한다. 스레드는 생성 비용이 낮고 메모리를 더 쉽게 공유하지만, 한 스레드의 스택 버그가 다른 세션의 데이터를 오염시킬 수 있다. PostgreSQL의 프로세스 모델은 밀도가 낮은 대신 더 강한 격리를 얻는다. 관련 비교 주제: 새 연결당
fork()+ 공유 메모리 등록이 스레드 생성 + TLS 설정에 비해 얼마나 많은 오버헤드를 추가하는가? -
프로세스 모델 확장 해결책으로서의 연결 풀링 — PgBouncer(트랜잭션 모드 풀링)와 PG17+의 내장 풀 프로토타입은 클라이언트 연결을 백엔드 프로세스에서 분리해 세션당 포크 오버헤드를 줄인다.
PostgresMain초기화 비용을 이해하는 것은 풀이 무엇을 지연하고 무엇을 재사용하는지 이해하기 위한 전제 조건이다. -
Oracle의 전용 서버 vs 공유 서버 모델 — Oracle은 전용 모드에서 클라이언트당 서버 프로세스 하나를 배정한다(PostgreSQL과 동일). 그러나 공유 서버 모드(MTS)에서는 더 적은 서버 프로세스에 많은 클라이언트를 다중화한다. PostgreSQL에는 MTS에 해당하는 것이 없다. pgBouncer가 외부적으로 그 역할을 담당한다.
-
Greenplum / Citus 코디네이터 백엔드 —
PostgresMain이 쿼리의 일부를 원격 세그먼트로 디스패치하도록 확장된 분산 PostgreSQL 포크들이다. 여기서 설명하는 생애주기가 두 프로젝트 모두가 확장하는 기반이다. 변경 사항은 대부분 초기화가 아니라exec_simple_query와 플랜 실행에 있다. -
B_IO_WORKER(PG18 비동기 I/O) — 새로운B_IO_WORKER백엔드 타입은 다른 생애주기 경로를 따른다.InitPostgres를 호출하지 않으며 카탈로그 상태를 보유하지 않는다. 이 생애주기는 계획 중인postgres-aio.md에서 다룬다.
소비된 원시 소스 파일
섹션 제목: “소비된 원시 소스 파일”- 없음(REL_18_STABLE / 273fe94 소스 트리에서 직접 합성).
소스 코드 경로 (REL_18_STABLE / 273fe94)
섹션 제목: “소스 코드 경로 (REL_18_STABLE / 273fe94)”src/backend/tcop/postgres.c—PostgresMain, 명령 루프, 시그널 핸들러,start/finish_xact_command,ReadCommand,SocketBackendsrc/backend/utils/init/postinit.c—BaseInit,InitPostgres,PerformAuthentication,ShutdownPostgressrc/backend/utils/init/miscinit.c—InitializeSessionUserId,InitializeClientEncoding,InitializeSearchPathsrc/backend/postmaster/launch_backend.c—postmaster_child_launch,PostmasterChildName,SubPostmasterMain(Windows 재진입)src/include/miscadmin.h—BackendType열거형,Am*Process매크로
교과서 앵커
섹션 제목: “교과서 앵커”- Architecture of a Database System(Hellerstein, Moody, Doan, Balsa, Hellerstein 2007) §2 “Process Models” — 스레드 vs 프로세스 모델 비교. KB의
knowledge/research/dbms-papers/fntdb07-architecture.md에 있음. - Database Internals(Petrov 2019) ch. 1 — 일반적인 데이터베이스 프로세스 모델 맥락.