# 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
| **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
Количество слотов, добавляемых при росте файла.
| 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.
| 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-операций.
| 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
| 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: когда что выбирать
| Частые 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** — задаётся при создании базы, изменение требует пересоздания.
| Развернутый сервер (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
| 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.
| 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
| 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).
| 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
| 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
| 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
| 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.
| `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
| `stats` | `quick_cache = { features = ["stats"] }` | Включает hit/miss/eviction счётчики. Overhead: 1 atomic increment per operation (~5ns). **Рекомендуется** для мониторинга |
**Рекомендация:** включить `stats` для наблюдаемости. Позволяет мониторить hit rate через `cache.stats()` и подбирать `max_size`.
### rustix
| `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)
| 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
```
---
## Дополнительные рекомендации
### 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:**
| 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:**
| 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