Page Cache 활용

Kafka는 OS의 Page Cache를 적극 활용하여 높은 처리량과 낮은 지연시간을 달성합니다.

Page Cache 개념

정의

Page Cache는 운영체제가 디스크 I/O를 최적화하기 위해 메모리에 유지하는 캐시입니다.

┌─────────────────────────────────────────────────────────────────┐
│                      Memory Architecture                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────┐                                            │
│  │  Application    │ ← Kafka Broker (JVM Heap)                  │
│  │     Memory      │                                            │
│  └────────┬────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  ┌─────────────────┐                                            │
│  │   Page Cache    │ ← OS가 관리하는 파일 캐시                    │
│  │  (OS Memory)    │   디스크 데이터의 메모리 복사본              │
│  └────────┬────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  ┌─────────────────┐                                            │
│  │      Disk       │ ← 실제 저장소                               │
│  │   (.log files)  │                                            │
│  └─────────────────┘                                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Kafka의 Page Cache 활용 전략

전통적인 메시징 시스템:
┌───────────┐    ┌───────────┐    ┌───────────┐
│  Produce  │───>│ In-Memory │───>│   Disk    │
│           │    │   Queue   │    │  (백업)    │
└───────────┘    └───────────┘    └───────────┘
                       ↓
               JVM 힙 메모리 사용
               GC 오버헤드 발생

Kafka:
┌───────────┐    ┌───────────┐    ┌───────────┐
│  Produce  │───>│Page Cache │───>│   Disk    │
│           │    │ (OS 관리)  │    │ (log file)│
└───────────┘    └───────────┘    └───────────┘
                       ↓
               OS가 캐시 관리
               JVM GC 영향 없음

쓰기 경로 (Write Path)

Producer → Broker

1. Producer가 메시지 전송
         ↓
2. Broker가 메시지 수신
         ↓
3. write() 시스템 콜
         ↓
4. 커널이 Page Cache에 쓰기 (메모리)
         ↓
5. 즉시 ACK 반환 (async)
         ↓
6. OS가 백그라운드에서 디스크 flush

Write-behind 캐싱

// Page Cache → Disk flush 간격
// OS 커널 파라미터로 제어
vm.dirty_ratio = 20           // 전체 메모리의 20%가 dirty일 때 flush
vm.dirty_background_ratio = 5  // 5%가 dirty일 때 백그라운드 flush 시작
vm.dirty_expire_centisecs = 3000  // 30초 이상 된 dirty 페이지 flush

Kafka 설정

// 메시지를 디스크에 동기화하는 주기
log.flush.interval.messages = 10000  // N개 메시지마다 flush
log.flush.interval.ms = 1000         // N ms마다 flush
 
// 일반적으로 기본값(무제한) 사용 권장
// OS의 Page Cache가 더 효율적으로 관리

읽기 경로 (Read Path)

Consumer ← Broker

시나리오 1: 최신 메시지 읽기 (Cache Hit)
┌───────────┐    ┌───────────┐
│  Consumer │<───│Page Cache │  ← 메모리에서 직접 읽기
│           │    │ (HIT!)    │     디스크 I/O 없음
└───────────┘    └───────────┘

시나리오 2: 과거 메시지 읽기 (Cache Miss)
┌───────────┐    ┌───────────┐    ┌───────────┐
│  Consumer │<───│Page Cache │<───│   Disk    │
│           │    │ (MISS)    │    │           │
└───────────┘    └───────────┘    └───────────┘
                       ↑
                 디스크에서 로드 후 캐시

순차 읽기 최적화

┌─────────────────────────────────────────────────────────────────┐
│                   Sequential Read Optimization                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Consumer가 순차적으로 읽을 때:                                   │
│                                                                 │
│  요청: offset 1000                                              │
│  OS Readahead: offset 1000 ~ 2000 미리 로드                      │
│                                                                 │
│  Page Cache:                                                    │
│  ┌────┬────┬────┬────┬────┬────┬────┬────┐                     │
│  │1000│1001│1002│...│1998│1999│2000│    │                     │
│  └────┴────┴────┴────┴────┴────┴────┴────┘                     │
│    ↑                              ↑                             │
│  요청됨                        Prefetch됨                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Producer-Consumer 파이프라인

실시간 처리 시나리오

Timeline:
┌───────────────────────────────────────────────────────────────┐
│                                                               │
│  t=0    Producer write → Page Cache                          │
│  t=1    Consumer read ← Page Cache (same data!)              │
│                                                               │
│  결과: 디스크 I/O 없이 메모리에서 직접 전달                       │
│                                                               │
└───────────────────────────────────────────────────────────────┘

