Linux Page Cache - 백엔드 개발자의 시선

에이치비아이·2026년 1월 31일

들어가며

백엔드 개발자로서 Kafka, Elasticsearch 같은 시스템을 사용하다 보면 "Page Cache 덕분에 빠르다"는 설명을 종종 접하게 됩니다. 하지만 정작 Page Cache가 무엇인지, 어떻게 동작하는지는 잘 모르는 경우가 많습니다.

이 글에서는 Linux Page Cache의 동작 원리부터 Dirty Page 개념, 그리고 이를 적극 활용하는 소프트웨어들까지 살펴보겠습니다.

Page Cache란?

Page Cache는 Linux 커널이 디스크 I/O 성능을 향상시키기 위해 사용하는 메모리 캐싱 메커니즘입니다. 디스크에서 읽은 데이터를 RAM에 저장해두어 반복적인 디스크 접근을 줄입니다.

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ Application │ ──> │ Page Cache  │ ──> │    Disk     │
│             │ <── │   (RAM)     │ <── │             │
└─────────────┘     └─────────────┘     └─────────────┘

주요 특징

특징설명
투명성애플리케이션이 인식하지 못하게 자동 동작
Write-back쓰기 시 즉시 디스크에 기록하지 않음
LRU 기반최근 사용되지 않은 페이지부터 제거
동적 크기사용 가능한 메모리에 따라 자동 조절

읽기 동작

  1. 애플리케이션이 파일 읽기 요청
  2. 커널이 Page Cache 확인
  3. Cache Hit: 메모리에서 즉시 반환
  4. Cache Miss: 디스크에서 읽어 Page Cache에 저장 후 반환

쓰기 동작

  1. 데이터를 Page Cache에 먼저 기록
  2. 커널의 flush 데몬이 비동기적으로 디스크에 기록
  3. sync 명령 또는 fsync() 호출 시 즉시 디스크에 기록

여기서 중요한 점은 write() 호출이 반환되었다고 해서 데이터가 디스크에 기록된 것이 아니라는 것입니다.

관련 명령어

# 메모리 사용량 확인 (buff/cache 항목이 Page Cache)
free -h

# Page Cache 상세 정보
cat /proc/meminfo | grep -E "Cached|Buffers|Dirty"

# Page Cache 수동 정리
echo 1 > /proc/sys/vm/drop_caches  # pagecache만
echo 2 > /proc/sys/vm/drop_caches  # dentries, inodes
echo 3 > /proc/sys/vm/drop_caches  # 전체

Dirty Page 이해하기

Dirty Page는 Page Cache에 있는 데이터 중 수정되었지만 아직 디스크에 기록되지 않은 페이지입니다.

Clean Page: 메모리 내용 = 디스크 내용 (안전)
Dirty Page: 메모리 내용 ≠ 디스크 내용 (유실 위험)

상태 변화

┌────────────────┐    write()   ┌────────────────┐    flush     ┌────────────────┐
│   Clean Page   │ ───────────> │   Dirty Page   │ ──────────>  │   Clean Page   │
│  (Disk=Memory) │              │  (Disk≠Memory) │              │  (Disk=Memory) │
└────────────────┘              └────────────────┘              └────────────────┘

Dirty Page가 디스크에 기록되는 시점

조건설명
dirty_expire_centisecs 초과기본 30초 이상 dirty 상태 유지 시
dirty_background_ratio 초과메모리의 10% 이상이 dirty일 때 (비동기)
dirty_ratio 초과메모리의 20% 이상이 dirty일 때 (동기, 블로킹)
sync/fsync() 호출명시적 동기화 요청 시
메모리 부족새 페이지 할당 필요 시

튜닝 파라미터

# dirty page가 디스크에 기록되기 시작하는 비율 (기본: 10%)
/proc/sys/vm/dirty_background_ratio

# dirty page 최대 비율 - 초과 시 동기 쓰기 (기본: 20%)
/proc/sys/vm/dirty_ratio

# dirty page 유지 최대 시간 (기본: 30초)
/proc/sys/vm/dirty_expire_centisecs

확인 방법

# 현재 dirty page 크기
cat /proc/meminfo | grep Dirty

# 실시간 모니터링
watch -n 1 'grep -E "Dirty|Writeback" /proc/meminfo'

위험성

전원 장애 발생 시:
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│     App     │     │ Page Cache  │  X  │    Disk     │
│   저장 완료   │     │ Dirty Page  │ ──> │  기록 안됨     │
└─────────────┘     └─────────────┘     └─────────────┘
                         ↓
                    데이터 유실!

데이터 안전성 확보

// 방법 1: fsync - 특정 파일만 동기화
write(fd, data, size);
fsync(fd);  // 디스크 기록 완료까지 대기

// 방법 2: O_SYNC 플래그 - 매 write마다 동기화
open("file", O_WRONLY | O_SYNC);

