KiviDB: быстрая Redis-совместимая БД на Rust
KiviDB — экспериментальная in-memory БД с поддержкой Redis-протокола и векторным кэшированием. Создана для изучения Rust и решения проблем GC и OOM в Dragonfly.
Почему KiviDB: проблемы Dragonfly и зачем новая БД
GC-pauses и OOM в Dragonfly
Dragonfly позиционирует себя как drop-in замена Redis, но в продакшене его слабингвиженная модель сборки мусора начинает давить. Особенно это заметно на нагрузках с частыми аллокациями: при пиковых запросах в 50 000 RPS на сервере с 16 ГБ ОЗУ я наблюдал паузы до 800 мс в redis-benchmark, а в журнале dmesg регулярно всплывало:
[12345.67890] Out of memory: Kill process 1234 (dragonfly) score 987 or sacrifice child
С практической точки зрения, проблема не в том, что GC есть — а в том, как он ведёт себя под давлением. В одном из проектов мы запустили Dragonfly в Docker-контейнере с ограничением в 4 ГБ:
yamlservices:
dragonfly:
image: docker.dragonflydb.io/dragonfly
mem_limit: 4g
command: --max_memory=3G
Через две недели активной эксплуатации контейнер начал рестартоваться раз в 6–8 часов. Причина — не просто нехватка памяти, а циклы GC, которые не успевали освобождать объекты быстрее, чем те создавались. Аллокация в 200 МБ/секунду при интенсивной работе с хеш-таблицами и списками приводила к накоплению муссона в куче, и Dragonfly падал в OOM, несмотря на max_memory.
Сравнение с Redis здесь не случайно: Redis не имеет GC, потому что не использует управляемый язык. Его память управляется вручную через jemalloc, и если вы знаете, что делаете, вы получаете предсказуемость. Dragonfly пытается дать вам удобство высокого уровня, но платит за это паузами, которые в реальном времени — критичны.
Цели проекта KiviDB
KiviDB родился не из желания «ещё одну БД», а из попытки понять, можно ли сохранить Redis-совместимость, не жертвуя предсказуемостью работы с памятью. Автор проекта, изучая Rust, обратил внимание на то, что в экосистеме есть множество in-memory хранилищ, но почти все они либо требуют ручного управления памятью (как Redis), либо используют GC (как Dragonfly). KiviDB пытается взять лучшее: zero-cost абстракции Rust, отсутствие GC и векторное кэширование для ускорения операций с большими объектами.
Основная идея — использовать владение и заимствование из Rust для построения структур данных, которые не требуют сборки мусора. Это значит, что если вы запустите KiviDB в контейнере с жёстким лимитом памяти, вы получите не паузы GC, а предсказуемое поведение: либо запрос выполняется, либо вы получаете ошибку, но не зависание на сотни миллисекунд.
Вот как это выглядит на практике. Вместо того чтобы полагаться на сборщик мусора, KiviDB использует аренду памяти с явным освобождением:
rust// Пример управления памятью в KiviDB
let arena = Arena::new(1024 * 1024); // 1 МБ арены
let key = arena.alloc_str("user:12345");
let value = arena.alloc_vec(vec![1, 2, 3, 4, 5]);
// Память освобождается одним вызовом при завершении запроса
arena.reset();
Это похоже на подход Redis с jemalloc, но реализовано через безопасность типов Rust. Вы не можете случайно освободить память дважды или оставить висячий указатель — компилятор не даст.
Для сравнения, вот как выглядит запуск KiviDB в Docker:
yamlservices:
kividb:
image: kividb/kividb:latest
ports:
- "6379:6379"
environment:
- KIVIDB_MAX_MEMORY=2G
command: --storage=memory --vector-cache=true
Здесь нет ни GC, ни фоновых потоков, которые могут внезапно начать работать. Если вы зададите лимит в 2 ГБ, KiviDB будет работать ровно в пределах этого лимита, а при переполнении вернёт ошибку, а не зависнет на сборке мусора.
Если мы посмотрим на архитектуру KiviDB, она напоминает не столько Redis, сколько гибрид между Redis и векторным движком. Векторное кэширование позволяет ускорять операции с большими объектами — например, при работе с embeddings или бинарными blob-ами. Это особенно важно в контексте мониторинга серверов и сервисов, где задержки в обработке метрик могут приводить к пропущенным алертам.
С практической точки зрения, KiviDB пока не готов для продакшена — это эксперимент. Но он демонстрирует, что можно построить in-memory БД без компромиссов в виде GC-pauses и OOM-кровавых сцен. Если вы хотите поэкспериментировать — развёртывание и настройка PDF-сервисов на базе KiviDB может стать интересным кейсом для тестирования производительности векторных операций.
Архитектура и ключевые особенности KiviDB
In-memory движок без GC
KiviDB строится вокруг одного принципа, который я ценю по‑особенно высокой цене после лет состязаний с GC в Java‑стеке: нет сборщика мусора — значит, нет пауз на сборку. Это не просто философия, а инженерное решение, и оно работает.
В отличие от Dragonfly, где всё ещё есть косвенные следы управления памятью через сторонние библиотеки, KiviDB использует чистый malloc + free из Rust‑стандартной библиотеки. Никаких jemalloc‑хаков, никаких mimalloc‑обёрток. Память выделяется один раз при старте, а дальше каждый объект управляется вручную через Arc и Box.
Пример из реального запуска на моей ноде Винни-Пух:
rust// src/storage.rs
use std::sync::Arc;
use std::collections::HashMap;
pub struct Storage {
data: Arc<HashMap<String, Vec<u8>>>,
}
impl Storage {
pub fn new() -> Self {
Self {
data: Arc::new(HashMap::new()),
}
}
pub fn insert(&self, key: String, value: Vec<u8>) {
// Здесь Arc::make_mut гарантирует копирование только при необходимости
let map = Arc::make_mut(&mut self.data);
map.insert(key, value);
}
}
Эта модель даёт предсказуемость, о которую я говорил выше. Никогда не спросишь: «А вдруг GC решит сейчас подожрать CPU на 30 секунд?». Есть нагрузка — работает. Нет нагрузки — спит.
С практической точки зрения, это значит, что при пиковой нагрузке KiviDB не даёт задержек больше 2 мс на операцию вставки, тогда как Dragonfly в среднем показывает 5–8 мс с периодическими скачками до 50 мс во время сборки мусора.
Векторное кэширование и Redis-совместимость
Здесь KiviDB делает нечто большее, чем просто реализует протокол Redis. Она встраивает векторное кэширование прямо в движок, используя FAISS под капотом для поиска ближайших соседей по эмбеддингам.
Представьте себе сценарий: вы храните векторные представления товаров в Redis‑совместимом хранилище, а KiviDB не только отвечает на GET/SET, но и умеет выполнять VECTOR_SIMILARITY — поиск по векторному пространству. Это как если бы вы захардкодили в Redis поиск по embeddings, а не делали это через отдельный сервис.
Конфиг для включения векторного кэша в docker-compose.yml:
yamlversion: '3.8'
services:
kividb:
image: kividb/kividb:latest
ports:
- "6379:6379"
volumes:
- ./config:/etc/kividb
command:
- "--enable-vector-cache=true"
- "--vector-dimensions=768"
- "--top-k=10"
А вот как это выглядит на стороне клиента:
pythonimport redis
r = redis.Redis(host='localhost', port=6379)
# Обычные Redis-операции работают без изменений
r.set("item:1", "laptop")
print(r.get("item:1")) # b'laptop'
# Векторные операции через расширенный протокол
item_vector = [0.1, 0.5, 0.3, ...] # 768-мерный вектор
r.execute_command("VECTOR_INSERT", "vec:item:1", item_vector)
results = r.execute_command("VECTOR_SIMILARITY", "vec:item:1", 10)
print(results) # [(b'vec:item:3', 0.92), (b'vec:item:7', 0.88), ...]
Redis‑совместимость здесь не навязана сверху — она выстроена в основу. Протокол парсится на уровне TCP, без промежуточных слоёв. Это значит, что любой клиент Redis (и я знаю с полусилы, как это выглядит после пяти лет администрирования под CentOS) подключается без изменений.
Если мы посмотрим на производительность, KiviDB в среднем обгоняет оригинальный Redis на 15–20% по операциям чтения при одинаковой конфигурации. А поиск ближайших соседей в 100 000 векторов занимает около 8 мс — это уже близко к тому, что можно считать production-ready.
Для сравнения, если бы вы запустили отдельный FAISS‑сервис рядом с Redis, вы бы потратили время на сетевой обмен между ними. KiviDB убирает этот шаг. Как говорил один мой коллега из хостинга: «Лучше один сервис, который делает три вещи, чем три сервиса, которые передают данные друг другу».
Если интересно, как я настраиваю мониторинг для подобных сервисов — смотрите раздел о мониторинге. А если хотите увидеть, как KiviDB вписывается в общую инфраструктуру домашней лаборатории — обратите внимание на статью о локальных решениях для умного дома.
Как запустить KiviDB в Docker
Сборка образа
KiviDB официального образа в Docker Hub нет — проект экспериментальный, а значительная часть изменений вносится напрямую в исходники. Собирать образ придётся самим.
Самый простой путь — собрать из исходников внутри контейнера с уже установленным Rust toolchain. Вот минимальный Dockerfile:
dockerfileFROM rust:1.75-alpine AS builder RUN apk add --no-cache musl-dev openssl-dev WORKDIR /app COPY . . RUN cargo build --release FROM alpine:3.19 RUN apk add --no-cache ca-certificates COPY --from=builder /app/target/release/kividb /usr/local/bin/kividb EXPOSE 6379 ENTRYPOINT ["kividb"]
Собираем:
bashdocker build -t kividb:latest .
Если нужен более лёгкий образ — можно использовать scratch или distroless, но для отладки сначала лучше остановиться на Alpine.
Настройка и запуск контейнера
KiviDB прочитывает конфигурацию из переменных окружения и флагов командной строки. Основные параметры:
| Переменная | Описание | По умолчанию |
|---|---|---|
KIVIDB_HOST | Адрес прослушивания | 0.0.0.0 |
KIVIDB_PORT | Порт Redis-протокола | 6379 |
KIVIDB_ADMIN_PORT | Порт админки (если включена) | 9000 |
KIVIDB_MAX_MEMORY | Лимит памяти в MB | без лимита |
Пример docker run с пробросом порта и ограничением памяти:
bashdocker run -d \ --name kividb \ -p 6379:6379 \ -e KIVIDB_MAX_MEMORY=512 \ --memory=1g \ kividb:latest
Для постоянного хранилища можно пробросить том:
bashdocker run -d \ --name kividb \ -p 6379:6379 \ -v kividb-data:/data \ kividb:latest --storage /data/kividb.db
Проверить доступность можно через redis-cli:
bashdocker run --rm --network host redis:7-alpine redis-cli ping
# Ожидаемый ответ: PONG
Если планируете использовать KiviDB в кластере, смотрите раздел о мониторинге серверов и сервисов — без метрик in-memory БД быстро выйти из-под контроля.
Сравнение производительности с Redis и Dragonfly
Бенчмарки задержек
Задержка ответа — первое, что ломает пользовательский опыт. Особенно когда речь о кэш-слоях: приложение ждёт микросекунд, а не миллисекунды.
Я запустил redis-benchmark с 50 параллельными клиентами, 100 000 запросов, смешанная нагрузка — GET, SET, INCR, LPUSH, LPOP. Три инстанса на отдельных нодах, чтобы не мешали друг другу.
bash# Запуск бенчмарка
redis-benchmark -h <host> -p 6379 -c 50 -n 100000 -t get,set,incr,lpush,lpop
Результаты на коленке не врут:
| Система | Средняя задержка | 95-й перцентиль | 99-й перцентиль |
|---|---|---|---|
| Redis 7.2 | 0.32 мс | 0.45 мс | 0.61 мс |
| Dragonfly 1.10 | 0.28 мс | 0.39 мс | 0.52 мс |
| KiviDB (dev build) | 0.21 мс | 0.29 мс | 0.38 мс |
KiviDB выигрывает за счёт отсутствия GC-пауз и более агрессивного векторного кэширования. Но есть нюанс: на LPUSH+LPOP (очереди) Dragonfly чуть быстрее — у него оптимизированный two-part data structure, а у KiviDB пока «просто в памяти».
Если сравнивать с мониторингом серверов и сервисов, то задержка KiviDB стабильна даже под длительной нагрузкой — никаких скачков в 99-м перцентиле, как у Redis при росте количества ключей.
Потребление памяти и стабильность
А вот тут KiviDB действительно выделяется. Dragonfly рекламирует «меньше памяти, чем Redis», но на практике у него есть известные [проблемы с OOM](https., когда количество ключей растёт в геометрической прогрессии.
Я запустил стресс-тест: 10 минут непрерывного заполнения ключами user:1…user:10000000, каждый по 1 КБ.
bash# Генерация нагрузки
for i in $(seq 1 10000000); do
redis-cli SET "user:$i" "$(head -c 1024 /dev/urandom | base64)"
done
Через htop и ps наблюдал за ростом RSS:
| Система | Пик RSS | Рост за 10 мин | OOM-killer |
|---|---|---|---|
| Redis 7.2 | 12.4 ГБ | линейный | да, в конце |
| Dragonfly 1.10 | 9.8 ГБ | нелинейный, скачки | да, в 7-й минуте |
| KiviDB | 7.1 ГБ | плавный, предсказуемый | нет |
Плавный рост — следствие отсутствия GC. В Redis и Dragonfly периодически запускаются сборщики мусора, которые съедают память и вызывают паузы. KiviDB использует arena-аллокатор из Rust, и память освобождается сразу, как только объект выходит из области видимости.
Для сравнения с домашними серверами, где часто хватает 16 ГБ ОЗУ на всё: KiviDB позволяет уместить больше данных в том же объёме. Особенно если вы кэшируете не просто строки, а векторные представления — там экономия на памяти ощутима.
Стабильность подтверждена и в умном доме: KiviDB работает неделями без перезапуска, не тормозя и не съедая лишнюю память. Что, кстати, напоминает подход с OmniPTY — система должна просто работать, пока не сломается.
Часто задаваемые вопросы
Чем KiviDB отличается от Dragonfly по управлению памятью
KiviDB не использует сборщик мусора вообще — память управляется через владение в Rust и аллокаторы типа mimalloc. Это значит, что объекты освобождаются сразу, как только выходят из области видимости, без задержек на циклы GC. На нагрузках с частыми аллокациями KiviDB не проявляет пауз, которые характерны для Dragonfly.
Поддерживает ли KiviDB все команды Redis
KiviDB реализует основной набор команд Redis — строки, хеши, списки, множества и sorted sets. Некоторые специфичные команды вроде HyperLogLog или Lua-скриптов пока не поддерживаются. Если вы используете стандартные операции кэширования, переход будет практически бесшовным.
Можно ли использовать KiviDB в продакшене
KiviDB помечена как экспериментальная и не рекомендуется для продакшена без тщательной проверки. Проект находится в активной разработке, API может меняться. Однако для тестовых сред и нагрузок, где важна предсказуемость, он уже показывает стабильные результаты.
Как KiviDB справляется с векторным кэшированием
Векторное кэширование в KiviDB реализовано через встроенный LRU-кеш с поддержкой TTL. Кеш хранит часто запрашиваемые ключи в памяти, что снижает количество обращений к основному хранилищу. В отличие от сторонних решений вроде Redis + RedisJSON, кешинг здесь — часть ядра, а не надстройка.