# 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: настройки

> **Дефолтный Bitcask I/O backend — `Pwrite` (блокирующий `pwrite`), не io_uring.**
> io_uring SQPOLL поднимает выделенный kernel-поллер на каждый шард, который
> busy-polling'ом жжёт CPU: на 8 ядрах CPU-ratio ≈ 15× даже при одном прикладном
> потоке (см. `docs/research/sqpoll-noise.md`). io_uring теперь **opt-in** и на
> compile-time (Cargo-фича `io-uring`, off by default — без неё код `io/uring.rs`
> не компилируется), и в рантайме (`Config::io_backend`). Без фичи (или вне Linux)
> `Uring` падает в `Pwrite` с однократным `warn!`. Разделы ниже применимы, только
> если вы включили и фичу, и backend.

### Включение io_uring

Собирать с фичей: `--features io-uring` (Linux). Затем в коде:

```rust
use armdb::{Config, IoBackend};

let cfg = Config::balanced()
    .io_backend(IoBackend::Uring { sqpoll_idle_ms: Some(2000) })
    .build();
```

При `IoBackend::Uring { sqpoll_idle_ms: Some(2000) }` 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,ignore
// В 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

Presets — sugar-методы на [`FixedConfig`](../../armdb/src/fixed/config.rs): `balanced()`, `counters()`, `sessions()`, `financial()`. Каждый возвращает builder; поля переопределяются цепочкой.

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

```rust
use armdb::FixedConfig;

let cfg = FixedConfig::counters().build();
```

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

```rust
use armdb::FixedConfig;

let cfg = FixedConfig::sessions().build();
```

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

```rust
use armdb::FixedConfig;

let cfg = FixedConfig::financial().build(); // enable_fsync: true — fdatasync per write
```

### 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).

**Per-collection:** `cache` задаётся в `Config` при `open()` каждой коллекции — общего block cache на `Db` нет. Единственный preset с ненулевым baseline — [`Config::large_values()`](../../armdb/src/config.rs) (512 MiB); остальные workload-профили оставляют `max_size: 0` — для Var-коллекций задайте cache явно или используйте `large_values()`.

| Сценарий | 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 (Bitcask)

| Сценарий | enable_fsync | Почему |
|----------|:------------:|--------|
| Default | false | Максимальный throughput. Потеря: unflushed write buffer при crash |
| `Config::durable()` preset | true | **No-op на Bitcask write path** — флаг читается только FixedStore. Durability Bitcask = малый `write_buffer_size` + `Flusher`/`flush()`/`close()` |
| Реплицированная конфигурация | false | Follower = durability backup. Leader может не fsync |

**Промежуточный вариант:** периодический `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,ignore
#[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,ignore
#[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.

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

Presets — sugar-методы на [`Config`](../../armdb/src/config.rs): `balanced()`, `low_latency()`, `high_throughput()`, `durable()`, `large_values()`. Каждый возвращает builder; поля переопределяются цепочкой.

#### Embedded / Low-latency

```rust
use armdb::{Config, CacheConfig};

let cfg = Config::low_latency()
    .cache(CacheConfig {
        max_size: 256 * 1024 * 1024,
        estimated_items: 60_000,
    })
    .build();
```

#### High-throughput server

```rust
use armdb::{Config, CacheConfig};

let cfg = Config::high_throughput()
    .cache(CacheConfig {
        max_size: 4 * 1024 * 1024 * 1024,
        estimated_items: 1_000_000,
    })
    .build();
```

#### Durability-first

```rust
use armdb::{Config, CacheConfig};

let cfg = Config::durable()
    .cache(CacheConfig {
        max_size: 1024 * 1024 * 1024,
        estimated_items: 250_000,
    })
    .build();
// enable_fsync в preset — no-op для Bitcask; durability = малый write_buffer + Flusher/close
```

### 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
