개요

pg_ctl start로 PostgreSQL을 띄우면 프로그램 하나가 도는 것처럼 보입니다. 실제로는 프로세스 여러 개가 역할을 나눠 맡은 작은 조직이 뜹니다. 접속을 받는 프로세스, 쿼리를 처리하는 프로세스, 메모리에 쌓인 변경을 디스크로 내보내는 프로세스, 죽은 데이터를 청소하는 프로세스가 모두 따로 있습니다.

이 글은 다음 질문에 답합니다.

  • 서버를 띄우면 어떤 프로세스들이 생기고, 각각 무슨 일을 하는가
  • 클라이언트가 접속하면 내부에서 어떤 순서로 무슨 일이 일어나는가
  • backend 하나가 죽었는데 왜 모든 접속이 끊기는가

DB 내부를 처음 보는 개발자도 따라올 수 있도록 용어가 처음 나올 때마다 뜻을 풀어 쓰겠습니다.

기준 버전: 이 연재는 PostgreSQL 18을 기준으로 합니다. 정확히는 REL_18_STABLE 브랜치의 커밋 39a0db1(18.6 개발 버전)입니다. 본문의 소스 링크는 모두 이 커밋에 고정했고, 실습 결과는 이 소스를 Rocky Linux 9.8에서 그대로 빌드해 실행한 출력입니다.

먼저 결론부터 정리하면 이렇습니다.

프로세스한 줄 설명
postmaster문지기. 접속을 받아 fork하고, 자식 프로세스를 감시합니다. 쿼리는 처리하지 않습니다.
client backend접속 하나를 전담하는 프로세스. 클라이언트 하나당 하나씩 생깁니다.
백그라운드 프로세스디스크 쓰기, 읽기(PG18), 청소, WAL 보관 같은 뒷일을 나눠 맡습니다.

한눈에 보기

아래 그림은 PostgreSQL 18이 기본 설정으로 떠 있을 때의 프로세스와 그 사이의 관계입니다. 그림 위쪽의 버튼으로 “연결 처리”, “디스크 쓰기 담당” 같은 관점별로 강조해서 볼 수 있고, SRC 표시가 있는 상자는 해당 소스 위치로 연결됩니다.

PostgreSQL 18 프로세스 구조. 실선은 데이터나 요청이 흐르는 방향, 점선은 비동기 요청입니다. 새 창에서 크게 보기

그림을 읽는 요령은 세 가지입니다.

  1. 왼쪽의 postmaster는 입구만 지킵니다. 접속을 받으면 자신을 복제(fork)해 backend를 만들고, 그다음부터 클라이언트는 backend와 직접 이야기합니다.
  2. 가운데의 공유 메모리가 모든 프로세스의 작업대입니다. 테이블 데이터를 캐시하는 shared buffers, 변경 기록(WAL)을 잠시 모아 두는 WAL buffers, 누가 어떤 락을 잡았는지 같은 정보가 여기 있습니다.
  3. 오른쪽의 백그라운드 프로세스들이 디스크를 상대합니다. backend는 되도록 메모리에서 일을 끝내고, 느린 디스크 쓰기는 뒤에서 따로 처리합니다.

용어 정리

  • 프로세스: 운영체제가 따로 메모리 공간을 주고 실행하는 프로그램 단위입니다. ps 명령에 한 줄씩 보이는 것이 프로세스입니다.
  • fork: 실행 중인 프로세스가 자신을 그대로 복제해 자식 프로세스를 만드는 유닉스 시스템 콜입니다. 부모가 열어 둔 파일, 메모리 매핑 같은 것을 자식이 물려받습니다.
  • WAL(Write-Ahead Log): 데이터 파일을 고치기 전에 “무엇을 바꿀지"를 먼저 적어 두는 로그입니다. 장애가 나면 이 로그를 다시 재생해서 복구합니다. 7편에서 자세히 다룹니다.

서버를 띄우면 생기는 프로세스

서버를 시작하고 아무도 접속하지 않은 상태에서 postmaster와 그 자식들을 봅니다. ps -p $PM --ppid $PM은 postmaster 자신과 postmaster를 부모로 둔 프로세스만 보여 줍니다.

$ pg_ctl -D $PGDATA -l /home/postgres/server.log start
waiting for server to start.... done
server started
$ PM=$(head -1 $PGDATA/postmaster.pid); ps -o pid,ppid,cmd --forest -p $PM --ppid $PM
    PID    PPID CMD
     33       1 /usr/local/pgsql/bin/postgres -D /var/lib/postgresql/data
     34      33  \_ postgres: io worker 0
     35      33  \_ postgres: io worker 1
     36      33  \_ postgres: io worker 2
     37      33  \_ postgres: checkpointer 
     38      33  \_ postgres: background writer 
     40      33  \_ postgres: walwriter 
     41      33  \_ postgres: autovacuum launcher 
     42      33  \_ postgres: logical replication launcher 
$ head -1 $PGDATA/postmaster.pid
33
$ cat /home/postgres/server.log
2026-09-26 10:24:03.341 UTC [33] LOG:  starting PostgreSQL 18.6 on aarch64-unknown-linux-gnu, compiled by gcc (GCC) 11.5.0 20240719 (Red Hat 11.5.0-14), 64-bit
2026-09-26 10:24:03.341 UTC [33] LOG:  listening on IPv6 address "::1", port 5432
2026-09-26 10:24:03.341 UTC [33] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2026-09-26 10:24:03.342 UTC [33] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2026-09-26 10:24:03.346 UTC [39] LOG:  database system was shut down at 2026-09-26 10:24:02 UTC
2026-09-26 10:24:03.347 UTC [33] LOG:  database system is ready to accept connections

그림에 있던 프로세스들이 그대로 보이고, 모든 프로세스의 부모(PPID)가 33번 postmaster입니다. 백그라운드 프로세스든 뭐든 fork는 postmaster만 합니다. 39번이 비어 있는 이유는 기동 순서에서 다룹니다.

postmaster: 문을 지키고 자식을 감시하는 프로세스

postmaster는 서버를 띄웠을 때 가장 먼저 실행되는 프로세스입니다. ps에서는 /usr/local/pgsql/bin/postgres -D ...처럼 실행 파일 이름 그대로 보이고, PID는 $PGDATA/postmaster.pid 파일의 첫 줄에 기록됩니다.

postmaster.c 맨 위의 주석이 이 프로세스의 성격을 잘 설명합니다. 요약하면 다음과 같습니다.

  • 접속 요청이 오면 backend를 fork합니다.
  • 기동, 종료처럼 시스템 전체에 걸친 일을 관리합니다. 다만 그 일을 직접 하지 않고, 알맞은 때에 자식 프로세스를 띄워 시킵니다.
  • 공유 메모리를 만들기는 하지만 평소에는 건드리지 않습니다. 락 관리에도 참여하지 않습니다.
  • backend가 죽으면 공유 메모리를 초기화해서 시스템을 되살립니다.

세 번째 항목이 postmaster 설계의 핵심입니다. 공유 메모리는 여러 프로세스가 함께 쓰는 곳이라, 어떤 backend가 락을 잡은 채로 죽으면 망가진 상태로 남을 수 있습니다. postmaster가 그 메모리를 만지지 않으면 자식이 어떻게 죽든 postmaster만은 멀쩡하게 남아서 뒷수습을 할 수 있습니다. 실제로 postmaster는 공유 메모리의 프로세스 목록(PGPROC 배열)에 들어 있지 않고, 그래서 pg_stat_activity에도 나오지 않습니다.

pg_stat_activity에는 postmaster가 없다

postgres=# SELECT pid, backend_type FROM pg_stat_activity ORDER BY pid;
 pid |         backend_type         
