백엔드 데이터 인프라 84편. Kafka Design Persistence — *Don't fear the filesystem!*. Sequential I/O 가 random memory access 보다 빠를 수 있는 이유, page cache 활용, BTree O(log N) 대신 append-only log O(1) 의 의미, 디스크 1급 설계 철학을 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 130편 중 84편이에요. 83편 에서 Kafka 의 설계 동인 5가지를 잡았다면, 이번 84편은 그 중 high throughput + large backlog 의 핵심인 Persistence 입니다. Kafka 가 디스크를 1급으로 쓰는 결정이 어째서 빠른가, 그 비밀을 풀어봅니다.
Persistence가 어렵게 느껴지는 이유
처음 듣는 사람의 직관은 "디스크는 메모리보다 1,000배 느리다" 입니다. 그러니 Kafka 가 디스크를 1급으로 쓴다고 하면 "느린 거 아닌가?" 하는 의문이 따라오죠.
답은 두 가지 사실에 있습니다.
첫째, 디스크 성능은 sequential 인가 random 인가에 따라 6,000배 차이가 납니다. random 은 ms 단위지만 sequential 은 수백 MB/sec 가 나오고, 이건 메모리 random access 와 비슷한 수준이에요.
둘째, modern OS 가 page cache (OS 가 디스크 데이터를 메모리에 자동 캐싱) 로 free 메모리를 자동 활용합니다. 그래서 디스크 + page cache 조합이 사실상 memory-mapped 처럼 동작하죠.
이 두 사실 위에 Kafka 가 어떻게 설계됐는지를 따라가면 "Don't fear the filesystem!" 의 의미가 보입니다.
디스크 성능의 진실
Sequential vs Random — 6,000배 차이
공식 문서의 인용 데이터:
- JBOD (여러 디스크를 묶지 않고 그대로 노출) with 7200rpm SATA (PC·서버용 표준 디스크 인터페이스) RAID-5 (패리티 분산 저장 RAID 방식)
- Sequential write — ~600 MB/sec
- Random write — ~100 KB/sec
6,000배 차이. 즉 sequential 패턴만 잘 따르면 디스크가 놀랄 만큼 빠르다는 얘기예요.
Modern OS Page Cache
OS 는 free 메모리를 모두 page cache 로 활용합니다. 디스크 읽기·쓰기는 다 page cache 를 경유하죠.
- 메모리에 이미 있는 데이터 = 메모리 속도
- 메모리에 없는 데이터 = 디스크 + 메모리에 캐시
- Read-ahead·write-behind 로 연속 패턴 자동 최적화
결과적으로 32GB 머신이라면 free 메모리 28~30GB 가 모두 디스크 cache 가 됩니다. 서비스를 재시작해도 warm 캐시가 유지된다는 게 큰 장점이에요.
JVM 의 memory overhead 함정
여기서 시험 함정 하나. 왜 Kafka 는 자체 in-process 캐시를 안 만들고 OS pagecache 에 의존할까요?
JVM (Java Virtual Machine, 자바 실행 환경) 의 두 가지 문제 때문입니다.
- 객체 메모리 overhead — Java 객체는 원본 데이터의 2배 이상 메모리를 씁니다 (object header·padding 등)
- GC 부담 — heap 안 데이터가 늘면 GC stop-the-world 가 길어져서, 32GB heap 이면 수 초의 GC pause 가 나올 수 있어요
따라서:
- in-process cache 10GB = JVM 메모리 20GB+ + GC 부담 큼
- OS pagecache = byte-level 저장, GC 없음
- 같은 32GB 머신 = OS pagecache 가 압도적
Kafka 의 설계 — 즉시 디스크
Kafka 의 결정은 이렇습니다.
"All data is immediately written to a persistent log on the filesystem without necessarily flushing to disk. In effect this just means that it is transferred into the kernel's pagecache."
핵심:
- 메시지 도착 → 즉시 디스크에 write (정확히는 pagecache 에 write)
- fsync (메모리 버퍼를 디스크에 강제 flush) 강제 X (옵션) — pagecache 가 나중에 디스크에 flush
- consumer 가 읽을 때도 page cache 활용 → 메모리 속도
이게 바로 page cache-centric design 입니다. 애플리케이션이 자체 캐시를 만들지 않고 OS pagecache 를 그대로 사용하죠.
한 줄 정리 — Kafka 의 속도 비밀 = sequential 디스크 I/O + OS page cache + JVM heap 회피.
O(1) 자료구조 — Append-Only Log
전통 메시지 큐의 BTree
전통적 메시지 큐 (RabbitMQ·ActiveMQ 등) 는 per-consumer queue 에 BTree (정렬 키 기반 균형 트리 인덱스) 또는 random access 자료구조를 씁니다.
문제는 다음과 같아요.
- BTree 연산 = O(log N)
- 일반적 O(log N) 은 "사실상 constant" 라고 하지만 디스크에서는 다릅니다
- 디스크 seek = 10 ms, parallelism 도 제한적
- data 가 늘수록 cache miss 비율이 커져 성능이 super-linear 로 떨어집니다
Kafka 의 단순한 선택
"a persistent queue could be built on simple reads and appends to files"
핵심 결정은 모든 연산이 O(1) 이라는 점. reads do not block writes.
Topic Partition = 한 개의 파일 (또는 segment 들의 sequence)
Producer: → append (O(1))
Consumer: → 자기 offset 부터 sequential read (O(1) + sequential)
Data size 와 성능 분리 — 1GB partition 과 1TB partition 의 append/read 속도가 똑같음. 거대 데이터 보존이 비용 없이 가능.
결과적 이점 — 메시지 큐 + 영구 보존
이 설계가 가져오는 직접적인 결과는 다음과 같습니다.
(1) 매우 긴 retention 가능
"in Kafka, instead of attempting to delete messages as soon as they are consumed, we can retain messages for a relatively long period (say a week)."
전통 큐는 consume 되는 즉시 delete 합니다 (성능 유지 때문). Kafka 는 consume 후에도 그대로 보존하고, 일주일·한 달·infinite 까지 잡을 수 있어요.
(2) Consumer 가 여러 명 + 독립
각 consumer 는 자기 offset 만 관리하면 됩니다. 다른 consumer 에 영향을 주지 않으니까 real-time, batch, ML 학습용 과거 데이터 처리를 모두 같은 데이터 위에서 돌릴 수 있죠.
(3) 재처리 자연스러움
Consumer 가 offset 을 되감기만 하면 과거 메시지를 다시 읽을 수 있습니다. 버그 수정 후 재처리, 새 분석 시스템 도입 후 과거 데이터 학습 같은 시나리오가 자연스럽게 풀려요.
(4) 매우 큰 데이터 보존
SATA 7200rpm 1+TB 디스크는 seek 이 느려도 sequential 은 빠릅니다. 그러니 값싼 하드웨어로 거대 데이터를 보존할 수 있어요.
Segment 파일 모델
여기까지 따라오셨다면 한 가지 의문이 들 거예요. "한 partition 이 1TB 면 단일 파일인가?" 답은 segment 로 분할한다는 것입니다.
Topic "payments", Partition 0
├─ 00000000000000000000.log (첫 segment, 1GB 까지)
├─ 00000000000000050000.log (다음 segment, offset 50000부터)
├─ 00000000000000100000.log (...)
└─ 00000000000000150000.log (현재 active segment)
- 각 segment = 기본 1GB (
log.segment.bytes) - Active segment (가장 최신) 만 append 받음
- 오래된 segment 는 retention 만료 시 통째로 삭제됩니다 (한 메시지씩이 아니라 segment 단위라 매우 효율적)
각 segment 옆에는 index 파일 (.index·.timeindex) 이 붙어서 offset → 파일 위치 lookup 을 빠르게 잡아줍니다.
00000000000000050000.log
00000000000000050000.index
00000000000000050000.timeindex
그래서 정말 빠르나? — 벤치마크
LinkedIn 의 Kafka 발표 데이터 (대략):
- 단일 broker = 초당 수십~수백 MB 처리
- 3 broker cluster = 초당 GB 단위
- 지연 시간 = 수 ms (producer ack 옵션에 따라)
전통 메시지 큐 대비 10~100배 처리량이 나오고, 데이터베이스와는 비교가 어렵습니다 (DB 가 그만큼의 처리량을 노리지 않으니까요).
한계 — 디스크 1급의 비용
여기서 중요한 자리. 디스크 1급이 만능은 아닙니다.
- 디스크 비용 — 거대 retention 은 거대 디스크를 요구하고, SSD 면 비용이 확 뜁니다
- 디스크 장애 모니터링 — broker 마다 디스크 health 를 추적해야 합니다
- Compaction 비용 (107편) — log compaction 은 디스크 I/O 를 추가로 씁니다
- Tiered Storage (106편) — 오래된 segment 를 S3 로 옮기는 옵션 (Kafka 3.6+)
시험 직전 한 번 더 — Kafka Persistence 함정 압축 노트
- Don't fear the filesystem! — Kafka 의 핵심 철학
- Sequential vs Random — 6,000배 차이 (600 MB/sec vs 100 KB/sec)
- Sequential 패턴 = 디스크 수백 MB/sec (메모리 random access 와 비슷)
- Modern OS Page Cache — free 메모리 자동 활용, 32GB 머신의 28~30GB
- Read-ahead·write-behind 로 sequential 자동 최적화
- 서비스 재시작 후 = warm 캐시 유지 (in-process cache 와 다름)
- JVM 회피 이유 = 객체 overhead (데이터 2배+) + GC stop-the-world
- in-process 10GB cache = JVM 20GB+ + 수 초 GC pause
- 즉시 디스크 (pagecache) — fsync 강제 X
- 애플리케이션 자체 캐시 만들지 않고 OS pagecache 그대로 활용
- 전통 메시지 큐 = BTree O(log N) — 디스크 seek 때문에 사실상 더 큼
- Kafka = append-only log O(1) — data 크기 무관 동일 속도
- 결과 1 = 매우 긴 retention (consume 후도 보존, 며칠~infinite)
- 결과 2 = 여러 consumer 독립 (각자 offset 만)
- 결과 3 = 재처리 자연스러움 (offset 되감기)
- 결과 4 = 거대 데이터 보존 (SATA 디스크로도 OK)
- Segment 파일 — partition 을 1GB 단위 segment 로 분할
- Active segment 만 append, 오래된 segment 통째 삭제
- 각 segment 옆 =
.index·.timeindex빠른 lookup - 벤치마크 = 단일 broker 수십~수백 MB/sec, 3 broker GB/sec
- 한계 = 디스크 비용·디스크 장애 모니터링·compaction I/O
- Tiered Storage (Kafka 3.6+) = 오래된 segment 를 S3 로
공식 문서: Kafka Design — Persistence 에서 자세한 설계를 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 79편 — Java Redis Client (Jedis vs Lettuce 깊이)
- 80편 — Apache Kafka란 + 이벤트 스트리밍의 표준
- 81편 — Kafka 활용 영역 7가지 (메시징·활동 추적·로그·이벤트 소싱)
- 82편 — Kafka Quickstart (5분 hands-on · topic·producer·consumer)
- 83편 — Kafka Design: Motivation (왜 이렇게 설계됐나)
다음 글:
- 85편 — Kafka Design: Efficiency (Zero-Copy · Batch 압축)
- 86편 — Kafka Design: Producer (Partition 선택·ACK·Idempotent)
- 87편 — Kafka Design: Consumer (Pull · Consumer Group · Offset)
- 88편 — Kafka Message Delivery Semantics (at-most·at-least·exactly-once)
- 89편 — Kafka Replication (ISR · Leader Election · Unclean)