armdb 0.1.13

sharded bitcask key-value storage optimized for NVMe
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# armdb — Tuning Guide

Рекомендации по настройке Linux, файловых систем, io_uring, Config и feature flags для максимальной производительности armdb.

---

## Linux: настройки ядра и системы

### Scheduler и NVMe

```bash
# NVMe по умолчанию использует none (noop) scheduler — это оптимально.
# Проверить:
cat /sys/block/nvme0n1/queue/scheduler
# Должно быть: [none]

# Глубина очереди NVMe (по умолчанию 1023 — оставить как есть):
cat /sys/block/nvme0n1/queue/nr_requests
```

### Лимит файловых дескрипторов

armdb открывает по 2+ fd на каждый data file × shard_count. При 32 шардах и 10 файлах на шард — 640+ fd. Плюс io_uring, tag files, hint files.

```bash
# Проверить текущий лимит:
ulimit -n

# Рекомендация: минимум 65536, для продакшена — 1048576
# /etc/security/limits.conf:
*  soft  nofile  1048576
*  hard  nofile  1048576

# Или systemd unit:
# LimitNOFILE=1048576
```

### Transparent Huge Pages (THP)

armdb аллоцирует SkipList узлы через `Box` и AlignedBuf через `alloc(Layout::from_size_align(cap, 4096))`. THP может помочь при больших индексах (>10M entries), но может увеличить latency из-за compaction в ядре.

```bash
# Для latency-sensitive нагрузок — отключить:
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

# Для throughput-oriented нагрузок (batch ingestion) — оставить always:
echo always > /sys/kernel/mm/transparent_hugepage/enabled
```

### vm.dirty_ratio и vm.dirty_background_ratio

armdb использует O_DIRECT, поэтому page cache ядра **не участвует** в основном write path. Но hint files, tag files и метаданные пишутся через обычный I/O.

```bash
# Для armdb эти параметры не критичны, но для стабильности latency:
sysctl -w vm.dirty_ratio=10
sysctl -w vm.dirty_background_ratio=5
```

### vm.swappiness

SkipList индекс должен оставаться в RAM. Swap катастрофически деградирует lookup.

```bash
# Минимизировать swap:
sysctl -w vm.swappiness=1
```

### NUMA

На multi-socket системах привязывать процесс к одному NUMA node:

```bash
numactl --membind=0 --cpunodebind=0 ./my_app
```

armdb pointer-chasing в SkipList чувствителен к NUMA — cross-node memory access добавляет ~40ns per hop.

---

## io_uring: настройки

### Текущая конфигурация

armdb создаёт один `IoUring` на шард с параметрами:
- `setup_sqpoll(2000)` — kernel-side polling с 2s idle timeout
- SQ size: 256 entries

### Требования к ядру

```bash
# Минимум Linux 5.11 для SQPOLL
# Рекомендуется 6.1+ для стабильности и исправлений io_uring
uname -r

# SQPOLL требует CAP_SYS_NICE или настройку:
sysctl -w kernel.io_uring_group=1000   # GID вашего пользователя (Linux 6.4+)
# Или запускать с CAP_SYS_NICE:
# setcap cap_sys_nice+ep ./my_app
```

### Лимиты io_uring

```bash
# Максимум незакоммиченных io_uring операций на процесс:
cat /proc/sys/kernel/io_uring_max_entries    # default: 32768

# При 32 шардах × 256 entries = 8192 — укладывается в лимит.

# memlock limit для registered buffers:
ulimit -l
# Рекомендация: unlimited или >= 256MB
# /etc/security/limits.conf:
*  soft  memlock  unlimited
*  hard  memlock  unlimited
```

### SQPOLL CPU affinity

Текущий код не привязывает SQPOLL thread к конкретному CPU. Для максимальной производительности — добавить `setup_sqpoll_cpu()`:

```rust
// В io/uring.rs: привязать kernel thread к ядру, соседнему с writer thread
IoUring::builder()
    .setup_sqpoll(2000)
    .setup_sqpoll_cpu(cpu_id)    // <-- привязка к конкретному ядру
    .build(256)
```

