DB Study

2장. 아키텍처 — 프로세스와 메모리

PostgreSQL은 스레드가 아니라 프로세스로 일합니다. 접속 하나에 프로세스 하나 — 이 40년째 유지되는 결정이 왜 내려졌고, 서버 안에서 쿼리가 어떤 길을 걷는지 해부합니다.

기초 ⏱ 약 25분 🎬 애니메이션 3개 선수 지식: 1장 개요
🧭
이 장에서 배우는 것

① postmaster와 backend — fork 기반 프로세스 모델 ② shared buffers를 중심으로 한 공유 메모리 구성 ③ 쿼리가 처리되는 4단계(Parse→Rewrite→Plan→Execute) ④ 커넥션 풀링이 필수인 이유와 PGDATA·설정 계층

2.1 프로세스 모델 — postmaster는 감독관이다

MySQL·Oracle이 스레드 기반인 것과 달리, PostgreSQL은 프로세스 기반 멀티태스킹 DBMS입니다. 서버를 시작(pg_ctl start)하면 postmaster라는 최상위 프로세스가 뜹니다. postmaster의 역할은 단 하나 — 감독관(supervisor). 쿼리는 직접 처리하지 않고, 접속 요청이 오면 자식 프로세스(backend)를 fork()하고, 자식이 죽으면 뒷수습을 합니다.

👮 postmaster 5432 포트 감시 중 백그라운드 프로세스들 checkpointer background writer walwriter autovacuum launcher 💻 클라이언트 A ⚙️ backend A 약 5~10MB fork() 💻 클라이언트 B ⚙️ backend B 별도 프로세스 fork() 💥 backend A 크래시! B는 멀쩡 ✓ (프로세스 격리) 🛡 크래시 격리: 한 연결의 죽음이 다른 연결을 해치지 않는다 postmaster가 공유 메모리 오염 여부를 판단, 필요 시에만 전체 재시작

실제 서버에서 ps로 보면 이 구조가 그대로 보입니다.

$ ps -ef | grep postgres
postgres  1001    1  ... /usr/pgsql-17/bin/postgres -D /var/lib/pgsql/data  ← postmaster
postgres  1005 1001  ... postgres: checkpointer
postgres  1006 1001  ... postgres: background writer
postgres  1007 1001  ... postgres: walwriter
postgres  1008 1001  ... postgres: autovacuum launcher
postgres  2001 1001  ... postgres: myuser mydb 10.0.0.5(54321) idle  ← backend
보조 프로세스역할
Checkpointer주기적으로 shared buffers의 dirty page를 디스크에 반영(체크포인트, 9장)
Background Writer오래된 dirty page를 선제적으로 디스크에 기록
WAL WriterWAL buffer를 pg_wal/로 flush (COMMIT 시엔 backend가 직접 fsync)
Autovacuum Launcher/Workerdead tuple이 쌓인 테이블을 찾아 VACUUM·ANALYZE 수행(8장)
Archiverarchive_mode=on일 때 WAL 세그먼트를 외부로 복사
WAL Sender / Receiver스트리밍 복제의 발신·수신측(10장)
Parallel Worker병렬 쿼리 실행 시 동적으로 생성

2.2 Shared Memory — 모두가 공유하는 작업대

프로세스는 서로 메모리를 공유하지 않는 게 원칙이지만, DB는 모든 backend가 같은 데이터 페이지·같은 트랜잭션 상태를 봐야 합니다. 그래서 시작 시 공유 메모리(shared memory) 영역을 만들어 모두가 붙습니다.

영역내용
shared_buffers8KB 디스크 페이지의 캐시. 기본 128MB, 프로덕션은 물리 RAM의 20~25%가 출발점
WAL BuffersCOMMIT 전 WAL 레코드의 임시 저장소. 기본 shared_buffers/32, 최대 16MB
CLOG (pg_xact)모든 트랜잭션 ID의 커밋 상태를 비트 2개로 저장 — MVCC 가시성 판단의 필수 경로(3장)
Lock Table테이블 락 등 heavy-weight lock의 해시 테이블(7장)
ProcArray지금 살아 있는 트랜잭션 XID 목록. 스냅샷을 뜰 때마다 스캔

이 중 주인공은 단연 shared_buffers입니다. 쿼리가 이 캐시를 어떻게 통과하는지 봅시다.

⚙️ backend SELECT / UPDATE 🧠 shared buffers page 3 page 12 page 7 ✨ dirty! 💾 디스크 base/… heap pg_wal/ ① page 7 주세요 ② 캐시에 없음 (cache miss) ③ 디스크에서 로드 ④ UPDATE는 버퍼에서 수정 ⑤ WAL 레코드 먼저 기록 (9장) ⑥ checkpointer가 나중에 flush ⑦ 다음 조회는 캐시 히트 — 디스크 I/O 없음 🎉
💡
shared_buffers는 왜 RAM의 25%인가

PostgreSQL은 OS 페이지 캐시 위에 자체 캐시를 하나 더 두는 이중 구조입니다. shared_buffers를 너무 키우면 같은 페이지가 두 캐시에 중복 저장되는 낭비가 커지므로, 20~25%에서 시작해 워크로드로 검증하는 것이 통설입니다.

2.3 쿼리의 4단계 — Parse → Rewrite → Plan → Execute

backend 안에서 SELECT 한 문장은 네 단계를 거칩니다. (이 파이프라인의 애니메이션은 6장에서 자세히 다룹니다.)

