PostgreSQL은 왜 shared_buffers를 25%만 권고하고, Aurora는 왜 75%일까?
PostgreSQL은 25%, Aurora는 75% — 같은 엔진인데 권고치가 정반대인 이유를, OS 페이지 캐시를 2차 캐시로 활용하는 PostgreSQL의 디자인 철학과 OS 캐시를 우회하도록 재설계된 Aurora의 분리 스토리지 구조에서 찾습니다.
들어가며
PostgreSQL 을 운영하다가 이런 의문을 가지게 되었습니다.
“Oracle 이나 MySQL 은 버퍼 캐시로 메모리의 60~70%를 잡으라고 하는데, 왜 PostgreSQL 공식 문서는 25%를 권고하지?
메모리가 남아도는데 왜 활용을 안 하는 거지?”“OS 캐시를 2차 캐시로 활용하는 것이 효율적이면 왜 다른 RDBMS 에서는 적용을 안하는거지?”
그리고 더 흥미로운 점은, AWS 의 Aurora PostgreSQL 은 바닐라 PostgreSQL 과 동일한 엔진을 쓰면서도 shared_buffers 를 전체 메모리의 약 75%(정확히는 SUM(DBInstanceClassMemory/10922), 대형 인스턴스 기준)로 설정한다는 것입니다. 같은 PostgreSQL 인데 왜 설정이 정반대일까요?
이 글에서는 PostgreSQL 의 shared_buffers 25% 권고가 나오게 된 아키텍처적 이유를 다른 RDBMS 와 비교하며 살펴보고, Aurora PostgreSQL 이 왜 정반대 방향(약 75%)으로 가는지를 공개된 아키텍처 정보를 바탕으로 추론해봅니다.
1. PostgreSQL 의 이중 캐싱 구조
PostgreSQL 의 25% 권고는 OS 버퍼 캐시를 2차 캐시로 적극 활용하는 아키텍처에서 나옵니다.
읽기 경로
1
2
shared_buffers →(miss)→ OS page cache →(miss)→ 물리 디스크
1차 캐시 2차 캐시
PostgreSQL 은 전통적으로 파일 I/O 에 O_DIRECT 를 사용하지 않습니다. read()/write() 시스템 콜을 사용하므로, 모든 I/O 가 커널의 page cache 를 통과합니다. 따라서 shared_buffers 에서 evict 된 페이지도 OS page cache 에 남아있으면 물리 I/O 없이 다시 읽힙니다.
PostgreSQL 16 부터 실험적인
io_direct파라미터가 도입되었고, 17/18 에서 비동기 I/O(AIO) 작업과 함께 direct I/O 경로가 계속 확장되고 있습니다. 다만 아직 프로덕션 기본값은 버퍼드 I/O 이며, 본문의 “25% 권고” 철학은 이 전통적 동작을 전제로 합니다. 향후 direct I/O 가 표준이 되면 권고치 자체가 재검토될 여지가 있습니다.
메모리 효율 비교 (32GB 서버 예시)
| 구성 | shared_buffers | OS page cache | 실질 총 캐시 |
|---|---|---|---|
| 25% 모델 | 8GB | ~20GB | ~28GB |
| 70% 모델 | 22GB | ~6GB | ~22GB (중복 낭비 포함) |
70%로 크게 잡아도 OS 가 쓸 메모리(커널, 페이지 캐시)에서 빼고 나면 page cache 가 매우 작아져서, 실질적인 총 캐시 용량이 오히려 줄어듭니다. 같은 페이지가 두 곳에 중복 적재되는 Double Buffering 문제까지 겹치면 낭비는 더 커집니다.
PostgreSQL 의 설계 철학은 간단합니다.
“OS 가 이미 LRU 기반으로 훌륭한 page cache 를 관리해준다. 우리는 그 위에 핫 데이터만 빠르게 잡고, 나머지는 OS 에 맡기자.”
2. 그럼 Oracle, MySQL 은 왜 60~70%를 권고할까? — O_DIRECT 가 핵심
Oracle 의 읽기 경로
1
2
3
4
Buffer Cache(SGA) →(miss)→ 물리 디스크
↑
OS page cache 완전 우회
(O_DIRECT + AIO)
Oracle 은 FILESYSTEMIO_OPTIONS=SETALL 로 data/redo 파일 모두에 O_DIRECT + 비동기 I/O 를 적용해 OS page cache 를 의도적으로 우회합니다. 2차 캐시가 없으므로 Buffer Cache 자체를 충분히 크게 잡아야 하고, 따라서 60~70% 권고가 필연적입니다.
MySQL/InnoDB 의 경우 — “부분적” O_DIRECT
MySQL 의 innodb_flush_method=O_DIRECT 설정은 Oracle 과 조금 다르다는 점을 짚어둘 필요가 있습니다.
| 파일 종류 | I/O 방식 |
|---|---|
| 데이터 파일 (ibd) | O_DIRECT (OS cache 우회) |
| Redo log (ib_logfile) | 버퍼드 I/O + fsync (OS cache 사용) |
데이터 파일은 O_DIRECT 로 buffer pool 과의 이중 캐싱을 제거하지만, redo log 는 여전히 버퍼드 I/O 로 쓰고 매 커밋마다 fsync() 로 내립니다. Redo log 는 순차 쓰기 패턴이고 매번 fsync 가 걸리므로 이중 캐싱 낭비가 거의 없고, 오히려 O_DIRECT 의 정렬(alignment) 제약이 까다롭기 때문입니다.
핵심은 동일합니다 — InnoDB 의 buffer pool 도 “OS cache 2차 캐시에 의존하지 않는” 구조이므로, buffer pool 을 메모리의 50~70%로 크게 잡는 것이 표준 튜닝이 됩니다.
“그럼 Oracle 도 O_DIRECT 끄고 25%로 하면 안 되냐?”
할 수는 있지만, Oracle 의 핵심 기능들이 이를 허용하지 않습니다. 많은 고민을 AI와 함께 진행하면서 크게 네 가지 구조적 이유를 도출하였습니다.
① Double Buffering 제거
Oracle 이 O_DIRECT 를 끄면 같은 페이지가 Buffer Cache 와 OS page cache 에 중복 적재됩니다. Oracle 은 이를 명백한 낭비로 보고 설계 단계에서 O_DIRECT 를 선택했습니다. PostgreSQL 은 반대로 이 “낭비”를 무료로 얻는 2차 캐시로 봅니다. 철학의 차이입니다.
② RAC(Real Application Clusters)
Oracle RAC 는 여러 서버가 동일 디스크를 공유하는 클러스터 아키텍처입니다. Buffer Cache 는 Cache Fusion 프로토콜로 노드 간 페이지를 주고받지만, OS page cache 는 서버 간에 공유할 수 없습니다. 더 나아가 한 노드가 OS page cache 에 오래된 페이지를 들고 있는 상태에서 다른 노드가 디스크의 최신 페이지를 읽으면 캐시 일관성이 깨집니다. RAC 는 태생적으로 O_DIRECT(또는 ASM/RAW device)를 전제로 설계됐습니다.
③ Write 타이밍의 예측 가능성
1
2
3
4
5
버퍼드 I/O 환경:
Write(data) → page cache → (커널이 언제 flush할지 모름) → fsync()로 강제 → 디스크
O_DIRECT 환경:
Write(data) → 디스크 컨트롤러 → fsync()로 플래터까지 확정 → 디스크
여기서 흔한 오해 하나 — O_DIRECT 자체는 durability 를 보장하지 않습니다. O_DIRECT 는 “커널 page cache 를 거치지 않는다”는 뜻이지 “스토리지의 플래터까지 내려갔다”는 뜻이 아닙니다. 스토리지 컨트롤러의 write cache 에 머물 수 있으므로 durability 를 위해서는 여전히 fsync()(또는 O_SYNC/O_DSYNC)가 필요합니다. Oracle 의 log writer 도 write 후 명시적으로 sync 를 호출합니다.
O_DIRECT 의 진짜 이점은 커널 writeback 타이밍의 불확실성을 제거하는 것입니다. 버퍼드 I/O 에서는 커널이 언제 flush 할지 예측하기 어렵고, 그 사이에 장애가 나면 순서가 꼬일 수 있습니다. O_DIRECT + fsync 조합은 write ordering 을 훨씬 예측 가능하게 만들어주고, 이는 redo log 기반 복구 메커니즘의 correctness 와 성능에 중요합니다.
3. PostgreSQL 코드 레벨에서 보는 설계 흔적
PostgreSQL 소스 src/backend/storage/buffer/bufmgr.c 를 보면, 버퍼 eviction 에 Clock-Sweep 알고리즘을 씁니다.
1
2
3
4
5
6
7
8
9
/* Clock sweep: usage count가 0인 첫 번째 버퍼 evict */
while (true) {
buf = &BufferDescriptors[ClockSweepTick];
if (pg_atomic_read_u32(&buf->state) & BM_LOCKED)
continue;
if (usage_count == 0)
break; /* evict 대상 */
usage_count--; /* 한 번 봤으니 카운트 감소 */
}
Oracle 의 멀티 LRU 체인(Hot/Warm/Cold)에 비해 단순합니다. 그 이유는 명확합니다. evict 된 후에도 OS page cache 가 받아주기 때문에 정교한 eviction 알고리즘의 필요성이 낮습니다.
또 하나 주목할 파라미터가 effective_cache_size 입니다. 이건 실제 메모리를 할당하는 게 아니라, 플래너에게 “OS cache 포함 총 가용 캐시 크기는 이만큼이다”를 알려주는 힌트입니다. 인덱스 스캔 vs 시퀀셜 스캔 비용 계산에 활용됩니다.
1
2
shared_buffers = 8GB (실제 PostgreSQL이 관리)
effective_cache_size = 24GB (OS cache 포함 추정치 → 플래너 힌트)
이 파라미터의 존재 자체가 “PostgreSQL 은 OS page cache 를 2차 캐시로 계산에 넣는다”는 설계 의도를 보여줍니다.
4. 정리: 아키텍처 선택의 차이
| 구분 | PostgreSQL | MySQL/InnoDB(O_DIRECT) | Oracle |
|---|---|---|---|
| 데이터 파일 I/O | read/write syscall (OS cache 통과) | O_DIRECT (OS cache 우회) | O_DIRECT + AIO |
| 로그 파일 I/O | read/write syscall + fsync | 버퍼드 + fsync | O_DIRECT + AIO |
| 캐시 레벨 | 2단계 (shared_buffers + OS page cache) | 데이터: 1단계 / 로그: OS cache 경유 | 1단계 (SGA만) |
| Double buffering | 허용 (2차 캐시로 활용) | 데이터에서 제거 | 전면 제거 |
| 권고 버퍼 비율 | 25% (나머지는 OS에 양보) | 50~70% | 60~70% |
| 클러스터 지원 | 없음 | 없음 | RAC (O_DIRECT/ASM 전제) |
PostgreSQL 의 25% 는 “OS 를 믿고 협력한다”는 설계 철학의 결과이고, Oracle/MySQL 의 60~70% 는 “우리가 직접 다 통제한다”는 설계 철학의 결과입니다. 어느 한쪽이 우월한 것이 아닙니다. RAC 같은 클러스터 기능, 예측 가능한 write 제어, Huge Pages 활용을 극대화하려면 O_DIRECT 가 필요하고, 그 선택이 60~70% 권고를 만든 것입니다.
참고로 PostgreSQL 도 매우 큰 서버(128GB+ RAM) 환경에서는 shared_buffers 를 40~50%까지 올리는 DBA 가 있습니다. OS page cache 의 이득이 줄어드는 지점을 넘어서는 경우입니다.
5. 그런데 Aurora PostgreSQL 은 왜 약 75%일까?
여기서부터가 흥미로운 부분입니다. Aurora PostgreSQL 은 같은 PostgreSQL 엔진을 쓰면서도 shared_buffers 를 대형 인스턴스 기준 약 75%로 설정합니다(공식: SUM(DBInstanceClassMemory/10922), 작은 인스턴스에서는 비율이 달라질 수 있습니다). 왜 정반대 방향으로 갔을까요?
AWS 는 Aurora 의 전체 내부 구현을 공개하지 않았지만, 공개된 공식 블로그, PostgreSQL 코드 구조를 결합하면 유력한 추론이 가능합니다.
핵심 전제: Aurora 는 PostgreSQL 의 스토리지 엔진을 교체했다
PostgreSQL 내부에는 smgr(Storage Manager)라는 추상화 레이어가 있습니다.
1
2
3
src/backend/storage/smgr/
├── smgr.c ← 추상 인터페이스 (smgrread, smgrwrite, smgrsync)
└── md.c ← 실제 구현: 로컬 파일시스템 I/O
바닐라 PostgreSQL 의 smgrread() 는 결국 md.c 를 거쳐 read() 시스템 콜을 호출하고, 커널 VFS 와 OS page cache 를 통과해 로컬 디스크에 도달합니다.
Aurora 는 md.c(로컬 파일시스템) 대신 자체 스토리지 드라이버로 교체했습니다. 이 교체가 모든 차이의 출발점입니다.
유력한 이유 ①: OS page cache 가 Aurora 에서는 쓸모가 없다
바닐라 PostgreSQL 의 더티 페이지 흐름은 이렇습니다.
1
2
3
4
shared_buffers에서 dirty page evict
→ OS page cache에 write (메모리에 남음)
→ 커널이 나중에 로컬 디스크로 flush
→ evict 이후에도 OS page cache에 남아 2차 캐시로 기능
Aurora PostgreSQL 은 전혀 다릅니다.
1
2
3
shared_buffers에서 dirty page evict
→ Aurora storage driver가 redo log record를 Aurora 스토리지 노드로 전송
→ 로컬 디스크에 data page 자체는 기록하지 않음
Aurora 의 유명한 설계 원칙 중 하나가 “The log is the database” 입니다. Aurora 는 data page 를 로컬 디스크에 쓰지 않고, WAL(redo log) 레코드만 Aurora 분산 스토리지로 전송합니다. 스토리지 노드가 이 로그를 적용해 페이지를 재구성합니다.
그 결과:
- OS page cache 가 캐시할 “로컬 파일”이 사실상 존재하지 않음
- OS page cache 는 Aurora data page 에 대해 사실상 비활성
- 2차 캐시가 사라짐 →
shared_buffers가 유일한 캐시 - 그러므로 크게 잡아야 함
이 구조는 사실상 Oracle 이 O_DIRECT 로 OS cache 를 제거한 것과 동일한 효과를 냅니다.
유력한 이유 ②: 네트워크 스토리지 = 캐시 미스 비용이 훨씬 크다
| 스토리지 종류 | 읽기 지연시간 |
|---|---|
| 로컬 NVMe SSD | ~0.1ms |
| EBS (네트워크 블록) | ~0.5~1ms |
| Aurora 분산 스토리지 (네트워크) | ~1~4ms |
Aurora 에서 캐시 미스가 발생하면 6개 스토리지 노드(3 AZ × 2)에 분산된 데이터를 네트워크로 읽어와야 합니다. 이 비용은 로컬 SSD 에 비해 훨씬 큽니다.
1
캐시 미스 비용 ↑ → 캐시 히트율을 높여야 함 → shared_buffers를 크게 잡아야 함
바닐라 PostgreSQL 이 25%에서도 잘 동작하는 이유는 OS page cache 가 0.1ms 로컬 SSD 의 miss 를 저렴하게 처리해주기 때문입니다. Aurora 에는 그 “저렴함”이 없습니다.
유력한 이유 ③: Double Buffering 문제 자체가 해소됨
바닐라 PostgreSQL 에서 shared_buffers 를 70%로 올리면:
1
2
3
shared_buffers(70%) + OS page cache(~20%)
→ 같은 페이지가 두 곳에 중복 적재
→ Double Buffering = 낭비
Aurora 에서 shared_buffers 를 70%로 올리면:
1
2
3
shared_buffers(70%) → 캐시로 사용
OS page cache(~20%) → Aurora data page를 캐시할 로컬 파일 없음 → 사실상 유휴
→ Double Buffering 없음 = 낭비 없음
바닐라에서 70%를 주면 손해지만, Aurora 에서는 70%를 줘도 손해가 없습니다.
유력한 이유 ④: Cluster Cache Management(CCM)
Aurora 는 Cluster Cache Management(CCM) 라는 공식 기능을 제공합니다(AWS 공식 블로그).
AWS 공식 설명에 따르면, 일반적인 Aurora PostgreSQL 은 약 35초 정도의 빠른 페일오버를 제공하지만, 새로 승격된 writer 인스턴스의 버퍼 캐시가 기존 primary 와 다를 수 있습니다. 리드 레플리카는 primary 와 정확히 같은 워크로드를 돌리지 않기 때문에, 승격 직후 캐시가 “Cold” 상태일 수 있고 애플리케이션이 평소 성능에 도달하는 데 시간이 걸립니다.
CCM 은 이 문제를 해결합니다. 동작 방식은 대략 이렇습니다.
- Read-only 노드(RO)가 자신의 버퍼 캐시에 있는 버퍼 주소들을 bloom filter 형태로 Read/Write 노드(RW)에 전송합니다(같은 주소를 반복 전송하지 않기 위함).
- RW 노드는 자신의 버퍼 캐시에서 자주 사용되는 버퍼(기본값: usage count > 3)를 RO 노드로 보냅니다.
- 페일오버 발생 시, 지정된 리더(tier-0)가 이미 워밍된 캐시 상태로 새 writer 로 승격됩니다.
AWS 가 공개한 pgbench 벤치마크 결과에 따르면, CCM 이 비활성화된 클러스터는 페일오버 후 평균 TPS 90 퍼센타일에 도달하는 데 약 357초가 더 걸렸습니다.
여기서 주목할 점은 이런 기능이 존재한다는 사실 자체입니다. OS page cache 에 의존하는 아키텍처라면 이런 기능을 만들 이유가 없습니다. 페일오버해도 새 노드의 OS page cache 가 어느 정도 잡혀 있을 가능성이 있기 때문입니다. CCM 의 존재는 Aurora 가 shared_buffers 를 유일하고 핵심적인 캐시로 취급하고 있음을 역으로 보여주는 증거입니다.
CCM 은 apg_ccm_enabled 클러스터 파라미터를 1로 설정하고 writer/reader 인스턴스의 promotion tier 를 0으로 지정하면 활성화됩니다. 활성화 여부는 다음 쿼리로 확인할 수 있습니다.
1
SELECT * FROM aurora_ccm_status();
6. 정리: 바닐라 vs Aurora 구조 비교
바닐라 PostgreSQL 의 읽기 계층:
1
2
shared_buffers (25%) → miss → OS page cache (50%) → miss → 로컬 디스크 (0.1ms)
1차 캐시 2차 캐시 (무료) 저렴한 fallback
Aurora PostgreSQL 의 읽기 계층:
1
2
shared_buffers (~75%) → miss → Aurora 분산 스토리지 (1~4ms, 네트워크)
유일한 캐시 비싼 fallback (OS page cache 없음)
| 비교 항목 | Vanilla PostgreSQL | Aurora PostgreSQL |
|---|---|---|
| 스토리지 I/O 경로 | read() → OS page cache → 로컬 디스크 | Aurora smgr driver → 네트워크 스토리지 노드 |
| OS page cache 역할 | 핵심 2차 캐시 | Data page 에 대해 사실상 비활성 |
| Data page 로컬 기록 | O (checkpoint 시) | X (redo log record 만 전송) |
| Double Buffering | 존재 (25%가 최적인 이유) | 없음 (70%를 줘도 낭비 없음) |
| 캐시 미스 비용 | 낮음 (로컬 SSD) | 높음 (네트워크 RTT) |
| 페일오버 캐시 워밍 | OS cache 에 간접 의존 | CCM 공식 지원 |
| 권고 shared_buffers | 25% | 약 75% (대형 인스턴스 기본값) |
마치며
정리하면 이렇습니다.
- PostgreSQL 이 25%를 권고하는 이유는 OS page cache 를 공짜 2차 캐시로 활용하는 설계 철학 때문입니다.
shared_buffers를 키우면 오히려 OS 가 쓸 메모리가 줄고, Double Buffering 으로 낭비가 생깁니다. (단, PG 16+ 에서 실험적 direct I/O 가 도입 중이므로 장기적으로는 이 권고치가 재검토될 수 있습니다.) - Oracle/MySQL 이 60~70%를 권고하는 이유는
O_DIRECT로 OS cache 를 의도적으로 우회하기 때문입니다. 다만 둘의 구현 강도는 다릅니다 — Oracle 은 data/redo 전면 O_DIRECT, MySQL InnoDB 는 데이터 파일만 O_DIRECT 이고 redo log 는 버퍼드 + fsync 입니다. RAC, 예측 가능한 write 제어, Huge Pages 활용 같은 요구사항이 이 선택을 강제합니다. - Aurora PostgreSQL 이 약 75%를 설정하는 이유는 ① data page 를 로컬에 쓰지 않는 아키텍처로 OS page cache 의 2차 캐시 역할이 소멸했고, ② 네트워크 스토리지의 높은 접근 비용 때문에 캐시 미스를 줄이는 것이 훨씬 중요해졌기 때문으로 추론됩니다. Oracle 이
O_DIRECT로 OS cache 를 제거하고 70%를 권고하는 것과 구조적으로 동일한 논리입니다.
DBA 입장에서 이 이야기의 실무적 함의는 분명합니다. “메모리의 몇 %를 버퍼에 주느냐”는 튜닝 숫자가 아니라, 그 DBMS 가 OS·스토리지와 어떻게 협력하도록 설계됐는지를 보여주는 지표입니다. 같은 PostgreSQL 이라도 바닐라와 Aurora 가 다른 숫자를 쓰는 이유가 여기에 있습니다.
shared_buffers권고치는 튜닝 숫자가 아니라 그 DBMS 가 OS·스토리지와 어떻게 협력하도록 설계됐는지를 보여주는 지표입니다. 값을 조정하기 전에 내 환경의 스토리지 스택부터 점검해 보세요.
참고 자료
- PostgreSQL 공식 문서 — Server Configuration: Memory
- PostgreSQL 공식 문서 —
io_direct파라미터 (PG 16+) - PostgreSQL 소스코드 —
src/backend/storage/buffer/bufmgr.c,src/backend/storage/smgr/ - MySQL 공식 문서 —
innodb_flush_method - Oracle 공식 문서 —
FILESYSTEMIO_OPTIONS - Amazon Web Services — Introduction to Aurora PostgreSQL cluster cache management
- Verbitski et al., “Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases”, SIGMOD 2017
- Verbitski et al., “Amazon Aurora: On Avoiding Distributed Consensus for I/Os, Commits, and Membership Changes”, SIGMOD 2018