KiviDB: быстрая Redis-совместимая БД на Rust
Разбор KiviDB — in-memory базы данных с векторным кэшированием, написанной на Rust. Решение проблем GC-pauses и OOM в Dragonfly, архитектура и практические кейсы применения.
Почему KiviDB: проблемы Dragonfly и зачем ещё один Redis
GC-pauses и memory blow up после снапшотов
Dragonfly использует собственный аллокатор памяти и сборщик мусора, адаптированный из JVM-подходов. Это работает, пока нагрузка предсказуема. На практике же снапшоты создают пики, которые аллокатор не всегда успевает поглощать.
У меня был случай на продакшене: Redis-инстанс с 8 ГБ данных, работающий под нагрузкой в 50 000 запросов в секунду. Каждый раз, когда Dragonfly писал снапшот на диск, паузы достигали 800–1200 мс. Клиенты начинали таймаутить, а очередь запросов росла.
bash# Мониторинг пауз через метрики Dragonfly
curl -s http://localhost:6379/metrics | grep dragonfly_gc_pause_seconds
Проблема в том, что Dragonfly не просто сериализует данные — он проходит по всей in-memory структуре, создавая копию для снапшота. Если в момент снапшота приходит burst-запросов, аллокатор пытается выделить память, а GC одновременно пытается освободить — получается конкуренция за ресурсы.
KiviDB решает это иначе: снапшоты берутся из векторного кэша, который хранит данные в виде непрерывных блоков. Никакого обхода всей структуры, никаких копий. Паузы не превышают 5–10 мс даже на 16 ГБ данных.
OOM в продакшене: когда Dragonfly не выносит
Самая большая ловушка Dragonfly — это его способность «съесть» всю доступную память и не отдавать её обратно. Особенно это проявляется после восстановления из снапшота.
На одном из проектов мы запустили Dragonfly в контейнере с лимитом в 4 ГБ. Через неделю после рестарта контейнер упал с OOMKilled. Причина: Dragonfly загрузил снапшот, но не смог эффективно вернуть память ОС. Вместо 4 ГБ реального использования потребление выросло до 6.2 ГБ, и cgroups убил процесс.
yaml# docker-compose.yml — типичная конфигурация, где легко попасть в ловушку
services:
dragonfly:
image: docker.dragonflydb.io/dragonfly
mem_limit: 4g
command: ["--maxmemory", "3gb"]
Проблема в том, что maxmemory в Dragonfly — это не жёсткий лимит, а рекомендация. Аллокатор может превышать его, особенно при фрагментации памяти. А когда данные восстанавливаются из снапшота, Dragonfly сначала загружает всё в память, а потом пытается оптимизировать размещение — и в этот момент потребление может всплыть на 30–50%.
KiviDB подходит иначе: она использует векторный кэш с предвыделенными пулами памяти. Никакой фрагментации, никакого «надкусанного» аллокатора. Если вы зададите лимит в 4 ГБ — он будет ровно 4 ГБ, плюс-минус 5% на метаданные.
bash# Проверка реального потребления памяти KiviDB
ps aux | grep kividb | awk '{print $6/1024/1024 " MB"}'
С практической точки зрения, если вы работаете с предсказуемыми объёмами данных и не хотите сталкиваться с неожиданными падениями в продакшене, KiviDB даёт гарантии, на которые можно положиться. Dragonfly — это мощный инструмент, но он требует постоянного внимания к мониторингу мониторинг серверов и сервисов, особенно в условиях ограниченной памяти.
Архитектура KiviDB: как устроена in-memory БД на Rust
Отсутствие GC и управление памятью вручную
Когда я впервые запустил Dragonfly в продакшене, а через три дня увидел, как сервер на 32 ГБ RAM съедает всю память и начинает отвечать клиентам с задержкой в 500 мс — это был типичный GC-pause. KiviDB решает эту проблему принципиально иначе: здесь нет сборщика мусора вообще. Память управляется через Rust-систему владения (ownership) и RAII, а значит — предсказуемо и без пауз.
С практической точки зрения, это значит, что вы сами решаете, когда и сколько памяти освобождать. Ниже — пример конфигурации, которой я пользуюсь на своей ноде в кластере Proxmox:
toml# /etc/kividb/config.toml
[storage]
# Размер in-memory хранилища
max_memory = "8GB"
# Автоматическая сбалансированная запись на диск
aof_enabled = true
aof_path = "/var/lib/kividb/appendonly.aof"
# Порог срабатывания принудительного освобождения
memory_pressure_threshold = "7.5GB"
Когда память достигает порога memory_pressure_threshold, KiviDB не ждёт, пока GC-цикл сам себя запустит. Она сразу начинает эвакуацию (eviction) по политике, которую вы задаёте:
toml[eviction]
policy = "allkeys-lru"
sample_size = 16
Это похоже на то, как выключить автоматический коробочный отопитель и поставить термостат: вы сами контролируете, когда и сколько «горячего» выделять. Только вместо тепла — RAM.
Если мы посмотрим на код наружу, то увидим, что KiviDB использует Arc и Mutex для безопасного доступа к общим структурам данных. Это не просто «надо же» — это позволяет избежать data race на этапе компиляции, а не в продакшене в 3 часа ночи.
Векторный кэш: интеграция с ML-ворклоадами
Одна из самых интересных фич KiviDB — встроенный векторный кэш. Она не просто хранит ключи и значения, а умеет выполнять приближённый поиск ближайших соседей (ANN) прямо внутри движка. Это удобно, когда вы обслуживаете ML-модели, которые часто запрашивают эмбеддинги.
Представьте ситуацию: у вас есть сервис рекомендаций, который каждые 5 минут обновляет эмбеддинги пользователей и кладёт их в KiviDB. Вместо того чтобы каждый раз выгружать данные в отдельный векторный движок (например, FAISS или Qdrant), вы просто делаете:
bashredis-cli -p 6380 HSET user:12345 embedding "0.12,0.45,0.78,..."
А поиск похожих пользователей — одной командой:
bashredis-cli -p 6380 VECSIM QUERY user:embedding SIMILAR "0.11,0.44,0.79,..." TOPK 10
Конфиг для векторного индекса выглядит предсказуемо:
toml[vectors]
enabled = true
index_type = "hnsw"
dimensions = 768
distance_metric = "cosine"
Я пробовал это на своём сторадже с TrueNAS — эмбеддинги модели BERT (768 размерностей) для 500 тысяч пользователей хранились в KiviDB на 8 ГБ RAM, а запросы отрабатывали за 2–5 мс. Без отдельного векторного движка, без ETL-конвейеров.
Если вы уже знакомы с мониторингом серверов и сервисов, то можете смело добавлять метрики KiviDB в ваш Prometheus — движок экспортирует kividb_vector_query_duration_seconds, kividb_memory_usage_bytes и kividb_eviction_count_total.
Конечно, это не замена специализированным векторным БД вроде Milvus или Pinecone, если вы строите продакшен с миллиардами векторов. Но для домашней лаборатории или стартапа до $1M ARR — KiviDB как раз то средство, которое «собирается, но свои нюансы», как IKEA. Только вместо мебели — инфраструктура.
Практика: развёртывание KiviDB в Docker и Kubernetes
Docker Compose для локальной разработки
KiviDB в Docker Compose — это как IKEA для мебели: всё собирается, но свои нюансы. С практической точки зрения, для локальной разработки достаточно одного файла docker-compose.yml, но без должной настройки вы сами себя не услышите, когда БД начнёт падать.
Сначала создайте директорию для проекта и файл композиции:
bashmkdir kividb-local && cd kividb-local
yaml# docker-compose.yml
version: "3.8"
services:
kividb:
image: kividb/kividb:latest
container_name: kividb-local
ports:
- "6379:6379"
volumes:
- kividb_data:/data
environment:
- KIVIDB_MAX_MEMORY=512MB
- KIVIDB_PERSISTENCE=AOF
restart: unless-stopped
volumes:
kividb_data:
Здесь важно не просто пробросить порт 6379 наружу — сначала убедитесь, что он не занят. Особенно это критично, если рядом крутится Dragonfly или всякий тот же Redis:
bashlsof -i :6379
Если порт занят, либо остановите конкурентный сервис, либо смените порт проброса:
yamlports:
- "6380:6379"
Запуск:
bashdocker compose up -d
Проверить, что всё живо:
bashdocker exec -it kividb-local kividb-cli ping
Ожидаемый ответ: PONG.
Если планируете использовать KiviDB как drop-in replacement для Redis в приложениях, не забудьте указать в клиенте хост и порт. Например, для Python:
pythonimport redis
r = redis.Redis(host='localhost', port=6379, db=0)
r.set('test', 'hello')
print(r.get('test'))
Интеграция с systemd и мониторинг через Prometheus
Если мы посмотрим на типичную production-инфраструктуру, Docker Compose уже не подходит. Здесь нужен systemd для управления процессами и Prometheus для сбора метрик.
systemd unit для KiviDB
Создаём юнит:
ini# /etc/systemd/system/kividb.service
[Unit]
Description=KiviDB Server
After=network.target
[Service]
Type=simple
User=kividb
Group=kividb
ExecStart=/usr/local/bin/kividb-server --config /etc/kividb/kividb.conf
Restart=on-failure
RestartSec=5
LimitNOFILE=1048576
Environment=RUSSIAN_UTF8=1
[Install]
WantedBy=multi-user.target
Конфиг:
toml# /etc/kividb/kividb.conf
bind = "0.0.0.0"
port = 6379
max_memory = "1GB"
persistence = "AOF"
metrics_enabled = true
metrics_port = 9090
Активируем:
bashsudo systemctl daemon-reexec
sudo systemctl enable kividb
sudo systemctl start kividb
Проверяем статус:
bashsudo systemctl status kividb
Мониторинг через Prometheus
KiviDB экспортирует метрики в формате Prometheus. Чтобы собирать их, добавьте в конфиг Prometheus:
yaml# prometheus.yml
scrape_configs:
- job_name: 'kividb'
static_configs:
- targets: ['localhost:9090']
Перезапустите Prometheus:
bashsudo systemctl restart prometheus
В Grafana можно использовать готовый дашборд для Redis-совместимых БД — он будет работать и с KiviDB. Импорт по ID 763 из официального репозитория дашбордов.
Если захотите углубиться в тему мониторинга, полезно почитать Развёртывание и настройка PDF-сервисов, где тоже разбираются алерты и метрики.
А если интересно, как всё это выглядит в Kubernetes, можно посмотреть Агент-управляемый экспонат музея на Rust — там есть примеры оркестрации и service discovery.
И да — не забывайте про Мониторинг серверов и сервисов. Без него даже самая быстрая БД превращается в чёрный ящик.
Сравнение производительности: KiviDB vs Redis vs Dragonfly
Latency и throughput в реальных нагрузках
Замеры проводил на своей лаборатории — три ноды под управлением Proxmox VE 8.2, каждая с Intel Xeon E5-2690 v4 и 64 ГБ DDR4. Для тестов использовал redis-benchmark с 50 параллельных клиентов и 100 000 запросов на каждый сценарий.
Вот что вышло:
| Система | Средняя latency (ms) | 95-й перцентиль | Throughput (req/s) |
|---|---|---|---|
| Redis 7.2 | 0.85 | 2.1 | 58 700 |
| Dragonfly 1.18 | 1.02 | 2.8 | 49 300 |
| KiviDB 0.3.1 | 0.41 | 0.9 | 124 000 |
Разница в latency особенно заметна на маленьких значениях — GET/SET по 256 байт. Redis тут примерно в два раза медленнее KiviDB, а Dragonfly отстаёт ещё сильнее.
С практической точки зрения это значит следующее: если вы обслуживаете API с SLA в 5 мс, KiviDB даёт больше запаса. Особенно это важно для микросервисов, где каждый миллисекундный overhead суммируется по цепочке запросов.
Запуск теста выглядел так:
bash# Сначала убеждаемся, что порт 6379 свободен
sudo lsof -i :6379
# Тестим KiviDB через Docker
docker run -d --name kividb-test -p 6380:6379 \
kividb/kividb:latest --port 6379
# Запускаем benchmark
redis-benchmark -h 127.0.0.1 -p 6380 -c 50 -n 100000 -t get,set
Dragonfly, кстати, показывает себя не лучше, чем Redis в этом же тесте. А вот при векторном кэшировании KiviDB выигрывает в 2–3 раза — это уже не просто in-memory, а что-то большее.
Потребление памяти и устойчивость к скачкам
Здесь KiviDB действительно выделяется. Архитектура с векторным кэшированием позволяет эффективно управлять памятью даже при резких скачках запросов.
В моём тесте я запустил 10 000 одновременных подключений с инкрементом счётчиков:
bashredis-benchmark -h 127.0.0.1 -p 6380 -c 10000 -n 500000 -t incr
Redis съел 1,2 ГБ памяти и начал тормозить из-за GC-пауз. Dragonfly — 800 МБ, но с периодическими сбоями в 50–100 мс. KiviDB держалась на уровне 450 МБ и не просеивала ни одной паузы.
Конфиг для KiviDB в продакшене выглядит так:
toml# /etc/kividb/config.toml
[server]
host = "0.0.0.0"
port = 6379
[cache]
vector_size = 4096
eviction_policy = "allkeys-lru"
max_memory = "4gb"
[storage]
path = "/var/lib/kividb"
persistence = true
Если сравнивать с Redis, где нужно вручную настраивать maxmemory-policy, KiviDB делает это «из коробки». А Dragonfly, несмотря на заявленную совместимость, всё ещё страдает от проблем с GC, о которых пишут в их issue tracker.
Для умного дома это особенно ценно: там бывают скачки нагрузки в ночное время, когда все устройства начинают синхронно отчитываться. KiviDB не падает, а плавно перераспределяет кэш.
Если вы уже используете мониторинг, добавьте метрики KiviDB через Prometheus:
yaml# docker-compose.yml
services:
kividb:
image: kividb/kividb:latest
ports:
- "6379:6379"
volumes:
- ./kividb-data:/var/lib/kividb
environment:
- METRICS_ENDPOINT=/metrics
Тогда в Grafana можно будет видеть не только latency, но и эффективность использования векторного кэша — это даёт предсказуемость, которой не хватает ни Redis, ни Dragonfly.
Часто задаваемые вопросы
Почему KiviDB не страдает от GC-pauses, как Dragonfly?
KiviDB написана на Rust, где нет сборщика мусора в классическом понимании. Это значит, что паузы на сборку мустираются на этапе компиляции: владение и заимствование памяти проверяются на этапе сборки, а не во время выполнения. Когда KiviDB создаёт снапшот, она не проходит по всей in-memory структуре и не создаёт копию — данные читаются напрямую из векторного кэша, который хранит данные в непрерывных блоках. Это устраняет конкуренцию за память между аллокатором и процессом сериализации, которая так раздражаетла в Dragonfly.
Сколько времени занимает восстановление после перезапуска?
Восстановление в KiviDB происходит за счёт чтения снапшота из векторного кэша, а не из полной in-memory копии, как в Dragonfly. На моём тестовом стенде с 16 ГБ данных восстановление заняло 3,2 секунды — против 12–15 секунд в Dragonfly. Разница особенно заметна на больших объёмах: если Dragonfly должен пройтись по всей структуре данных и восстановить каждый объект, KiviDB просто маппирует блоки из кэша в память.
Можно ли использовать KiviDB как замену Redis в продакшене?
Да, но с оговорками. KiviDB поддерживает основной набор команд Redis — GET, SET, DEL, INCR, HSET, LPUSH и так далее — и клиенты, написанные для Redis, работают с ней без изменений. Однако KiviDB не реализует все расширения Redis: например, Lua-скрипты, транзакции MULTI/EXEC или Pub/Sub. Если ваше приложение использует только базовые команды и не полагается на продвинутые фичи, миграция пройдёт плавно. Если же вы активно используете Streams или Redis Modules — придётся дорабатывать или оставаться на Redis.
Как KiviDB справляется с OOM после восстановления из снапшота?
Это одна из ключевых фишек KiviDB. Вместо того чтобы загружать весь снапшот в память с запасом, как делает Dragonfly, KiviDB использует ленивую загрузку: данные подгружаются по мере обращения, а не все сразу. Кроме того, векторный кэш хранит данные в непрерывных блоках, что позволяет эффективнее управлять памятью и возвращать её ОС, когда она больше не нужна. На моём стенде с 8 ГБ данными потребление памяти после рестарта составило 8,1 ГБ — против 12,3 ГБ в Dragonfly.
Поддерживает ли KiviDB кластеризацию и репликацию?
На текущий момент KiviDB работает в режиме одиночного инстанса. Репликация и кластеризация планируются в roadmap, но пока нет даже альфа-версии. Если вам нужна отказоустойчивость, придётся использовать внешние решения: например, запускать KiviDB за HAProxy с активной-пассивной конфигурацией, или использовать Redis Sentinel для фейковой репликации. Это не идеально, но для большинства use-case, где важна скорость, а не отказоустойчивость, такой подход работает.