-----+------------------------------
  34 | io worker
  35 | io worker
  36 | io worker
  37 | checkpointer
  38 | background writer
  40 | walwriter
  41 | autovacuum launcher
  42 | logical replication launcher
  66 | client backend
(9 rows)

backend_type 열의 문자열은 GetBackendTypeDesc()가 정한 이름입니다. 66번은 이 쿼리를 실행한 psql의 backend입니다. 앞의 ps에는 있던 postmaster(33)가 이 목록에는 없습니다. PGPROC 배열에 들어가지 않는다는 설계가 그대로 드러나는 부분입니다.

본체는 기다렸다가 fork하는 무한 루프

postmaster의 본체는 ServerLoop()라는 무한 루프입니다. 핵심만 남기면 이렇습니다.

/* src/backend/postmaster/postmaster.c, ServerLoop() (일부 생략) */
for (;;)
{
    /* 소켓에 새 접속이 오거나, 시그널 핸들러가 latch를 세울 때까지 잠든다 */
    nevents = WaitEventSetWait(pm_wait_set, DetermineSleepTime(), events, ...);

    for (int i = 0; i < nevents; i++)
    {
        if (pending_pm_shutdown_request) process_pm_shutdown_request(); /* 종료 요청 */
        if (pending_pm_reload_request)   process_pm_reload_request();   /* 설정 reload */
        if (pending_pm_child_exit)       process_pm_child_exit();       /* 자식 종료 */
        if (pending_pm_pmsignal)         process_pm_pmsignal();         /* 자식의 요청 */

        if (events[i].events & WL_SOCKET_ACCEPT)
        {
            if (AcceptConnection(events[i].fd, &s) == STATUS_OK)
                BackendStartup(&s);                 /* 접속이 오면 fork */
            closesocket(s.sock);                    /* postmaster 쪽 소켓은 바로 닫는다 */
        }
    }

    LaunchMissingBackgroundProcesses();             /* 빠진 백그라운드 프로세스를 다시 띄운다 */
    ...
}

루프는 “기다린다 → 온 일을 처리한다 → 빠진 프로세스가 있으면 띄운다"를 반복합니다. 할 일이 없으면 잠들어 있으니 CPU를 거의 쓰지 않습니다.

postmaster에게 일을 시키는 방법은 유닉스 시그널입니다. 시그널 핸들러는 PostmasterMain()에서 등록합니다.

시그널보내는 쪽postmaster가 하는 일
SIGHUPpg_ctl reload, pg_reload_conf()설정 파일을 다시 읽고 자식들에게도 전달
SIGTERMpg_ctl stop -m smart새 접속을 막고, 기존 세션이 모두 끝나면 종료
SIGINTpg_ctl stop -m fast (기본값)진행 중인 트랜잭션을 취소시키고 종료
SIGQUITpg_ctl stop -m immediate모든 자식을 즉시 끝내고 종료. 다음 기동 때 복구 과정을 거침
SIGCHLD운영체제자식 프로세스가 끝났다는 알림. 종료 코드를 보고 정상인지 판단
SIGUSR1자식 프로세스“autovacuum worker를 띄워 달라” 같은 자식의 요청

backend: 접속 하나에 프로세스 하나

클라이언트가 접속할 때마다 postmaster는 backend 프로세스를 하나씩 만듭니다. 접속이 100개면 backend도 100개입니다. 한 번 만들어진 backend는 그 접속이 끝날 때까지 그 클라이언트만 상대합니다. 요즘 많이 쓰는 “스레드 풀” 방식의 서버와 가장 다른 점입니다.

접속 하나가 처리되는 순서

접속 하나가 처리되는 순서. postmaster는 fork까지만 하고, 인증부터는 backend가 맡습니다. 새 창에서 크게 보기

순서대로 짚어 보겠습니다.

1. accept와 fork. 접속이 오면 postmaster는 BackendStartup()에서 자식 슬롯을 하나 잡고 곧바로 fork합니다. fork는 postmaster_child_launch()가 수행합니다. 눈여겨볼 점은 인증보다 fork가 먼저라는 것입니다. 소스 주석은 그 이유를 이렇게 설명합니다. 인증 코드를 단순한 단일 스레드 방식으로 짤 수 있고, 무엇보다 SSL이나 PAM처럼 느리게 막힐 수 있는 라이브러리가 postmaster를 붙잡아 다른 클라이언트까지 못 들어오게 하는 일을 막을 수 있습니다.

2. 인증과 초기화. 자식 프로세스는 BackendMain()에서 시작합니다. 클라이언트가 보낸 첫 메시지(StartupMessage)에서 사용자와 DB 이름을 읽고, pg_hba.conf 규칙에 따라 인증합니다. 그다음 InitProcess()로 공유 메모리에 자기 자리(PGPROC)를 얻습니다. 이 자리를 얻어야 락을 잡고 공유 메모리를 쓸 수 있습니다.

3. 쿼리 처리. 준비가 끝나면 backend는 PostgresMain()의 메인 루프에 들어갑니다. 클라이언트에 “준비됐다(ReadyForQuery)“를 보내고 다음 명령을 기다렸다가, 받은 쿼리를 파싱, 계획, 실행해 결과를 돌려주는 일을 반복합니다. 쿼리 처리 과정은 10편에서 다룹니다.

4. 종료. 클라이언트가 Terminate 메시지를 보내거나 연결이 끊기면 backend 프로세스가 끝납니다. 운영체제는 부모인 postmaster에게 SIGCHLD를 보내고, postmaster는 CleanupBackend()에서 슬롯을 돌려받습니다.

접속할 때마다 backend가 하나씩 생긴다

접속 과정을 로그로 보려고 PG18의 log_connections 세부 옵션을 켭니다. PG18부터 log_connections는 on/off 대신 receipt, authentication, authorization, setup_durations처럼 단계별로 고를 수 있습니다. log_line_prefix의 %b는 로그 줄에 backend 타입을 찍어 줍니다.

postgres=# ALTER SYSTEM SET log_line_prefix = '%m [%p] %b ';
postgres=# ALTER SYSTEM SET log_connections = 'receipt,authentication,authorization,setup_durations';
postgres=# ALTER SYSTEM SET log_disconnections = on;
postgres=# SELECT pg_reload_conf();
 pg_reload_conf 
----------------
 t
(1 row)

세션 세 개를 백그라운드로 열어 둡니다. 하나는 TCP로 접속해 가만히 있고, 하나는 Unix 소켓으로 접속해 가만히 있고, 하나는 긴 쿼리를 실행합니다.

$ psql -X -q -h 127.0.0.1 &
$ psql -X -q &
$ psql -X -c "SELECT pg_sleep(3600)" &
$ PM=$(head -1 $PGDATA/postmaster.pid); ps -o pid,ppid,cmd --forest -p $PM --ppid $PM
    PID    PPID CMD
     33       1 /usr/local/pgsql/bin/postgres -D /var/lib/postgresql/data
...  (앞과 같은 백그라운드 프로세스 8줄)
     83      33  \_ postgres: postgres postgres 127.0.0.1(45092) idle
     92      33  \_ postgres: postgres postgres [local] idle
     99      33  \_ postgres: postgres postgres [local] SELECT

접속 세 개에 backend 세 개(83, 92, 99)가 생겼고, 부모는 모두 postmaster입니다. backend는 ps의 프로세스 이름을 사용자 DB 접속지 상태 형식으로 바꿔 둡니다. idle은 명령을 기다리는 중, SELECT는 쿼리를 실행하는 중이라는 뜻이라, ps만으로도 각 세션이 무엇을 하는지 대략 알 수 있습니다.

