4.1 버퍼 매니저
PostgreSQL의 buffer manager는 디스크 페이지와 backend 사이에 있는 캐시입니다. shared_buffers 영역에 8KB 페이지를 담아 두고, backend가 디스크 대신 메모리에서 페이지를 읽을 수 있게 합니다. 페이지를 어떻게 가져오고, 어떻게 내보내며, dirty page는 어떻게 디스크로 돌려놓는지가 본 절의 주제입니다.
버퍼 매니저 구조
flowchart LR
BE["backend"]
HASH["buffer hash table<br/>(BufferTag → Buffer ID)"]
POOL["buffer pool<br/>shared_buffers<br/>(N개 page slot)"]
DESC["buffer descriptors<br/>(refcount, usage_count, dirty, ...)"]
DISK["disk<br/>(PGDATA)"]
BE -- "원하는 페이지(BufferTag)" --> HASH
HASH -- "Buffer ID" --> DESC
DESC -- "ref/pin" --> POOL
POOL <-- "read/write 8KB" --> DISK
classDef be fill:#fef3c7,stroke:#b45309,color:#78350f
classDef cache fill:#ede9fe,stroke:#6d28d9,color:#3b0764
classDef disk fill:#d1fae5,stroke:#047857,color:#064e3b
class BE be
class HASH,POOL,DESC cache
class DISK disk
shared_buffers는 N개의 8KB 슬롯- 페이지 식별자(
BufferTag=tablespace, db, relfilenode, fork, blocknum)를 hash table로 슬롯에 매핑 - 각 슬롯에 descriptor가 붙어 pin/usage count/dirty 비트를 관리
페이지 요청 흐름
flowchart TD
Q["backend가 페이지 P 요청"]
HIT{"hash table에 P 존재?"}
RETURN["pin 증가 → 반환"]
EVICT["victim 슬롯 선택<br/>(clock-sweep)"]
DIRTYCHK{"victim이 dirty?"}
WRITE["fsync 동반 디스크 쓰기"]
READ["디스크에서 P 읽기"]
INSTALL["슬롯에 페이지 설치 + hash table 등록"]
Q --> HIT
HIT -- "yes (cache hit)" --> RETURN
HIT -- "no (cache miss)" --> EVICT
EVICT --> DIRTYCHK
DIRTYCHK -- "yes" --> WRITE --> READ
DIRTYCHK -- "no" --> READ
READ --> INSTALL --> RETURN
classDef ok fill:#d1fae5,stroke:#047857,color:#064e3b
classDef warn fill:#fed7aa,stroke:#c2410c,color:#7c2d12
classDef proc fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
class RETURN ok
class WRITE,READ,EVICT,INSTALL proc
class HIT,DIRTYCHK warn
Clock-sweep 알고리즘
victim 페이지 선택은 LRU가 아니라 clock-sweep입니다.
| 필드 | 의미 |
|---|---|
usage_count | 0~5 사이 정수. 페이지를 만질 때마다 증가, sweep 시 감소 |
refcount (pin) | 현재 누가 들고 있는지 — 0이면 evict 후보 |
알고리즘:
- clock hand가 슬롯을 순회
refcount > 0이면 건너뜀usage_count > 0이면 1 감소 후 다음 슬롯usage_count = 0이면 그 슬롯이 victim
자주 쓰는 페이지(hot page)는 usage_count가 4~5에 머물러 잘 evict되지 않습니다. 한 번만 쓰인 페이지(scan)는 빠르게 0으로 떨어집니다.
Buffer Access Strategy — scan 보호
shared_buffers를 다 채우는 대용량 SELECT/COPY가 hot page를 쓸어내지 않도록, PostgreSQL은 Strategy 개념을 도입했습니다.
| Strategy | 사용처 | 동작 |
|---|---|---|
| Bulk read | seq scan > shared_buffers/4 | 작은 ring buffer(256KB)에서 재활용 — pool 오염 방지 |
| Bulk write | COPY FROM, CREATE TABLE AS | 16MB ring buffer |
| VACUUM | VACUUM | 256KB ring buffer |
운영 의미: 큰 테이블을 한 번 풀스캔해도 shared_buffers의 hot page는 살아남는다. 단, 그 큰 테이블이 자주 스캔되는 경우 hot으로 인정받지 못해 매번 디스크에서 다시 읽는 일이 생긴다 — 인덱스를 만드는 게 답입니다.
Dirty page를 디스크로
shared_buffers의 dirty page를 디스크에 보내는 주체는 세 가지.
| 주체 | 트리거 | 효과 |
|---|---|---|
| backend | victim 슬롯이 dirty인데 강제로 써야 할 때 | 쿼리 latency에 직접 영향 |
| background writer | 일정 간격(bgwriter_delay 기본 200ms)으로 LRU 가장자리 dirty page 점진적 flush | 쿼리 latency 보호 |
| checkpointer | 체크포인트 시점에 한꺼번에 flush | I/O burst, 다음 절에서 자세히 |
background writer가 충분히 일을 하지 않으면 backend가 직접 dirty page를 쓰게 되고, 그 backend의 SELECT가 갑자기 느려집니다. 관련 통계:
SELECT * FROM pg_stat_bgwriter;| 컬럼 | 의미 |
|---|---|
buffers_clean | background writer가 쓴 페이지 수 |
buffers_backend | backend가 직접 쓴 페이지 수 — 이게 크면 bgwriter가 부족 |
buffers_checkpoint | checkpointer가 쓴 페이지 수 |
maxwritten_clean | bgwriter가 한 라운드에서 최대 한도까지 쓴 횟수 — 한도 초과 신호 |
(PG 16+에서는 pg_stat_io 뷰가 더 세밀한 통계를 제공합니다.)
튜닝 파라미터
| 파라미터 | 기본 | 의미 |
|---|---|---|
shared_buffers | 128MB | 버퍼 풀 크기. 1.4 참고 |
bgwriter_delay | 200ms | bgwriter 라운드 간격 |
bgwriter_lru_maxpages | 100 | 한 라운드에서 쓸 최대 페이지 |
bgwriter_lru_multiplier | 2.0 | 다음 라운드 쓰기 예측 계수 |
vacuum_buffer_usage_limit | 2MB | VACUUM의 ring buffer 크기 (PG 16+) |
temp_buffers | 8MB | 세션 임시 테이블용 별도 풀 |
bgwriter는 보수적인 기본값을 갖습니다. 쓰기 부하가 높은 시스템에서는:
bgwriter_delay = 50ms
bgwriter_lru_maxpages = 1000
bgwriter_lru_multiplier = 4.0정도로 키워 backend 직접 쓰기 비중을 줄입니다.
pg_buffercache — 풀 내부 들여다보기
CREATE EXTENSION pg_buffercache;
-- 어떤 relation이 풀을 가장 많이 차지하는가
SELECT c.relname,
pg_size_pretty(count(*) * 8192) AS in_pool
FROM pg_buffercache b
JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid)
GROUP BY c.relname
ORDER BY count(*) DESC
LIMIT 10;
-- hot page 분포 (usage_count 6이 가장 hot)
SELECT usagecount, count(*)
FROM pg_buffercache
GROUP BY usagecount
ORDER BY usagecount;용량 계획·튜닝 시에 가장 유용한 진단 도구입니다.
Direct I/O와 비동기 I/O (PG 18)
PostgreSQL 18부터 io_method 파라미터로 비동기 I/O를 활성화할 수 있습니다.
io_method | 동작 |
|---|---|
sync (기본) | 전통적인 동기 read/write |
worker | 별도 worker 프로세스가 비동기 read 수행 |
io_uring (Linux) | 커널의 io_uring으로 비동기 read 발급 |
prefetch와 결합해 인덱스 스캔·시퀀셜 스캔 latency가 개선됩니다. 단, 검증 시간이 짧아 운영 도입은 점진적.
O_DIRECT는 PG가 표준으로 안 쓴다 — fsync 효율과 안정성 트레이드오프를 OS에 맡깁니다.정리
- buffer manager는 8KB 페이지를
shared_buffers에 캐시 - 페이지 찾기: hash table → buffer descriptor → pool slot
- victim 선택은 clock-sweep — usage_count + pin count
- 큰 스캔은 ring buffer로 hot page 보호
- dirty page는 backend·bgwriter·checkpointer가 협력해 디스크에 flush
pg_stat_bgwriter·pg_stat_io·pg_buffercache로 진단- PG 18 비동기 I/O는 점진적 도입
다음 절(4.2)에서는 페이지 안정성을 책임지는 WAL과 체크포인트를 봅니다.