콘텐츠로 이동

(KO) PostgreSQL 백그라운드 워커 — 동적 워커 프레임워크

목차

데이터베이스 서버는 본질적으로 태스크 디스패치 시스템이다. 오래 살아있는 제어 주체가 실제 작업—쿼리 실행, 버퍼 플러시, 튜플 vacuum, WAL 전송—을 수행하는 하위 실행 단위를 생성·감시·수거한다. 모든 엔진이 대답해야 하는 설계 질문은 실행 단위가 무엇이고, 그 생명주기를 누가 소유하는가이다. 이 설계 공간을 세 가지 속성이 정의한다.

  1. 프로세스 대 스레드. 하위 단위는 자체 주소 공간을 가진 OS 프로세스인가, 아니면 공유 주소 공간 안의 스레드인가? 프로세스는 결함 격리를 제공한다—충돌한 워커가 다른 워커의 스택을 덮어쓸 수 없다—대신 컨텍스트 전환 비용이 크고, 공유해야 하는 모든 데이터를 위한 명시적 공유 메모리 영역이 필요하다. 스레드는 공유 비용을 줄이는 대신 운명을 함께한다: 포인터 하나가 망가지면 서버 전체가 내려간다. Architecture of a Database System (Hellerstein, Stonebraker & Hamilton 2007, §2 “Process Models”)은 세 가지 정형 모델—process-per-connection, thread-per-connection, process/thread pool—을 정리하면서, 이 선택이 캐싱·락킹·입장 제어에 관한 이후의 모든 결정에 스며드는 “가장 근본적인” 선택 중 하나라고 말한다.

  2. 정적 대 동적 워커 집합. 하위 프로세스가 시작 시에 결정된 고정 집단인가, 아니면 부하에 따라 늘어나고 줄어들 수 있는가? 8개 코어에 2초 동안 팬아웃했다가 하나로 돌아오는 병렬 쿼리는 서브초 단위로 워커를 생성하고 해체한다. 반면 백그라운드 vacuum 런처는 오래 살아있는 헬퍼 하나만 필요하다. 범용 프레임워크는 둘 다 지원해야 한다: 부트 시 한 번 등록과 런타임 즉시 등록.

  3. 감시와 재시작 정책. 워커가 죽으면 누가 알아채고, 어떻게 되는가? 충돌한 헬퍼를 재시작하는 감시자는 자가 치유를 제공한다. 정상 종료 후 잊어버리는 감시자는 좀비 누적을 막는다. 이 정책은 워커마다 인코딩되어야 한다. 자신의 슬라이스를 끝낸 병렬 워커는 절대 재시작하면 안 되지만, 연결이 끊긴 논리 복제 apply 워커는 재시작해야 하기 때문이다.

Database System Concepts (Silberschatz, Korth & Sudarshan, 7e, §20 “Database-System Architectures”)는 서버를 공유 메모리로 협력하는 프로세스 집합으로 보고, 장애를 감지해 복구를 트리거하는 “프로세스 모니터”가 가용성의 핵심이라고 설명한다. PostgreSQL의 postmaster가 바로 이 프로세스 모니터다. 백그라운드 워커 프레임워크는 그 감시 로직을 일반화·플러그인화한 확장이다: 포스트마스터가 코드 자체를 담고 있지 않은 프로세스들(확장, 병렬 워커, apply 워커)을 내장 보조 프로세스와 동일한 재시작·장애 복구 체계 아래에서 fork하고 수거할 수 있게 한다.

PostgreSQL 특유의 중요한 제약이 이 모듈 전체를 형성한다. 포스트마스터의 신뢰성 불변 조건: 포스트마스터는 백엔드와 공유하는 공유 메모리에서 절대 락을 잡으면 안 된다—스핀락조차도. 공유 메모리를 오염시킨 백엔드(와일드 라이트, 버그)가 포스트마스터를 멈추거나 충돌시킬 수 있고, 포스트마스터가 죽으면 누구도 장애 복구를 못 한다. 따라서 백그라운드 워커의 공유 메모리 프로토콜 전체가 포스트마스터 측에서 락 없이 동작하도록 설계되었다: 백엔드들은 통상적인 LWLock으로 서로 조율하지만, 단일하고 신중하게 순서가 지켜진 플래그(in_use)와 메모리 배리어로 포스트마스터에게 슬롯을 넘긴다. 이것이 이 모듈 전체의 설계 핵심이다.

이 절은 엔진들이 감시-워커 프레임워크를 만들 때 반복하는 공학 패턴을 정리한다. PostgreSQL의 선택이 공유된 설계 공간 안에서의 선택으로 읽히게 하기 위해서다.

프로세스 모델을 쓰는 거의 모든 엔진은 감시자(Oracle의 PMON/SMON 시대 모니터, SQL Server의 SQLOS 스케줄러, PostgreSQL의 포스트마스터)와 사용자에게 보이는 작업을 수행하는 워커를 분리한다. 감시자는 fork/exec(또는 스레드 생성)를 소유하고, 살아있는 프로세스 목록을 유지하며, SIGCHLD/종료 이벤트를 잡고, 재시작 여부를 결정한다. 워커는 감시자가 저렴하고 안전하게 읽을 수 있는 채널로 상태를 보고한다.

워커에서 감시자로 “이런 워커를 시작해 달라”고 전달하기 위해, 엔진들은 공유 메모리 안의 고정 크기 디스크립터 슬롯 배열을 사용한다. 배열이 고정 크기(연결 리스트가 아님)인 이유는 공유 메모리가 시작 시에 한 번 할당되고, 감시자는 신뢰할 수 없는 포인터를 따라가지 않고 스캔할 수 있어야 하기 때문이다. 각 슬롯은 워커의 설정과 소규모 상태 기계(free → claimed → running → exited)를 담는다. 슬롯 수는 하드 상한이다. PostgreSQL에서는 max_worker_processes GUC가 이 상한이다.

ABA 문제를 막는 제너레이션 카운터

섹션 제목: “ABA 문제를 막는 제너레이션 카운터”

