RocksDB performance tuning
RocksDB — это default state backend для production Flink job-ов. Он хранит state на disk в LSM-tree format, что даёт unbounded state size (десятки GB и больше per task), но требует careful tuning, чтобы не стать bottleneck-ом.
Этот урок про конкретные knobs RocksDB в Flink: какой делает что, как diagnose когда что-то wrong, как настраивать для разных workloads. Будем смотреть на конкретные конфиги, метрики, и production experience.
Буферизация I/O: write-through, write-back, syncRocksDB internals в одном схеме
RocksDB — это LSM-tree (Log-Structured Merge tree). Базовая структура:
Все tuning крутится вокруг балансирования трёх things:
- Write throughput — насколько быстро принимаем writes.
- Read latency — сколько занимает один get.
- Disk space amplification — сколько раз данные переписываются (compactions cost CPU + disk IO).
Tuning параметры влияют на этот треугольник. Нет универсально лучшего config — есть config под конкретный workload.
Key RocksDB metrics
Прежде чем tuning, нужно мониторить. Flink exposes RocksDB stats через metrics system:
# flink-conf.yaml
state.backend.rocksdb.metrics.estimate-num-keys: true
state.backend.rocksdb.metrics.estimate-table-readers-mem: true
state.backend.rocksdb.metrics.size-all-mem-tables: true
state.backend.rocksdb.metrics.cur-size-active-mem-table: true
state.backend.rocksdb.metrics.num-running-compactions: true
state.backend.rocksdb.metrics.num-running-flushes: true
state.backend.rocksdb.metrics.compaction-pending: true
state.backend.rocksdb.metrics.block-cache-usage: true
state.backend.rocksdb.metrics.block-cache-hit: true
state.backend.rocksdb.metrics.block-cache-miss: true
state.backend.rocksdb.metrics.num-immutable-mem-table: true
state.backend.rocksdb.metrics.is-write-stopped: true
Метрики ниже — critical для health monitoring:
| Metric | Healthy | Warning | Critical |
|---|---|---|---|
| is-write-stopped | 0 | rare 1 | persistent 1 |
| num-running-compactions | 1-2 | 3-4 | 5+ |
| compaction-pending | 0-5 | 10-20 | 50+ |
| block-cache-hit-rate | over 85% | 60-85% | under 60% |
| num-immutable-mem-table | 0-1 | 2-3 | 4+ |
| cur-size-active-mem-table | under write-buffer-size | near limit | over limit |
is-write-stopped=1 — критическая ситуация: RocksDB не принимает writes, потому что too many L0 files или other backpressure. Job будет в backpressure пока не resolve.
Write buffer size
state.backend.rocksdb.writebuffer.size — размер in-memory MemTable. По умолчанию 64MB.
Effect:
- Больший buffer -> реже flush -> меньше L0 files -> меньше compactions -> больше throughput для writes.
- Больший buffer -> больше memory -> выше RAM cost per task.
- Больший buffer -> длиннее recovery time (после restart MemTable rebuilt из WAL).
Tuning rules:
- Heavy write workload (много state updates): 128-256MB.
- Medium: 64-128MB (default OK).
- Light writes, read-heavy: 32-64MB (быстрее recovery).
state.backend.rocksdb.writebuffer.size: 256mb
Связанный параметр — writebuffer.count (default 2): сколько MemTables держать одновременно (1 active + N immutable). Больше count -> больше памяти, но дольше можно writing без flush stalls.
state.backend.rocksdb.writebuffer.count: 4 # для heavy writes
Total MemTable memory = writebuffer.size * writebuffer.count = 256MB * 4 = 1GB per RocksDB instance. На TaskManager с 4 slots это 4GB. Учитывайте при memory planning.
Block cache
state.backend.rocksdb.block.cache-size — размер LRU cache для SST blocks. Default 256MB на RocksDB instance.
Read path:
- Check MemTable -> cache miss in MemTable -> check immutable -> … -> check L0 file
- Open L0 file -> read block index -> find target block
- Check block cache for this block -> hit returns immediately, miss reads block from disk
Block cache critical для read performance:
- Hit rate over 85% — отличное состояние.
- 60-85% — приемлемо.
- Under 60% — много disk IO per read, latency высокая.
Tuning:
- State-heavy reads (lookups в keyed state): больше cache, 1-4GB.
- Mostly writes, rare reads: default 256MB OK.
- Total state size намного больше cache — cache всё равно даст benefit для recent/hot data.
state.backend.rocksdb.block.cache-size: 2gb
В Flink 2.x появилось shared cache между RocksDB instances одного TaskManager:
state.backend.rocksdb.memory.managed: true
state.backend.rocksdb.memory.fixed-per-slot: 1gb # включает shared cache
Это даёт more efficient memory usage — slots делятся cache-ом вместо each having свой.
Bloom filters
Bloom filter — это probabilistic data structure, которая отвечает “key точно нет в file” или “key возможно есть”. При read RocksDB consults bloom filter перед reading блока из SST. Если bloom filter says “точно нет” — skip file entirely.
Это критично для read performance: без bloom filter каждый read с cache miss требует binary search через index. С bloom filter большинство SST files skipped без чтения index.
Включается:
state.backend.rocksdb.use-bloom-filter: true
state.backend.rocksdb.bloom-filter.bits-per-key: 10 # default
state.backend.rocksdb.bloom-filter.block-based-mode: false # full-key filter, faster
bits-per-key:
- 10 — false positive rate ~1%, memory cost ~10 bits per key.
- 15 — ~0.1% false positives, ~15 bits per key.
- 20 — ~0.01%, ~20 bits per key.
Чем больше bits — точнее filter, меньше unnecessary disk reads, но больше memory.
Production rule: всегда включайте bloom filters для read-heavy state. Default 10 bits — хороший trade-off для большинства workloads.
# Production-ready config для read-heavy state
state.backend.rocksdb.use-bloom-filter: true
state.backend.rocksdb.bloom-filter.bits-per-key: 10
state.backend.rocksdb.bloom-filter.block-based-mode: false
state.backend.rocksdb.block.cache-size: 2gb
state.backend.rocksdb.block.blocksize: 16kb # smaller blocks = better bloom hit rate
Compaction triggers и stalls
Compaction — это процесс merge L0 files в L1, L1 в L2, и так далее. Цель — поддерживать LSM-tree shape: O(log N) levels, growing размер.
Compaction triggers:
# Сколько L0 files перед compaction L0->L1
state.backend.rocksdb.compaction.level0-file-num-compaction-trigger: 4
# Slowdown threshold (RocksDB начинает throttle writes когда L0 has столько files)
state.backend.rocksdb.compaction.level0-slowdown-writes-trigger: 20
# Stop threshold (RocksDB полностью stops writes — write stall)
state.backend.rocksdb.compaction.level0-stop-writes-trigger: 36
Compaction stalls случаются когда compaction не успевает за writes:
- MemTable filled, flush в L0.
- L0 accumulates files.
- Когда L0 reaches
level0-slowdown-writes-trigger, RocksDB начинает throttle writes (per-write delay). - Если writes continue выше compaction speed, L0 reaches
level0-stop-writes-trigger— writes stopped полностью. Это write stall.
Признаки write stall в Flink:
is-write-stopped=1в metrics.- Backpressure на operator chain.
num-running-compactionsстабильно at max.- Throughput drops до zero.
Solutions:
1. Больше compaction parallelism:
state.backend.rocksdb.thread.num: 4 # параллельных compaction threads
Default 1 — недостаточно для high-write workloads. Увеличьте до 4-8 для heavy writes.
2. Бoльший L0 trigger:
state.backend.rocksdb.compaction.level0-file-num-compaction-trigger: 8
state.backend.rocksdb.compaction.level0-slowdown-writes-trigger: 40
state.backend.rocksdb.compaction.level0-stop-writes-trigger: 60
Это даёт RocksDB больше time для compaction catch-up перед stalling. Trade-off: medlennее read (больше L0 files для search), больше disk space temporary.
3. Universal compaction для very-heavy writes:
state.backend.rocksdb.compaction.style: UNIVERSAL
Universal compaction оптимизирована для writes — она reduces write amplification ценой больше space amplification. Подходит для time-series workloads, где data appended sequentially.
Compression
Compression reduces disk usage и IO bandwidth. RocksDB supports multiple compression algorithms per level.
state.backend.rocksdb.compression.per-level: NONE,SNAPPY,LZ4,LZ4,ZSTD,ZSTD,ZSTD
Per-level это L0,L1,L2,L3,L4,L5,L6. Recommendation:
- L0: NONE (no compression for fresh data, fast flush).
- L1-L2: SNAPPY or LZ4 (fast, ~50% reduction).
- L3+: ZSTD (slower, ~70% reduction — но these levels rare-accessed).
Compression overhead per operation:
- SNAPPY: ~0.5 μs per KB.
- LZ4: ~0.3 μs per KB (faster).
- ZSTD: ~2 μs per KB (slower, better ratio).
Trade-off:
- Больше compression -> меньше disk usage и IO, но больше CPU.
- В read-heavy workload где disk IO bottleneck — больше compression помогает.
- В write-heavy с fast SSD — меньше compression лучше.
Disk type и IOPS
RocksDB performance критично зависит от disk type:
| Disk | Throughput | IOPS | Recommendation |
|---|---|---|---|
| HDD | 100-200 MB/s | 100 IOPS | Не используйте для production |
| SATA SSD | 500 MB/s | 100K IOPS | OK для medium workloads |
| NVMe SSD | 3 GB/s | 1M IOPS | Recommended for production |
| Local NVMe | 7 GB/s | 5M IOPS | Best для high throughput |
Cloud SSD options:
- AWS gp3: 500 IOPS baseline, scalable до 16K. Hint: prefer io2 для high IOPS workloads.
- GCP balanced PD: 6K IOPS up to. Local NVMe better.
- Azure Premium SSD v2: 80K IOPS available.
Production rule: для serious Flink jobs use local NVMe disk (ephemeral storage). State persistence через checkpoint в blob storage, RocksDB just local working set.
Flink config для disk location:
state.backend.rocksdb.localdir: /mnt/nvme/rocksdb # local NVMe
# or comma-separated for multiple disks (RocksDB stripes data)
state.backend.rocksdb.localdir: /mnt/nvme1/rocksdb,/mnt/nvme2/rocksdb
Production tuning template
Hot starting config для production stateful job:
# State backend
state.backend: rocksdb
state.checkpoints.dir: s3://my-bucket/checkpoints
# RocksDB MemTable
state.backend.rocksdb.writebuffer.size: 128mb
state.backend.rocksdb.writebuffer.count: 4
state.backend.rocksdb.writebuffer.min-flush-merge: 2
# Block cache (critical for reads)
state.backend.rocksdb.block.cache-size: 2gb
state.backend.rocksdb.block.blocksize: 16kb
# Bloom filter
state.backend.rocksdb.use-bloom-filter: true
state.backend.rocksdb.bloom-filter.bits-per-key: 10
state.backend.rocksdb.bloom-filter.block-based-mode: false
# Compaction
state.backend.rocksdb.compaction.style: LEVEL
state.backend.rocksdb.compaction.level0-file-num-compaction-trigger: 4
state.backend.rocksdb.compaction.level0-slowdown-writes-trigger: 30
state.backend.rocksdb.compaction.level0-stop-writes-trigger: 50
state.backend.rocksdb.thread.num: 4
# Compression
state.backend.rocksdb.compression.per-level: NONE,LZ4,LZ4,ZSTD,ZSTD,ZSTD,ZSTD
# Local disk
state.backend.rocksdb.localdir: /mnt/nvme/rocksdb
# Memory management (shared per-slot)
state.backend.rocksdb.memory.managed: true
state.backend.rocksdb.memory.fixed-per-slot: 1.5gb
# Metrics
state.backend.rocksdb.metrics.estimate-num-keys: true
state.backend.rocksdb.metrics.block-cache-hit: true
state.backend.rocksdb.metrics.block-cache-miss: true
state.backend.rocksdb.metrics.num-running-compactions: true
state.backend.rocksdb.metrics.is-write-stopped: true
# Incremental checkpoints (важно для big state)
state.backend.incremental: true
Этот config — starting point для most production stateful Flink jobs. Дальше tune per-workload по metrics.
Diagnosing slow gets
Если read latency высокая (state operations slow), checklist:
1. Block cache hit rate. Hit under 80 percent? Увеличить cache size.
# Через Flink metrics
flink_taskmanager_job_task_operator_*_blockCacheHitRate < 80%
2. Bloom filter включён? Без bloom filter каждый L0 miss требует binary search через index. Включите.
3. Слишком много L0 files? level0-file-num-compaction-trigger достигнут? L0 files searched linearly — slow. Compaction должен догнать.
4. Big values (large MapState values)? Каждый read = читать целый block. Big values -> big blocks -> slow reads. Consider splitting в multiple ValueState или sharding.
5. CPU compression overhead. Если ZSTD на L1 — decompress cost on every read. Consider lighter compression on hot levels.
Diagnosing write stalls
If is-write-stopped=1:
1. Check compaction throughput. num-running-compactions constantly at limit? compaction-pending accumulating? Compaction not keeping up.
Fixes:
- Increase compaction threads:
state.backend.rocksdb.thread.num: 8. - Smaller writes per second через better batching upstream.
- Faster disk (NVMe).
2. Check disk IOPS saturation.
iostat -x 1
# Watch %util column — если 100%, disk saturated
If disk saturated, more compaction threads will not help — need faster disk or sharding state.
3. Disable WAL для transient state.
state.backend.rocksdb.write.no-wal: true # риск потери writes между checkpoints
WAL adds disk write per RocksDB.put. Disabling halves write IO. Trade-off: между checkpoints recent writes lost on crash. Acceptable если Flink checkpoint frequency высокая.
Попробуй сам
-
Baseline benchmark. Запустите простой stateful Flink job (Word Count с ValueState) с default RocksDB config. Measure throughput и latency.
-
Tune для writes. Удвойте writebuffer.size и compaction threads. Re-run benchmark. Should see improvement в write-heavy workload.
-
Block cache impact. Установите cache-size=64mb (минимум). Re-run и observe drop in throughput / increase в latency. Это shows cache impact на reads.