Продвинутый

KiviDB: быстрая Redis-совместимая БД на Rust

Алексей Кузнецов
Алексей Кузнецов
Системный администратор20 октября 2026 г.12 мин чтения

Разбор KiviDB — in-memory базы данных с векторным кэшированием, написанной на Rust. Решение проблем GC-pauses и OOM в Dragonfly, архитектура и практические к...

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), вы просто делаете:

bash
redis-cli -p 6380 HSET user:12345 embedding "0.12,0.45,0.78,..."

А поиск похожих пользователей — одной командой:

bash
redis-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, но без должной настройки вы сами себя не услышите, когда БД начнёт падать.

Сначала создайте директорию для проекта и файл композиции:

bash
mkdir 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:

bash
lsof -i :6379

Если порт занят, либо остановите конкурентный сервис, либо смените порт проброса:

yaml
ports:
  - "6380:6379"

Запуск:

bash
docker compose up -d

Проверить, что всё живо:

bash
docker exec -it kividb-local kividb-cli ping

Ожидаемый ответ: PONG.

Если планируете использовать KiviDB как drop-in replacement для Redis в приложениях, не забудьте указать в клиенте хост и порт. Например, для Python:

python
import 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

Активируем:

bash
sudo systemctl daemon-reexec
sudo systemctl enable kividb
sudo systemctl start kividb

Проверяем статус:

bash
sudo systemctl status kividb

Мониторинг через Prometheus

KiviDB экспортирует метрики в формате Prometheus. Чтобы собирать их, добавьте в конфиг Prometheus:

yaml
# prometheus.yml
scrape_configs:
  - job_name: 'kividb'
    static_configs:
      - targets: ['localhost:9090']

Перезапустите Prometheus:

bash
sudo 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.20.852.158 700
Dragonfly 1.181.022.849 300
KiviDB 0.3.10.410.9124 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 одновременных подключений с инкрементом счётчиков:

bash
redis-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, где важна скорость, а не отказоустойчивость, такой подход работает.

Поделиться:TelegramX / TwitterVK

Читайте также