미묘한 위험이 있다. 백엔드 A가 워커를 등록하고 슬롯 5를 가리키는 핸들을 얻는다. 워커가 종료되고, 슬롯 5가 백엔드 B의 새 워커를 위해 재활용된다. 이제 A의 오래된 핸들은 자신 것이 아닌 살아있는 워커를 가리키는 것처럼 보인다. 표준 방어책은 슬롯과 함께 저장하고 핸들로 복사하는 제너레이션 카운터(태그 또는 에포크라고도 부른다)다. 슬롯을 재사용할 때마다 카운터를 올린다. 핸들은 자신의 제너레이션이 슬롯의 제너레이션과 일치할 때만 유효하다. 이것은 락-프리 프로그래밍의 고전적인 ABA 문제 해법을 슬롯 재사용에 적용한 것이다. PostgreSQL의 BackgroundWorkerHandle{slot, generation} 쌍인 이유가 바로 여기에 있다.

포인터가 아닌 이름으로 주소 전달

섹션 제목: “포인터가 아닌 이름으로 주소 전달”

감시자가 워커를 fork하고 워커가 감시자에게 알려진 함수를 호출해야 할 때, 프로세스 사이에서 주소가 다를 가능성이 있다면 날 함수 포인터를 넘길 수 없다. EXEC_BACKEND(Windows, 또는 --enable-exec-backend 디버그 빌드) 아래에서는 자식이 새 exec이므로 ASLR이 같은 함수를 다른 주소에 배치할 수 있다. 이식성 있는 패턴은 (library_name, function_name) 문자열을 전달하고 워커가 동적 로더로 로컬에서 해석하도록 하는 것이다. PostgreSQL의 bgw_library_name / bgw_function_name 쌍과 LookupBackgroundWorkerFunction이 정확히 이것을 구현한다.

가변 크기 공유 상태를 위한 동적 공유 메모리

섹션 제목: “가변 크기 공유 상태를 위한 동적 공유 메모리”

고정 공유 메모리 영역은 부트 시에 크기가 정해지지만, 병렬 쿼리는 쿼리의 튜플 큐와 계측 요구에 맞는 공유 메모리 청크가 필요하다—부트 시에는 알 수 없다. 엔진들은 동적 공유 메모리 기능으로 답한다: 요청 시 생성되고, 다른 프로세스에 전달할 수 있는 핸들로 이름 붙이며, 마지막으로 분리하는 쪽이 해제하는 참조 카운트 방식의 퍼-오퍼레이션 세그먼트. PostgreSQL은 이를 계층으로 쌓는다: dsm.c(날 세그먼트) → dsa.c(세그먼트 위의 공유 힙, dsa_allocate/dsa_free) → shm_mq, shm_toc(메시지 큐와 목차). 백그라운드 워커 프레임워크와 DSM은 거의 항상 함께 다니는 짝이다: 워커를 등록하고, DSM 핸들을 bgw_main_arg로 넘긴다.

flowchart TB
  subgraph PM["Postmaster (no locks, ever)"]
    LIST["BackgroundWorkerList<br/>postmaster 전용 dlist (RegisteredBgWorker)"]
    SC["BackgroundWorkerStateChange()<br/>슬롯 스캔, 신규 등록 복사"]
    START["maybe_start_bgworkers()<br/>적격 워커 fork"]
  end
  subgraph SHM["공유 메모리"]
    ARR["BackgroundWorkerArray<br/>slot[max_worker_processes]<br/>in_use / terminate / pid / generation"]
    DSM["DSM 세그먼트 + DSA 영역<br/>퍼-오퍼레이션 공유 상태"]
  end
  subgraph BE["일반 백엔드 (BackgroundWorkerLock 가능)"]
    REG["RegisterDynamicBackgroundWorker()<br/>빈 슬롯 획득"]
    H["BackgroundWorkerHandle<br/>{slot, generation}"]
  end
  REG -->|"LWLock + write barrier,<br/>in_use=true 설정"| ARR
  REG -->|"PMSIGNAL_BACKGROUND_WORKER_CHANGE"| SC
  SC -->|"read barrier, 슬롯 복사"| LIST
  LIST --> START
  START -->|"postmaster_child_launch()<br/>fork"| WORKER["BackgroundWorkerMain()"]
  REG --> H
  H -.->|"GetBackgroundWorkerPid /<br/>WaitFor* / Terminate"| ARR
  WORKER -.->|"dsm_attach(bgw_main_arg)"| DSM
  BE -.->|"dsa_create / dsm_create"| DSM

PostgreSQL은 의도적으로 작은 공개 API를 노출한다—다섯 개의 등록/제어 함수와 설정 구조체 하나. 그 뒤에는 신중한 락-프리 프로토콜이 숨어있다. 구조체, 두 가지 등록 경로, 슬롯 배열, 생명주기, DSM/DSA 동반자를 순서대로 살펴본다.

호출자가 포스트마스터에 알리고 싶은 예비 워커의 모든 속성이 고정 레이아웃 구조체 하나에 담긴다. 공유 메모리로 memcpy되고 fork/exec 경계를 넘어야 하기 때문이다.

// BackgroundWorker — src/include/postmaster/bgworker.h
typedef struct BackgroundWorker
{
char bgw_name[BGW_MAXLEN];
char bgw_type[BGW_MAXLEN];
int bgw_flags;
BgWorkerStartTime bgw_start_time;
int bgw_restart_time; /* in seconds, or BGW_NEVER_RESTART */
char bgw_library_name[MAXPGPATH];
char bgw_function_name[BGW_MAXLEN];
Datum bgw_main_arg;
char bgw_extra[BGW_EXTRALEN];
pid_t bgw_notify_pid; /* SIGUSR1 this backend on start/stop */
} BackgroundWorker;

여기에는 포인터가 없다. Datum bgw_main_arg는 관례적으로 작은 스칼라다(DSM 핸들, OID, 슬롯 인덱스). 힙 주소는 절대 안 된다—워커는 다른 주소 공간에서 실행되기 때문이다. 진입점은 포인터가 아닌 이름으로 지정된다: bgw_library_name + bgw_function_name. bgw_flagsBGWORKER_SHMEM_ACCESS(필수), BGWORKER_BACKEND_DATABASE_CONNECTION(워커가 BackgroundWorkerInitializeConnection을 호출함), 내부 전용 BGWORKER_CLASS_PARALLEL의 비트마스크다. bgw_start_time(세 가지 값 중 하나: BgWorkerStart_PostmasterStart / _ConsistentState / _RecoveryFinished)은 포스트마스터가 시작 중 언제 이 워커를 실행할 수 있는지 알려준다. DB 연결이 필요한 워커는 포스트마스터 시작 시에 실행할 수 없다—카탈로그가 아직 준비되지 않았기 때문이다.

두 가지 등록 경로, 하나의 슬롯 배열