postgres=# SELECT pid, backend_type, client_addr, state, query FROM pg_stat_activity WHERE backend_type = 'client backend' ORDER BY pid;
 pid |  backend_type  | client_addr | state  |                                                            query                                                             
-----+----------------+-------------+--------+------------------------------------------------------------------------------------------------------------------------------
  83 | client backend | 127.0.0.1   | idle   | 
  92 | client backend |             | idle   | 
  99 | client backend |             | active | SELECT pg_sleep(3600)
 115 | client backend |             | active | SELECT pid, backend_type, client_addr, state, query FROM pg_stat_activity WHERE backend_type = 'client backend' ORDER BY pid
(4 rows)
$ grep -E 'connection (received|authenticated|authorized|ready)' /home/postgres/server.log | head -8
2026-09-26 10:24:03.750 UTC [83] not initialized LOG:  connection received: host=127.0.0.1 port=45092
2026-09-26 10:24:03.751 UTC [83] client backend LOG:  connection authenticated: user="postgres" method=trust (/var/lib/postgresql/data/pg_hba.conf:119)
2026-09-26 10:24:03.751 UTC [83] client backend LOG:  connection authorized: user=postgres database=postgres application_name=psql
2026-09-26 10:24:03.752 UTC [83] client backend LOG:  connection ready: setup total=1.821 ms, fork=0.140 ms, authentication=0.154 ms
2026-09-26 10:24:03.792 UTC [92] not initialized LOG:  connection received: host=[local]
2026-09-26 10:24:03.792 UTC [92] client backend LOG:  connection authenticated: user="postgres" method=trust (/var/lib/postgresql/data/pg_hba.conf:117)
2026-09-26 10:24:03.792 UTC [92] client backend LOG:  connection authorized: user=postgres database=postgres application_name=psql
2026-09-26 10:24:03.792 UTC [92] client backend LOG:  connection ready: setup total=1.050 ms, fork=0.170 ms, authentication=0.089 ms

로그가 위 그림의 흐름을 그대로 보여 줍니다.

  • connection received를 남긴 것은 postmaster가 아니라 이미 fork된 83번 프로세스입니다. 그런데 backend 타입이 not initialized로 찍혀 있습니다. fork 직후에는 아직 자기가 일반 backend인지 walsender인지 모르는 상태이기 때문입니다. StartupMessage를 읽은 뒤에야 client backend로 정해집니다.
  • setup total=1.821 ms 가운데 fork=0.140 ms입니다. 로컬에서 trust 인증을 쓴 조건이라 짧지만, 이 2ms 가까운 시간은 접속할 때마다 드는 비용입니다.

운영에서는: 커넥션이 많을수록 비싸다

접속 하나가 프로세스 하나이므로, 커넥션 수는 곧 프로세스 수입니다. 비용은 세 군데서 나옵니다.

  • 접속할 때마다 드는 비용: 위 로그에서 접속 준비에 1-2ms(fork 약 0.15ms)가 걸렸습니다. 로컬 소켓에 trust 인증을 쓴 가장 가벼운 조건입니다. 네트워크 왕복, TLS, 비밀번호 인증이 붙으면 더 걸립니다. 요청마다 새로 접속하는 애플리케이션이라면 이 비용을 매번 냅니다.
  • 프로세스마다 드는 메모리: idle backend 하나의 개인 메모리는 약 1.6MB였습니다(공유 메모리에서 확인). 쿼리를 실행하면 카탈로그 캐시와 정렬, 해시에 쓰는 작업 메모리(work_mem)가 여기에 더해집니다. 작업 메모리는 2편에서 다룹니다.
  • 공유 자원 경쟁: 모든 backend가 같은 공유 메모리의 락과 PGPROC 배열을 씁니다. 프로세스가 많아지면 그만큼 경쟁과 문맥 전환(context switch)이 늘어납니다.

그래서 수천 개의 애플리케이션 커넥션을 그대로 PostgreSQL에 붙이지 않고, PgBouncer 같은 커넥션 풀러를 앞에 둡니다. 풀러는 애플리케이션의 접속은 많이 받고, PostgreSQL에는 적은 수의 접속만 유지하면서 backend를 돌려 씁니다. fork와 인증 비용은 한 번만 내고, 프로세스 수도 일정하게 유지됩니다.

max_connections는 fork된 뒤에 확인한다

max_connections 제한이 실제로 걸리는 곳은 2단계(인증과 초기화)입니다. postmaster가 fork 전에 확인하는 자식 슬롯은 2 × (max_connections + max_wal_senders)개로 넉넉하게 잡혀 있습니다. 인증 도중에 실패하거나 먼저 나가는 세션이 있기 때문입니다. 정확한 제한은 PGPROC 자리가 모자랄 때 sorry, too many clients already로 걸립니다. 그래서 거절될 접속도 일단 fork는 됩니다.

max_connections를 넘으면: 먼저 fork하고 나중에 거절한다

max_connections를 5로 줄이고 재시작한 뒤, 세션 5개로 자리를 다 채우고 6번째로 접속해 봅니다.

postgres=# ALTER SYSTEM SET max_connections = 5;
postgres=# ALTER SYSTEM SET superuser_reserved_connections = 0;
$ pg_ctl -D $PGDATA -l /home/postgres/server.log restart -m fast
waiting for server to shut down.... done
server stopped
waiting for server to start.... done
server started
$ for i in 1 2 3 4 5; do psql -X -q & done
$ psql -X -c "SELECT 1"
psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL:  sorry, too many clients already
$ grep -B1 'too many clients' /home/postgres/server.log
2026-09-26 10:24:14.697 UTC [389] not initialized LOG:  connection received: host=[local]
2026-09-26 10:24:14.697 UTC [389] client backend FATAL:  sorry, too many clients already

거절 메시지를 남긴 것은 postmaster가 아니라 389번 프로세스입니다. 자리가 없는 접속도 먼저 fork되고, 자식이 PGPROC 자리를 얻으려다 실패해서 스스로 FATAL로 끝난 것입니다. (실습이 끝난 뒤 설정은 RESET하고 재시작했습니다.)

운영에서는: 거절될 접속도 fork 비용을 낸다

애플리케이션이 접속 실패를 짧은 간격으로 재시도하도록 되어 있으면, 거절될 접속을 만드느라 fork가 계속 일어납니다. 한도에 걸리는 상황이라면 재시도 간격을 늘리고 풀러를 두는 것이 근본적인 해결책입니다.

관리자용 여유분도 알아 두면 좋습니다. superuser_reserved_connections(기본 3)만큼의 자리는 superuser만 쓸 수 있어서, 일반 접속이 한도를 다 채워도 DBA는 들어와서 조치할 수 있습니다. 위 실습에서는 이 값을 0으로 두어서 superuser인 postgres도 거절되었습니다. PG16부터는 pg_use_reserved_connections 역할에게 줄 여유분을 reserved_connections로 따로 둘 수 있습니다.

walsender도 시작은 backend다

복제용 접속(standby나 pg_receivewal)도 처음에는 평범한 backend로 fork됩니다. StartupMessage에 복제 요청이 들어 있으면 그 자리에서 프로세스 타입이 B_WAL_SENDER로 바뀌고, 이후로는 쿼리 대신 WAL을 보내는 일을 합니다.

일반 backend가 walsender로 바뀌는 경우

pg_receivewal로 복제 접속을 만들어 봅니다.

$ mkdir -p /home/postgres/wal
$ pg_receivewal -D /home/postgres/wal &
$ PM=$(head -1 $PGDATA/postmaster.pid); ps -o pid,ppid,cmd --forest -p $PM --ppid $PM
    PID    PPID CMD
    412       1 /usr/local/pgsql/bin/postgres -D /var/lib/postgresql/data