Без привязки kernel SQPOLL thread может мигрировать между ядрами, добавляя ~1-5µs jitter.

### Registered buffers и registered files

Текущая реализация **не использует** registered buffers/files. Это потенциальная оптимизация:
- **Registered buffers** — избегают `copy_from_user` на каждый write submit (~100-200ns saving)
- **Registered files** — избегают `fdget/fdput` на каждый submit (~50ns saving)

При sustained >10M ops/sec суммарный выигрыш ≈ 5-10%.

---

## Файловая система

### Рекомендация: ext4 или XFS

| FS | Плюсы для armdb | Минусы |
|----|-----------------|--------|
| **ext4** | Стабильная, предсказуемая. `O_DIRECT` работает без сюрпризов. Быстрый fsync. | Ограничение 64TB per file, metadata журналирование при create/rename |
| **XFS** | Лучше с большими файлами и параллельным I/O. `O_DIRECT` — нативная оптимизация. Allocation groups = параллельный доступ к разным шардам | Чуть медленнее на мелких файлах (<1MB) |
| F2FS | Оптимизирована для Flash/SSD | Менее зрелая, append-only log поверх log — двойная индирекция |
| Btrfs | Copy-on-write, snapshots | O_DIRECT иногда fallback на buffered, нестабильная latency |
| ZFS | Checksums, snapshots | O_DIRECT не поддерживается (до OpenZFS 2.2), двойное кэширование |

**Лучший выбор:**
- **XFS** для production с большими данными (>100GB) — лучшая параллельность I/O по allocation groups совпадает с шардированием armdb
- **ext4** для малых и средних deployments (<100GB) — проще, стабильнее

### Параметры монтирования

```bash
# ext4:
mount -o noatime,nodiratime,discard /dev/nvme0n1p1 /data

# XFS:
mount -o noatime,nodiratime,discard,allocsize=64k /dev/nvme0n1p1 /data
```

| Параметр | Почему |
|----------|--------|
| `noatime` | Убирает обновление access time при каждом read. Для armdb с O_DIRECT это не критично (page cache не участвует), но при recovery и hint file reads — заметная экономия |
| `nodiratime` | Аналогично для директорий шардов |
| `discard` | TRIM для NVMe — поддерживает производительность при удалении файлов после compaction |

### Отдельный раздел для данных

Рекомендуется выделить отдельный NVMe partition (или весь диск) под данные armdb. Это исключает влияние других процессов на I/O latency и упрощает мониторинг.

---

## FixedConfig: настройка FixedStore backend

FixedStore используется через `FixedTree`/`FixedMap` (или `ConstTree<K, V, Fixed>`/`ZeroTree<K, V, Fixed, T>`).
Оптимален для частых обновлений фиксированных значений: счётчики, метрики, rate limiters, сессии.

### grow_step

Количество слотов, добавляемых при росте файла.

| Сценарий | grow_step | Почему |
|----------|:---------:|--------|
| Default | 1_000_000 | 1M слотов. При slot_size=80B → ~80MB per grow |
| Мелкие БД (<100K записей) | 10_000-100_000 | Меньше pre-allocation, быстрее первый запуск |
| Большие БД (>10M записей) | 5_000_000-10_000_000 | Реже grow → реже fdatasync при расширении |
| Известный upper bound | N entries / shard_count | Один grow при старте, дальше без расширений |

**Disk usage:** `file_size = 4096 + slot_count × slot_size`. При grow файл расширяется через `ftruncate` (мгновенно, без записи нулей на большинстве FS).

### sync_interval

Интервал между batched fdatasync.

| Сценарий | sync_interval | Почему |
|----------|:-------------:|--------|
| Default | 50 ms | Баланс durability/latency. Потеря: до 50ms writes при crash |
| Low-latency, durability не критична | 200-500 ms | Реже fdatasync → меньше p99.9 jitter |
| Durability важнее latency | 5-10 ms | Маленькое окно потерь. Плата: fdatasync каждые 5-10ms per shard |