섹션 제목: “두 가지 등록 경로, 하나의 슬롯 배열”

정적 등록은 부트 시에 알려진 워커를 위한 경로다. shared_preload_libraries를 처리하는 동안 포스트마스터 자신이나 확장의 _PG_init에서만 호출할 수 있다. 공유 메모리가 아닌 포스트마스터 전용 dlist에 추가한다—공유 메모리가 아직 존재하지 않기 때문이다.

// RegisterBackgroundWorker — src/backend/postmaster/bgworker.c
void
RegisterBackgroundWorker(BackgroundWorker *worker)
{
RegisteredBgWorker *rw;
static int numworkers = 0;
if (IsUnderPostmaster || !IsPostmasterEnvironment)
{
if (process_shared_preload_libraries_in_progress)
return;
ereport(LOG, ( /* ... must be registered in shared_preload_libraries */ ));
return;
}
if (BackgroundWorkerData != NULL)
elog(ERROR, "cannot register background worker \"%s\" after shmem init", ...);
/* ... SanityCheckBackgroundWorker, numworkers cap ... */
rw->rw_worker = *worker;
rw->rw_pid = 0;
rw->rw_crashed_at = 0;
rw->rw_terminate = false;
dlist_push_head(&BackgroundWorkerList, &rw->rw_lnode);
}

BackgroundWorkerShmemInit 시점에 이 전용 엔트리들이 공유 slot[] 배열로 1대1 복사된다. 포스트마스터의 전용 BackgroundWorkerList와 공유 BackgroundWorkerArray 사이의 대응 관계가 이때 만들어진다.

동적 등록은 일반 백엔드(병렬 쿼리 리더, 논리 복제 런처, 확장의 SQL 함수)가 런타임에 워커를 요청하는 경로다. 반드시 포스트마스터 아래에서 실행되어야 하고, BackgroundWorkerLock을 잡아 다른 백엔드와 조율하며, 빈 슬롯을 스캔하고, 슬롯을 채운 뒤—결정적으로—in_use를 true로 설정하기 전에 write 배리어를 발행하여 포스트마스터가 절반만 초기화된 슬롯을 절대 볼 수 없도록 한다.

// RegisterDynamicBackgroundWorker — src/backend/postmaster/bgworker.c
LWLockAcquire(BackgroundWorkerLock, LW_EXCLUSIVE);
/* ... parallel-class admission check against max_parallel_workers ... */
for (slotno = 0; slotno < BackgroundWorkerData->total_slots; ++slotno)
{
BackgroundWorkerSlot *slot = &BackgroundWorkerData->slot[slotno];
if (!slot->in_use)
{
memcpy(&slot->worker, worker, sizeof(BackgroundWorker));
slot->pid = InvalidPid; /* indicates not started yet */
slot->generation++;
slot->terminate = false;
generation = slot->generation;
if (parallel)
BackgroundWorkerData->parallel_register_count++;
pg_write_barrier(); /* postmaster must see contents before in_use */
slot->in_use = true;
success = true;
break;
}
}
LWLockRelease(BackgroundWorkerLock);
if (success)
SendPostmasterSignal(PMSIGNAL_BACKGROUND_WORKER_CHANGE);

백엔드는 포스트마스터에게 신호(PMSIGNAL_BACKGROUND_WORKER_CHANGE)를 보낸다. 핸들을 요청했다면 나중에 워커를 조회하거나 종료할 때 쓸 {slot, generation} 쌍을 받는다.

공유 슬롯은 다섯 개 필드를 담는다. bgworker.c의 주석 블록이 소유권 계약을 명시한다: in_use가 false이면 포스트마스터는 슬롯을 무시하고 백엔드가 소유한다. 백엔드가 write 배리어 후 in_use를 true로 뒤집으면 슬롯은 포스트마스터의 것이 된다. 이후 백엔드는 terminate 플래그만 설정할 수 있다.

// BackgroundWorkerSlot — src/backend/postmaster/bgworker.c
typedef struct BackgroundWorkerSlot
{
bool in_use;
bool terminate;
pid_t pid; /* InvalidPid = not started yet; 0 = dead */
uint64 generation; /* incremented when slot is recycled */
BackgroundWorker worker;
} BackgroundWorkerSlot;

백엔드가 보유하는 핸들은 제너레이션이 태그된 좌표다.

// BackgroundWorkerHandle — src/backend/postmaster/bgworker.c
struct BackgroundWorkerHandle
{
int slot;
uint64 generation;
};

포스트마스터 측 핸드오프는 신호에 응답해 실행되는 BackgroundWorkerStateChange다. 공유 메모리가 오염되었을 수 있다고 가정하면서도 포스트마스터를 충돌시키지 않도록 방어적으로 작성되었다. 백엔드가 write 배리어 아래 쓴 슬롯 내용을 in_use 이전에 보는 일이 없도록 read 배리어를 사용한다.

// BackgroundWorkerStateChange — src/backend/postmaster/bgworker.c
for (slotno = 0; slotno < max_worker_processes; ++slotno)
{
BackgroundWorkerSlot *slot = &BackgroundWorkerData->slot[slotno];
if (!slot->in_use)
continue;
pg_read_barrier(); /* pair with the registrant's write barrier */
rw = FindRegisteredWorkerBySlotNumber(slotno);
if (rw != NULL)
{
if (slot->terminate && !rw->rw_terminate) /* backend asked to kill */
{
rw->rw_terminate = true;
if (rw->rw_pid != 0)
kill(rw->rw_pid, SIGTERM);
else
ReportBackgroundWorkerPID(rw);
}
continue;
}
/* ... newly-registered: copy strings paranoidly (ascii_safe_strlcpy),
copy fixed fields, push onto BackgroundWorkerList ... */
}

포스트마스터가 락을 잡을 수 없으므로, 병렬 워커 집계는 원자적으로 함께 읽을 필요가 없는 두 카운터로 분리된다: parallel_register_count(백엔드가 락 아래 증가)와 parallel_terminate_count(락 없는 포스트마스터만 증가). 실제 개수는 둘의 차이이며, uint32 래핑을 넘어도 뺄셈은 정확하다.

