설치·접속: PostgreSQL 설치와 접속
부제: "서버 재시작이나 failover 직후 첫 트래픽이 유독 느릴 때" — shared_buffers가 텅 비어 첫 쿼리들이 전부 디스크를 친다. 지금 캐시에 뭐가 들었는지 실측하고, 핵심 테이블을 미리 데운다.
재시작하면 shared_buffers가 비워진다(콜드스타트). 첫 쿼리들은 캐시 미스로 전부 디스크를 읽어 평소보다 몇 배 느리다. pg_buffercache는 지금 공유 버퍼 안에 무엇이 얼마나 들었는지 실시간으로 들여다보고, pg_prewarm은 특정 릴레이션을 디스크에서 버퍼로 미리 끌어올린다. "추측하지 말고 보고, 보고 나서 데운다"가 흐름이다.
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
1단계 — pg_buffercache로 누가 shared_buffers를 점유하는지 본다
pg_buffercache 뷰는 버퍼 슬롯 하나가 한 행이다. relfilenode로 pg_class와 이으면 릴레이션별로 몇 개의 버퍼(= 8KB 페이지)를 차지하는지 집계할 수 있다. 아래는 예제 테이블 두 개(48MB짜리 hot, 17MB짜리 events)를 데운 직후의 점유 현황이다.
SELECT c.relname,
count(*) AS buffers,
pg_size_pretty(count(*) * 8192) AS cached,
pg_size_pretty(pg_relation_size(c.oid)) AS on_disk
FROM pg_buffercache b
JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid)
WHERE c.relname IN ('hot','events')
GROUP BY c.relname, c.oid
ORDER BY buffers DESC;
relname | buffers | cached | on_disk
-----------+---------+--------+---------
hot | 6144 | 48 MB | 48 MB
events | 2176 | 17 MB | 17 MBcached와 on_disk가 같다는 건 테이블 전체가 메모리에 올라와 있다는 뜻이다. "메모리가 부족한 것 같다"를 추측이 아니라 이렇게 숫자로 확인한다. WHERE 절을 빼고 ORDER BY buffers DESC LIMIT 10으로 돌리면 서버 전체에서 캐시를 가장 많이 먹는 상위 릴레이션을 뽑을 수 있다.
2단계 — 콜드 상태를 확인하고 pg_prewarm으로 데운다
재시작을 흉내 내려고 테이블을 새로 만들었다. CREATE TABLE AS 같은 대량 적재는 16MB짜리 전용 링 버퍼만 쓰기 때문에, 48MB 테이블을 만들어도 shared_buffers에는 일부(약 2080블록 = 16MB어치)만 남는다. 딱 재시작 직후의 "반쯤 식은" 상태다.
-- (준비) 48MB = 6144블록짜리 테이블. 대량 적재라 일부만 캐시에 남는다
CREATE TABLE hot AS
SELECT g AS id, md5(g::text) AS h1, md5((g+1)::text) AS h2, repeat('x', 60) AS filler
FROM generate_series(1, 300000) g;
ANALYZE hot;
SELECT pg_relation_size('hot') / 8192 AS heap_blocks;
-- BEFORE: 6144블록 중 몇 개나 캐시에 있나
SELECT count(*) AS before_cached
FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid)
WHERE c.relname = 'hot';
-- 데운다: 모든 블록을 shared_buffers로 끌어올리고, 적재한 블록 수를 반환
SELECT pg_prewarm('hot') AS blocks_loaded;
-- AFTER: 전부 올라왔나
SELECT count(*) AS after_cached
FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid)
WHERE c.relname = 'hot';
heap_blocks
-------------
6144
before_cached
---------------
2080
blocks_loaded
---------------
6144
after_cached
--------------
61446144블록 중 2080개만 캐시에 있던 콜드 상태(약 34%)가, pg_prewarm('hot') 한 번에 6144개 전부 올라왔다. 함수의 반환값 6144는 방금 적재한 블록 수다. 재시작 후 이 한 줄이면 첫 쿼리부터 캐시 히트로 돈다. 기본은 buffer 모드(PostgreSQL 공유 버퍼에 적재)이고, 'prefetch' 모드는 OS에 비동기 프리페치만 요청해 더 가볍다.
3단계 — autoprewarm으로 재시작을 관통해 자동 복원한다
매번 손으로 데울 순 없다. pg_prewarm을 shared_preload_libraries에 등록하면 autoprewarm 백그라운드 워커가 뜬다. 이 워커는 주기적으로(그리고 정상 종료 직전에) 현재 shared_buffers에 어떤 블록이 들었는지 목록을 autoprewarm.blocks 파일로 덤프해 둔다. 재시작하면 워커가 그 파일을 읽어 종료 시점의 캐시를 그대로 복원한다. 사람이 어떤 테이블을 데울지 고를 필요조차 없다.
# postgresql.conf — autoprewarm 워커를 띄우려면 preload 필요(재시작)
shared_preload_libraries = 'pg_prewarm'
pg_prewarm.autoprewarm = on # 기본 on
pg_prewarm.autoprewarm_interval = 300s # 덤프 주기(0이면 종료 시에만)-- 지금 즉시 스냅샷을 뜨고 싶으면 수동 호출
SELECT autoprewarm_dump_now();
정리하면, 콜드스타트 대응은 두 갈래다. 일회성(방금 만든 테이블, 배치 직전 특정 테이블)은 pg_prewarm()으로 그때그때 데우고, 재시작/failover를 상시 대비하려면 autoprewarm 워커에 맡긴다. 어느 쪽이든 시작은 pg_buffercache로 "지금 뭐가 비어 있나"를 눈으로 확인하는 것이다.
이렇게도 쓴다
전체 버퍼 풀의 사용/여유/더티 요약을 뷰 집계 없이 가볍게 한 줄로 본다. (조합: pg_buffercache_summary)
SELECT * FROM pg_buffercache_summary();
-- buffers_used | buffers_unused | buffers_dirty | buffers_pinned | usagecount_avg
usagecount(교체 우선순위) 분포를 봐서 "자주 쓰여 안 밀려나는 버퍼"가 얼마나 되는지 본다. (조합: pg_buffercache_usage_counts)
SELECT * FROM pg_buffercache_usage_counts();
인덱스도 데운다. 조회는 결국 인덱스부터 타므로 핵심 인덱스를 함께 올린다.
SELECT pg_prewarm('events_user_idx');
공유 버퍼가 아니라 OS 페이지 캐시까지만 채우는 가벼운 프리페치. shared_buffers를 밀어내지 않는다.
SELECT pg_prewarm('hot', 'prefetch');
큰 테이블의 앞부분(최근·핫 세그먼트)만 골라 데운다. (조합: 블록 범위 지정)
SELECT pg_prewarm('hot', 'buffer', 'main', 0, 1000); -- 0~1000블록만
언제 다른 도구로 가나: 데워도 안 빨라진다면 캐시 문제가 아니라 플랜 문제일 수 있다. 그때는 pg_stat_statements로 무거운 쿼리를 랭킹하고 auto_explain으로 플랜을 본다. 무작정 다 데우는 것도 답이 아니다 — shared_buffers보다 큰 테이블을 prewarm하면 정작 필요한 다른 페이지를 밀어낸다.