LLM WikiAccess-protected knowledge portal
← 스터디 홈
166편 · 약 14분

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

클라이언트
Put / Delete / Merge
WriteBatch API
WAL (Write-Ahead Log)
순차 쓰기 — fsync 선택
메모리
Active MemTable
SkipList / HashSkipList
write_buffer_size 기본 64 MB
↓ 가득 찼을 때
Immutable MemTable
읽기 전용, flush 대기
디스크 (SST 파일)
L0 (최대 4개 기본)
키 범위 중복 허용
flush로 직접 생성
↓ 컴팩션
L1 (기본 256 MB)
키 범위 비중첩
정렬 보장
↓ 컴팩션
L2 … Ln
레벨마다 10× 크기 증가
L2 = 2.56 GB, L3 = 25.6 GB…
RocksDB 쓰기 경로
  1. WAL: 크래시 복구용 순차 로그. sync_wal = true이면 각 쓰기마다 fsync. 성능 우선 시 sync_wal = false + 주기적 group commit.
  2. MemTable: 메모리 내 정렬 구조(기본 SkipList). write_buffer_size(기본 64 MB)를 채우면 Immutable MemTable이 된다.
  3. Flush: Immutable MemTable을 L0 SST 파일로 직렬화한다. L0 파일은 키 범위가 서로 겹칠 수 있어 읽기 시 모든 L0 파일을 확인해야 한다(level0_file_num_compaction_trigger 기본 4).
  4. Compaction: L0 → L1, L1 → L2 순으로 병합하며 중복 키·삭제 마커를 제거한다.

읽기 경로: 최신 버전 찾기

읽기는 최신 데이터가 있는 가장 위쪽 계층부터 확인한다.

  1. Active MemTable → Immutable MemTable 들(복수 가능)
  2. L0 파일 전체 (키 범위 중첩 때문에 모두 확인)
  3. 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/OL0·멀티레벨 탐색, 블룸 미스
공간 증폭 (SA)실제 데이터 크기 대비 디스크 사용량삭제된 키·오래된 버전 파일 잔존

Leveled Compaction(기본): WA 높음, RA 낮음, SA 낮음. Universal(Tiered) Compaction: WA 낮음, RA 높음, SA 높음. FIFO Compaction: SA 낮음, 오래된 파일 자동 삭제 (TTL 기반 데이터에 적합).

쓰기가 압도적이고 디스크 여유가 있다면 Universal로 WA를 낮추고, 읽기가 중요하고 공간이 타이트하면 Leveled를 유지한다.

컴팩션 전략 비교

Leveled (기본)
L0 → L1 → L2 … Ln
레벨 간 키 범위 비중첩
WA ↑ RA ↓ SA ↓
균일한 읽기 레이턴시
OLTP 권장
Universal (Tiered)
동일 크기 SST 그룹 병합
레벨 경계 느슨
WA ↓ RA ↑ SA ↑
쓰기 집약적 배치
분석·로그 수집 권장
FIFO
가장 오래된 파일 삭제
컴팩션 없음
SA ↓ WA 없음
TTL 기반 시계열
캐시 계층 전용
RocksDB 컴팩션 전략 비교

주요 튜닝 파라미터

# 쓰기 버퍼 (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 → PebbleRocksDB Go 재작성. Pebble은 RocksDB API 호환을 목표로
MyRocks (MySQL 스토리지 엔진)쓰기 집약 OLTP에서 InnoDB 대비 공간 50~70% 절감 사례
Debezium스키마 히스토리 저장소로 임베디드 RocksDB 사용
Flink RocksDB StateBackend스트리밍 상태를 SSD에 스필(spill)
ClickHouse MergeTree아이디어는 공유, 별도 구현

Write Stall: 운영 현장의 가장 흔한 병목

Write Stall은 RocksDB가 컴팩션을 따라잡지 못할 때 쓰기를 의도적으로 늦추거나 차단하는 자기 보호 메커니즘이다.

발생 단계:

  1. L0 파일 수 ≥ level0_slowdown_writes_trigger → 쓰기 속도 제한
  2. L0 파일 수 ≥ level0_stop_writes_trigger → 쓰기 완전 차단
  3. Pending compaction bytes ≥ soft_pending_compaction_bytes_limit → 속도 제한
  4. 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::BackupEngine API: 증분 백업 지원
  • checkpoint: 현재 SST 파일에 하드링크를 만들어 스냅샷 생성 (쓰기 중단 없음)

Open questions

  • Pebble(CockroachDB의 Go 재작성)이 장기적으로 RocksDB를 대체하는 생태계 경로가 형성될지 불분명하다.
  • DRAM 가격 하락과 CXL 확장 메모리가 보편화될 경우 LSM-Tree의 블록 캐시 전략이 어떻게 변화할지 아직 컨센서스가 없다.

References