...  (백그라운드 프로세스 8줄)
    530     412  \_ postgres: walsender postgres [local] streaming 0/3FCE280
$ ps -o pid,ppid,cmd -C pg_receivewal
    PID    PPID CMD
    523       0 pg_receivewal -D /home/postgres/wal
$ WS=$(pgrep -f 'postgres: walsender')
$ grep "\[$WS\]" /home/postgres/server.log
2026-09-26 10:24:20.908 UTC [530] not initialized LOG:  connection received: host=[local]
2026-09-26 10:24:20.909 UTC [530] walsender LOG:  connection authenticated: user="postgres" method=trust (/var/lib/postgresql/data/pg_hba.conf:124)
2026-09-26 10:24:20.909 UTC [530] walsender LOG:  replication connection authorized: user=postgres application_name=pg_receivewal
2026-09-26 10:24:20.909 UTC [530] walsender LOG:  connection ready: setup total=1.240 ms, fork=0.149 ms, authentication=0.218 ms
postgres=# SELECT pid, backend_type, application_name, state FROM pg_stat_activity WHERE backend_type = 'walsender';
 pid | backend_type | application_name | state  
-----+--------------+------------------+--------
 530 | walsender    | pg_receivewal    | active
(1 row)

530번은 일반 접속과 똑같이 not initialized 상태로 fork되었다가, StartupMessage에서 복제 요청을 확인한 순간부터 walsender로 찍힙니다. 인증에 쓰인 규칙도 일반 접속(117, 119행)과 달리 pg_hba.conf의 124행, 즉 replication 항목입니다. 복제의 동작은 9편에서 다룹니다.

백그라운드 프로세스: 뒷일을 나눠 맡는 프로세스들

PostgreSQL 18이 만들 수 있는 프로세스 종류는 BackendType 열거형에 모두 정의되어 있고, 각 타입의 시작 함수는 child_process_kinds[] 표에 있습니다. 이 가운데 클라이언트 접속과 상관없이 뒤에서 도는 프로세스들을 정리하면 다음과 같습니다.

프로세스 (ps 표시)하는 일언제 뜨나
checkpointer주기적으로 메모리의 변경된 페이지(dirty page)를 모두 디스크에 쓰고 fsync합니다. 이 시점이 장애 복구의 출발점이 됩니다.항상
background writerdirty page를 조금씩 미리 써 두어, backend가 빈 버퍼를 찾을 때 직접 디스크에 쓰는 일을 줄입니다.항상
walwriterWAL buffers에 쌓인 WAL을 주기적으로 디스크에 씁니다.정상 운영 중(PM_RUN)
io workerPG18 신규. backend 대신 실제 파일 읽기를 수행합니다. 아래에서 따로 설명합니다.항상, 기본 3개
autovacuum launcher어느 DB를 청소할지 정하고 postmaster에 worker를 요청합니다.autovacuum = on일 때
autovacuum worker실제로 VACUUM과 ANALYZE를 수행합니다.필요할 때만 잠깐
archiver다 쓴 WAL 파일을 archive_command로 다른 곳에 복사합니다.archive_mode = on일 때
startup기동할 때 WAL을 재생(redo)해 DB를 일관된 상태로 맞춥니다. standby에서는 계속 돕니다.기동과 복구 때
logger모든 프로세스의 stderr 출력을 모아 로그 파일에 씁니다.logging_collector = on일 때
walsenderstandby에 WAL을 보냅니다. 복제 접속마다 하나씩 생깁니다.복제 접속이 있을 때
walreceiverstandby 쪽에서 primary의 WAL을 받습니다.standby에서
walsummarizer증분 백업용 WAL 요약을 만듭니다.summarize_wal = on일 때
slotsync worker논리 복제 슬롯을 standby로 동기화합니다.standby에서 sync_replication_slots = on일 때
logical replication launcher논리 복제 구독(subscription)의 worker를 관리합니다. background worker의 한 종류입니다.항상

이 조건들은 소스에 그대로 나와 있습니다. postmaster는 ServerLoop를 돌 때마다 LaunchMissingBackgroundProcesses()를 불러 “지금 상태에서 있어야 하는데 없는 프로세스"를 띄웁니다. 예를 들어 checkpointer와 background writer는 기동 중에도 떠 있어야 하고, walwriter는 정상 운영(PM_RUN)일 때만 필요합니다. 복구 중에는 새 WAL을 쓸 일이 없기 때문입니다.

기동 순서

기동 순서도 소스에서 읽을 수 있습니다. PostmasterMain()의 끝부분은 io worker, checkpointer, background writer, startup 순서로 프로세스를 띄웁니다. startup이 WAL 재생을 마치고 끝나면 그제야 walwriter, autovacuum launcher 등이 뜹니다.

앞에서 본 ps 결과의 PID 번호가 바로 이 순서입니다.

  • io worker(34-36) → checkpointer(37) → background writer(38) 순서로, PostmasterMain()의 코드 순서와 같습니다.
  • 39번이 비어 있습니다. 서버 로그에서 database system was shut down at ...을 남긴 프로세스가 39번이고, 이것이 startup 프로세스입니다. 정상 종료 후의 기동이라 재생할 WAL이 없어 바로 끝났습니다.
  • 그 뒤에 walwriter(40), autovacuum launcher(41)가 떴습니다. startup이 끝나 PM_RUN이 된 뒤에 walwriter를 띄운다는 소스 조건과 정확히 맞습니다.

보조 프로세스와 logger

백그라운드 프로세스는 두 부류로 나뉩니다.

  • backend처럼 동작하는 프로세스: autovacuum worker, walsender, background worker 등. 특정 DB에 붙어 트랜잭션을 실행할 수 있습니다.
  • 보조 프로세스(auxiliary process): checkpointer, background writer, walwriter, io worker, startup, archiver 등. 공유 메모리에 자리(PGPROC)는 있지만 특정 DB에 붙지 않고, 트랜잭션을 실행하거나 일반 락을 잡을 수 없습니다(miscadmin.h의 주석).

logger는 예외입니다. 공유 메모리에 붙지 않고 PGPROC도 없어서, postmaster처럼 pg_stat_activity에 보이지 않습니다. 공유 메모리가 망가져도 로그만은 끝까지 남겨야 하기 때문입니다.

설정에 따라 생기는 프로세스: logger, archiver

logging_collector와 archive_mode를 켜고 재시작합니다. 두 설정 모두 재시작이 필요합니다.

$ mkdir -p /home/postgres/archive
postgres=# ALTER SYSTEM SET logging_collector = on;
postgres=# ALTER SYSTEM SET archive_mode = on;
postgres=# ALTER SYSTEM SET archive_command = 'cp %p /home/postgres/archive/%f';
$ pg_ctl -D $PGDATA -l /home/postgres/server.log restart -m fast
waiting for server to shut down.... done
server stopped
waiting for server to start.... done
server started
$ sleep 1
$ PM=$(head -1 $PGDATA/postmaster.pid); ps -o pid,ppid,cmd --forest -p $PM --ppid $PM
    PID    PPID CMD
    641       1 /usr/local/pgsql/bin/postgres -D /var/lib/postgresql/data
    642     641  \_ postgres: logger 
    643     641  \_ postgres: io worker 0
    644     641  \_ postgres: io worker 1
    645     641  \_ postgres: io worker 2
    646     641  \_ postgres: checkpointer 
    647     641  \_ postgres: background writer 
    649     641  \_ postgres: walwriter 
    650     641  \_ postgres: autovacuum launcher 
    651     641  \_ postgres: archiver 
    652     641  \_ postgres: logical replication launcher 
postgres=# SELECT pid, backend_type FROM pg_stat_activity ORDER BY pid;
 pid |         backend_type         
