Part 3. 벤치마크 실험 — db_bench 캐시 성능 검증
Part 2에서 캐시의 내부 구조를 분석했다. 실제로 "캐시 크기와 개수를 바꾸면 읽기 성능이 얼마나 달라지는가?" 이 질문을 db_bench 벤치마크 실험으로 검증한다.
10. 왜 읽기 성능을 측정하는가? — LSM-tree의 본질적 트레이드오프
벤치마크에 들어가기 전에, 왜 "읽기" 워크로드만 실험하는지를 먼저 짚는다.
Part 1에서 다뤘듯이 LevelDB의 LSM-tree 구조는 쓰기에 최적화된 설계다.
쓰기: Put(key, value)
→ WAL에 append (Sequential I/O)
→ Memtable에 삽입 (메모리, O(log n))
→ 끝. 매우 빠르다.
읽기: Get(key)
→ Memtable → Immutable → L0(최대 4개 전부 확인) → L1 → L2 → ...
→ 최악의 경우 모든 레벨의 SSTable을 열어야 한다
→ SSTable 하나 열 때마다 Disk I/O 발생
→ 느리다. 이걸 빠르게 하는 게 캐시다.
즉 LSM-tree에서 캐시가 해결해야 할 문제는 "읽기 성능"이다. 쓰기는 애초에 빠르니까 캐시의 도움이 필요 없다. 그래서 벤치마크 워크로드를 전부 읽기 계열(readrandom, seekrandom, readhot)로 설계한 것이다.
추가로 Compaction이 읽기 성능에 간접적으로 영향을 준다. Compaction이 발생하면 기존 SSTable이 삭제되고 새 SSTable이 생성되므로, Table Cache에 캐싱된 Index Block이 무효화된다. 하지만 Compaction 자체의 성능(Write Amplification)은 캐시와 직접 관련이 없으므로 이번 실험 범위에서 제외했다
11. 실험 환경과 워크로드
항목 값
──────────────────────────────────
플랫폼 AWS EC2 (t2.micro)
vCPU 1
메모리 1 GiB
스토리지 EBS
Key 크기 16 bytes
Value 크기 100 bytes
데이터 로드 100 MB (fillrandom)
읽기 크기 1 MB
벤치마크 툴 LevelDB 자체 db_bench
워크로드 3가지 — 왜 이 3개를 선택했는가?
캐시 효과는 "Working Set(자주 접근하는 데이터의 크기)"에 따라 완전히 달라진다. Working Set이 캐시보다 작으면 거의 다 히트하고, 캐시보다 크면 미스가 많아진다. 이 관계를 검증하기 위해 Working Set 크기가 극단적으로 다른 3가지 워크로드를 선택했다.
워크로드 접근 패턴 Working Set 캐시에 기대하는 효과
────────────────────────────────────────────────────────────────────────
readhot 전체의 1% 범위에서 매우 작음 작은 캐시로도 높은 히트율.
무작위 읽기 (약 1MB) 캐시 증가 효과 미미할 것
readrandom 전체 키 범위에서 큼 캐시 크기에 비례해서
무작위 읽기 (약 100MB) 성능이 올라갈 것
seekrandom 무작위 위치에서 가장 큼 큰 캐시에서만 효과가
Iterator 구성. (100MB+α) 나타날 것
모든 레벨 탐색 필요
이 3개를 비교하면 "캐시 크기 vs Working Set 크기"의 관계가 명확하게 드러난다.
12. 인덱스 캐시(Table Cache) 성능 실험
인덱스 캐시 개수(open_files)를 0개에서 10,000개까지 10배씩 늘리면서 실험했다. 블록 캐시는 기본값 4MB로 고정.
./db_bench --benchmarks="fillrandom,readrandom" --num=1000000 --open_files=100 --cache_size=4194304
먼저 open_files를 늘리면 ShardedLRUCache 내부에서 무슨 일이 일어나는지를 이해해야 결과가 와닿는다.
Table Cache의 ShardedLRUCache 분산 구조:
open_files=100일 때:
→ NewLRUCache(100 - 10) = NewLRUCache(90) (내부적으로 10개 파일 예약)
→ per_shard = 90 / 16 = 6 (올림)
→ 각 샤드에 최대 6개 SSTable의 Index Block 수용
ShardedLRUCache (Table Cache)
├── 샤드 0: 최대 6개 SSTable 캐싱 가능
├── 샤드 1: 최대 6개 SSTable 캐싱 가능
├── ...
└── 샤드 15: 최대 6개 SSTable 캐싱 가능
총 = 6 × 16 = 최대 96개 SSTable 캐싱 가능
100MB 데이터 → SSTable 약 100개 생성
→ 96개만 캐싱 가능 → 일부 SSTable은 매번 디스크에서 읽어야 함
open_files=1000일 때:
→ NewLRUCache(1000 - 10) = NewLRUCache(990)
→ per_shard = 990 / 16 = 62 (올림)
→ 각 샤드에 최대 62개 SSTable의 Index Block 수용
ShardedLRUCache (Table Cache)
├── 샤드 0: 최대 62개 SSTable 캐싱 가능
├── 샤드 1: 최대 62개 SSTable 캐싱 가능
├── ...
└── 샤드 15: 최대 62개 SSTable 캐싱 가능
총 = 62 × 16 = 최대 992개 SSTable 캐싱 가능
100MB 데이터 → SSTable 약 100개
→ 992개 수용 가능 → 100개 전부 캐싱 여유 → 디스크 접근 불필요
실험 결과:
open_files readrandom (μs/op) seekrandom (μs/op)
────────────────────────────────────────────────────────
0개 ~190 ~275
1개 ~148 ~270
10개 ~150 ~175
100개 ~80 ~115
1,000개 ~75 ~115
10,000개 ~73 ~120
핵심 발견: 100개 이상에서 성능 포화(Saturation) 발생.
위의 샤드 분산을 보면 이유가 명확하다.
100MB 데이터에서 SSTable이 약 100(LRUCache 16개, 개당 인덱스 파일 6개 -> 16*6=약100)개 생성되므로,
open_files=100이면 거의 전부 캐싱 가능하다.
그 이상 늘려도 캐싱할 SSTable이 더 없으니까 추가 개선이 없다.
- readrandom
- 예상: "캐시 크기에 비례해서 개선"
- 결과: 0→10에서 190→150으로 소폭 개선, 10→100에서 150→80으로 급감, 100 이후 평탄.
- 비례적이라기보다 100개 기준으로 명확한 포화 지점이 존재.
- SSTable ~100개 전부 캐싱되면 더 줄 게 없으니까 당연한 결과.
- seekrandom
- 예상: "큰 캐시에서만 효과"
- 결과: 0→10에서 275→175로 오히려 readrandom보다 급감 폭이 큼.
- seekrandom은 여러 레벨의 SSTable을 전부 탐색해야 하니까, 소수라도 Index Block이 캐싱되면 반복적 디스크 접근이 한꺼번에 줄어서 초반 개선이 더 큼.
- "큰 캐시에서만"이 아니라 "소수 캐싱만으로도 즉시 효과"가 정확함.
open_files(인덱스 캐시 개수) 설정 가이드라인:
- open_files 최적값 = 예상 SSTable 파일 개수 × 1.2배 (샤드 분산 올림/내림 오차 + 여유분)
- 100MB 데이터 → SSTable ~100개 → open_files = 120
- 1GB 데이터 → SSTable ~1,000개 → open_files = 1,200
- 10GB 데이터 → SSTable ~10,000개 → open_files = 12,000
- 포화 지점 이후로는 늘려도 효과 없음. 메모리만 낭비.
13. 블록 캐시(Block Cache) 성능 실험
블록 캐시 크기(cache_size)를 1KB에서 1GB까지 10배씩 늘리면서 실험했다. 인덱스 캐시는 기본값 1,000개로 고정.
마찬가지로 cache_size를 늘리면 ShardedLRUCache 내부에서 무슨 일이 일어나는지를 먼저 본다.
Block Cache의 ShardedLRUCache 분산 구조:
cache_size=4MB (기본값)일 때:
→ per_shard = 4,194,304 / 16 = 262,144 bytes (256KB)
→ Data Block 1개 = 4,096 bytes (4KB)
→ 262,144 / 4,096 = 64
→ 각 샤드에 약 64개 Data Block 수용
ShardedLRUCache (Block Cache)
├── 샤드 0: 256KB → Data Block 약 64개
├── 샤드 1: 256KB → Data Block 약 64개
├── ...
└── 샤드 15: 256KB → Data Block 약 64개
총 = 64 × 16 = 약 1,024개 Data Block 캐싱 가능
100MB 데이터 → SSTable 약 100개 × Data Block 약 500개 = 약 50,000개
→ 1,024 / 50,000 = 약 2%만 캐싱 가능 → 대부분 캐시 미스
cache_size=100MB일 때:
→ per_shard = 104,857,600 / 16 = 6,553,600 bytes (약 6.25MB)
→ 6,553,600 / 4,096 = 1,600
→ 각 샤드에 약 1,600개 Data Block 수용
ShardedLRUCache (Block Cache)
├── 샤드 0: 약 6.25MB → Data Block 약 1,600개
├── 샤드 1: 약 6.25MB → Data Block 약 1,600개
├── ...
└── 샤드 15: 약 6.25MB → Data Block 약 1,600개
총 = 1,600 × 16 = 약 25,600개 Data Block 캐싱 가능
전체 Data Block 약 50,000개 중 25,600개 = 약 51% 캐싱 가능
→ 절반 이상 히트 → 성능 급격히 개선
실험 결과:
cache_size readhot (μs/op) readrandom (μs/op) seekrandom (μs/op)
────────────────────────────────────────────────────────────────────────
1KB ~115 ~190 ~190
10KB ~120 ~190 ~165
100KB ~115 ~165 ~160
1MB ~100 ~100 ~100
10MB ~75 ~60 ~65
100MB ~30 ~30 ~35
1GB ~28 ~28 ~30
핵심 발견: 모든 워크로드에서 캐시 크기 증가에 따라 성능이 개선되지만, 워크로드별 감소 패턴이 다르다.
*데이터 블록 1개의 크기는 4KB
위의 샤드 분산을 보면 이유가 명확하다. cache_size=4MB(기본값)일 때,
전체 Data Block ~50,000개(100MB) 중 ~1,024개(4MB,약 2%)만 캐싱 가능하므로 대부분 캐시 미스.
cache_size=100MB일 때 ~25,600개(약 51%) 캐싱 가능해지면서 성능이 급격히 개선된다.
- readhot (Working Set ≈ 1MB)
- 예상: "작은 캐시로도 높은 히트율, 캐시 증가 효과 미미"
- 결과: 초기 latency ~115μs로 readrandom/seekrandom(~190μs)보다 확실히 낮음 → "작은 캐시에서도 히트율 높다"는 맞음
- 근데 "효과 미미"는 틀림. 1KB~100KB 구간은 ~115μs로 안정적이지만, 1MB부터 감소 시작해서 100MB 이후 ~28μs까지 하락. 약 4배 개선.
- Working Set 1MB라서 1MB 근처에서 포화될 거라 예상했으나, 실제로는 100MB까지 계속 개선됨.
- 이유: readhot이 1% "범위"를 읽더라도 그 안에서 접근하는 Data Block + Filter Block이 여러 개라 실질 Working Set이 예상보다 컸기 때문.
- 다만 readrandom의 190→28μs(약 7배) 대비 개선 폭이 작으므로, "상대적으로 둔감"하다는 표현이 정확.
- readrandom (Working Set ≈ 100MB)
- 예상: "캐시 크기에 비례해서 성능 향상"
- 결과: 방향은 맞지만 "선형 비례"가 아니라 "계단식 감소".
- 1KB~10KB: ~190μs로 거의 변화 없음 (캐시가 너무 작아 히트율 자체가 미미)
- 100KB~1MB: 165→100μs로 감소 시작 (캐싱 비율이 의미 있는 수준에 진입)
- 1MB→10MB: 100→60μs로 급감 (변곡점)
- 10MB→1GB: 60→28μs로 수렴
- 캐싱 비율이 임계점을 넘는 1MB 전후가 변곡점이고, 그 이전은 캐시를 늘려도 효과 없는 구간.
- seekrandom (Working Set ≈ 100MB+α)
- 예상: "큰 캐시에서만 효과, readrandom보다 훨씬 큰 캐시 필요"
- 결과: 과장됐음. 실제로는 readrandom과 거의 동일한 감소 패턴.
- 1KB~100KB: 190→160μs (readrandom과 비슷한 수준)
- 1MB: ~100μs로 readrandom과 동일 수준까지 하락
- 10MB→1GB: 65→30μs로 수렴
- seekrandom이 Index+Data+Filter 전부 필요해서 readrandom보다 차별적으로 큰 캐시가 필요할 거라 예상했지만, 100MB 데이터 규모에서는 차이가 거의 없었음.
- 이건 데이터가 100MB로 작아서 "캐시 부족 스트레스"가 약했기 때문. 10GB+ 규모에서는 seekrandom이 필요로 하는 Block 종류가 많아져 차이가 벌어질 것으로 예상.
cache_size(블록 캐시 크기) 설정 가이드라인:
- 핵심 원리: cache_size가 Working Set에 가까워질수록 히트율이 올라가며 성능이 개선됨. Working Set을 충분히 수용할 수 있는 크기를 넘으면 추가 개선이 줄어들며 수렴. .
- cache_size가 Working Set에 가까워질수록 -> 자주 접근하는 데이터의 총량(Working Set)을 캐시가 얼마나 커버하느냐
- Hot Data 워크로드 (특정 데이터 집중 접근) → 수 MB~수십 MB면 충분
- Random 워크로드 (전체 범위 균등 접근) → 데이터 크기의 10% 이상 권장
- Seek 워크로드 (Iterator 기반 범위 탐색) → Random과 동일하거나 그 이상. 데이터 규모가 커질수록 더 넉넉하게.
- 공통: 1MB 미만의 블록 캐시는 어떤 워크로드에서든 효과가 거의 없음. 최소 수 MB 이상 확보할 것.
14. 실험 환경의 한계와 향후 개선
학부 연구 환경의 비용과 시간 제약으로 실험 환경에 한계가 있었다. 한계를 인지하고 있어야 결과 해석을 정확히 할 수 있다.
한계점 영향 이상적 환경
─────────────────────────────────────────────────────────────────────────────
t2.micro (burstable 인스턴스) CPU credit 소진 시 성능 저하 c5/m5 고정 성능 인스턴스
→ 벤치마크 도중 throttling
→ 결과에 노이즈 발생 가능
100MB 데이터 실무 DB는 수십GB~TB 단위 10GB~100GB 데이터셋
→ 캐시를 좀만 키워도 전부
메모리에 올라감
→ "캐시 부족" 스트레스가 약함
EBS (네트워크 스토리지) 디스크 I/O latency 변동 큼 로컬 NVMe SSD
→ 캐시 미스 비용 측정이
부정확할 수 있음
4회 반복 평균 이상치 하나에 크게 흔들림 10~30회 반복
→ 통계적 유의성 부족
db_bench 단독 사용 워크로드 패턴이 단순 YCSB 등 다양한 벤치마크
→ 실무 접근 패턴(Zipf 분포, 병행 사용
시간대별 부하 변동) 미반영
다만 이러한 한계에도 불구하고, 캐시 크기와 읽기 성능의 상관관계, 워크로드별 캐시 효과 차이라는 핵심 트렌드는 이 환경에서도 충분히 관찰되었다. 인스턴스나 데이터 규모를 키우면 수치는 달라지겠지만 트렌드 자체가 뒤집힐 가능성은 낮다.
방향은 괜찮았으나 규모가 아쉬웠던 프로젝트이다.
'LevelDB' 카테고리의 다른 글
| 2. 캐시 내부 구조 (0) | 2026.02.12 |
|---|---|
| 1. LevelDB 기본 구조 (0) | 2026.02.12 |