### sync_batch_size

fdatasync каждые N write-операций.

| Сценарий | sync_batch_size | Почему |
|----------|:---------------:|--------|
| Default | 1000 | fdatasync после 1000 writes или sync_interval — что наступит раньше |
| High throughput (>1M ops/sec) | 5000-10000 | Амортизация fdatasync на больший batch |
| Low write rate (<1K ops/sec) | 100 | sync_interval доминирует, batch size не важен |

### enable_fsync

| Сценарий | enable_fsync | Почему |
|----------|:------------:|--------|
| Default | false | Batched fdatasync. Потеря: writes за последний batch |
| Критичная durability | true | fdatasync на **каждый** write. Плата: ~10-20μs per write (NVMe latency) |

**Важно:** `enable_fsync: true` убивает throughput (1 fdatasync per write ≈ max ~50-100K ops/sec per shard). Используйте только для критичных данных.

### shard_count и shard_prefix_bits

Идентичны Bitcask Config. Immutable, задаются при создании.

### Профили FixedConfig

#### Счётчики / метрики (частые updates, durability не критична)

```rust
FixedConfig {
    shard_count: 8,
    grow_step: 1_000_000,
    sync_interval: Duration::from_millis(200),
    sync_batch_size: 5000,
    enable_fsync: false,
    ..FixedConfig::default()
}
```

#### Rate limiters / сессии (средняя durability)

```rust
FixedConfig {
    shard_count: 16,
    grow_step: 500_000,
    sync_interval: Duration::from_millis(20),
    sync_batch_size: 1000,
    enable_fsync: false,
    ..FixedConfig::default()
}
```

#### Финансовые данные (максимальная durability)

```rust
FixedConfig {
    shard_count: 4,
    grow_step: 100_000,
    sync_interval: Duration::from_millis(1),
    sync_batch_size: 1,
    enable_fsync: true,  // fsync per write
    ..FixedConfig::default()
}
```

### FixedStore vs Bitcask: когда что выбирать

| Критерий | Bitcask (Config) | FixedStore (FixedConfig) |
|----------|:----------------:|:------------------------:|
| Частые updates одних ключей | compaction overhead | **in-place, без мусора** |
| Предсказуемая p99.9 latency | ~1-5ms (flush spikes) | **~10-50μs** |
| Variable-size values | да | нет (только [u8; V]) |
| Репликация (log shipping) | да (GSN) | нет |
| Disk usage | 1.3-2x | **1x** |
| Recovery | ~1-10s (10M) | **~300ms** (10M) |

### Linux: vm.dirty_ratio для FixedStore

FixedStore пишет через page cache (pwrite), поэтому `vm.dirty_ratio` **важен** (в отличие от Bitcask с O_DIRECT):

```bash
# Для FixedStore — агрессивнее сбрасывать dirty pages:
sysctl -w vm.dirty_ratio=5
sysctl -w vm.dirty_background_ratio=2

# Это предотвращает dirty page storms при высоком write rate
```

При 100M entries × 80B = 8GB данных и high write rate ядро может накопить гигабайты грязных страниц. Низкий `dirty_ratio` заставляет ядро сбрасывать их раньше, избегая latency spikes.

---

## Config: рекомендации по настройке (Bitcask)

### shard_count

**Immutable** — задаётся при создании базы, изменение требует пересоздания.

| Сценарий | shard_count | Почему |
|----------|:-----------:|--------|
| Развернутый сервер (16 cores) | 32 (default) | `num_cpus * 2` — достаточно для полной утилизации CPU |
| Embedded, low-latency | 4-8 | Меньше fd, меньше io_uring instances, быстрее recovery |
| Высокий write throughput (>10M ops/sec) | 64-128 | Больше шардов = меньше contention per shard |
| Single-threaded workload | 1-2 | Нет contention, минимум overhead |

**Важно:** каждый шард = 1 io_uring instance + 1 write buffer + N open files. На 128 шардах: ~128MB write buffers + ~128 io_uring rings.

### max_file_size