-----+------------------------------
 643 | io worker
 644 | io worker
 645 | io worker
 646 | checkpointer
 647 | background writer
 649 | walwriter
 650 | autovacuum launcher
 651 | archiver
 652 | logical replication launcher
 663 | client backend
(10 rows)

logger(642)는 다른 어떤 자식보다 먼저 떴습니다. 이후 프로세스들의 로그를 받아 적어야 하기 때문입니다. archiver(651)는 pg_stat_activity에 보이지만 logger는 보이지 않습니다. 공유 메모리에 붙지 않는 프로세스라서 그렇습니다.

PG18의 새 프로세스: io worker

PostgreSQL 18에는 비동기 I/O(AIO)가 들어왔고, 그 기본 구현이 io_method = worker입니다(aio.h). 18 이전에는 backend가 테이블을 읽을 때 read() 시스템 콜을 직접 불렀습니다. 18에서는 다음처럼 바뀌었습니다(method_worker.c).

  1. backend가 “이 블록들을 읽어 달라"는 요청을 공유 메모리의 제출 큐(submission queue)에 넣습니다.
  2. io worker가 큐에서 요청을 꺼내 실제 읽기 시스템 콜을 수행하고, 완료 처리까지 합니다.
  3. backend는 필요한 시점에만 결과를 기다립니다. 그 사이에 다른 일을 하거나 다음 읽기를 미리 요청할 수 있습니다.

요청이 몰리면 깨어난 worker가 다른 worker를 둘씩 더 깨웁니다(fan-out). worker 수는 io_workers(기본 3, 1-32)로 정하며, 서버를 재시작하지 않고 reload만으로 바꿀 수 있습니다. postmaster의 maybe_adjust_io_workers()가 설정값에 맞춰 worker를 늘리거나 줄입니다. 리눅스에서는 io_method = io_uring을 고를 수도 있는데, 그때는 io worker 대신 커널의 io_uring을 씁니다.

io worker 수를 재시작 없이 바꾸기

postgres=# SHOW io_method;
 io_method 
-----------
 worker
(1 row)

postgres=# SHOW io_workers;
 io_workers 
------------
 3
(1 row)

postgres=# ALTER SYSTEM SET io_workers = 5;
postgres=# SELECT pg_reload_conf();
 pg_reload_conf 
----------------
 t
(1 row)
$ sleep 1
$ ps -u postgres -o pid,ppid,cmd | grep 'io worker' | grep -v grep
     34      33 postgres: io worker 0
     35      33 postgres: io worker 1
     36      33 postgres: io worker 2
    141      33 postgres: io worker 3
    142      33 postgres: io worker 4
postgres=# ALTER SYSTEM RESET io_workers;
postgres=# SELECT pg_reload_conf();
 pg_reload_conf 
----------------
 t
(1 row)
$ sleep 1
$ ps -u postgres -o pid,ppid,cmd | grep 'io worker' | grep -v grep
     34      33 postgres: io worker 0
     35      33 postgres: io worker 1
     36      33 postgres: io worker 2

reload만으로 io worker가 5개로 늘었다가(141, 142 추가) 다시 3개로 줄었습니다. reload 요청(SIGHUP)을 받은 postmaster가 maybe_adjust_io_workers()로 개수를 맞춘 결과입니다.

프로세스끼리 소통하는 방법

프로세스는 원래 서로의 메모리를 볼 수 없습니다. PostgreSQL 프로세스들이 협업하는 방법은 세 가지입니다.

공유 메모리

postmaster는 기동할 때 CreateSharedMemoryAndSemaphores()로 큰 공유 메모리 영역을 만듭니다. 자식은 fork할 때 이 매핑을 그대로 물려받으므로, 모든 프로세스가 같은 가상 주소에서 같은 메모리를 봅니다. shared buffers, WAL buffers, 락 테이블, PGPROC 배열, PG18의 I/O 큐가 모두 여기에 있습니다. 크기는 shared_memory_size로 확인할 수 있습니다. 공유 메모리의 구성은 2편에서 자세히 다룹니다.

공유 메모리는 모든 프로세스에 같은 주소로 붙어 있다

postgres=# SHOW shared_buffers;
 shared_buffers 
----------------
 128MB
(1 row)

postgres=# SHOW shared_memory_size;
 shared_memory_size 
--------------------
 150MB
(1 row)

postgres=# SHOW shared_memory_type;
 shared_memory_type 
--------------------
 mmap
(1 row)

shared buffers는 128MB지만, 락 테이블과 PGPROC 등을 합친 공유 메모리 전체는 150MB입니다. 21MB짜리 테이블을 만든 뒤, 한 세션은 이 테이블을 한 번 읽고 idle로 두고, 다른 세션은 아무것도 하지 않고 idle로 둡니다.

postgres=# CREATE TABLE big AS SELECT g AS id, repeat('x', 100) AS pad FROM generate_series(1, 150000) g;

postgres=# SELECT pg_size_pretty(pg_relation_size('big'));
 pg_size_pretty 
----------------
 21 MB
(1 row)

postmaster와 두 backend의 공유 매핑(rw-s)을 비교합니다.

$ PM=$(head -1 $PGDATA/postmaster.pid)
$ for p in $PM $(pgrep -f 'postgres: postgres postgres \[local\] idle'); do
>   echo "== PID $p  $(ps -o cmd= -p $p)"
>   grep ' rw-s ' /proc/$p/maps | awk '{print "   ", $1, $6, $7}'
> done
== PID 412  /usr/local/pgsql/bin/postgres -D /var/lib/postgresql/data
    ffffaf5e3000-ffffaf5eb000 /dev/shm/PostgreSQL.1630556078 
    ffffaf5eb000-ffffaf5ec000 /SYSV00137e8a (deleted)
    ffffaf5ec000-ffffb8b9e000 /dev/zero (deleted)
== PID 583  postgres: postgres postgres [local] idle
    ffffaf450000-ffffaf481000 /dev/shm/PostgreSQL.3330517350 
    ffffaf4c2000-ffffaf5c2000 /dev/shm/PostgreSQL.3445492072 
    ffffaf5e3000-ffffaf5eb000 /dev/shm/PostgreSQL.1630556078 
    ffffaf5eb000-ffffaf5ec000 /SYSV00137e8a (deleted)
    ffffaf5ec000-ffffb8b9e000 /dev/zero (deleted)
== PID 593  postgres: postgres postgres [local] idle
    ffffaf4c2000-ffffaf5c2000 /dev/shm/PostgreSQL.3445492072 
    ffffaf5e3000-ffffaf5eb000 /dev/shm/PostgreSQL.1630556078 
    ffffaf5eb000-ffffaf5ec000 /SYSV00137e8a (deleted)
    ffffaf5ec000-ffffb8b9e000 /dev/zero (deleted)
  • ffffaf5ec000-ffffb8b9e000 /dev/zero (deleted)가 본체 공유 메모리입니다. 주소 범위를 계산하면 156,966,912바이트, 약 150MB로 shared_memory_size와 같습니다. 세 프로세스 모두 정확히 같은 주소에 붙어 있습니다. postmaster가 만든 매핑을 fork로 물려받았기 때문입니다. /dev/zero (deleted)는 shared_memory_type = mmap일 때 익명 공유 매핑이 /proc에 표시되는 방식입니다.
  • /SYSV00137e8a는 몇 바이트짜리 System V 공유 메모리입니다. 데이터를 담는 곳이 아니라, 같은 데이터 디렉터리에 postmaster가 두 개 뜨는 일을 막는 잠금 장치로 씁니다(sysv_shmem.c의 주석).
  • /dev/shm/PostgreSQL.*는 필요할 때 따로 만들어 붙이는 동적 공유 메모리(DSM)입니다. 프로세스마다 붙어 있는 조각이 다른 것도 그래서입니다.

