Продвинутый

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

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

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

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?», а он сразу отвечает: «Да, вот тебе конфиг из прошлого проекта».

С практической точки зрения, это даёт два ключевых преимущеца:

  1. Снижение нагрузки на embedding-модель. Если вы обслуживаете 10 000 запросов в минуту, и 30% из них семантически близки друг к другу, то без векторного кэша вы бы запустили модель 10 000 раз. С кэшем — 7 000 раз. Разница в latency: 120 мс → 3 мс.

  2. Предсказуемость. В отличие от 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.2185 0003,2 ГБ+15%
Dragonfly 1.18210 0004,8 ГБ+40%
KiviDB 0.8195 0002,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 перед деплоем.

Самый простой способ — проброс порта и запуск контейнера:

bash
docker run -d \
  --name kividb \
  -p 6379:6379 \
  -v kividb-data:/var/lib/kividb \
  kivibdb/kividb:0.12.0

Здесь важно не просто забросить docker run и забыть. Я всегда добавляю --restart unless-stopped, чтобы контейнер сам встал после перезагрузки ноды:

bash
docker run -d \
  --name kividb \
  --restart unless-stopped \
  -p 6379:6379 \
  -v kividb-data:/var/lib/kividb \
  kivibdb/kividb:0.12.0

Если порт 6379 уже занят — проверьте это заранее:

bash
sudo lsof -i :6379

Или проще:

bash
docker ps --filter "publish=6379"

docker-compose.yml с мониторингом через Grafana

В продакшене я не доверяю одиночным контейнерам. Вот docker-compose.yml, который поднимает KiviDB вместе с Prometheus и Grafana. Мониторинг здесь не роскошь — без него вы не услышите, когда кэш начнёт просеивать память.

yaml
version: "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) выглядит так:

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

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

python
import redis

r = redis.Redis(host="localhost", port=6379, db=0)

# Сохраняем векторное представление документа
r.set("doc:1", "вектор_представление_документа")

# Извлекаем
vector = r.get("doc:1")

Если вы используете LangChain — всё работает через RedisVectorStore:

python
from 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 циклами. Для продакшена рекомендуется включить мониторинг через встроенные метрики и настроить алерты на рост памяти.

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

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