flowchart LR
  FREE["슬롯 비어있음<br/>in_use=false"] -->|"백엔드: memcpy worker,<br/>generation++, write barrier,<br/>in_use=true"| CLAIMED["획득됨<br/>in_use=true, pid=InvalidPid"]
  CLAIMED -->|"postmaster: StateChange<br/>전용 리스트로 복사"| KNOWN["등록됨<br/>postmaster 리스트에"]
  KNOWN -->|"maybe_start_bgworkers:<br/>fork, ReportBackgroundWorkerPID"| RUN["실행 중<br/>pid > 0"]
  RUN -->|"종료 코드 0 또는 never-restart<br/>또는 terminate 플래그"| FORGET["ForgetBackgroundWorker<br/>in_use=false 복귀"]
  RUN -->|"종료 코드 1,<br/>재시작 간격 경과"| KNOWN
  CLAIMED -->|"시작 전 백엔드가<br/>terminate 설정"| FORGET
  FORGET --> FREE

생명주기: 실행, 충돌, 재시작, 수거

섹션 제목: “생명주기: 실행, 충돌, 재시작, 수거”

포스트마스터는 메인 루프에서 maybe_start_bgworkersStartBackgroundWorker로 적격 워커를 실행한다. postmaster_child_launch(B_BG_WORKER, …, &rw->rw_worker, …)로 fork한다. fork된 자식은 BackgroundWorkerMain을 실행한다. 이 함수는 상속받은 포스트마스터 메모리에서 등록 정보를 복사하고, PostmasterContext를 해제하고, 시그널 핸들러를 설치하고, InitProcess + BaseInit를 호출하고, 진입점을 해석하고, 최종적으로 사용자 함수를 호출한다. 재시작 계약은 종료 코드로 인코딩된다. ReportBackgroundWorkerExit에서 해석한다: 종료 코드 0 또는 BGW_NEVER_RESTART이면 워커를 잊는다(슬롯 해제). 종료 코드 1이면 등록을 유지해서 rw_crashed_at으로부터 bgw_restart_time초가 지난 후 maybe_start_bgworkers가 다시 실행한다.

런처와 가변 크기 상태를 공유해야 하는 워커는 bgw_main_arg로 전달된 핸들로 동적 공유 메모리 세그먼트를 연결한다. dsm_create는 세그먼트를 만든다(사전 할당된 메인 영역 슬롯을 우선 사용하고, 없으면 OS 수준 세그먼트로 폴백). 제어 배열에서 참조 카운트하고, 프로세스 간 이식 가능한 이름인 dsm_segment *의 핸들을 반환한다.

// dsm_create (excerpt) — src/backend/storage/ipc/dsm.c
seg = dsm_create_descriptor();
/* try main shared-memory region first, else create an OS segment: */
seg->handle = pg_prng_uint32(&pg_global_prng_state) << 1; /* even handles */
/* ... dsm_impl_op(DSM_OP_CREATE, ...) ... */
dsm_control->item[i].handle = seg->handle;
dsm_control->item[i].refcnt = 2; /* refcnt 1 == moribund, so start at 2 */

공유 메모리 안에서 크기가 작은 할당이 많을 때는 날 세그먼트가 너무 거칠다. dsa.c는 그 위에 공유 힙을 만든다: dsa_create는 DSM 세그먼트를 만들어 고정하고, 제어 객체와 빈 페이지 관리자를 얹는다. 할당은 dsa_pointer({세그먼트-인덱스, 오프셋} 유사 포인터)를 반환하고, 연결된 백엔드라면 dsa_get_address로 로컬 주소로 변환한다.

// dsa_get_address — src/backend/utils/mmgr/dsa.c
index = DSA_EXTRACT_SEGMENT_NUMBER(dp);
offset = DSA_EXTRACT_OFFSET(dp);
if (unlikely(area->segment_maps[index].mapped_address == NULL))
get_segment_by_index(area, index); /* map it in on demand */
return area->segment_maps[index].mapped_address + offset;

유사 포인터 간접 참조가 DSA 포인터를 공유 가능하게 만드는 핵심이다. dsa_pointer는 모든 백엔드에서 동일한 논리적 위치를 의미한다. 세그먼트가 각 백엔드에서 다른 가상 주소에 매핑되어 있어도 마찬가지다. (더 넓은 IPC 이야기—shm_mq, shm_toc, 메인 공유 메모리 영역—는 교차 참조된 postgres-shared-memory-ipc.md를 보라.)

아래 심볼들은 정형 앵커다. grep으로 찾을 수 있다. 콜 플로우 기준으로 묶었다. 줄 번호는 끝의 위치 힌트 표에만 있다.

공유 메모리 레이아웃과 초기화

