RocksDB LSM-Tree: 현대 분산 데이터베이스의 스토리지 엔진 원리와 운영 체크리스트
왜 RocksDB인가
CockroachDB, TiKV, MyRocks(MySQL), Cassandra(실험 브랜치), YugabyteDB — 현대 분산 데이터베이스의 스토리지 계층을 열어 보면 RocksDB가 반복해서 등장한다. Meta(구 Facebook)가 2012년 LevelDB를 포크해 프로덕션 워크로드에 맞게 키워 온 이 엔진은 단순한 키-값 저장소를 넘어 쓰기 집약적 워크로드를 SSD와 HDD 모두에서 안정적으로 소화하는 방법을 보여 준다.
이 글은 RocksDB가 채택한 LSM-Tree 구조의 원리, 읽기-쓰기-공간 증폭의 트레이드오프, 그리고 프로덕션에서 자주 마주치는 병목과 해법을 다룬다.
LSM-Tree의 핵심 아이디어
전통적인 B-Tree는 제자리 수정(in-place update) 구조다. 페이지를 찾아 덮어쓰므로 읽기는 빠르지만 랜덤 쓰기가 많다. SSD에서도 이 랜덤 I/O는 NAND 플래시의 수명과 쓰기 레이턴시에 부정적이다.
Log-Structured Merge Tree(LSM-Tree)는 반대 방향을 선택한다.
- 모든 쓰기를 순차 추가(append-only)로 처리한다.
- 데이터를 메모리에 쌓다가 일정 크기 이상이 되면 디스크에 불변(immutable) SST 파일로 내린다.
- 백그라운드 컴팩션이 주기적으로 파일을 병합·정렬해 오래된 버전을 제거한다.
결과적으로 쓰기 경로는 WAL 순차 쓰기 + 메모리 삽입으로 끝나 레이턴시가 매우 낮다. 읽기 경로가 다소 복잡해지는 트레이드오프는 블룸 필터와 블록 캐시로 완화한다.
쓰기 경로: WAL → MemTable → SST
WriteBatch API
순차 쓰기 — fsync 선택
SkipList / HashSkipList
write_buffer_size 기본 64 MB
읽기 전용, flush 대기
키 범위 중복 허용
flush로 직접 생성
키 범위 비중첩
정렬 보장
레벨마다 10× 크기 증가
L2 = 2.56 GB, L3 = 25.6 GB…
- WAL: 크래시 복구용 순차 로그.
sync_wal = true이면 각 쓰기마다 fsync. 성능 우선 시sync_wal = false+ 주기적 group commit. - MemTable: 메모리 내 정렬 구조(기본 SkipList).
write_buffer_size(기본 64 MB)를 채우면 Immutable MemTable이 된다. - Flush: Immutable MemTable을 L0 SST 파일로 직렬화한다. L0 파일은 키 범위가 서로 겹칠 수 있어 읽기 시 모든 L0 파일을 확인해야 한다(
level0_file_num_compaction_trigger기본 4). - Compaction: L0 → L1, L1 → L2 순으로 병합하며 중복 키·삭제 마커를 제거한다.
읽기 경로: 최신 버전 찾기
읽기는 최신 데이터가 있는 가장 위쪽 계층부터 확인한다.
- Active MemTable → Immutable MemTable 들(복수 가능)
- L0 파일 전체 (키 범위 중첩 때문에 모두 확인)
- L1 이상의 각 레벨 → 해당 레벨 내 딱 하나의 파일(키 범위 비중첩)
각 SST 파일에는 블룸 필터(Bloom Filter)가 있어 존재하지 않는 키를 빠르게 걸러 낸다. bloom_filter_bits_per_key = 10이 기본값이며 False Positive 확률은 약 1%.
블록 캐시(block_cache_size, 기본 8 MB이지만 프로덕션에서는 수 GB로 설정)는 자주 읽히는 데이터 블록을 메모리에 유지한다. HotPath 데이터가 캐시에 충분히 들어갈 때 읽기 증폭이 크게 줄어든다.
세 가지 증폭: WA · RA · SA 트레이드오프
LSM-Tree 튜닝은 세 축의 균형 문제다.
| 증폭 | 정의 | RocksDB에서 원인 |
|---|---|---|
| 쓰기 증폭 (WA) | 논리적 1 바이트 쓰기 → 실제 디스크 N 바이트 | 컴팩션 시 데이터 반복 재작성 |
| 읽기 증폭 (RA) | 논리적 1 바이트 읽기 → 실제 N 바이트 I/O | L0·멀티레벨 탐색, 블룸 미스 |
| 공간 증폭 (SA) | 실제 데이터 크기 대비 디스크 사용량 | 삭제된 키·오래된 버전 파일 잔존 |
Leveled Compaction(기본): WA 높음, RA 낮음, SA 낮음. Universal(Tiered) Compaction: WA 낮음, RA 높음, SA 높음. FIFO Compaction: SA 낮음, 오래된 파일 자동 삭제 (TTL 기반 데이터에 적합).
쓰기가 압도적이고 디스크 여유가 있다면 Universal로 WA를 낮추고, 읽기가 중요하고 공간이 타이트하면 Leveled를 유지한다.
컴팩션 전략 비교
레벨 간 키 범위 비중첩
균일한 읽기 레이턴시
OLTP 권장
레벨 경계 느슨
쓰기 집약적 배치
분석·로그 수집 권장
컴팩션 없음
TTL 기반 시계열
캐시 계층 전용
주요 튜닝 파라미터
# 쓰기 버퍼 (MemTable 크기)
write_buffer_size = 128MB # 기본 64MB, 쓰기 집약 시 늘림
max_write_buffer_number = 4 # 동시 immutable MemTable 수
# L0 → L1 트리거
level0_file_num_compaction_trigger = 4 # L0 파일 4개 → 컴팩션 시작
level0_slowdown_writes_trigger = 20 # 쓰기 속도 제한
level0_stop_writes_trigger = 36 # 쓰기 완전 차단 (write stall)
# 레벨 크기
max_bytes_for_level_base = 512MB # L1 목표 크기
max_bytes_for_level_multiplier = 10 # 레벨마다 10× 증가
# 블룸 필터
bloom_filter_bits_per_key = 10 # FPR ~1%
whole_key_filtering = true # 전체 키 필터 (prefix_extractor 없을 때)
# 블록 캐시
block_cache_size = 4GB # 프로덕션에서 여유 RAM의 30~50%
# 압축
compression = lz4 # L0~L2: 속도 우선
bottommost_compression = zstd # 하위 레벨: 공간 절약누가 RocksDB를 쓰는가
| 프로젝트 | 역할 |
|---|---|
| TiKV (TiDB 스토리지 계층) | 분산 트랜잭션 KV — RocksDB Leveled |
| CockroachDB → Pebble | RocksDB Go 재작성. Pebble은 RocksDB API 호환을 목표로 |
| MyRocks (MySQL 스토리지 엔진) | 쓰기 집약 OLTP에서 InnoDB 대비 공간 50~70% 절감 사례 |
| Debezium | 스키마 히스토리 저장소로 임베디드 RocksDB 사용 |
| Flink RocksDB StateBackend | 스트리밍 상태를 SSD에 스필(spill) |
| ClickHouse MergeTree | 아이디어는 공유, 별도 구현 |
Write Stall: 운영 현장의 가장 흔한 병목
Write Stall은 RocksDB가 컴팩션을 따라잡지 못할 때 쓰기를 의도적으로 늦추거나 차단하는 자기 보호 메커니즘이다.
발생 단계:
- L0 파일 수 ≥
level0_slowdown_writes_trigger→ 쓰기 속도 제한 - L0 파일 수 ≥
level0_stop_writes_trigger→ 쓰기 완전 차단 - Pending compaction bytes ≥
soft_pending_compaction_bytes_limit→ 속도 제한 - Pending compaction bytes ≥
hard_pending_compaction_bytes_limit→ 차단
진단:
# RocksDB 내부 통계 (stats dump)
rocksdb.is-write-stopped # 현재 차단 여부
rocksdb.num-files-at-level0 # L0 파일 수
rocksdb.estimate-pending-compaction-bytes완화책:
- 컴팩션 스레드 수 증가:
max_background_compactions = 8 - L0 트리거 임계값 상향 (단, 읽기 증폭 증가)
- SSD 교체로 컴팩션 I/O 대역폭 확보
- Universal Compaction 전환 (WA 감소)
실전 운영 체크리스트
모니터링 지표:
rocksdb.block-cache-hit/rocksdb.block-cache-miss→ 캐시 히트율 목표 >95%rocksdb.bloom-filter-useful→ 블룸 필터 효율 확인rocksdb.stall-micros→ 누적 Write Stall 시간rocksdb.compaction-pending→ 백로그 여부
파일 시스템:
- XFS 또는 ext4 (direct I/O 지원 필수)
noatime마운트 옵션 활성화- WAL과 데이터를 별도 NVMe 디스크로 분리 (
wal_dir)
메모리 설정:
block_cache_size를 시스템 여유 RAM의 30~50%로 설정allow_mmap_reads = false(direct I/O와 혼용 금지)
백업:
rocksdb::BackupEngineAPI: 증분 백업 지원checkpoint: 현재 SST 파일에 하드링크를 만들어 스냅샷 생성 (쓰기 중단 없음)
Open questions
- Pebble(CockroachDB의 Go 재작성)이 장기적으로 RocksDB를 대체하는 생태계 경로가 형성될지 불분명하다.
- DRAM 가격 하락과 CXL 확장 메모리가 보편화될 경우 LSM-Tree의 블록 캐시 전략이 어떻게 변화할지 아직 컨센서스가 없다.
References
- RocksDB GitHub
- RocksDB Wiki — Compaction
- RocksDB Wiki — Write Stalls
- RocksDB Wiki — Tuning Guide
- The Log-Structured Merge-Tree (LSM-Tree) — O'Neil et al. 1996
- MyRocks: LSM-Tree Database Storage Engine — VLDB 2020
- Pebble: RocksDB-inspired key-value store (CockroachDB)
- TiKV — RocksDB Usage
- Flink RocksDB State Backend