이번에는 메모리 사용량을 봅니다. VmRSS는 ps나 top이 보여 주는 RSS 값이고, 리눅스는 이를 개인 메모리(RssAnon), 파일 매핑(RssFile), 공유 메모리(RssShmem)로 나눠 보여 줍니다.

$ PM=$(head -1 $PGDATA/postmaster.pid)
$ for p in $PM $(pgrep -f 'postgres: postgres postgres \[local\] idle'); do
>   echo "== PID $p  $(ps -o cmd= -p $p)"
>   grep -E '^(VmRSS|RssAnon|RssShmem)' /proc/$p/status
> done
== PID 412  /usr/local/pgsql/bin/postgres -D /var/lib/postgresql/data
VmRSS:	   22336 kB
RssAnon:	     796 kB
RssShmem:	   13372 kB
== PID 583  postgres: postgres postgres [local] idle
VmRSS:	   34524 kB
RssAnon:	    1848 kB
RssShmem:	   24520 kB
== PID 593  postgres: postgres postgres [local] idle
VmRSS:	   11388 kB
RssAnon:	    1616 kB
RssShmem:	    3280 kB

테이블을 읽은 583번은 RSS가 34MB로, 아무것도 안 한 593번(11MB)의 세 배입니다. 하지만 늘어난 부분은 거의 RssShmem(24MB)이고, 자기만 쓰는 RssAnon은 1.6-1.8MB로 비슷합니다. 21MB 테이블을 shared buffers로 읽어 들이면서 공유 메모리 페이지를 만진 만큼 그 프로세스의 RSS에 잡힌 것입니다. 같은 페이지를 다른 backend가 만지면 그쪽 RSS에도 똑같이 잡힙니다.

운영에서는: RSS를 합산하면 메모리 사용량이 부풀려진다

같은 shared buffers 페이지를 backend 100개가 읽었다면, 그 페이지는 100개 프로세스의 RSS에 모두 잡힙니다. 모니터링 도구로 postgres 프로세스들의 RSS를 합산하면 실제 사용량보다 훨씬 큰 값이 나오는 이유입니다. 프로세스별 개인 메모리를 보려면 /proc/<pid>/status의 RssAnon이나 smem의 USS/PSS 같은 지표를 보는 것이 정확합니다.

시그널과 latch

“일이 생겼으니 깨어나라"는 알림은 시그널로 보냅니다. 받는 쪽은 latch라는 장치로 잠들어 있다가 깨어납니다. 앞에서 본 ServerLoop의 WaitEventSetWait()가 latch를 기다리는 부분입니다. 자식이 postmaster에게 부탁할 때도 같은 방식을 씁니다. 예를 들어 autovacuum launcher는 worker가 필요하면 SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_WORKER)로 요청만 보내고, fork는 postmaster가 StartAutovacuumWorker()에서 합니다. 그래서 autovacuum worker의 부모 프로세스는 launcher가 아니라 postmaster입니다.

autovacuum worker는 누가 fork하나

autovacuum이 자주 돌도록 autovacuum_naptime을 1초로 줄이고, 테이블 전체를 UPDATE해서 청소할 거리(dead tuple)를 만듭니다. 그다음 worker가 나타날 때까지 ps를 0.01초 간격으로 확인합니다.

postgres=# ALTER SYSTEM SET autovacuum_naptime = '1s';
postgres=# ALTER SYSTEM SET log_autovacuum_min_duration = 0;
postgres=# SELECT pg_reload_conf();
 pg_reload_conf 
----------------
 t
(1 row)


postgres=# CREATE TABLE av_test AS SELECT g AS id, 0 AS v FROM generate_series(1, 200000) g;
postgres=# UPDATE av_test SET v = 1;
$ for i in $(seq 1 1000); do
>   ps -u postgres -o pid,ppid,cmd | grep 'autovacuum worker' | grep -v grep && break
>   sleep 0.01
> done
    294      33 postgres: autovacuum worker postgres
$ sleep 3
$ grep -E 'autovacuum worker LOG:  automatic (vacuum|analyze) of table "postgres.public.av_test"' /home/postgres/server.log | cut -c1-120
2026-09-26 10:24:09.034 UTC [294] autovacuum worker LOG:  automatic vacuum of table "postgres.public.av_test": index sca
2026-09-26 10:24:09.079 UTC [294] autovacuum worker LOG:  automatic analyze of table "postgres.public.av_test"
2026-09-26 10:24:10.032 UTC [297] autovacuum worker LOG:  automatic vacuum of table "postgres.public.av_test": index sca
2026-09-26 10:24:11.028 UTC [299] autovacuum worker LOG:  automatic vacuum of table "postgres.public.av_test": index sca

ps로 잡은 294번 worker의 부모는 launcher(41)가 아니라 postmaster(33)입니다. 로그를 보면 294, 297, 299처럼 worker가 약 1초마다 새 PID로 생겼다 사라집니다. worker는 할 일을 마치면 종료하고, 다음 주기에 launcher가 다시 요청하면 postmaster가 새로 fork합니다. (실습용 설정은 이 단계 끝에서 ALTER SYSTEM RESET으로 되돌렸습니다.) VACUUM 자체는 5편에서 다룹니다.

postmaster 생존 확인

자식들은 postmaster가 살아 있는지 계속 확인합니다. 리눅스에서는 prctl(PR_SET_PDEATHSIG)로 “부모가 죽으면 나에게 시그널을 보내 달라"고 커널에 등록해 둡니다. postmaster가 사라지면 자식들도 스스로 종료합니다. 감독이 없는 상태로 공유 메모리를 계속 만지면 위험하기 때문입니다.

postmaster가 죽으면

postmaster 자체에 kill -9를 보냅니다. 운영 서버에서는 절대 하면 안 되는 일입니다.

$ kill -9 641
$ sleep 2
$ ps -u postgres -o pid,ppid,stat,cmd | grep -v -e 'ps -u' -e 'sleep infinity'
    PID    PPID STAT CMD
    664       0 Ss   bash -s

bash -s는 이 명령을 실행한 셸입니다. PostgreSQL 프로세스는 하나도 남지 않았습니다. 부모가 죽은 것을 알아챈 자식들이 스스로 종료했습니다.

$ psql -X -c "SELECT 1"
psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: Connection refused
	Is the server running locally and accepting connections on that socket?