| Сценарий | max_file_size | Почему |
|----------|:-------------:|--------|
| Default | 256 MB | Баланс между частотой ротации и длительностью compaction |
| Мелкие записи (key 8B + value 32B) | 64-128 MB | Быстрее compaction, меньше IO amplification |
| Крупные записи (value 1-10 KB) | 512 MB - 1 GB | Меньше ротаций, меньше hint file overhead |
| Высокий write rate (>5M ops/sec) | 512 MB | Реже ротация = реже блокировка на hint file generation |
| Ограниченное место на диске | 64 MB | Мельче файлы = быстрее compaction освобождает место |

### write_buffer_size

Критичный параметр: определяет максимальный объём данных, потерянных при аварии (без fsync), и частоту flush.

| Сценарий | write_buffer_size | Почему |
|----------|:-----------------:|--------|
| Default | 1 MB | Баланс latency/durability |
| Durability важнее latency | 64-256 KB | Чаще flush → меньше потеря при crash. Плата: flush каждые ~1000-5000 entries |
| Maximum throughput | 4-8 MB | Реже flush → меньше syscalls. Плата: до 8MB потеря при crash per shard |
| Burst write (пиковая нагрузка) | 8-16 MB | Абсорбирует burst без flush stalls |

**Формула:** при entry ~50 bytes и write_buffer_size = 1MB → flush каждые ~20000 entries. При 2M ops/sec per shard → flush каждые ~10ms.

### compaction_threshold

| Сценарий | compaction_threshold | Почему |
|----------|:--------------------:|--------|
| Default | 0.3 | 30% мусора → compaction. Баланс I/O amplification и disk usage |
| Экономия места | 0.15-0.2 | Aggressивнее чистит. Плата: больше I/O на compaction |
| Минимум I/O | 0.5-0.7 | Реже compaction. Плата: больше мёртвых данных на диске |
| Append-mostly (мало updates) | 0.5 | Мало мусора → компакция редко нужна |
| Update-heavy (перезапись тех же ключей) | 0.2 | Быстро накапливается мусор → чаще чистить |

### cache (CacheConfig)

Block Cache критичен для VarTree. Для ConstTree/ConstMap — не используется (значения inline).

| Сценарий | max_size | estimated_items | Почему |
|----------|:--------:|:---------------:|--------|
| ConstTree only | 0 (default) || Кэш не нужен |
| VarTree, данные помещаются в RAM | 0 || Все значения в write buffer или OS cache |
| VarTree, горячий рабочий набор <1GB | 512 MB - 1 GB | 250_000 | Кэширует ~250K блоков. 1 блок ≈ 128 мелких записей → 32M записей покрыто |
| VarTree, большая БД (>50GB) | 4-8 GB | 1_000_000 - 2_000_000 | Покрывает hot working set |
| VarTree, scan-heavy workload | 2-4 GB | 500_000 | S3-FIFO устойчив к scan eviction — можно давать больше памяти |

**Формула estimated_items:** `max_size / 4096`. Не обязательно точное — это подсказка для преаллокации hash table.

### shard_prefix_bits

| Сценарий | shard_prefix_bits | Почему |
|----------|:-----------------:|--------|
| Default (random keys) | 0 | Hash полного ключа — равномерное распределение |
| Составной ключ `[user_id: u64, post_id: u64]` | 64 | Все посты одного пользователя в одном шарде → prefix_iter без cross-shard scan |
| Составной ключ `[tenant_id: u32, ...]` | 32 | Per-tenant locality |
| Случайные UUID ключи | 0 | Prefix не имеет смысла — UUID равномерно распределён |

### reversed

| Сценарий | reversed | Почему |
|----------|:--------:|--------|
| Default | false | Ascending order (oldest first) |
| "Последние N записей" — частый паттерн | true | `prefix_iter().take(N)` вернёт newest first без `.rev()` |
| Time-series с монотонными ID | true | Forward iteration = newest first |
| Лексикографический порядок нужен | false | Натуральный порядок ключей |

### enable_fsync