섹션 제목: “공유 메모리 레이아웃과 초기화”
  • BackgroundWorkerSlot — 워커별 공유 슬롯: in_use, terminate, pid, generation, 내장된 worker. 락-프리 프로토콜 전체는 이 다섯 필드 쓰기 순서에 관한 계약이다.
  • BackgroundWorkerArray — 공유 헤더: total_slots, parallel_register_count, parallel_terminate_count, slot[FLEXIBLE_ARRAY_MEMBER]. shmem 해시에서 "Background Worker Data"라는 이름으로 한 인스턴스만 존재하며, 정적 포인터 BackgroundWorkerData가 가리킨다.
  • BackgroundWorkerShmemSize — 배열 크기를 offsetof(BackgroundWorkerArray, slot) + max_worker_processes * sizeof(BackgroundWorkerSlot)로 계산한다. max_worker_processes가 재시작 없이 변경할 수 없는 하드 상한인 이유다.
  • BackgroundWorkerShmemInit — 부트 시 포스트마스터 전용 BackgroundWorkerList를 처음 N개 슬롯에 복사한다(in_use 표시, rw->rw_shmem_slot 기록). 나머지는 free로 표시. !IsUnderPostmaster로 포스트마스터만 초기화하도록 보호한다.
  • RegisteredBgWorker (bgworker_internals.h) — 슬롯의 포스트마스터 전용 미러: rw_worker, rw_pid, rw_crashed_at, rw_shmem_slot, rw_terminate, dlist 링크 rw_lnode.
  • RegisterBackgroundWorker — 정적 경로. 포스트마스터 외부에서의 호출을 거부한다(process_shared_preload_libraries_in_progress 케이스는 허용). shmem 초기화 후 호출을 거부한다. numworkers > max_worker_processes 상한을 적용한다. BackgroundWorkerList에 push한다. 0이 아닌 bgw_notify_pid를 거부한다—동적 워커만 알림을 요청할 수 있다.
  • RegisterDynamicBackgroundWorker — 동적 경로. 포스트마스터 아래가 아니면 false 반환. BackgroundWorkerLock을 독점 취득하고, 병렬 클래스 입장 검사를 적용하고, !slot->in_use를 스캔하고, 슬롯을 채우고, generation++, pg_write_barrier(), slot->in_use = true, 락 해제, PMSIGNAL_BACKGROUND_WORKER_CHANGE 전송. 선택적으로 BackgroundWorkerHandle을 반환한다.
  • SanityCheckBackgroundWorker — 공유 검증기: BGWORKER_SHMEM_ACCESS 필요; DB 연결 워커의 BgWorkerStart_PostmasterStart 시작 금지; bgw_restart_time 범위 검사; 병렬 워커의 재시작 설정 금지(카운터 집계가 충돌-재시작을 거친 병렬 워커를 처리할 수 없음); bgw_type 기본값을 bgw_name으로 설정.
  • BackgroundWorkerStateChange — 변경 신호에 대한 포스트마스터의 반응. total_slots를 검증하고, pg_read_barrier()로 슬롯을 스캔하고, terminate를 실행 중인 워커에 SIGTERM으로 전파하고, ascii_safe_strlcpy(NUL 종료가 되지 않은 오염된 문자열에 대한 방어)를 사용해 새 등록을 전용 리스트에 복사한다. allow_new_workers=false(종료 중)이면 대기 중인 모든 슬롯을 terminate로 강제한다.
  • FindRegisteredWorkerBySlotNumberBackgroundWorkerList를 선형 스캔하여 슬롯 번호에서 전용 RegisteredBgWorker로 매핑한다.
  • maybe_start_bgworkers (postmaster.c) — 실행 루프. 이미 실행 중인 워커는 건너뛰고, terminate된 것은 잊고, rw_crashed_at을 현재 시각과 비교해 bgw_restart_time을 준수하고, bgworker_should_start_now로 게이트하고, 한 번의 패스에서 MAX_BGWORKERS_TO_LAUNCH(100)로 상한을 둬서 등록 홍수가 포스트마스터의 다른 작업을 굶기지 않도록 한다.
  • bgworker_should_start_now (postmaster.c) — 현재 pmState를 어떤 bgw_start_time 값이 적격인지에 매핑한다. fall-through로 “PostmasterStart < ConsistentState < RecoveryFinished” 순서를 구현한다.
  • StartBackgroundWorker (postmaster.c) — 자식 슬롯을 할당하고, postmaster_child_launch(B_BG_WORKER, …)로 fork하고, rw_pid를 기록하고, ReportBackgroundWorkerPID를 호출한다. fork 실패는 충돌처럼 처리한다(rw_crashed_at = now). 재시도 전에 포스트마스터가 물러선다.
  • BackgroundWorkerMain — 자식의 main. 시작 BackgroundWorkerTopMemoryContext로 복사하고, PostmasterContext를 삭제하고, MyBgworkerEntry / MyBackendType = B_BG_WORKER를 설정하고, 시그널 핸들러를 설치하고(SIGTERM용 bgworker_die 주목), sigsetjmp 오류 복구를 설정하고, InitProcess + BaseInit를 호출하고, LookupBackgroundWorkerFunction으로 진입점을 해석하고, 호출한다. 반환 시 proc_exit(0)(재시작 없음).
  • LookupBackgroundWorkerFunction(bgw_library_name, bgw_function_name)을 주소로 해석한다. "postgres"이면 InternalBGWorkers[] 테이블(ParallelWorkerMain, ApplyLauncherMain, ApplyWorkerMain, ParallelApplyWorkerMain, TablesyncWorkerMain)에서 검색한다. 그 외에는 load_external_function. EXEC_BACKEND에서도 살아남는 이름-아닌-포인터 메커니즘이다.
  • BackgroundWorkerInitializeConnection / …ByOid — DB 연결 워커가 InitPostgres를 실행하고 InitProcessing에서 NormalProcessing으로 전환하기 위해 호출한다. BGWORKER_BYPASS_ALLOWCONN / …_ROLELOGINCHECK 플래그는 INIT_PG_OVERRIDE_*로 매핑된다.
  • bgworker_die — SIGTERM 핸들러: bgw_type을 명시하며 ereport(FATAL, …). **BackgroundWorkerBlockSignals / …Unblock**은 임계 구간을 위해 sigprocmask를 감싼다.

핸들 기반 제어 (요청 백엔드의 API)

섹션 제목: “핸들 기반 제어 (요청 백엔드의 API)”
  • GetBackgroundWorkerPidBackgroundWorkerLock을 공유 모드로 잡고, handle->generationslot->generation과 비교(ABA 방어)하고, slot->pid(>0 / InvalidPid / 0)에 따라 BGWH_STARTED / _NOT_YET_STARTED / _STOPPED를 반환한다.
  • WaitForBackgroundWorkerStartup / …ShutdownGetBackgroundWorkerPid 주위의 래치-대기 루프. WL_POSTMASTER_DEATH도 감시하고 BGWH_POSTMASTER_DIED를 반환한다. 호출자가 bgw_notify_pid를 자신으로 설정해 포스트마스터의 SIGUSR1이 래치를 깨우도록 해야 한다.
  • TerminateBackgroundWorker — 락 아래에서(제너레이션이 여전히 일치할 경우에만) slot->terminate = true를 설정하고 포스트마스터에 신호를 보낸다. 워커가 살아있든 아니든 안전하게 호출할 수 있다.
  • ReportBackgroundWorkerPID — 포스트마스터가 slot->pid를 쓰고 bgw_notify_pid에 SIGUSR1을 보낸다.
  • ReportBackgroundWorkerExit — 워커 종료 시 slot->pid를 쓴다. rw_terminate 또는 bgw_restart_time == BGW_NEVER_RESTART이면 대기자에게 알리기 전에 ForgetBackgroundWorker를 호출한다(슬롯 재사용 경합을 좁히기 위해).
  • ForgetBackgroundWorker — 병렬 워커의 경우 parallel_terminate_count를 올리고, pg_memory_barrier(), 슬롯을 해제(in_use=false), 전용 엔트리를 unlink하고 pfree한다.
  • ResetBackgroundWorkerCrashTimes — 서버 전체 충돌-재시작 사이클 후: BGW_NEVER_RESTART 워커는 잊고, 나머지는 rw_crashed_at / rw_pid / rw_notify_pid를 0으로 리셋해 즉시 재실행한다. 병렬 워커가 재시작 분기에 도달하지 않음을 assert한다.
  • ForgetUnstartedBackgroundWorkers — 정상 종료 중, 대기자가 있는 아직 시작하지 않은 워커를 제거하고 대기자에게 알린다.
  • BackgroundWorkerStopNotifications — 죽어가는 백엔드의 bgw_notify_pid를 지워서 포스트마스터가 그 백엔드에 신호 보내기를 멈추게 한다.
  • dsm_create / dsm_attach / dsm_detach — 날 세그먼트를 생성/연결/분리한다. dsm_control_item.refcnt에서 참조 카운트된다(refcnt 1 == 소멸 중이므로, 살아있는 세그먼트는 2에서 시작). **dsm_segment_handle**은 bgw_main_arg로 사용하는 프로세스 이식 가능한 이름을 반환한다.
  • dsm_pin_segment / dsm_pin_mapping — 세그먼트의 수명을 생성 리소스 오너 / 백엔드 너머로 연장한다. DSA는 영역이 단일 연결 백엔드보다 오래 살도록 백킹 세그먼트를 고정한다.
  • dsa_create_ext / dsa_attach / dsa_attach_in_place — 공유 힙을 만들거나 합류한다. **dsa_get_handle**은 워커에 전달할 dsa_handle을 반환한다. **dsa_allocate_extended / dsa_free**는 힙 연산이다. **dsa_get_address**는 dsa_pointer를 로컬 주소로 변환하며, 세그먼트를 필요할 때 on-demand로 매핑한다.