// 방법 3: O_DIRECT - Page Cache 우회
open("file", O_WRONLY | O_DIRECT);

Page Cache를 적극 활용하는 소프트웨어

Apache Kafka

Kafka는 자체 버퍼 풀을 만들지 않고 OS Page Cache에 전적으로 의존합니다.

Producer → Broker(Page Cache) → Consumer
              ↓ (비동기)
            Disk

왜 가능한가:

  • 메시지가 순차적으로 append됨 (Sequential I/O)
  • Consumer가 최신 데이터를 읽을 때 이미 Page Cache에 있음
  • Zero-copy(sendfile())로 Page Cache에서 Network로 직접 전송
  • JVM Heap 대신 OS 메모리 활용으로 GC 부담 없음

sendfile()이란?
sendfile()은 커널 내에서 파일 데이터를 소켓으로 직접 전송하는 시스템 콜입니다. 일반적인 파일 전송은 Disk → Page Cache → User Buffer → Socket Buffer → NIC 경로로 여러 번 복사가 발생하지만, sendfile()은 User Buffer를 거치지 않고 Page Cache에서 Socket Buffer로 직접 전달합니다. 이를 Zero-copy라고 부르며, CPU 복사 비용을 제거하여 처리량을 높입니다.

Elasticsearch / Lucene

Lucene 세그먼트 파일을 mmap으로 매핑하여 OS가 알아서 캐싱하게 합니다.

mmap이란?
mmap()은 파일을 프로세스의 가상 메모리 주소 공간에 직접 매핑하는 시스템 콜입니다. 일반적인 read() 호출은 Disk → Page Cache → User Buffer로 데이터를 복사하지만, mmap은 Page Cache를 가상 메모리에 직접 매핑하여 복사 없이 배열처럼 접근할 수 있습니다. 파일 전체를 메모리에 올리지 않고 접근한 부분만 Page Fault를 통해 로드(Lazy Loading)되므로 대용량 인덱스 파일에 효율적입니다.

RocksDB (LSM-Tree 기반)

Kafka Streams, Flink, TiKV 등에서 사용하는 LSM-Tree 기반 스토리지입니다. 읽기에 mmap을 활용하여 Page Cache 이점을 얻습니다.

Write → MemTable → SST Files (Page Cache) → Disk

사용처:

  • Kafka Streams 상태 저장소
  • Flink 체크포인트
  • TiKV (TiDB 스토리지)
  • MySQL MyRocks

Redis (RDB/AOF 저장 시)

메모리 데이터 → fork() → 자식 프로세스가 RDB 저장
                              ↓
                         Page Cache → Disk

Copy-on-Write를 활용하여 fork() 시 부모/자식이 Page Cache를 공유합니다.

fork()란?
fork()는 현재 프로세스를 복제하여 자식 프로세스를 생성하는 시스템 콜입니다. Redis는 RDB 스냅샷 저장 시 fork()로 자식 프로세스를 만들고, 자식이 디스크에 데이터를 쓰는 동안 부모는 계속 클라이언트 요청을 처리합니다. 이때 Copy-on-Write(COW) 방식으로 부모/자식이 메모리 페이지를 공유하다가, 부모가 데이터를 수정할 때만 해당 페이지를 복사합니다. 덕분에 대용량 데이터도 빠르게 fork하고, 메모리 사용량도 최소화할 수 있습니다.

Nginx / 정적 파일 서버

# sendfile로 Page Cache → Socket 직접 전송
sendfile on;
tcp_nopush on;

# 자주 접근하는 파일 FD 캐시
open_file_cache max=1000 inactive=60s;

전략 비교

소프트웨어전략이유
Kafka전적으로 의존Sequential I/O, Zero-copy
Elasticsearch적극 활용 (mmap)읽기 중심, 인덱스 캐싱
RocksDB부분 활용LSM-Tree 특성상 순차 쓰기
PostgreSQL이중 캐싱Shared Buffer + OS Cache
MySQL InnoDB우회 (O_DIRECT)자체 Buffer Pool로 세밀한 제어
Oracle우회자체 SGA 버퍼

정리

Page Cache가 효과적인 경우

  • 순차적 읽기/쓰기 (Sequential I/O)
  • 읽기 중심 워크로드
  • 최근 데이터 재접근이 많은 경우
  • Zero-copy를 활용한 네트워크 전송

핵심 포인트

대부분의 경우 OS가 알아서 처리하므로 직접 신경 쓸 일은 거의 없습니다. 다만 Kafka, Elasticsearch 같은 소프트웨어를 사용할 때 "왜 이렇게 빠르지?", "메모리 설정은 왜 이렇게 권장하지?"라는 의문이 생긴다면, Page Cache가 어떻게 활용되고 있는지 살펴보면 좋습니다. 내부 동작 원리를 이해하면 성능 튜닝이나 장애 대응 시 더 나은 판단을 내릴 수 있습니다.


참고자료

profile
백엔드 개발자입니다.

0개의 댓글