| Сценарий | enable_fsync | Почему |
|----------|:------------:|--------|
| Default | false | Максимальный throughput. Потеря: unflushed write buffer при crash |
| Финансовые данные / критичная durability | true | Каждый flush → fsync. Плата: +1-5ms latency per flush |
| Реплицированная конфигурация | false | Follower = durability backup. Leader может не fsync |

**Промежуточный вариант:** `enable_fsync: false` + периодический `Compactor::start()` с flush + fsync каждые N секунд. Гарантирует durability с ограниченным окном потерь.

---

## Feature flags для зависимостей

### parking_lot

armdb использует `parking_lot::Mutex` для per-shard write lock.

| Feature | Cargo.toml | Эффект |
|---------|------------|--------|
| `deadlock_detection` | `parking_lot = { features = ["deadlock_detection"] }` | Debug only. Добавляет ~50ns overhead per lock. Полезно при разработке |
| `hardware_lock_elision` | `parking_lot = { features = ["hardware_lock_elision"] }` | Использует Intel TSX (Hardware Lock Elision). На поддерживаемых CPU: uncontended lock = ~5ns вместо ~20ns. **Рекомендуется** для Intel Xeon |
| `nightly` | `parking_lot = { features = ["nightly"] }` | Доступ к nightly API. Не рекомендуется для production |

**Рекомендация для production:** `parking_lot` без дополнительных features (default). HLE на серверных Intel может дать ~2-5% throughput на write path.

### quick_cache

| Feature | Cargo.toml | Эффект |
|---------|------------|--------|
| `stats` | `quick_cache = { features = ["stats"] }` | Включает hit/miss/eviction счётчики. Overhead: 1 atomic increment per operation (~5ns). **Рекомендуется** для мониторинга |

**Рекомендация:** включить `stats` для наблюдаемости. Позволяет мониторить hit rate через `cache.stats()` и подбирать `max_size`.

### rustix

| Feature | Cargo.toml | Эффект |
|---------|------------|--------|
| `io_uring` | Уже включён | io_uring syscalls |
| `linux_latest` | Уже включён | Доступ к latest Linux-specific API |
| `fs` | Уже включён | File system operations |
| `mm` | `rustix = { features = ["mm"] }` | Memory mapping. Потенциально полезен для `madvise` на аллокациях |
| `process` | `rustix = { features = ["process"] }` | CPU affinity (`sched_setaffinity`). Полезно для привязки потоков к ядрам |

**Рекомендация:** текущие features достаточны. Добавить `process` если потребуется CPU pinning для write threads.

### rustix-uring

Крейт `rustix-uring` — обёртка над io_uring. Не имеет значимых optional features. Основные оптимизации — на уровне параметров `IoUring::builder()`.

### ring (encryption)

| Feature | Cargo.toml | Эффект |
|---------|------------|--------|
| default | `ring = "0.17"` | AES-256-GCM с hardware acceleration (AES-NI). Авто-обнаружение |

ring автоматически использует AES-NI на x86_64 и ARM crypto extensions на AArch64. Дополнительных features для включения hardware acceleration не требуется.

**Проверить AES-NI на сервере:**
```bash
grep -o aes /proc/cpuinfo | head -1
# Должно вывести: aes
```

---

## Дополнительные рекомендации

### CPU governor

```bash
# Для стабильной latency — performance governor:
cpupower frequency-set -g performance

# Или для конкретных ядер:
echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
```

Power-saving governors (powersave, ondemand) добавляют ~1-10µs jitter из-за frequency ramp-up.

### jemalloc вместо glibc malloc

armdb аллоцирует много мелких объектов (SkipList узлы ~100-150 bytes). jemalloc лучше справляется с fragmentation и thread-local caches:

```toml
# Cargo.toml вашего приложения:
[dependencies]
tikv-jemallocator = "0.6"
```

```rust
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
```

Ожидаемый эффект: ~5-15% throughput на write path при >10M entries из-за меньшей lock contention в аллокаторе.

### mimalloc как альтернатива

```toml
[dependencies]
mimalloc = { version = "0.1", default-features = false }
```