$ pg_ctl -D $PGDATA -l /home/postgres/server.log start
pg_ctl: another server might be running; trying to start server anyway
waiting for server to start.... done
server started
$ sleep 1
$ tail -n 8 $PGDATA/log/$(ls -t $PGDATA/log | head -1)
2026-09-26 10:24:29.194 UTC [689] postmaster LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2026-09-26 10:24:29.196 UTC [696] startup LOG:  database system was interrupted; last known up at 2026-09-26 10:24:25 UTC
2026-09-26 10:24:29.222 UTC [696] startup LOG:  database system was not properly shut down; automatic recovery in progress
2026-09-26 10:24:29.223 UTC [696] startup LOG:  unexpected pageaddr 0/26EA000 in WAL segment 000000010000000000000005, LSN 0/56EA000, offset 7249920
2026-09-26 10:24:29.223 UTC [696] startup LOG:  redo is not required
2026-09-26 10:24:29.225 UTC [694] checkpointer LOG:  checkpoint starting: end-of-recovery immediate wait
2026-09-26 10:24:29.229 UTC [694] checkpointer LOG:  checkpoint complete: wrote 0 buffers (0.0%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.005 s; sync files=2, longest=0.001 s, average=0.001 s; distance=6 kB, estimate=6 kB; lsn=0/56EA048, redo lsn=0/56EA048
2026-09-26 10:24:29.230 UTC [689] postmaster LOG:  database system is ready to accept connections

postmaster.pid가 남아 있어서 pg_ctl이 “다른 서버가 떠 있을 수도 있다"고 경고했지만, 파일에 적힌 PID의 프로세스가 없으니 그대로 기동했습니다. 정상 종료가 아니었으므로 startup이 복구 과정을 거쳤습니다. 마지막 체크포인트 뒤로 재생할 WAL 레코드가 없어서 redo is not required로 끝났습니다.

운영에서는: 컨테이너에서는 PID 1이 좀비를 치워야 한다

PostgreSQL을 컨테이너에서 띄울 때 PID 1을 sleep infinity 같은 프로세스로 두면, 위처럼 postmaster가 죽은 뒤 재기동이 실패할 수 있습니다. --init 없이 띄운 컨테이너에서 재현한 기록입니다.

2026-09-26 10:12:12.213 UTC [56] postmaster FATAL:  lock file "postmaster.pid" already exists
2026-09-26 10:12:12.213 UTC [56] postmaster HINT:  Is another postmaster (PID 26) running in data directory "/var/lib/postgresql/data"?
    PID    PPID STAT CMD
      1       0 Ss   /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep infinity
     26       1 Zs   [postgres] <defunct>
     27       1 Zs   [postgres] <defunct>
     28       1 Zs   [postgres] <defunct>
     29       1 Zs   [postgres] <defunct>
     30       1 Zs   [postgres] <defunct>
     31       1 Zs   [postgres] <defunct>
     33       1 Zs   [postgres] <defunct>
     34       1 Zs   [postgres] <defunct>
     35       1 Zs   [postgres] <defunct>

(ps 출력에서 이 명령을 실행한 셸(bash -s) 줄은 생략했습니다. PID 1은 sleep infinity이고, 실습 이미지에서는 sleep이 coreutils 단일 바이너리를 거쳐 실행되어 이렇게 표시됩니다.)

postmaster가 죽자 자식들의 부모는 PID 1로 바뀌었습니다. 그런데 sleep은 끝난 자식 프로세스를 회수(wait)하지 않으므로, 죽은 postmaster(26)와 자식들이 모두 좀비(Z)로 남았습니다. 좀비도 PID는 차지하고 있어서, 새 postmaster는 postmaster.pid에 적힌 26번이 아직 살아 있다고 판단하고 기동을 거부했습니다. PostgreSQL을 컨테이너에서 직접 띄울 때 PID 1을 sleep이나 셸 스크립트로 두었다면 --init(tini)이나 좀비를 회수하는 init을 쓰는 것이 안전합니다. 공식 postgres 이미지처럼 postmaster 자체를 PID 1로 두는 경우에는, postmaster가 자식을 회수하므로 이 문제가 없습니다.

backend 하나가 죽으면 왜 모두 재시작하나

PostgreSQL을 운영하다 보면 “쿼리 하나가 죽었는데 모든 접속이 끊겼다"는 일을 겪게 됩니다. 이유는 postmaster의 판단 기준에 있습니다.

자식이 끝나면 postmaster는 CleanupBackend()에서 종료 코드를 봅니다. 종료 코드가 0(정상)이나 1(FATAL 에러로 스스로 종료)이 아니면 크래시로 봅니다. kill -9, 세그멘테이션 폴트, 리눅스 OOM killer에 의한 종료가 모두 여기에 해당합니다. 그 프로세스가 공유 메모리를 쓰던 중에 죽었는지 postmaster로서는 알 방법이 없습니다. 공유 메모리가 망가졌을 수 있다고 가정하고 모두 정리하는 것이 안전하다고 판단합니다.

크래시 후 postmaster의 상태(PMState) 변화. 위쪽 초록 화살표가 재시작 경로입니다. 새 창에서 크게 보기

그림의 상태 이름은 소스의 PMState 값 그대로입니다. 순서는 다음과 같습니다.

  1. PM_RUN → PM_WAIT_BACKENDS: HandleChildCrash()가 “terminating any other active server processes"를 로그에 남기고, 살아 있는 모든 자식에게 SIGQUIT을 보냅니다. SIGQUIT을 받은 backend는 quickdie()에서 클라이언트에게 경고를 보내고 곧바로 끝납니다.
  2. 응답 없는 자식은 SIGKILL: 5초 안에 끝나지 않는 자식에게는 SIGKILL을 보냅니다.
  3. PM_NO_CHILDREN → PM_STARTUP: 자식이 모두 사라지면 “all server processes terminated; reinitializing”을 남기고 공유 메모리를 새로 만든 뒤 startup 프로세스를 띄웁니다. startup은 마지막 체크포인트부터 WAL을 재생합니다. 커밋된 데이터는 WAL에 남아 있으므로 잃지 않습니다.
  4. 재시작하지 않는 경우: restart_after_crash = off이거나 startup 프로세스 자체가 실패하면, postmaster도 재시작하지 않고 종료합니다. startup이 실패했다면 다시 해도 또 실패할 가능성이 크기 때문입니다.

반대로 pg_terminate_backend()로 끝낸 backend는 종료 코드 1로 스스로 끝나므로 이 과정을 거치지 않습니다. 두 경우를 나란히 비교해 보겠습니다.

정상 종료와 비정상 종료의 차이

세션 셋을 백그라운드로 엽니다. 둘은 idle, 하나는 긴 쿼리를 실행합니다.

$ psql -X -q &
$ psql -X -q &
$ psql -X -c "SELECT pg_sleep(3600)" &
postgres=# CREATE TABLE t AS SELECT generate_series(1, 1000) AS id;
$ PM=$(head -1 $PGDATA/postmaster.pid); ps -o pid,ppid,cmd --forest -p $PM --ppid $PM
    PID    PPID CMD
    412       1 /usr/local/pgsql/bin/postgres -D /var/lib/postgresql/data
    413     412  \_ postgres: io worker 0
    414     412  \_ postgres: io worker 1
    415     412  \_ postgres: io worker 2
    416     412  \_ postgres: checkpointer 
    417     412  \_ postgres: background writer 
    419     412  \_ postgres: walwriter 
    420     412  \_ postgres: autovacuum launcher 
    421     412  \_ postgres: logical replication launcher 
    430     412  \_ postgres: postgres postgres [local] idle
    439     412  \_ postgres: postgres postgres [local] idle
    447     412  \_ postgres: postgres postgres [local] SELECT

먼저 정상 종료. idle 세션 하나(430)를 pg_terminate_backend()로 끝냅니다.

postgres=# SELECT pg_terminate_backend(430);
 pg_terminate_backend 
----------------------
 t
(1 row)
$ sleep 1
$ PM=$(head -1 $PGDATA/postmaster.pid); ps -o pid,ppid,cmd --forest -p $PM --ppid $PM
    PID    PPID CMD
    412       1 /usr/local/pgsql/bin/postgres -D /var/lib/postgresql/data
...  (413-421, 위와 같은 백그라운드 프로세스 8줄)
    439     412  \_ postgres: postgres postgres [local] idle
    447     412  \_ postgres: postgres postgres [local] SELECT
$ grep '\[430\]' /home/postgres/server.log | tail -2
2026-09-26 10:24:17.448 UTC [430] client backend FATAL:  terminating connection due to administrator command
2026-09-26 10:24:17.448 UTC [430] client backend LOG:  disconnection: session time: 0:00:02.281 user=postgres database=postgres host=[local]

430번만 사라졌고 나머지 PID는 그대로입니다. backend가 FATAL로 스스로 끝나면서 종료 코드 1을 냈기 때문에, postmaster는 정상 종료로 처리했습니다.

이번에는 비정상 종료. 남은 idle 세션(439)에 kill -9를 보냅니다.

$ kill -9 439
$ sleep 2
$ PM=$(head -1 $PGDATA/postmaster.pid); ps -o pid,ppid,cmd --forest -p $PM --ppid $PM
    PID    PPID CMD
    412       1 /usr/local/pgsql/bin/postgres -D /var/lib/postgresql/data
    484     412  \_ postgres: io worker 0
    485     412  \_ postgres: io worker 1
    486     412  \_ postgres: io worker 2
    488     412  \_ postgres: checkpointer 
    489     412  \_ postgres: background writer 
    490     412  \_ postgres: walwriter 
    491     412  \_ postgres: autovacuum launcher 
    492     412  \_ postgres: logical replication launcher 

postmaster(412)를 뺀 모든 프로세스의 PID가 바뀌었습니다. 백그라운드 프로세스까지 전부 새로 fork된 것이고, 아무 잘못이 없던 447번 세션도 사라졌습니다. 로그에 전 과정이 남아 있습니다.

$ sed -n '/terminated by signal 9/,$p' /home/postgres/server.log
2026-09-26 10:24:18.553 UTC [412] postmaster LOG:  client backend (PID 439) was terminated by signal 9: Killed
2026-09-26 10:24:18.553 UTC [412] postmaster LOG:  terminating any other active server processes
2026-09-26 10:24:18.553 UTC [412] postmaster LOG:  all server processes terminated; reinitializing
2026-09-26 10:24:18.564 UTC [487] startup LOG:  database system was interrupted; last known up at 2026-09-26 10:24:14 UTC
2026-09-26 10:24:18.597 UTC [487] startup LOG:  database system was not properly shut down; automatic recovery in progress
2026-09-26 10:24:18.597 UTC [487] startup LOG:  redo starts at 0/3F9BF38
2026-09-26 10:24:18.600 UTC [487] startup LOG:  invalid record length at 0/3FC3028: expected at least 24, got 0
2026-09-26 10:24:18.600 UTC [487] startup LOG:  redo done at 0/3FC2E90 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
2026-09-26 10:24:18.601 UTC [488] checkpointer LOG:  checkpoint starting: end-of-recovery immediate wait
2026-09-26 10:24:18.605 UTC [488] checkpointer LOG:  checkpoint complete: wrote 22 buffers (0.1%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.003 s, total=0.005 s; sync files=20, longest=0.001 s, average=0.001 s; distance=156 kB, estimate=156 kB; lsn=0/3FC3028, redo lsn=0/3FC3028
2026-09-26 10:24:18.606 UTC [412] postmaster LOG:  database system is ready to accept connections

위 그림의 상태 변화가 로그 한 줄 한 줄에 대응합니다.

로그상태
was terminated by signal 9종료 코드가 0, 1이 아니므로 크래시로 판단
terminating any other active server processesPM_WAIT_BACKENDS. 모든 자식에게 SIGQUIT
all server processes terminated; reinitializingPM_NO_CHILDREN. 공유 메모리 재생성
[487] startup ... automatic recovery in progress, redo startsPM_STARTUP. 새 startup 프로세스(487)가 WAL 재생
invalid record length ...WAL의 끝에 도달했다는 뜻으로, 복구 중에 흔히 보이는 정상 메시지
database system is ready to accept connectionsPM_RUN 복귀

크래시부터 복귀까지 약 50ms가 걸렸습니다. 재생할 WAL이 156kB뿐이라 빨랐던 것이고, 쓰기가 많은 운영 서버라면 마지막 체크포인트 이후 쌓인 WAL 양만큼 오래 걸립니다(8편에서 다룹니다).

아무 잘못이 없던 옆 세션(447번)의 psql에는 다음 메시지가 찍혔습니다.

WARNING:  terminating connection because of crash of another server process
DETAIL:  The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT:  In a moment you should be able to reconnect to the database and repeat your command.
server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
connection to server was lost

“다른 서버 프로세스가 비정상 종료해서 공유 메모리가 망가졌을 수 있으니, 이 프로세스도 트랜잭션을 롤백하고 끝내라고 postmaster가 명령했다"는 뜻입니다. 앞에서 본 설계 이유가 메시지에 그대로 적혀 있습니다. 커밋까지 끝난 데이터는 WAL 재생으로 복구되었습니다.

postgres=# SELECT count(*) FROM t;
 count 
-------
  1000
(1 row)

운영에서는: backend에 kill -9를 쓰면 안 된다

위 실습 결과가 곧 이유입니다. 문제가 된 세션 하나를 끝내려고 kill -9를 쓰면, 그 순간 서버의 모든 세션이 끊기고 복구 과정을 거칩니다. 세션을 끝낼 때는 다음을 씁니다.

SELECT pg_cancel_backend(pid);     -- 실행 중인 쿼리만 취소 (세션은 유지)
SELECT pg_terminate_backend(pid);  -- 세션 종료 (다른 세션에는 영향 없음)

운영자가 직접 kill -9를 쓰지 않아도 같은 일이 일어날 수 있습니다. 대표적인 것이 리눅스 OOM killer입니다. 메모리가 모자라 커널이 backend 하나를 죽이면 똑같이 전체 재시작이 일어나고, 커널이 postmaster를 골라 죽이면 서버가 아예 내려갑니다. 그래서 PostgreSQL 문서는 전용 DB 서버에서 vm.overcommit_memory = 2로 메모리 overcommit을 막아 OOM killer가 개입할 상황 자체를 줄이라고 권합니다(Linux Memory Overcommit).

로그에 terminating any other active server processes와 all server processes terminated; reinitializing이 보이면 크래시 재시작이 일어난 것입니다. 바로 앞 줄에 어떤 프로세스가 어떤 시그널로 죽었는지(was terminated by signal 9) 나오므로, 그 PID를 기준으로 원인을 추적합니다.

정리

  • PostgreSQL은 프로세스 기반입니다. postmaster가 모든 프로세스의 부모이고, fork는 postmaster만 합니다.
  • postmaster는 접속을 받아 fork하고 자식을 감시할 뿐, 쿼리를 처리하거나 공유 메모리를 만지지 않습니다. 그래서 자식이 어떻게 죽든 뒷수습을 할 수 있습니다.
  • backend는 접속 하나에 하나입니다. fork가 먼저이고, 인증과 max_connections 확인은 fork된 자식이 합니다.
  • 백그라운드 프로세스들이 디스크 쓰기(checkpointer, background writer, walwriter), 읽기(PG18의 io worker), 청소(autovacuum), 보관(archiver)을 나눠 맡습니다. 어떤 프로세스가 뜰지는 postmaster 상태와 설정으로 정해집니다.
  • 모든 프로세스는 같은 공유 메모리를 같은 주소로 봅니다. 그래서 프로세스 하나가 비정상 종료하면 postmaster는 공유 메모리를 믿을 수 없다고 보고, 모두 내린 뒤 WAL로 복구해서 다시 띄웁니다.
  • 운영에서는 커넥션 풀러로 프로세스 수를 관리하고, 세션은 pg_terminate_backend()로 끝내고, OOM killer가 backend를 죽이지 않도록 메모리 설정을 관리합니다.

다음 글에서는 이 프로세스들이 함께 쓰는 공유 메모리 안쪽, 즉 shared buffers, WAL buffers, 그리고 backend마다 따로 쓰는 work_mem을 살펴봅니다.

참고 자료

소스 코드 (모두 REL_18_STABLE 커밋 39a0db1 기준)

PostgreSQL 18 공식 문서