Build a Bitcask-style KV store (9 scenes)
Scene 08 · Design your KV — and feel its limits
Capstone: every choice is a trade against the hash-in-RAM aesthetic. The absence of range/prefix iteration is the ceiling that bridges to LSM.
Previously

Every Bitcask choice is a trade against the same hash-in-RAM aesthetic. Sync, file size, merge cadence, sharding, key cardinality — they all push back on the same wall. One trade has no good answer at all.

Scene 08
Design your KV — and feel its limits
Diagram
Free-form canvas. Left: palette of Bitcask building blocks — data file (with max_file_size knob), keydir RAM budget, sync_strategy selector, merge_window selector, frag/dead-bytes triggers, hint-file toggle, vnode count. Center: drop zones for workload SLO cards — photo blobs, event tags, idempotency keys. Right: a verifier pane that names the earlier scene each placement satisfies or violates, and explicitly flags 'use LSM' when keydir RAM exceeds budget.
Workload SLO — Photo blobs · 10M × 2 MB10M photo URLs → 2 MB blobs. Bursty writes; reads must be sub-5ms. Lose at most 1 s of writes on crash. RAM ~ 760 MB across vnodes.CLUSTERRF=4 · partitions=9768vnode 1 (~2,442 files)6Pv1.000.datav1.001.datav1.002.datav1.003.datav1.004.datav1.005.datavnode 2 (~2,442 files)6Pv2.000.datav2.001.datav2.002.datav2.003.datav2.004.datav2.005.datavnode 3 (~2,442 files)6Pv3.000.datav3.001.datav3.002.datav3.003.datav3.004.datav3.005.datavnode 4 (~2,442 files)6Pv4.000.datav4.001.datav4.002.datav4.003.datav4.004.datav4.005.datamerge fires @ ≥ 60% dead bytesWORKLOAD: PHOTO BLOBS · 10M × 2 MBclient (GET/PUT)owns v1.000.data, v2.000.data, v3.000.datamerge workerowns v1.000.data, v2.000.datarestart (hints)owns v1.000.data, v2.000.data, v3.000.data +1FEATURE FLAGSONhint files.hint siblingmerge windowoff-peakdelete_mode=keeptombstone guardTRADE-OFFSThroughput3/5Durability3/5Complexity2/5WARNINGSkeydir RAM 730 MB total · 182 MB/vnode — fits 256 MB budget(bitcask-04).
One workload card on the canvas: photo blobs, 10M × 2 MB, p99 < 5 ms reads, lose < 1 s on crash. Watch the verifier walk the four checks — RAM math, sync trilemma, merge cadence, hint files — and name the earlier scene behind each.
Implementation
verify_workload(workload, config)
top-level verifier — runs the checks in dependency order
1def verify_workload(workload, config):
2 violations = []
3 # bitcask-04: RAM ceiling — most decisive.
4 violations += keydir_ram_check(workload, config)
5 # bitcask-01: hash index has no order.
6 violations += range_query_check(workload)
7 # bitcask-06a: sync_strategy vs crash-loss SLO.
8 violations += sync_strategy_check(workload, config)
9 # bitcask-07: delete-then-recreate trap.
10 violations += tombstone_check(workload, config)
11 return violations
keydir_ram_check(workload, config)
the decisive verdict — bitcask-04 ceiling
1def keydir_ram_check(workload, config):
2 per_key = 44.5 + workload.avg_key_len # bitcask-04
3 total = workload.key_count * per_key
4 per_vnode = total / config.vnode_count
5 if per_vnode <= RAM_BUDGET_PER_VNODE:
6 return [] # fits
7 if total / MAX_VNODES > RAM_BUDGET_PER_VNODE:
8 return [reject('use LSM (LevelDB/RocksDB):'
9 ' keydir blows budget at any'
10 ' vnode count')]
11 return [warn('add vnodes — per-vnode keydir'
12 ' over 256 MB budget')]
range_query_check(workload)
no Bitcask config can satisfy ordered access — bitcask-01
1def range_query_check(workload):
2 if not workload.requires_range_or_prefix:
3 return []
4 # keydir is a hash table; hash tables have no order.
5 # list_keys + filter is O(N) — produces the answer,
6 # not efficiently. No knob fixes this.
7 return [reject('use LSM: ordered access requires'
8 ' a sorted index (SSTables /'
9 ' B-tree), not a hash')]
recommend_engine(workload, config)
closing function — when to switch storage engines
1def recommend_engine(workload, config):
2 if workload.requires_range_or_prefix:
3 return 'LSM' # bitcask-01 ceiling
4 per_key = 44.5 + workload.avg_key_len
5 needed = workload.key_count * per_key
6 budget = RAM_BUDGET_PER_VNODE * MAX_VNODES
7 if needed > budget:
8 return 'LSM' # bitcask-04 ceiling
9 if workload.deletes_and_recreates:
10 config.delete_mode = 'keep' # bitcask-07
11 return 'Bitcask' # honest fit