```rust
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
```

mimalloc может быть быстрее jemalloc на workloads с частыми мелкими аллокациями. Рекомендуется профилировать оба.

### Мониторинг

armdb экспортирует метрики через `metrics` crate:

| Метрика | Тип | Что показывает |
|---------|-----|----------------|
| `armdb.flush.count` | counter | Количество flush операций |
| `armdb.flush.bytes` | counter | Байт записано на диск |
| `armdb.rotation` | counter | Ротации файлов |
| `armdb.compaction.runs` | counter | Циклы compaction |
| `armdb.compaction.entries` | counter | Записей перенесено compaction |
| `armdb.compaction.duration_seconds` | histogram | Длительность compaction |

Подключить через `metrics-exporter-prometheus` или `metrics-exporter-tcp`.

### Recovery time

Время старта зависит от количества записей, backend'а и наличия hint/bitmap files:

**Bitcask:**

| Записей | С hint files | Без hint (full scan) |
|--------:|:------------:|:--------------------:|
| 1M | ~50-200 ms | ~500 ms - 1s |
| 10M | ~200-500 ms | ~2-5s |
| 100M | ~1-3s | ~20-60s |
| 1B | ~5-15s | ~3-10 min |

**FixedStore:**

| Записей | Clean shutdown (bitmap) | Dirty (full scan) |
|--------:|:-----------------------:|:-----------------:|
| 1M | ~10-20 ms | ~30 ms |
| 10M | ~100-200 ms | ~300 ms |
| 100M | ~1-2 s | ~3 s |

**Рекомендация:** всегда использовать graceful shutdown (`tree.close()`) — для Bitcask это генерирует hint files, для FixedStore записывает bitmap sidecar.

### Профили конфигурации

#### Embedded / Low-latency

```rust
Config {
    shard_count: 4,
    max_file_size: 64 * 1024 * 1024,       // 64 MB
    write_buffer_size: 256 * 1024,          // 256 KB
    compaction_threshold: 0.3,
    enable_fsync: false,
    cache: CacheConfig { max_size: 256 * 1024 * 1024, estimated_items: 60_000 },
    ..Config::default()
}
```

#### High-throughput server

```rust
Config {
    shard_count: 64,
    max_file_size: 512 * 1024 * 1024,       // 512 MB
    write_buffer_size: 4 * 1024 * 1024,     // 4 MB
    compaction_threshold: 0.25,
    enable_fsync: false,
    cache: CacheConfig { max_size: 4 * 1024 * 1024 * 1024, estimated_items: 1_000_000 },
    ..Config::default()
}
```

#### Durability-first

```rust
Config {
    shard_count: 16,
    max_file_size: 128 * 1024 * 1024,       // 128 MB
    write_buffer_size: 64 * 1024,            // 64 KB — частый flush
    compaction_threshold: 0.3,
    enable_fsync: true,
    cache: CacheConfig { max_size: 1024 * 1024 * 1024, estimated_items: 250_000 },
    ..Config::default()
}
```

### Checklist перед production deploy

**Общее (оба backend'а):**
- [ ] `ulimit -n` >= 65536
- [ ] NVMe scheduler = `none`
- [ ] FS = ext4/XFS с `noatime,discard`
- [ ] `vm.swappiness` = 1
- [ ] CPU governor = `performance`
- [ ] jemalloc или mimalloc
- [ ] Graceful shutdown обработан (SIGTERM → `tree.close()`)
- [ ] `shard_count` подобран под количество ядер и write concurrency

**Bitcask:**
- [ ] `ulimit -l` >= 256MB (для io_uring registered buffers)
- [ ] Метрики подключены (flush rate, compaction duration, cache hit rate)
- [ ] Block Cache включён для VarTree (`cache.max_size > 0`)

**FixedStore:**
- [ ] `vm.dirty_ratio` = 5, `vm.dirty_background_ratio` = 2
- [ ] `grow_step` подобран под ожидаемое количество записей
- [ ] `sync_interval` / `sync_batch_size` настроены под durability requirements