위치 힌트 (2026-06-05 기준, REL_18 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심볼파일
BackgroundWorkerSlotsrc/backend/postmaster/bgworker.c74
BackgroundWorkerArraysrc/backend/postmaster/bgworker.c94
BackgroundWorkerHandle (struct)src/backend/postmaster/bgworker.c102
InternalBGWorkers[]src/backend/postmaster/bgworker.c114
BackgroundWorkerShmemSizesrc/backend/postmaster/bgworker.c145
BackgroundWorkerShmemInitsrc/backend/postmaster/bgworker.c161
FindRegisteredWorkerBySlotNumbersrc/backend/postmaster/bgworker.c220
BackgroundWorkerStateChangesrc/backend/postmaster/bgworker.c245
ForgetBackgroundWorkersrc/backend/postmaster/bgworker.c428
ReportBackgroundWorkerPIDsrc/backend/postmaster/bgworker.c460
ReportBackgroundWorkerExitsrc/backend/postmaster/bgworker.c482
BackgroundWorkerStopNotificationssrc/backend/postmaster/bgworker.c513
ForgetUnstartedBackgroundWorkerssrc/backend/postmaster/bgworker.c540
ResetBackgroundWorkerCrashTimessrc/backend/postmaster/bgworker.c578
SanityCheckBackgroundWorkersrc/backend/postmaster/bgworker.c631
bgworker_diesrc/backend/postmaster/bgworker.c703
BackgroundWorkerMainsrc/backend/postmaster/bgworker.c717
BackgroundWorkerInitializeConnectionsrc/backend/postmaster/bgworker.c852
BackgroundWorkerInitializeConnectionByOidsrc/backend/postmaster/bgworker.c886
RegisterBackgroundWorkersrc/backend/postmaster/bgworker.c939
RegisterDynamicBackgroundWorkersrc/backend/postmaster/bgworker.c1045
GetBackgroundWorkerPidsrc/backend/postmaster/bgworker.c1157
WaitForBackgroundWorkerStartupsrc/backend/postmaster/bgworker.c1212
WaitForBackgroundWorkerShutdownsrc/backend/postmaster/bgworker.c1257
TerminateBackgroundWorkersrc/backend/postmaster/bgworker.c1296
LookupBackgroundWorkerFunctionsrc/backend/postmaster/bgworker.c1337
GetBackgroundWorkerTypeByPidsrc/backend/postmaster/bgworker.c1371
BackgroundWorker (struct)src/include/postmaster/bgworker.h89
RegisteredBgWorkersrc/include/postmaster/bgworker_internals.h32
StartBackgroundWorkersrc/backend/postmaster/postmaster.c4105
bgworker_should_start_nowsrc/backend/postmaster/postmaster.c4166
maybe_start_bgworkerssrc/backend/postmaster/postmaster.c4213
dsm_createsrc/backend/storage/ipc/dsm.c516
dsm_attachsrc/backend/storage/ipc/dsm.c665
dsm_detachsrc/backend/storage/ipc/dsm.c803
dsm_pin_segmentsrc/backend/storage/ipc/dsm.c955
dsm_segment_handlesrc/backend/storage/ipc/dsm.c1123
dsa_create_extsrc/backend/utils/mmgr/dsa.c421
dsa_get_handlesrc/backend/utils/mmgr/dsa.c498
dsa_attachsrc/backend/utils/mmgr/dsa.c510
dsa_allocate_extendedsrc/backend/utils/mmgr/dsa.c671
dsa_get_addresssrc/backend/utils/mmgr/dsa.c942
LaunchParallelWorkers (registration block)src/backend/access/transam/parallel.c601
logicalrep_worker_launch (registration block)src/backend/replication/logical/launcher.c469

/data/hgryoo/references/postgresREL_18_STABLE, 커밋 273fe94 (PG 18.x) 기준으로 작성했다. 스팟 검사 항목:

  • 공개 API 표면. bgworker.h에서 grep -n으로 문서화된 export를 정확히 확인했다: RegisterBackgroundWorker, RegisterDynamicBackgroundWorker, GetBackgroundWorkerPid, WaitForBackgroundWorkerStartup, WaitForBackgroundWorkerShutdown, GetBackgroundWorkerTypeByPid, TerminateBackgroundWorker, BackgroundWorkerInitializeConnection[ByOid], BackgroundWorkerBlockSignals / …Unblock, MyBgworkerEntry GUC-DLLIMPORT. 플래그 매크로 BGWORKER_SHMEM_ACCESS (0x0001), BGWORKER_BACKEND_DATABASE_CONNECTION (0x0002), BGWORKER_CLASS_PARALLEL (0x0010), 연결 바이패스 플래그 (0x0001/0x0002)가 인용된 대로 존재한다.
  • 락-프리 프로토콜 불변 조건. RegisterDynamicBackgroundWorker에서 slot->in_use = true 직전의 pg_write_barrier()와, BackgroundWorkerStateChange에서 !slot->in_use skip 후의 pg_read_barrier()가 인용된 위치에 모두 존재한다. write/read 배리어 쌍이 핵심 세부사항이며, 추론이 아닌 소스를 직접 확인해 검증했다.
  • 카운터 집계. parallel_register_countBackgroundWorkerLock 아래에서만 증가한다(RegisterDynamicBackgroundWorker). parallel_terminate_count는 포스트마스터 컨텍스트 함수에서만 락 없이 증가한다(BackgroundWorkerStateChange, ForgetBackgroundWorker). 구조체 주석이 포스트마스터는 락-프리로 유지해야 한다고 명시한 것과 일치한다.
  • InternalBGWorkers[] 멤버십. 다섯 개 내부 진입점(ParallelWorkerMain, ApplyLauncherMain, ApplyWorkerMain, ParallelApplyWorkerMain, TablesyncWorkerMain)이 배열에 정확히 들어있다. §“PostgreSQL의 접근 방식”에서 이름을 붙인 병렬 쿼리와 논리 복제 소비자가 확인되었다.
  • 소비자 등록 블록. parallel.c의 병렬 쿼리 블록(bgw_function_name = "ParallelWorkerMain", BGWORKER_CLASS_PARALLEL, BGW_NEVER_RESTART, bgw_main_arg = dsm_segment_handle(pcxt->seg))과 launcher.c의 논리 런처 블록(ApplyWorkerMain / ParallelApplyWorkerMain / TablesyncWorkerMain, BGW_NEVER_RESTART, bgw_main_arg = Int32GetDatum(slot))을 전체 읽고 충실히 인용했다.
  • 범위 경계. contrib/ 코드는 핵심 동작으로 주장하지 않는다. worker_spi는 예시로만 언급한다. PG19 전용 심볼은 없다—위치 힌트 표의 모든 심볼은 REL_18 트리에서 기재된 줄에서 확인했다.

PostgreSQL 너머 — 비교 설계와 연구 방향

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 방향”

프레임워크의 가치는 호출자에서 가장 잘 드러난다. 모든 호출자는 “BackgroundWorker를 채우고, 등록하고, 선택적으로 핸들을 보유한다”로 귀결된다. 차이는 전적으로 플래그와 정책 필드에 있다.

병렬 쿼리 (parallel.c, LaunchParallelWorkers)는 가장 까다로운 소비자다: 수명이 짧고, 팬아웃하고, 절대 재시작하지 않고, max_parallel_workers에 상한이 있다. gather DSM 세그먼트의 핸들을 워커 인수로 전달해서 워커가 리더의 튜플 큐와 플랜에 연결할 수 있게 한다.

// LaunchParallelWorkers (registration) — src/backend/access/transam/parallel.c
worker.bgw_flags =
BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION
| BGWORKER_CLASS_PARALLEL;
worker.bgw_start_time = BgWorkerStart_ConsistentState;
worker.bgw_restart_time = BGW_NEVER_RESTART;
sprintf(worker.bgw_library_name, "postgres");
sprintf(worker.bgw_function_name, "ParallelWorkerMain");
worker.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(pcxt->seg));
worker.bgw_notify_pid = MyProcPid;
/* ... loop: RegisterDynamicBackgroundWorker(&worker, &pcxt->worker[i].bgwhandle) ... */

