본문으로 건너뛰기

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_count0~5 사이 정수. 페이지를 만질 때마다 증가, sweep 시 감소
refcount (pin)현재 누가 들고 있는지 — 0이면 evict 후보

알고리즘:

  1. clock hand가 슬롯을 순회
  2. refcount > 0이면 건너뜀
  3. usage_count > 0이면 1 감소 후 다음 슬롯
  4. 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 readseq scan > shared_buffers/4작은 ring buffer(256KB)에서 재활용 — pool 오염 방지
Bulk writeCOPY FROM, CREATE TABLE AS16MB ring buffer
VACUUMVACUUM256KB ring buffer

운영 의미: 큰 테이블을 한 번 풀스캔해도 shared_buffers의 hot page는 살아남는다. 단, 그 큰 테이블이 자주 스캔되는 경우 hot으로 인정받지 못해 매번 디스크에서 다시 읽는 일이 생긴다 — 인덱스를 만드는 게 답입니다.

Dirty page를 디스크로

shared_buffers의 dirty page를 디스크에 보내는 주체는 세 가지.

주체트리거효과
backendvictim 슬롯이 dirty인데 강제로 써야 할 때쿼리 latency에 직접 영향
background writer일정 간격(bgwriter_delay 기본 200ms)으로 LRU 가장자리 dirty page 점진적 flush쿼리 latency 보호
checkpointer체크포인트 시점에 한꺼번에 flushI/O burst, 다음 절에서 자세히

background writer가 충분히 일을 하지 않으면 backend가 직접 dirty page를 쓰게 되고, 그 backend의 SELECT가 갑자기 느려집니다. 관련 통계:

SELECT * FROM pg_stat_bgwriter;
컬럼의미
buffers_cleanbackground writer가 쓴 페이지 수
buffers_backendbackend가 직접 쓴 페이지 수 — 이게 크면 bgwriter가 부족
buffers_checkpointcheckpointer가 쓴 페이지 수
maxwritten_cleanbgwriter가 한 라운드에서 최대 한도까지 쓴 횟수 — 한도 초과 신호

(PG 16+에서는 pg_stat_io 뷰가 더 세밀한 통계를 제공합니다.)

튜닝 파라미터

파라미터기본의미
shared_buffers128MB버퍼 풀 크기. 1.4 참고
bgwriter_delay200msbgwriter 라운드 간격
bgwriter_lru_maxpages100한 라운드에서 쓸 최대 페이지
bgwriter_lru_multiplier2.0다음 라운드 쓰기 예측 계수
vacuum_buffer_usage_limit2MBVACUUM의 ring buffer 크기 (PG 16+)
temp_buffers8MB세션 임시 테이블용 별도 풀

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가 개선됩니다. 단, 검증 시간이 짧아 운영 도입은 점진적.

OS 페이지 캐시는 여전히 한 층 더 있다 (1.4 참고). PostgreSQL이 디스크에 쓰는 것 같아도 OS가 또 캐싱합니다. 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과 체크포인트를 봅니다.