조건:
- Consumer가 Producer를 closely 따라가는 경우
- Consumer Lag이 적은 경우
- Page Cache가 충분한 경우

지연된 소비 시나리오

Timeline:
┌───────────────────────────────────────────────────────────────┐
│                                                               │
│  t=0    Producer write → Page Cache                          │
│  ...    (시간 경과, 새 데이터로 캐시 교체)                        │
│  t=1000 Consumer read ← Disk (cache evicted)                 │
│                                                               │
│  결과: 디스크 읽기 필요                                          │
│                                                               │
└───────────────────────────────────────────────────────────────┘

발생 조건:
- Consumer Lag이 큰 경우
- 과거 데이터 재처리
- Page Cache 부족

메모리 할당 전략

권장 메모리 비율

┌─────────────────────────────────────────────────────────────────┐
│                    서버 메모리 할당 예시 (64GB)                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────┐                                          │
│  │   JVM Heap       │  6GB (10%)  - Broker 애플리케이션          │
│  ├──────────────────┤                                          │
│  │   Page Cache     │  50GB+ (80%)  - 로그 파일 캐싱             │
│  ├──────────────────┤                                          │
│  │   OS + Others    │  8GB (10%)  - 운영체제, 네트워크 버퍼       │
│  └──────────────────┘                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

JVM 힙 크기 설정

# Kafka Broker 시작 시
export KAFKA_HEAP_OPTS="-Xmx6g -Xms6g"
 
# 힙이 너무 크면:
# - Page Cache 공간 부족
# - GC 시간 증가
# - 전체 성능 저하

Page Cache 모니터링

시스템 메트릭

# 메모리 사용량 확인
free -h
 
# 예시 출력:
#               total        used        free      shared  buff/cache   available
# Mem:           62Gi       5.2Gi       2.1Gi       1.0Mi        55Gi        56Gi
 
# buff/cache: Page Cache로 사용 중인 메모리

vmstat로 I/O 모니터링

vmstat 1
 
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  1  0      0 2100000 100000 55000000  0    0   100  5000 1000 2000  5  2 93  0  0
 
# bi: blocks read from disk
# bo: blocks written to disk
# 낮을수록 Page Cache 효과적

Kafka 메트릭

# JMX 메트릭
kafka.log:type=Log,name=LogFlushRateAndTimeMs
kafka.log:type=Log,name=Size

성능 최적화

OS 튜닝

# /etc/sysctl.conf
 
# Page Cache flush 임계값
vm.dirty_ratio = 80
vm.dirty_background_ratio = 5
 
# Readahead 최적화
blockdev --setra 4096 /dev/sda
 
# Swappiness 최소화 (Page Cache 유지)
vm.swappiness = 1

파일시스템 선택

# XFS 권장 (ext4도 가능)
# Kafka 로그 디렉토리 마운트 옵션
/dev/sdb1 /kafka-logs xfs defaults,noatime 0 0
 
# noatime: 접근 시간 업데이트 비활성화 (성능 향상)

RAID 구성

RAID 10 권장:
- 높은 읽기/쓰기 성능
- 디스크 장애 대응

┌─────────────────────────────────────────┐
│              RAID 10                     │
│  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐       │
│  │Disk1│ │Disk2│ │Disk3│ │Disk4│       │
│  │Mirror│ │Mirror│ │Mirror│ │Mirror│    │
│  └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘       │
│     └───┬───┘     └───┬───┘            │
│         └─────┬───────┘                 │
│            Stripe                       │
└─────────────────────────────────────────┘

Page Cache와 복제

Follower의 Page Cache 활용

Leader Broker          Follower Broker
┌─────────────┐       ┌─────────────┐
│ Page Cache  │       │ Page Cache  │
│  (Write)    │──────>│  (Write)    │
└─────────────┘       └─────────────┘
                           ↑
                   Fetch 요청으로 복제

Follower도 Page Cache에 쓰기
→ Follower에서 읽는 Consumer도 Cache 활용

Fetch from Follower (Kafka 2.4+)

// Consumer가 Follower에서 읽기 가능
// 가까운 Replica에서 읽어 네트워크 지연 감소
props.put("client.rack", "rack-a");
 
// Broker 설정
replica.selector.class = org.apache.kafka.common.replica.RackAwareReplicaSelector

관련 문서