리더는 부분적 성공을 허용한다: 등록 실패(슬롯 소진) 시 더 적은 워커로 실행한다. BGWORKER_CLASS_PARALLEL 플래그는 워커를 전용 register/terminate 카운터로 라우팅해서, 병렬 쿼리가 max_worker_processes도 소비하는 상황에서도 max_parallel_workers를 초과할 수 없도록 한다. 이 등록 위에 만들어진 리더/워커 튜플 큐와 shm_toc 기계는 postgres-parallel-query.md를 참조하라.

논리 복제 apply (launcher.c, logicalrep_worker_launch)는 수명이 길고 DB에 연결된 소비자다. 각 apply / parallel-apply / tablesync 워커는 동적으로 등록되고, 프레임워크가 자동 재시작하지 않는다(BGW_NEVER_RESTART—런처 자체가 자체 워커 슬롯으로 재시작 정책을 관리한다). 논리 복제 슬롯 인덱스를 인수로 전달한다.

// logicalrep_worker_launch (registration) — src/backend/replication/logical/launcher.c
bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
/* function name is ApplyWorkerMain / ParallelApplyWorkerMain / TablesyncWorkerMain */
bgw.bgw_restart_time = BGW_NEVER_RESTART;
bgw.bgw_notify_pid = MyProcPid;
bgw.bgw_main_arg = Int32GetDatum(slot);
if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle)) { /* clean up slot */ }

apply 런처 자체는 반대로 정적으로 RegisterBackgroundWorker로 등록되며 5초 bgw_restart_time을 가진다. 프레임워크가 죽으면 직접 되살린다—두 가지 등록 경로와 두 가지 재시작 정책이 함께 나타나는 유일한 곳이다. postgres-logical-replication-apply.md를 참조하라.

서드파티 확장이 원래 동기다. 확장은 shared_preload_libraries에 자신을 등록하고, _PG_init에서 bgw_library_name을 자신의 .so로, bgw_function_name을 export된 진입점으로 설정해 RegisterBackgroundWorker를 호출한다. LookupBackgroundWorkerFunctionload_external_function으로 이를 해석한다. 트리 내 worker_spi contrib 모듈이 정형 예시다(여기서는 예시로만 언급; contrib/는 핵심 동작 범위 밖). 확장은 SQL 호출 가능 함수에서 동적으로 등록할 수도 있다. 스케줄된 태스크마다 워커를 실행하고 핸들로 완료를 모니터링하는 작업 스케줄러 확장이 쓰는 패턴이다.