단계하는 일산출물
ParseLexer+Parser(yacc)가 문자열을 분석. 문법 오류는 여기서 검출Parse Tree
RewriteRule System이 View를 기반 테이블 쿼리로 치환Query Tree
Plan통계 기반으로 Seq Scan/Index Scan, 조인 순서·방식을 비용 비교로 결정Plan Tree
ExecuteVolcano(iterator) 모델 — 노드가 튜플을 하나씩 위로 올림결과 튜플
-- Rewrite 단계의 실체: 뷰는 "펼쳐진다"
CREATE VIEW active_users AS SELECT * FROM users WHERE status = 'active';

SELECT id FROM active_users WHERE country = 'KR';
-- Rewriter가 아래로 치환한 뒤 Planner에게 전달:
-- SELECT id FROM users WHERE status = 'active' AND country = 'KR';

2.4 커넥션 풀링 — "연결 = 프로세스"의 청구서

연결 하나가 프로세스 하나라는 구조는 연결 급증을 허용하지 않습니다. 10,000 커넥션 = 10,000 프로세스 = 수십 GB 메모리에, ProcArray·Lock 스캔까지 급격히 나빠집니다. 커뮤니티 합의는 명확합니다 — max_connections는 200~500 수준으로 두고, 그 앞에 pooler(pgBouncer 등)를 세운다.

앱 서버 #1 앱 서버 #2 앱 서버 #3 … 클라이언트 3,000개 😱 직접 연결: 프로세스 3,000개 = 수십 GB 🚦 pgBouncer transaction 모드 PostgreSQL backend 1 backend 2 backend 3 … 총 20개 트랜잭션 끝나면 즉시 반납 클라이언트 3,000 : backend 20 — 메모리는 1/150, 처리량은 유지 주의: transaction 모드에선 SET·LISTEN/NOTIFY·명시적 PREPARE 같은 세션 상태가 깨질 수 있음
pgBouncer 모드backend 재할당 단위비고
session세션 종료 시풀 효과 미미
transaction트랜잭션 종료 시권장. 세션 변수·prepared statement 제약 주의
statement쿼리 하나마다멀티 문장 트랜잭션 불가, 거의 안 씀

2.5 PGDATA — 데이터 디렉터리 지도

initdb가 만드는 데이터 디렉터리($PGDATA)의 핵심 구조입니다. 장애 대응 때 "어디에 뭐가 있는지"를 알면 절반은 이긴 겁니다.

경로내용
base/<db_oid>/<filenode>테이블·인덱스 본체. 1GB 단위로 분할(.1, .2…)
base/…/_fsm, _vmFree Space Map · Visibility Map (4장)
global/pg_database, pg_authid 등 클러스터 공통 카탈로그
pg_wal/WAL 세그먼트(기본 16MB/파일)
pg_xact/CLOG — 트랜잭션 커밋 상태 비트맵
postgresql.conf / postgresql.auto.conf메인 설정 / ALTER SYSTEM이 기록하는 오버라이드(conf보다 우선)
pg_hba.conf접속 인증 규칙(어느 IP·사용자·DB가 어떤 방식으로)
postmaster.pid실행 중인 postmaster의 PID·포트
-- 특정 테이블의 실제 파일 경로 찾기
SELECT pg_relation_filepath('orders');
-- base/16384/24576

2.6 설정 계층 — 같은 파라미터, 다른 우선순위

동일 파라미터가 여러 곳에서 지정될 수 있고, 우선순위는 명확합니다: SET(세션) > ALTER ROLE … SET > ALTER DATABASE … SET > postgresql.auto.conf > postgresql.conf.

-- 현재 세션만
SET work_mem = '256MB';

-- 특정 사용자 접속 시 항상 적용
ALTER ROLE analyst SET work_mem = '512MB';

-- 서버 전역 (postgresql.auto.conf에 기록)
ALTER SYSTEM SET shared_buffers = '4GB';
SELECT pg_reload_conf();

-- 재시작이 필요한지 reload로 충분한지 확인
SELECT name, setting, context FROM pg_settings
WHERE name IN ('shared_buffers', 'work_mem');
--  shared_buffers | context = postmaster ← 재시작 필요
--  work_mem       | context = user       ← SET으로도 가능
⚠️
postgresql.conf를 고쳤는데 안 바뀌어요?

ALTER SYSTEM으로 기록된 postgresql.auto.confpostgresql.conf를 덮어씁니다. 설정이 안 먹을 때 가장 먼저 auto.conf를 확인하세요. pg_settingssource 컬럼이 어느 파일에서 온 값인지 알려줍니다.

✍️ 이해도 체크

PostgreSQL이 스레드 대신 프로세스 모델을 유지하는 가장 큰 이유는?
✅ 핵심은 크래시 격리입니다. backend 하나가 SIGSEGV로 죽어도 다른 연결은 살아남습니다. 대가는 연결당 5~10MB의 메모리와 fork 비용 — 그래서 pgBouncer 같은 커넥션 풀러가 사실상 필수가 됩니다.
UPDATE가 실행되었을 때 변경된 페이지가 디스크에 반영되는 시점은?
✅ 데이터 페이지는 shared buffers 안에서 수정되어 dirty 상태로 머물고, 디스크 반영은 checkpointer·background writer의 몫입니다. 대신 변경 내역은 WAL에 먼저 기록·fsync되므로(9장) 크래시가 나도 커밋된 데이터는 잃지 않습니다.
📚
원문으로 더 깊이

이 장의 원문 문서: chapters/ch02_architecture.md — CLOG·SUBTRANS·MultiXact 등 SLRU 구조, PGDATA 전체 트리, 설정 context 표가 담겨 있습니다.