Продвинутый

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

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

KiviDB — экспериментальная in-memory БД с поддержкой Redis-протокола и векторным кэшированием. Создана для изучения Rust и решения проблем GC и OOM в Dragonfly.

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 ГБ:

yaml
services:
  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:

yaml
services:
  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:

yaml
version: '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"

А вот как это выглядит на стороне клиента:

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

dockerfile
FROM 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"]

Собираем:

bash
docker 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 с пробросом порта и ограничением памяти:

bash
docker run -d \
  --name kividb \
  -p 6379:6379 \
  -e KIVIDB_MAX_MEMORY=512 \
  --memory=1g \
  kividb:latest

Для постоянного хранилища можно пробросить том:

bash
docker run -d \
  --name kividb \
  -p 6379:6379 \
  -v kividb-data:/data \
  kividb:latest --storage /data/kividb.db

Проверить доступность можно через redis-cli:

bash
docker 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.20.32 мс0.45 мс0.61 мс
Dragonfly 1.100.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.212.4 ГБлинейныйда, в конце
Dragonfly 1.109.8 ГБнелинейный, скачкида, в 7-й минуте
KiviDB7.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, кешинг здесь — часть ядра, а не надстройка.

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

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