flowchart TB
  subgraph STATIC["정적 — shared_preload_libraries 전용"]
    EXT["확장 _PG_init"] --> RBW["RegisterBackgroundWorker"]
    LAUNCH["apply 런처 (5초 재시작)"] --> RBW
    RBW --> PLIST["BackgroundWorkerList (전용)"]
  end
  subgraph DYNAMIC["동적 — 런타임 임의 백엔드에서"]
    PQ["병렬 리더<br/>BGWORKER_CLASS_PARALLEL"] --> RDBW["RegisterDynamicBackgroundWorker"]
    LR["logicalrep_worker_launch<br/>apply / tablesync"] --> RDBW
    SQLFN["확장 SQL 함수"] --> RDBW
    RDBW --> SLOT["BackgroundWorkerSlot 획득"]
  end
  PLIST --> PM["postmaster: maybe_start_bgworkers → fork"]
  SLOT -->|"신호"| PM
  • Oracle은 고정된 이름 있는 백그라운드 프로세스 집합을 사용한다(PMON, SMON, DBWn, LGWR, CKPT, ARCn, 잡 큐 Jnnn, 병렬 Pnnn 슬레이브). 병렬 슬레이브와 잡 큐 프로세스가 PostgreSQL 동적 bgworker에 가장 가까운 유사체다—풀에 있다가 일시적 작업에 할당되고 반환된다. 하지만 집합은 Oracle 내부 리소스 관리자가 관리하며, 임의 사용자 코드를 위한 플러그인 C API로 노출되지 않는다. RegisterBackgroundWorker에 필적하는 “자체 감시 프로세스를 등록하라”는 확장 표면이 없다.
  • SQL Server는 논리 CPU에 바인딩된 스케줄러에 바인딩된 워커 스레드가 있는 사용자 모드 협력 스케줄러(SQLOS)를 실행한다. 병렬성은 하나의 프로세스 안에서 스레드 기반이다. 결함 격리 트레이드오프는 PostgreSQL의 역이다: 기본적으로 컨텍스트 전환과 공유 메모리가 저렴하지만, 프로세스 수준 폭발 반경 격리가 없고, 외부에서 제공하는 감시 프로세스 개념이 없다.
  • MySQL/InnoDB는 서버 프로세스 안에 소수의 고정 백그라운드 스레드(purge, page-cleaner, master, I/O)를 둔다. 확장성은 서버 자체 스레드에서 실행되는 플러그인/컴포넌트를 통하며, 새로운 감시 프로세스를 생성하지 않는다. 스레드가 힙을 공유하므로 DSM 핸들 전달 관례가 없다.

PostgreSQL은 서드파티가 대상으로 삼는 문서화된 안정적 C ABI로 프로세스 감시자를 노출하는 점에서 특이하다. 이는 프로세스-퍼-백엔드 모델(새 실행 단위는 자연스럽게 새 프로세스)과 원래 POSTGRES 설계 논문(Stonebraker & Rowe 1986)에서 추적되는 확장성 정신의 직접적 결과다. 비용은 DSM/DSA 장치다: 워커들이 힙을 공유하지 않으므로 공유 상태의 모든 바이트를 세그먼트에 명시적으로 배치하고 핸들이나 유사 포인터로 주소를 지정해야 한다.

  • 코어별 할당자 동시성. dsa.c 자체 헤더 주석은 현재의 크기 클래스별 단일 락 설계를 병목으로 지목한다: “Per-core pools to increase concurrency and strategies for reducing the resulting fragmentation are areas for future research.” 코어 수가 늘어남에 따라, 병렬 쿼리 공유 상태를 뒷받침하는 DSA 할당자는 현대 시스템 연구 할당자(그리고 Scalable Lock Manager PG 작업, 2013)가 경합하는 공유 구조체에 적용하는 NUMA 인식·샤딩 프리리스트 처리의 후보가 된다.
  • 워커 풀링 대 오퍼레이션별 fork. 각 병렬 쿼리는 현재 새 워커를 fork하고 해체한다. 멀티코어 장비에서 짧은 쿼리의 경우 fork/exec과 InitProcess 비용이 적지 않다. 프로세스를 오퍼레이션 사이에 재사용하는 지속적 워커 풀(SQL Server SQLOS가 구현하는 모델)은 반복되는 설계 논의다. 프레임워크의 슬롯-핸들 프로토콜은 대체로 풀 준비가 되어있지만, 연결/데이터베이스 어피니티와 카탈로그 캐시 리셋 시맨틱이 안전한 재사용을 미묘하게 만든다.
  • 워커 클래스 간 입장 제어. 현재 병렬 워커는 전용 카운터를 갖지만, apply 워커, autovacuum 워커, 확장 워커는 모두 우선 순위 없이 같은 max_worker_processes 풀에서 가져간다. Architecture of a Database System이 입장 제어를 위해 스케치하는 방향인 단일 리소스 거버너 아래 이것들을 통합하면, 운영자가 서브시스템별이 아닌 전체적으로 워커 예산을 추론할 수 있을 것이다.
  • 소스 트리. /data/hgryoo/references/postgres @ REL_18_STABLE (커밋 273fe94, PG 18.x):
    • src/backend/postmaster/bgworker.c — 프레임워크 본체.
    • src/include/postmaster/bgworker.h, src/include/postmaster/bgworker_internals.h — 공개 + 내부 API.
    • src/backend/postmaster/postmaster.cmaybe_start_bgworkers, StartBackgroundWorker, bgworker_should_start_now.
    • src/backend/storage/ipc/dsm.c — 동적 공유 메모리 세그먼트.
    • src/backend/utils/mmgr/dsa.c — DSM 위의 공유 힙 할당자.
    • src/backend/access/transam/parallel.c — 병렬 쿼리 소비자.
    • src/backend/replication/logical/launcher.c — 논리 apply 소비자.
  • 교차 참조 KB 문서 (인접 메커니즘은 이곳에 위임, 중복 금지):
    • postgres-postmaster.md — 감시자, pmState, 자식 슬롯 관리, 서버 전체 충돌-재시작.
    • postgres-shared-memory-ipc.md — 메인 공유 메모리 영역, shm_mq, shm_toc, 더 넓은 DSM/DSA 사용 이야기.
    • postgres-parallel-query.md — 병렬 워커가 위의 등록과 튜플 큐, shm_toc를 사용하는 방법.
    • postgres-logical-replication-apply.md — apply/tablesync 워커 생명주기와 정적 등록된 런처.
    • postgres-aux-processes.md, postgres-backend-lifecycle.md — 형제 프로세스 모델 문서.
  • 이론 앵커 (knowledge/research/dbms-general/, .omc/plans/postgres-paper-bibliography.md):
    • Hellerstein, Stonebraker & Hamilton, Architecture of a Database System (2007), §2 “Process Models”과 §“Admission Control”.
    • Silberschatz, Korth & Sudarshan, Database System Concepts (7e), §20 “Database-System Architectures” (프로세스 모니터, 공유 메모리 모델).
    • Stonebraker & Rowe, “The Design of POSTGRES” (1986) — 플러그인 워커 ABI 뒤의 확장성 정신.
    • Scalable Lock Manager (2013) — 경합하는 공유 구조체 확장성, DSA 코어별 풀 연구 방향과 관련.