Learning Platform
Глоссарий Troubleshooting
Урок 20.03 · 26 мин
Продвинутый
RocksDBWrite BufferBlock CacheBloom FilterCompactionLSM Tuning

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, sync

RocksDB internals в одном схеме

RocksDB — это LSM-tree (Log-Structured Merge tree). Базовая структура:

RocksDB LSM-tree: путь данных от write до read
1. Write — WAL + MemTableWrite: RocksDBValueState.update вызывает RocksDB.put. Данные сначала пишутся в WAL (Write-Ahead Log) для durability, затем в active MemTable (in-memory skip list). Это быстрый O(log N) path.
2. Immutable MemTableMemTable достигает write-buffer-size (default 64MB) — становится immutable, новые writes идут в новую active MemTable. Старая в queue для flush на диск.
3. Flush -> L0 SSTFlush: immutable MemTable записывается на диск как L0 SST file. Это sequential write, fast. После flush MemTable удаляется из памяти. WAL также может быть очищен.
4. L0 -> L1 compactionL0 -> L1 compaction: когда в L0 накапливается level0-file-num-compaction-trigger (default 4) files, начинается compaction в L1. L0 files имеют overlapping ranges — все читаются и merge-ятся в L1.
5. Level compactionsL1+ compaction: L1, L2, ... LN. Каждый level в 10x больше предыдущего (default level-multiplier=10). Compaction между levels — picks file from L_n + overlapping files from L_n+1, merges, writes new SST в L_n+1.
6. Read pathRead: get(key) проверяет MemTable, immutable MemTables, потом L0 (linear scan через все L0 files), потом L1, L2... через binary search. Bloom filters skip уровни без key. Block cache кэширует SST blocks.

Все tuning крутится вокруг балансирования трёх things:

  1. Write throughput — насколько быстро принимаем writes.
  2. Read latency — сколько занимает один get.
  3. 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:

MetricHealthyWarningCritical
is-write-stopped0rare 1persistent 1
num-running-compactions1-23-45+
compaction-pending0-510-2050+
block-cache-hit-rateover 85%60-85%under 60%
num-immutable-mem-table0-12-34+
cur-size-active-mem-tableunder write-buffer-sizenear limitover 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:

  1. Check MemTable -> cache miss in MemTable -> check immutable -> … -> check L0 file
  2. Open L0 file -> read block index -> find target block
  3. 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:

  1. MemTable filled, flush в L0.
  2. L0 accumulates files.
  3. Когда L0 reaches level0-slowdown-writes-trigger, RocksDB начинает throttle writes (per-write delay).
  4. Если 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:

DiskThroughputIOPSRecommendation
HDD100-200 MB/s100 IOPSНе используйте для production
SATA SSD500 MB/s100K IOPSOK для medium workloads
NVMe SSD3 GB/s1M IOPSRecommended for production
Local NVMe7 GB/s5M IOPSBest для 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 высокая.


Попробуй сам

  1. Baseline benchmark. Запустите простой stateful Flink job (Word Count с ValueState) с default RocksDB config. Measure throughput и latency.

  2. Tune для writes. Удвойте writebuffer.size и compaction threads. Re-run benchmark. Should see improvement в write-heavy workload.

  3. Block cache impact. Установите cache-size=64mb (минимум). Re-run и observe drop in throughput / increase в latency. Это shows cache impact на reads.

Проверка знанийKnowledge check
Production Flink job показывает write stalls (is-write-stopped=1 каждые ~30 секунд по 5-10 секунд). num-running-compactions stuck at 1 (limit), compaction-pending растёт до 80. L0 files visible до 50. Throughput drops by 50% during stalls. Disk type — gp3 SSD на AWS с 3000 IOPS. Объясните root cause и предложите конкретный план fix-ов в порядке priority.
ОтветAnswer
Root cause — single compaction thread не успевает за write rate, L0 files накапливаются до stop-writes-trigger, RocksDB останавливает writes. План fixes по priority и cost/risk: (1) ПЕРВЫМ: увеличить compaction parallelism — state.backend.rocksdb.thread.num: 4. Zero risk, immediate benefit. Большинство stalls resolve. (2) Поднять L0 thresholds — level0-slowdown-writes-trigger: 30 -> 50, level0-stop-writes-trigger: 50 -> 80. Это даёт compaction больше времени catch-up. Trade-off — read latency немного hurt (больше L0 files для search). (3) Проверить disk IOPS — gp3 с 3000 IOPS baseline это вероятно bottleneck для heavy-write Flink job. Если iostat показывает %util=100% — increase IOPS на volume (gp3 поддерживает до 16K через config) или migrate на io2/local NVMe. Это infrastructure cost, но даёт real ceiling lift. (4) Увеличить writebuffer.size до 256mb — большие MemTables = реже flush = меньше L0 files. Trade-off: больше recovery time после crash. (5) Если все compaction tuning не помогает — рассмотреть UNIVERSAL compaction style. Она reduces write amplification но increases space amplification. Хороша для time-series-like workloads. (6) Long-term: переключиться на ForStDB (disaggregated state) — он подавляет много RocksDB pain points через s3-backed storage. Это major architectural change, не immediate fix. (7) Verification: после каждого изменения наблюдать is-write-stopped рейтинг в течение 30+ минут. Должен снизиться до 0. compaction-pending должен оставаться under 10.

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 4. Production Flink job показывает write stalls (is-write-stopped=1 каждые ~30s по 5-10s). num-running-compactions stuck at 1, compaction-pending растёт до 80, L0 files visible до 50, throughput drops 50%. Disk type — gp3 SSD на AWS с 3000 IOPS. Plan?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5