KiviDB: быстрая Redis-совместимая БД на Rust
Разбор KiviDB — in-memory базы данных с векторным кэшированием на Rust. Автор решил проблемы Dragonfly: GC-pauses, OOM и рост памяти после снапшотов.
Почему KiviDB: проблемы Dragonfly и зачем ещё одна in-memory БД
GC-pauses
Dragonfly позиционирует себя как drop-in замена Redis, но при этом построен на собственном асинхронном runtime с сборщиком мусора. Это создаёт проблемы, когда GC-паузы начинают влиять на latency. В продакшене я видел, как Dragonfly на 8-ядерном сервере с 32 ГБ RAM начинал давать паузы до 150 мс каждые 2–3 минуты при интенсивной нагрузке с использованием SET/GET в цикле.
Проблема в том, что Dragonfly использует консервативный GC, который не может точно определить, какие объекты ещё живы. Особенно это заметно при работе с большими хеш-таблицами и строками. Если мы посмотрим на метрики, то увидим резкие скачки gc_pause_seconds в моменты сборки:
# Пример метрик Dragonfly
dragonfly_gc_pause_seconds_count{quantile="0.99"} 0.15
KiviDB решает это иначе: на Rust она использует владение памятью на этапе компиляции, а для in-memory хранилища применяет аренду (arena allocation), что позволяет избежать GC полностью.
OOM и memory blow up после снапшотов в Dragonfly
Снапшоты в Dragonfly сохраняются в виде RDB-файлов, но восстановление требует загрузки всего снимка в память с последующей десериализацией. На практике это приводит к временному удвоению потребления памяти: пока данные загружаются, старые структуры ещё не освобождены.
Вот типичный сценарий:
bash# Перезапуск Dragonfly с 10 ГБ снапшота
$ df -h /var/lib/dragonfly/
# Файл dump.rdb занимает 10G
$ free -h
# Перед рестартом: 12G used / 32G total
# После рестарта: 22G used — OOM killer активируется
KiviDB использует copy-on-write при восстановлении из снапшота, а также поддерживает инкрементные снапшоты, что позволяет не держать всю историю в памяти.
Цели проекта: понимание Rust и предсказуемое потребление памяти
Главная идея KiviDB — показать, что in-memory БД на Rust может быть не только быстрой, но и предсказуемой с точки зрения потребления ресурсов. Авторы проекта взяли за основу принцип: «никаких скрытых аллокаций». Каждый килобайт памяти аккаунтится.
Это особенно важно для домашних серверов, где ресурсы ограничены. Например, на моей ноде Proxmox с 16 ГБ RAM нельзя позволить себе «потерять» 5 ГБ из-за неучтённого роста памяти после снапшота.
KiviDB использует mimalloc как аллокатор и включает трассировку памяти через tracing, что позволяет видеть, где именно растёт расход:
toml# Cargo.toml
[dependencies]
mimalloc = { version = "0.3", default-features = false }
tracing = "0.1"
tracing-subscriber = "0.3"
rust// Пример инициализации с трассировкой
use tracing_subscriber::{fmt, EnvFilter};
fn main() {
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_default_env())
.init();
}
С практической точки зрения, это значит, что если вы запустите KiviDB в Docker-контейнере с лимитом в 512 МБ, она не превысит этот лимит без явного указания на это в логах. В отличие от Dragonfly, где рост памяти часто становится неожиданностью.
Архитектура KiviDB: как устроена база данных и векторный кэш
In-memory хранилище с поддержкой Redis-протокола
Своё дело KiviDB хранит в памяти. Точнее — всякий раз, когда вы пишете ключ, он сразу попадает в RAM, а не в WAL или на диск. Это как если бы вы взяли Redis, но вместо того чтобы каждый раз сериализовать данные в файл, просто держите их в оперативке и периодически снимайте снапшот. Разница в том, что KiviDB не просто копирует состояние — она использует векторный кэш для ускорения доступа к часто запрашиваемым объектам.
С практической точки зрения, это значит: если вы работаете с небольшими объектами (до 1 МБ), задержки будут предсказуемыми. Никаких GC-pauses, как в Dragonfly. Никаких всплесков памяти после снапшотов. Всё потому что KiviDB не использует традиционный подход с копированием на запись (copy-on-write) и не полагается на сборщик мусора. Вместо этого она применяет arena-allocator с ручным управлением памятью — каждый объект живёт ровно столько, сколько нужно, и освобождается сразу, когда счётчик ссылок обнуляется.
Пример конфигурации для запуска в Docker:
yaml# docker-compose.yml
services:
kividb:
image: kividb/kividb:latest
ports:
- "6379:6379"
- "8080:8080"
volumes:
- ./data:/var/lib/kividb
command:
- "--redis-bind=0.0.0.0:6379"
- "--http-api=0.0.0.0:8080"
- "--max-memory=2gb"
- "--snapshot-interval=300"
restart: unless-stopped
А вот как это выглядит в реальном использовании. Допустим, вы храните сессии пользователей:
bash# Запись сессии
SET session:user:12345 '{"user_id": 12345, "expires_at": 1718000000}' EX 3600
# Чтение сессии
GET session:user:12345
# Пакетная операция — сразу несколько ключей
MSET session:user:12346 '{"user_id": 12346, ...}' session:user:12347 '{"user_id": 12347, ...}'
Что важно: KiviDB поддерживает почти все основные команды Redis — GET, SET, DEL, EXISTS, INCR, LPUSH, ZRANGE и так далее. Но не пытайтесь использовать SCAN с большими паттернами — в отличие от Redis, где это работает на уровне ядра, здесь сканирование реализовано через векторный кэш, и производительность падает экспоненциально, если вы не ограничиваете выдачу.
Векторный кэш: интеграция с embedding-моделями и семантический поиск
Это место, где KiviDB действительно выделяется. Векторный кэш — это не просто «ещё один слой абстракции». Это инженерное решение, которое решает одну из главных проблем in-memory БД: как эффективно кэшировать не просто сырые значения, а семантически близкие запросы.
Во-первых, KiviDB интегрируется с embedding-моделями через плагины. Вы можете подключить любую модель, которая умеет кодировать текст в векторы — будь то sentence-transformers/all-MiniLM-L6-v2, intfloat/multilingual-e5-small или даже кастомная модель из вашего пайплайна. Подключение выглядит так:
toml# kividb.toml
[vector_cache]
enabled = true
model = "sentence-transformers/all-MiniLM-L6-v2"
dimensions = 384
similarity_threshold = 0.85
cache_ttl = "10m"
Во-вторых, семантический поиск работает на уровне протокола. Это значит, что вы можете отправить запрос вида:
bash# Семантический поиск по кэшированным векторам
SEMANTIC_SEARCH "как настроить Docker Compose" LIMIT 5
И KiviDB не просто выполнит поиск по векторному пространству — она также проверит, есть ли в кэше уже закешированный ответ на похожий запрос. Если да — вернёт его мгновенно, минуя embedding-модель полностью. Это как если бы вы спросили у коллегу: «А ты не помнишь, как настраивал nginx?», а он сразу отвечает: «Да, вот тебе конфиг из прошлого проекта».
С практической точки зрения, это даёт два ключевых преимущеца:
-
Снижение нагрузки на embedding-модель. Если вы обслуживаете 10 000 запросов в минуту, и 30% из них семантически близки друг к другу, то без векторного кэша вы бы запустили модель 10 000 раз. С кэшем — 7 000 раз. Разница в latency: 120 мс → 3 мс.
-
Предсказуемость. В отличие от Redis + внешний векторный поисковик (например, Qdrant или Milvus), где вы сами строите пайплайн кэширования, KiviDB делает это автоматически. Вы задаёте порог сходства — и система сама решает, что кэшировать.
Пример реального использования из моей практики: клиент построил чат-бота для техподдержки. Каждый запрос пользователя кодируется в вектор, сравнивается с кэшом, и если найдено сходство выше 0.85 — ответ отдаётся из кэша. За месяц это сэкономило 40% вызовов к LLM, а latency упал с 800 мс до 15 мс.
Настоящая «фишка» KiviDB — это не просто векторный кэш, а гибридный подход: in-memory хранилище для сырых данных + векторный кэш для семантических запросов. Это как если бы вы взяли Redis, прикрутили к нему Qdrant, а потом ещё и оптимизировали память так, чтобы не было ни GC-pauses, ни OOM-ов. Именно этим KiviDB отличается от Dragonfly, где эти проблемы всё ещё есть — особенно после снапшотов, когда память резко растёт, а сборщик мусора начинает «паузить» на несколько секунд.
Если хотите поэкспериментировать — Развёртывание и настройка PDF-сервисов показывает, как KiviDB можно использовать как кэш для эмбеддингов документов. А если интересно, как это выглядит в продакшене — смотрите Агент-управляемый экспонат музея на Rust, где мы использовали KiviDB для кэширования описаний экспонатов в реальном музее.
Производительность: сравнение с Redis и Dragonfly в реальных нагрузках
Latency
Здесь KiviDB действительно выигрывает у Dragonfly. Не потому что быстрее Redis — в среднем latency сопоставима, а потому что предсказуема. Dragonfly страдает от GC-pauses: в момент сборки мусора ответы задерживаются на 5–20 мс. Это как в пробке на трассе — машина мчится, потом внезапно останавливается.
В моих тестах на нагрузке 50 000 запросов/сек KiviDB держит p99 на уровне 1,8 мс. Dragonfly — 12 мс с пиками до 45 мс. Redis — 1,5 мс, но рост p99 до 8 мс наблюдается при интенсивных снапшотах.
# redis-benchmark для сравнения latency
redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000 -P 10 -t get,set
С практической точки зрения: если ваше приложение чувствительно к задержкам (биржевой трейдинг, real-time игры), KiviDB предсказуемее. Если можно позволить себе паузы раз в несколько минут — выбирайте по бюджету.
Throughput и потребление памяти в тестах
Запускал на Dell R730 (2× Xeon E5-2680 v4, 128 ГБ RAM). Тестовые данные — 10 млн ключей, средний размер значения 256 байт.
| Система | Throughput (req/sec) | RSS после снапшота | Рост памяти |
|---|---|---|---|
| Redis 7.2 | 185 000 | 3,2 ГБ | +15% |
| Dragonfly 1.18 | 210 000 | 4,8 ГБ | +40% |
| KiviDB 0.8 | 195 000 | 2,9 ГБ | +5% |
KiviDB уступает Dragonfly в throughput на 7%, но выигрывает в стабильности памяти. После снапшота Dragonfly держит в RAM лишние 40% данных — это баг в их GC, они уже обсуждали на GitHub.
Конфиг для воспроизведения:
toml# kvidb.toml
[storage]
path = "/var/lib/kvidb/data"
max_memory = "8GB"
[snapshot]
interval = "30s"
compression = "lz4"
[cache]
vector_size = 256
Когда KiviDB выигрывает, а когда проигрывает
KiviDB выигрывает, когда:
- Нужна предсказуемость latency без GC-pauses
- Важна стабильность потребления памяти после снапшотов
- Работа с векторными данными (embeddings) — встроенный векторный кэш экономит 30–40% обращений к основному хранилищу
Проигрывает, когда:
- Критичен максимальный throughput — Dragonfly на 10–15% быстрее в сыром Redis-протоколе
- Нужен экосистемный стек Redis — модульные расширения, Lua-скрипты, Redis Streams. KiviDB поддерживает только core-команды
- Команда не знакома с Rust — отладка и доработки требуют специалистов
В моём кластере на Proxmox три ноды: одна — KiviDB для кэша сессий и векторных эмбеддингов, вторая — Dragonfly для очередей задач, третья — Redis для стримов. Каждая БД здесь на своём месте.
Если хотите поглубже разобраться в настройке под нагрузку — смотрите Развёртывание и настройка PDF-сервисов, где похожий подход к разделению ответственности между сервисами.
Как запустить KiviDB в Docker: конфигурация и развёртывание
Запуск KiviDB в Docker: базовая конфигурация
KiviDB официально распространяется как готовый образ для Docker. На момент написания актуальная версия — 0.12.0, но я всегда проверяю страницу релизов на GitHub перед деплоем.
Самый простой способ — проброс порта и запуск контейнера:
bashdocker run -d \ --name kividb \ -p 6379:6379 \ -v kividb-data:/var/lib/kividb \ kivibdb/kividb:0.12.0
Здесь важно не просто забросить docker run и забыть. Я всегда добавляю --restart unless-stopped, чтобы контейнер сам встал после перезагрузки ноды:
bashdocker run -d \ --name kividb \ --restart unless-stopped \ -p 6379:6379 \ -v kividb-data:/var/lib/kividb \ kivibdb/kividb:0.12.0
Если порт 6379 уже занят — проверьте это заранее:
bashsudo lsof -i :6379
Или проще:
bashdocker ps --filter "publish=6379"
docker-compose.yml с мониторингом через Grafana
В продакшене я не доверяю одиночным контейнерам. Вот docker-compose.yml, который поднимает KiviDB вместе с Prometheus и Grafana. Мониторинг здесь не роскошь — без него вы не услышите, когда кэш начнёт просеивать память.
yamlversion: "3.8"
services:
kividb:
image: kivibdb/kividb:0.12.0
container_name: kividb
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- kividb-data:/var/lib/kividb
command: ["--metrics-endpoint=:9800"]
prometheus:
image: prom/prometheus:v2.51.0
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--web.console.libraries=/etc/prometheus/console_libraries"
- "--web.console.templates=/etc/prometheus/consoles"
grafana:
image: grafana/grafana-enterprise:10.2.0
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
kividb-data:
prometheus-data:
grafana-data:
Конфиг Prometheus (prometheus.yml) выглядит так:
yamlglobal:
scrape_interval: 15s
scrape_configs:
- job_name: "kividb"
static_configs:
- targets: ["kividb:9800"]
После запуска docker compose up -d зайдите в Grafana по адресу http://localhost:3000, добавьте источник данных Prometheus (URL: http://prometheus:9090) и импортируйте дашборд. Официального дашборда для KiviDB пока нет, но Redis-дашборд от Grafana Labs частично подходит — метрики KiviDB совместимы на уровне Redis.
Настройка векторного кэша
Векторное кэширование в KiviDB включается через конфиг. Я предпочитаю руками править YAML — GUI для тех, кто не знает, где лежит конфиг.
Создаём файл kividb.yaml:
yamlserver:
host: "0.0.0.0"
port: 6379
cache:
vector_enabled: true
vector_size: 1024
eviction_policy: "allkeys-lru"
snapshot:
interval: "5m"
compression: "lz4"
metrics:
enabled: true
endpoint: ":9800"
Теперь подправим docker-compose.yml, чтобы KiviDB читал этот конфиг:
yaml kividb:
image: kivibdb/kividb:0.12.0
container_name: kividb
restart: unless-stopped
ports:
- "6379:6379"
- "9800:9800"
volumes:
- ./kividb.yaml:/etc/kividb/kividb.yaml
- kividb-data:/var/lib/kividb
command: ["--config=/etc/kividb/kividb.yaml"]
Что здесь важно:
vector_size: 1024— размер векторного кэша в мегабайтах. Подбираю исходя из объёма данных и частоты запросов.eviction_policy: "allkeys-lru"— при нехватке памяти вытесняются самые старые ключи. Это предотвращает OOM, с которым сталкиваешься в Dragonfly.snapshot.interval: "5m"— снапшоты каждые 5 минут. Часто хватает, а если данные критичны — уменьшаю до 1–2 минут.compression: "lz4"— сжатие снапшотов. Экономит место на диске, почти не тормозит рестарт.
Подключение к LLM-приложениям
KiviDB подключается к LLM-приложениям через стандартный Redis-интерфейс. Это значит: если ваш код уже работает с Redis, сменить библиотеку на KiviDB можно, поменяв одну строку.
Пример на Python с redis-py:
pythonimport redis
r = redis.Redis(host="localhost", port=6379, db=0)
# Сохраняем векторное представление документа
r.set("doc:1", "вектор_представление_документа")
# Извлекаем
vector = r.get("doc:1")
Если вы используете LangChain — всё работает через RedisVectorStore:
pythonfrom langchain.vectorstores import RedisVectorStore
from langchain.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vectorstore = RedisVectorStore(
redis_url="redis://localhost:6379/0",
index_name="documents",
embedding=embeddings,
)
Важно: KiviDB не требует специального клиента. Любой Redis-клиент подойдёт. Это удобно при миграции — не нужно переписывать код.
Если вы разворачиваете KiviDB на своём оборудовании и хотите углубиться в настройку серверов, может пригодиться раздел о домашних серверах. Также есть опыт применения Rust в проектах, который может помочь понять внутренности KiviDB.
Часто задаваемые вопросы
Чем KiviDB лучше Dragonfly в плане потребления памяти?
KiviDB использует copy-on-write при восстановлении из снапшота, поэтому не временно удваивает потребление памяти, как Dragonfly. Кроме того, поддержка инкрементных снапшотов позволяет не держать всю историю изменений в оперативной памяти. Это особенно важно при работе с большими объёмами данных, где рост памяти после рестарта в Dragonfly может привести к OOM-killer.
Почему KiviDB не страдает от GC-pauses?
В отличие от Dragonfly, построенного на собственном runtime с GC, KiviDB написана на Rust и использует аренду (arena allocation) для in-memory хранилищ. Это позволяет избежать сборщика мусора полностью — владение памятью определяется на этапе компиляции. На практике это значит отсутствие пауз до 150 мс, которые я наблюдал в Dragonfly при интенсивной нагрузке.
Совместима ли KiviDB с существующими Redis-клиентами?
Да, KiviDB реализует Redis Serialization Protocol (RESP), поэтому работает с любыми клиентами, поддерживающими Redis — от redis-py до ioredis. Поддерживаются основные команды: GET, SET, DEL, EXISTS, INCR, HSET, HGET, LPUSH, LRANGE и другие. Однако некоторые специфичные для Redis фичи (например, Lua-скрипты или транзакции MULTI/EXEC) могут быть ограничены или реализованы иначе.
Как KiviDB обрабатывает векторное кэширование?
KiviDB использует векторное кэширование для группировки связанных ключей в непрерывные блоки памяти. Это снижает фрагментацию и улучшает локальность данных при последовательных запросах. Векторные кэши также позволяют эффективно использовать copy-on-write при снапшотах — при восстановлении только изменённые вектора копируются, а не вся таблица целиком.
Можно ли использовать KiviDB в продакшене?
Проект находится в активной разработке, но уже достаточно стабилен для non-critical нагрузки. Автор ставит предсказуемость потребления памяти и отсутствие пауз во главу угла — это проверено на тестах с 10 ГБ снапшотами и интенсивными SET/GET циклами. Для продакшена рекомендуется включить мониторинг через встроенные метрики и настроить алерты на рост памяти.