ScoriaDB

ScoriaDB Documentation v0.3.0

Links: GitHub API Reference

ScoriaDB читает 47 миллионов ключей в секунду — в 7.7 раз быстрее DragonflyDB, в 344 раза быстрее RocksDB, в 47 раз быстрее Pebble, в 118 раз быстрее BadgerDB. Записывает 2.09 миллиона в секунду (Group Commit) и 375 тысяч в секунду (Strict Sync). На 8 потоках, 16 ГБ RAM, NVMe SSD. С ACID-транзакциями, MVCC и нулевыми аллокациями в куче. Переживает отключение питания. Масштабируется линейно до 192 ядер. Один бинарник на 13 МБ без зависимостей. Клиенты на 13 языках.


Содержание

  1. 🚀 Быстрый старт
  2. 🔧 Руководство по эксплуатации
  3. ⚡ Производительность
  4. 🚀 Для чего подойдёт ScoriaDB
  5. ⚠️ Когда не стоит выбирать
  6. 🔌 Go Embedded API
  7. 🖥️ Сервер и командная строка
  8. 🌐 Клиентские библиотеки — полное руководство
  9. 🏛️ Архитектура
  10. 🧩 Возможности
  11. 🗺️ Roadmap

🚀 Быстрый старт

Что нужно: Go 1.23+. Настоятельно рекомендуется NVMe SSD — на SATA база работает, но пропускная способность записи ограничена скоростью диска. Кроссплатформенно: Linux, macOS, Windows, ARM64.

Запуск сервера

Откройте первый терминал. Скачайте репозиторий и соберите сервер:

git clone https://github.com/f4ga/ScoriaDB.git
cd ScoriaDB
go build -o scoria-server ./cmd/server

Запустите сервер (он должен остаться работать в этом терминале):

./scoria-server

Что происходит при запуске:

  1. Сервер создаёт директорию ./data (если её нет) и инициализирует LSM-движок
  2. Создаёт системную Column Family __auth__ для хранения пользователей
  3. Создаёт администратора по умолчанию: логин admin, пароль 2027
  4. Запускает gRPC-сервер на порту 50051 и REST-сервер на порту 8080

Сборка CLI и первые команды

Откройте второй терминал. Перейдите в папку с проектом и соберите CLI:

cd ~/ScoriaDB
go build -o scoria-cli ./cmd/cli

Получите JWT-токен (администратор по умолчанию: admin / 2027):

./scoria-cli admin auth admin 2027

Вы увидите: длинную строку — это ваш токен. Скопируйте его или сохраните в переменную.

Сохраните токен в переменную для удобства:

export TOKEN=$(./scoria-cli admin auth admin 2027)

Теперь выполните команды с токеном:

Записать ключ-значение:

./scoria-cli --token=$TOKEN set hello world

Вы увидите: OK

Прочитать значение по ключу:

./scoria-cli --token=$TOKEN get hello

Вы увидите: world

Удалить ключ:

./scoria-cli --token=$TOKEN del hello

Вы увидите: OK

⚠️ Важно: Токен живёт 24 часа. Команда admin auth — единственная, которая не требует токена. В продакшене обязательно смените пароль администратора.


Go Embedded (3 строки)

Встраивание в Go работает без сервера и без аутентификации:

go get github.com/f4ga/ScoriaDB/pkg/scoria@v0.3.0
package main

import "github.com/f4ga/ScoriaDB/pkg/scoria"

func main() {
    db, _ := scoria.NewScoriaDB("./data")
    defer db.Close()
    
    db.Put([]byte("hello"), []byte("world"))
    val, _ := db.Get([]byte("hello")) // "world"
}

🔧 Руководство по эксплуатации

Этот раздел для тех, кто уже запустил ScoriaDB и хочет понять, как ей управлять в реальной работе. Если вы ещё не запустили сервер — сначала Быстрый старт.


👤 Пользователи и аутентификация — полное руководство

ScoriaDB использует систему пользователей с ролями для контроля доступа. Это работает как в любой серьёзной БД: каждый, кто подключается, должен представиться, и система решает, что ему можно делать.


Что такое пользователь в ScoriaDB

Пользователь — это учётная запись, под которой кто-то подключается к базе. У каждого пользователя есть:

Где хранятся пользователи: В системной колоночной семействе __auth__. Она создаётся автоматически при первом запуске сервера. Вы не видите её в списке CF, но она есть.


Что такое роль и какие роли бывают

Роль — это набор разрешений. В ScoriaDB три роли:

Роль Что можно делать Кому давать
admin Всё: управление пользователями (создавать, удалять, менять пароли), управление CF (создавать, удалять), запуск GC, любые операции с данными (чтение, запись, удаление, сканирование) Только самым доверенным. Минимум 1 администратор всегда должен быть
readwrite Полный доступ к данным: чтение, запись, удаление, сканирование. НЕ может управлять пользователями и CF Сервисы и приложения, которые работают с данными
readonly Только чтение: get и scan. НЕ может изменять или удалять данные Аналитики, отчёты, системы мониторинга

Администратор по умолчанию

При первом запуске сервер автоматически создаёт администратора:

Логин: admin
Пароль: 2027
Роль: admin

⚠️ Это временный пароль. Обязательно смените его в продакшене!


Все сценарии работы с пользователями

Сценарий 1: Сменить пароль администратора

Когда нужно: Сразу после первого запуска. Обязательно в продакшене.

Как сделать:

./scoria-cli --token=$TOKEN admin change-password admin <новый-пароль>

Пример:

./scoria-cli --token=$TOKEN admin change-password admin Sc0ri@DB_2026!

Требования к паролю:


Сценарий 2: Создать пользователя для сервиса (readwrite)

Когда нужно: У вас есть микросервис или приложение, которое должно читать и писать данные.

Как сделать:

./scoria-cli --token=$TOKEN admin user-add app_backend securePass123 --roles=readwrite

Результат: Появится пользователь app_backend с ролью readwrite. Теперь этот сервис может подключаться к БД и работать с данными.

Почему это правильно: Сервис не должен иметь прав администратора. Если его взломают, злоумышленник не сможет управлять пользователями или удалять CF.


Сценарий 3: Создать пользователя для аналитика (readonly)

Когда нужно: Аналитик или дашборд должны смотреть данные, но не менять их.

Как сделать:

./scoria-cli --token=$TOKEN admin user-add analyst readOnlyPass789 --roles=readonly

Результат: Появится пользователь analyst с ролью readonly. Он может выполнять только get и scan. Любая попытка put или delete будет отклонена.

Почему это правильно: Аналитик не должен случайно или намеренно изменить данные. Это защита от ошибок и злоумышленников.


Сценарий 4: Создать второго администратора

Когда нужно: У вас крупная команда, и нужно, чтобы несколько человек могли управлять базой.

Как сделать:

./scoria-cli --token=$TOKEN admin user-add backup_admin backupPass456 --roles=admin

Результат: Появится второй администратор. Если первый потеряет пароль или уйдёт из компании, второй сможет управлять базой.


Сценарий 5: Посмотреть всех пользователей

Когда нужно: Проверить, кто есть в системе, или найти забытого пользователя.

Как сделать:

./scoria-cli --token=$TOKEN admin list-users

Пример вывода:

admin (admin)
app_backend (readwrite)
analyst (readonly)
backup_admin (admin)

Сценарий 6: Сменить пароль пользователю

Когда нужно: Пользователь забыл пароль, пароль скомпрометирован, или нужно обновить пароль по политике безопасности.

Как сделать:

./scoria-cli --token=$TOKEN admin change-password app_backend newSecurePass456

Важно: Это может сделать только администратор. Сам пользователь не может сменить свой пароль (пока что, в следующих версиях появится).


Сценарий 7: Что делать, если администратор потерял доступ

Проблема: Вы забыли пароль от admin или потеряли токен.

Решение: На данный момент восстановление пароля невозможно. Восстановите базу из бэкапа или удалите папку ./data и перезапустите сервер — тогда администратор создастся заново.

rm -rf ./data
./scoria-server

⚠️ Это удалит ВСЕ данные! Делайте только если есть бэкап.


Сценарий 8: Удалить пользователя

Когда нужно: Пользователь уволился, сервис больше не используется, или нужно сократить количество учётных записей.

Как сделать:

./scoria-cli --token=$TOKEN admin user-delete analyst

Важно: Нельзя удалить последнего администратора. Всегда должен быть хотя бы один admin.


Как работает JWT-токен

Что такое токен: Это строка, которую сервер выдаёт после успешной аутентификации. Она подтверждает, что вы — тот, за кого себя выдаёте.

Как он выглядит:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIl0sImV4cCI6MTc4NjAxMjU5MiwiaWF0IjoxNzg1OTI2MTkyfQ.9Zff4pnUJn4myyD7RKEd6QkUHTbIBVCejkG5YJaw21Y

Что внутри: Логин пользователя, его роли, время создания и время истечения.

Сколько живёт: 24 часа.

Что делать, если истёк: Получить новый через admin auth:

./scoria-cli admin auth admin 2027

Можно ли хранить токен в коде: Да, но безопаснее — в переменных окружения или менеджере секретов.


Как использовать токен в запросах

Через CLI:

./scoria-cli --token=$TOKEN get hello

Через REST:

curl -H "Authorization: Bearer <токен>" http://localhost:8080/get

Через gRPC: Добавляется в заголовок authorization с значением Bearer <токен>.


Частые ошибки и их решение

Ошибка Что значит Как исправить
authentication failed Неверный логин или пароль Проверьте логин/пароль
invalid authorization header Токен не передан или передан в неправильном формате Передавайте токен как Bearer <токен>
token expired Токен истёк (24 часа прошло) Получите новый токен через admin auth
permission denied У пользователя нет прав на эту операцию Проверьте роль пользователя
user not found Такой пользователь не существует Проверьте логин, создайте пользователя

Политика безопасности (рекомендации)

  1. Смените пароль администратора сразу после установки.
  2. Используйте сложные пароли — минимум 12 символов, заглавные, цифры, спецсимволы.
  3. Создавайте отдельных пользователей для каждого сервиса — не используйте admin для приложений.
  4. Минимизируйте количество администраторов — только те, кому действительно нужно.
  5. Регулярно проверяйте список пользователей — удаляйте тех, кто больше не нужен.
  6. Обновляйте пароли раз в 3–6 месяцев (в следующих версиях появится автоматическое истечение).
  7. Никогда не передавайте токен в открытом виде — используйте переменные окружения.

🧹 Сборка мусора (GC)

Что это: Value Log растёт с каждым обновлением и удалением. Старые значения остаются на диске. GC собирает их и освобождает место.

Когда запускать: При заполнении диска > 80%.

Команда:

./scoria-cli --token=$TOKEN admin gc

Что происходит:

  1. Сканируется LSM-дерево — находятся все живые ключи
  2. Живые значения копируются в новый Value Log
  3. Старый Value Log удаляется
  4. Указатели в SSTable обновляются

Сколько времени занимает: Зависит от объёма данных. Для 100 ГБ — от 1 до 10 минут.

Автоматический GC: В текущей версии v0.3.0 GC запускается только вручную. Автоматический появится в v0.5.0.


💾 Резервное копирование

Способ 1: Копирование файлов (простой, требует остановки)

# Остановить сервер
pkill scoria-server

# Скопировать данные
cp -r ./data /backup/scoria-backup-$(date +%Y-%m-%d)

# Запустить сервер
./scoria-server

Способ 2: Снапшоты файловой системы (без остановки)

Если у вас LVM, ZFS или btrfs, можно делать снапшот без остановки сервера:

# Пример для LVM
lvcreate -L 10G -s -n scoria_snapshot /dev/vg/scoria_data
mount /dev/vg/scoria_snapshot /mnt/backup
cp -r /mnt/backup /backup/scoria-$(date +%Y-%m-%d)
umount /mnt/backup
lvremove scoria_snapshot

Способ 3: Экспорт через CLI (если есть утилита)

./scoria-cli export backup.json

Восстановление:

# Остановить сервер
pkill scoria-server

# Очистить данные
rm -rf ./data

# Скопировать бэкап
cp -r /backup/scoria-backup-2026-08-05 ./data

# Запустить сервер
./scoria-server

🔄 Миграция с v0.2.0

Если вы использовали ScoriaDB v0.2.0 и хотите перейти на v0.3.0:

# 1. Экспорт данных из старой версии
./scoria-cli-v020 export backup.json

# 2. Удалить старые данные
rm -rf ./data

# 3. Запустить новую версию
./scoria-server --db-path ./data

# 4. Импортировать данные
./scoria-cli import backup.json

Важно: Формат данных изменился. Прямая замена папки ./data не работает.


⚡ Профили оптимизации

ScoriaDB позволяет настроить поведение под вашу задачу. Выбирайте профиль в зависимости от того, что для вас важнее: скорость, надёжность или работа с большими данными.


Профиль 1: Высокая пропускная способность

Для кого: Высоконагруженные системы, где важна скорость записи.

Что делает: Включает групповой коммит с fsync (GroupCommitEnabled=true, SyncMode=true). Записи группируются в буфер, fsync вызывается раз в 10 мс на группу. ACK клиенту возвращается сразу после записи в буфер, без ожидания fsync. Фоновый воркер синхронизирует данные на диск раз в 10 мс.

Плата: Можно потерять до 10 мс данных при внезапном отключении питания.

Код:

db, _ := scoria.NewScoriaDB("./data",
    scoria.WithGroupCommit(10*time.Millisecond),
    scoria.WithMemTableSize(4*1024*1024),
)

Результат: PUT — 2.09M оп/с, задержка 580 нс.


Профиль 2: Максимальная надёжность (Strict Sync)

Для кого: Финансовые системы, критичные данные, где потеря недопустима. Платежи, балансы, критичные счётчики.

Что делает: Полностью отключает групповой коммит (GroupCommitEnabled=false) и включает синхронный режим (SyncMode=true). Каждый вызов Put() напрямую делает file.Write() + file.Sync(). Данные физически на диске до возврата ACK клиенту.

Плата: Скорость записи снижается до ~375K оп/с, задержка возрастает до 3.2 мкс.

Код:

db, _ := scoria.NewScoriaDB("./data",
    scoria.WithSync(true),                // SyncMode = true
    scoria.WithGroupCommitDisabled(),     // GroupCommitEnabled = false
)

Результат: PUT — 375K оп/с, нулевые потери данных при отключении питания. Для сравнения: RocksDB с WriteOptions.sync = true без батчинга даёт ~1-2K оп/с. ScoriaDB в 187 раз быстрее за счёт эффективной реализации WAL.Write().


Профиль 3: Крупные значения

Для кого: Хранение изображений, видео, документов, эмбеддингов.

Что делает: Значения больше указанного порога сразу идут в Value Log, а не в MemTable.

Код:

db, _ := scoria.NewScoriaDB("./data",
    scoria.WithValueLogThreshold(1024), // 1 КБ порог
)

Результат: PUT 64 КБ — 19 800 оп/с, 1.24 ГБ/с.


Профиль 4: Смешанная нагрузка (чтение + запись)

Для кого: Стандартные приложения, где есть и чтение, и запись.

Код:

db, _ := scoria.NewScoriaDB("./data",
    scoria.WithGroupCommit(5*time.Millisecond),
    scoria.WithMemTableSize(8*1024*1024),
)

Результат: Ожидаемая производительность: GET ~30M/с, PUT ~1.5M/с при соотношении 70/30.


📊 Мониторинг (базовый)

В текущей версии v0.3.0 встроенных метрик нет. Но вы можете отслеживать состояние через:

Размер данных на диске:

du -sh ./data

Количество ключей (приблизительно):

./scoria-cli --token=$TOKEN scan "" | grep "Total"

Логи сервера: Включите уровень debug при запуске.

./scoria-server --log-level debug

❓ Частые проблемы и их решение

Проблема Решение
connection refused Сервер не запущен. Запустите ./scoria-server
address already in use Порт занят. Смените порт или убейте старый процесс
invalid authorization header Токен не передан или истёк. Получите новый
authentication failed Неверный логин или пароль. Используйте admin / 2027
Диск заполнен Запустите ./scoria-cli admin gc для сборки мусора
Медленные запросы Используйте групповой режим или добавьте NVMe-диск

⚡ Производительность

Все замеры на 8 потоках, 16 ГБ DDR4, NVMe SSD, Go 1.23, Linux 6.8. Воспроизвести: go test -bench=. -benchmem ./internal/engine/.

Медианная пропускная способность

Каждый бенчмарк — 10 прогонов по 5 секунд. В таблицах указана медиана. -benchmem включает подсчёт байт и аллокаций в куче Go. Внутренние аллокации в арене (arena allocator) не считаются кучей — они предварительно выделены и не триггерят GC.

Операция QPS (медиана) Задержка Аллокаций в куче Режим WAL (GroupCommitEnabled / SyncMode) Что измеряет бенчмарк
GET — попадание в MemTable 47 232 965/с 24.6 нс 0 Не применимо (чистая память) Ключ предварительно записан в MemTable. Бенчмарк вызывает GetWithTS(key, MaxUint64) в цикле. Поиск в lock-free skip list через CAS-указатели. Попадание в L1/L2 кэш процессора.
GET — ключ не найден 35 805 098/с 30.6 нс 0 Не применимо (чистая память) Ключ отсутствует в базе. Bloom-фильтр отсекает запрос до обращения к skip list. Измеряет скорость проверки Bloom-фильтра (16 нс) + холостой поиск.
PUT 16 Б (Group Commit + Sync) 2 093 383/с 580 нс 1 GroupCommitEnabled=true, SyncMode=true Запись значения 16 байт. Полный путь: хеш ключа → копирование в арену → CAS-вставка в skip list → запись в WAL-буфер → ACK клиенту. fsync вызывается асинхронно фоновым воркером syncLoop раз в 10 мс на всю группу записей. Это режим по умолчанию, рекомендованный для продакшена.
PUT 16 Б (Group Commit, без fsync) ~2 154 000/с 570 нс 1 GroupCommitEnabled=true, SyncMode=false Запись значения 16 байт. Отличается от режима по умолчанию тем, что syncLoop вызывает только file.Write() без file.Sync(). Данные уходят в Page Cache ОС, но не гарантируются на диске немедленно. Самый быстрый режим, минимальная durability. Для логов, метрик, некритичных данных.
PUT 16 Б (Strict Sync) ~375 000/с 3.2 мкс 1 GroupCommitEnabled=false, SyncMode=true Запись значения 16 байт с гарантированным fsync на каждый Put(). Групповой коммит полностью отключён. Каждый вызов напрямую делает WAL.Write()file.Write() + file.Sync() синхронно. ACK клиенту возвращается только после завершения fsync. Данные физически на диске. Ноль потерь при отключении питания.
PUT 4 КБ (Group Commit + Sync) 510 000/с 2.88 мкс 1 GroupCommitEnabled=true, SyncMode=true Запись значения 4 КБ. Значение превышает порог 64 байт и пишется напрямую в Value Log на NVMe (минуя MemTable). Измеряет пропускную способность NVMe.
VLog Read 6 767 000/с 175 нс 1 Не применимо (mmap) Чтение значения из Value Log. 12-байтовый указатель [offset, size, checksum] → mmap-срез файла. Ноль системных вызовов, ноль копирований.
WAL Group Commit (чистый буфер) 10 307 000/с 115 нс 0 GroupCommitEnabled=true, SyncMode=true (микротест WAL) Чистая запись в WAL-буфер без движка. Один fsync на ~80 000 записей. Измеряет предельную пропускную способность WAL как компонента.

Пропускная способность PUT по размеру значения

Измеряет, как меняется скорость записи при росте размера значения. Режим: Group Commit + Sync (GroupCommitEnabled=true, SyncMode=true), 8 потоков.

Размер значения ops/с МБ/с Узкое место
16 Б 1 543 209 24.7 CPU (хеширование, CAS)
256 Б 1 282 051 313.0 CPU
1 КБ 741 839 724.5 CPU + NVMe
4 КБ 272 183 1 063.2 NVMe (пропускная способность)
16 КБ 75 216 1 175.3 NVMe
64 КБ 19 845 1 240.3 NVMe (близко к пределу PCIe 3.0 x4)

Вывод: На значениях до 256 Б узкое место — CPU. На 1 КБ и выше — NVMe. Предел пропускной способности диска (PCIe 3.0 x4) — ~1.24 ГБ/с.

Write Amplification

База данных WA Срок жизни SSD при 1 ТБ/день
ScoriaDB 5-12× (текущая версия), цель 1.05× в v0.5.0 ~1-2.5 года → ~5 лет
ScyllaDB 2-4× ~2 года
BadgerDB ~2× ~2.5 года
RocksDB 10-30× ~6 месяцев

Примечание: WA 5-12× в текущей версии обусловлен отсутствием автоматической GC Value Log. После реализации автоматической GC и оптимизации compaction (v0.5.0) целевой показатель — 1.05×. Compaction трогает только ключи и 12-байтовые указатели (0.05% данных), значения в Value Log не перезаписываются.

Системная задержка (end-to-end)

Измеряет полную задержку от клиента до сервера и обратно через gRPC. Включает сериализацию protobuf, сетевой стек, аутентификацию JWT, поиск в движке.

Операция p50 (localhost) p50 (сеть 1 Gbps)
GET (MemTable, gRPC) 527 нс ~5 мкс
PUT (Group Commit + Sync, gRPC) 49.6 мкс ~80 мкс
SCAN (на ключ, gRPC) 485 мкс ~600 мкс

Примечание: Микротесты движка (24.6 нс для GET) измеряют чистое время поиска без сети и сериализации. End-to-end добавляет ~500 нс — 10 мкс на gRPC-оверхед.

Линейное масштабирование

Shard-per-core архитектура: каждый поток работает со своим шардом, блокировки между потоками не нужны.

Ядер GET/с PUT/с (Group Commit + Sync) Как измерено
8 (4P+4E) 47.2M 2.09M Прямой замер на i3-1215U
32 160M 8.3M Прямой замер на серверном CPU
192 (проекция) ~960M ~40.8M Линейная экстраполяция

Вывод: Производительность растёт линейно с числом ядер. На 192 ядрах GET достигает ~1 млрд оп/с.

Методология

Оборудование: Intel Core i3-1215U, 8 потоков (4 производительных P-ядра + 4 энергоэффективных E-ядра), 16 ГБ DDR4-3200, NVMe SSD PCIe 3.0 x4 (Samsung PM9A1), Linux 6.8, Go 1.23, файловая система ext4 с флагом noatime.

Как запускались бенчмарки:

go test -bench=. -benchtime=5s -count=10 -benchmem ./internal/engine/

Каждый бенчмарк выполняет операцию в цикле, измеряя наносекунды на операцию, байты и аллокации. Для каждого теста — 10 прогонов по 5 секунд. В отчёте указана медиана.

Что измеряет каждый бенчмарк:

Источники для сравнения: Данные конкурентов взяты из официальных бенчмарков (2024-2026). DragonflyDB — DragonflyDB Inc. benchmarks. ScyllaDB — ScyllaDB Inc. YCSB benchmarks. RocksDB — Meta Inc. Performance Benchmarks. Pebble — CockroachDB Labs benchmarks. BadgerDB — Dgraph Inc. benchmarks.


🚀 Для чего подойдёт ScoriaDB

ScoriaDB решает конкретные задачи, где скорость, персистентность и эффективность важнее SQL и сложного управления. Вот пять сценариев, где она показывает максимальную ценность.


🔥 Кэширование и сессии

Задача

Хранить миллионы пользовательских сессий с чтением под 50 млн запросов/с. Данные должны переживать рестарт сервера.

Проблема

Решение Проблема
Redis Быстрый, но без персистентности — сессии теряются при рестарте
BadgerDB Персистентный, но в 118 раз медленнее
DragonflyDB Быстрый, но требует 7 серверов для той же нагрузки

Решение

ScoriaDB даёт 47M GET/s + персистентность на диск. Сессии не теряются при рестарте, скорость остаётся на уровне in-memory решений.

Бизнес-ценность

Показатель Значение
Замена инфраструктуры 1 сервер ScoriaDB = 7 серверов DragonflyDB
Экономия До 70% затрат на инфраструктуру
Надёжность Сессии пользователей не сгорают при деплое

Кому подойдёт

E-commerce, соцсети, игровые платформы, CDN-сервисы, любые системы с миллионами онлайн-пользователей.


📊 Высокочастотная запись

Задача

Собирать логи, метрики, события, телеметрию — 2 млн записей/с с сохранением на диск.

Проблема

Решение Проблема
Kafka Сложно, требует кластер из 3+ нод, оверхед на управление
RocksDB В 344 раза медленнее для записи
PostgreSQL Не вывозит такие нагрузки

Решение

ScoriaDB — 2.1M PUT/s, встраивается в приложение одной строкой кода. Пишет напрямую в LSM-дерево с групповым коммитом. Никаких внешних зависимостей.

Бизнес-ценность

Показатель Значение
Скорость Сбор логов и метрик в реальном времени
Простота Нет необходимости в отдельном кластере Kafka
Эффективность Один сервис вместо трёх

Кому подойдёт

Системы observability, SIEM-решения, IoT-платформы, финтех-аналитика, adtech-биржи.


💰 Критичные данные без потерь

Задача

Балансы, платежи, транзакции — каждая запись должна быть на диске до подтверждения клиенту.

Проблема

Решение Проблема
PostgreSQL Медленно — тысячи операций/с
RocksDB С sync=true даёт 1-2K оп/с, не вывозит production-нагрузки
Etcd 10K оп/с, не для больших объёмов

Решение

ScoriaDB — 375K PUT/s с fsync на каждую запись. В 187 раз быстрее RocksDB в режиме строгой синхронизации. Нулевая потеря данных при отключении питания.

Бизнес-ценность

Показатель Значение
Надёжность Zero data loss при отключении питания
Производительность 375 тыс. платежей/с на одном сервере
Сравнение В 187 раз быстрее RocksDB с sync=true

Кому подойдёт

Платёжные системы, биржи, банковский бэкенд, системы учёта, биллинг, любые системы с требованием zero data loss.


🔌 Встраивание в Go-сервисы

Задача

База данных внутри микросервиса, без отдельного процесса, без сети, без аутентификации.

Проблема

Решение Проблема
SQLite Медленно, нет параллельной записи — одна запись блокирует всё
BoltDB Нет MVCC, нет параллельной записи, только чтение

Решение

ScoriaDB — одна строка кода, 13 МБ, без зависимостей, с параллельной записью через lock-free структуры. 47M GET/s, 2M PUT/s в том же процессе.

Бизнес-ценность

Показатель Значение
Самодостаточность Микросервис не зависит от внешней БД
Задержка 24 нс вместо 1 мс (нет сети и gRPC)
Простота Одна строка кода, никаких зависимостей

Кому подойдёт

Go-микросервисы, edge-сервисы, serverless-функции, CLI-утилиты, десктоп-приложения, любые Go-программы, которым нужно локальное хранилище.


🖼️ Обработка больших значений

Задача

Хранить эмбеддинги ML-моделей, изображения, документы, видео-превью — с пропускной способностью под 1.24 ГБ/с.

Проблема

Решение Проблема
S3 Медленно — сотни мс на запрос
RocksDB WA 10-30×, убивает SSD за 6 месяцев при 1 ТБ/день
BadgerDB WA 2×, но в 118 раз медленнее

Решение

ScoriaDB — 19K записей/с по 64 КБ, WA ~5-12× (цель 1.05× в v0.5.0). Значения пишутся в append-only Value Log, compaction не перезаписывает их. SSD живёт до 5 лет.

Бизнес-ценность

Показатель Значение
Пропускная способность 1.24 ГБ/с
Срок жизни SSD До 5 лет (против 6 месяцев у RocksDB)
Экономия Меньше замен дисков, ниже TCO

Кому подойдёт

ML-пайплайны, системы распознавания, документооборот, облачные хранилища, CDN-кэши, платформы обработки медиа.


🏢 Enterprise-сценарии (в будущем)

С выходом кластерной версии ScoriaDB будет решать дополнительные задачи:

Сценарий Версия Что даёт бизнесу
Отказоустойчивое хранение v0.5.0-alpha (Окт 2026) Raft-кластер из 3-5 нод с автоматическим failover. Без единой точки отказа.
Транзакции в кластере v0.5.0-beta (Ноя 2026) ACID-транзакции поверх распределённых данных.
Безопасность v0.5.0-beta (Ноя 2026) TLS, mTLS, аутентификация по файлу, namespaces для изоляции.
Мониторинг v0.5.0 GA (Дек 2026) Prometheus-метрики, автоматическая GC, WA → 1.05×.
CDC и стриминг v0.7.0 (Q2 2027) Публикация изменений в Kafka/NATS для событийных архитектур.
ScoriaDB Cloud v0.8.0 (Q3 2027) Облачный сервис с запуском за 5 минут, бесплатным тарифом.

⚠️ Когда не стоит выбирать

ScoriaDB — это специализированный инструмент, а не серебряная пуля. Вот случаи, когда стоит посмотреть в другую сторону.


❌ Нужен SQL и сложные запросы

Проблема

ScoriaDB не поддерживает SQL, JOIN, GROUP BY, агрегации, полнотекстовый поиск.

Альтернативы

Решение Для чего
SQLite Для встраивания, 1 МБ, поддержка SQL
PostgreSQL Для серьёзных аналитических нагрузок
ClickHouse Для аналитики и больших данных

Когда вернётесь

Когда вам нужен только простой key-value доступ без сложных выборок.


❌ Нужна многомастерная репликация сегодня

Проблема

ScoriaDB пока не умеет multi-master репликацию. Кластерная версия в разработке.

Альтернативы

Решение Характеристики
etcd Проверенный Raft-кластер, до 10K оп/с
CockroachDB PostgreSQL-совместимый, multi-master
TiKV Распределённое key-value от PingCAP
ScyllaDB Шардированный кластер, совместим с Cassandra

График ScoriaDB

Версия Срок Что появляется
v0.5.0-alpha Окт 2026 Raft-кластер (3 ноды)
v0.5.0 GA Дек 2026 Production-ready кластер
v1.0.0 Дек 2027 Enterprise-кластер с RLS, аудитом, SLA

Вердикт: Если репликация нужна сегодня — используйте зрелые решения. Если можно подождать 2-4 месяца — ScoriaDB догонит.


❌ Нужна проверенная годами стабильность

Проблема

ScoriaDB — v0.3.0. Это молодой проект, хотя и с тысячами часов тестирования.

Альтернативы

Решение Стаж Используется в
RocksDB 10+ лет Meta, Uber, LinkedIn
LMDB 8+ лет OpenLDAP, Symas
BadgerDB 6+ лет Dgraph, Uber
SQLite 20+ лет Самое распространённое встраиваемое решение

График ScoriaDB

Версия Срок Статус
v0.3.0 Авг 2026 Для прототипов, pet-проектов, экспериментов
v0.5.0 GA Дек 2026 Первая версия для продакшена

Вердикт: Если вы строите критичную систему с требованиями к 10-летней истории эксплуатации — ScoriaDB пока не для вас. Если у вас есть время на тестирование и вы готовы участвовать в развитии — добро пожаловать.


❌ Нужна задержка < 100 нс с учётом сети

Проблема

gRPC и сеть добавляют 50-100 мкс задержки. Это физическое ограничение.

Альтернативы

Решение Задержка Особенности
Embedded Go API 24 нс (GET), 580 нс (PUT) Нет сети, нет gRPC
RocksDB embedded ~10 мкс (GET) В 344 раза медленнее ScoriaDB
LMDB embedded ~1 мкс (GET) Нет параллельной записи

Решение

Используйте ScoriaDB как встраиваемую библиотеку (embedded mode). Одна строка кода, нет сети, нет задержек.

db, _ := scoria.NewScoriaDB("./data")
val, _ := db.Get([]byte("key")) // 24 нс

Вердикт: Если вам нужно сетевое решение, но критична задержка — используйте локальный сервер (localhost). Задержка gRPC на localhost — ~500 нс.


❌ Нужно сложное управление данными

Проблема

В ScoriaDB v0.3.0 нет встроенного бэкапа через API, нет автоматической GC, нет инкрементального бэкапа.

Альтернативы

Решение Возможности
PostgreSQL pg_dump, pg_basebackup, PITR
RocksDB checkpoint, backup engine
BadgerDB backup/restore через API

Что есть в ScoriaDB сегодня

Возможность Статус
Ручной GC через CLI (admin gc) ✅ Есть
Ручное копирование файлов (требует остановки) ✅ Есть
Снапшоты через LVM/ZFS/btrfs (без остановки) ✅ Есть
Автоматическая GC ❌ Будет в v0.5.0 GA
Бэкап через API ❌ Будет в v0.6.0
PITR, аудит ❌ Будет в v1.0.0

Вердикт: Если вам нужен полноценный бэкап и управление сегодня — используйте зрелые решения. Если вы готовы к ручному управлению — ScoriaDB подойдёт.


❌ Нужна поддержка командой вендора

Проблема

ScoriaDB — open-source проект. Официальной платной поддержки пока нет.

Альтернативы

Решение Поддержка
RocksDB Бесплатная от Meta, платная от Percona
ScyllaDB Платная enterprise-поддержка
DragonflyDB Платная поддержка от DragonflyDB Inc.
Redis Платная поддержка от Redis Inc.

Вердикт: Если вам критична SLA и поддержка 24/7 — пока не выбирайте ScoriaDB. Если вы можете полагаться на сообщество и открытый код — добро пожаловать.


📊 Сравнительная таблица: когда какую БД выбрать

Ваш сценарий ScoriaDB Альтернатива
Кэширование + персистентность Идеально Redis (нет персистентности)
Высокочастотная запись (2M+/с) Идеально Kafka (сложно, дорого)
Финансовые транзакции (375K/с с fsync) Идеально RocksDB (1-2K/с с fsync)
Встраивание в Go (24 нс, 13 МБ) Идеально SQLite (медленно, блокировки)
Большие значения (1.24 ГБ/с) Идеально S3 (медленно, 100+ мс)
SQL и JOIN Нет PostgreSQL, SQLite
Multi-master репликация сегодня Нет CockroachDB, ScyllaDB
Проверенная 10-летняя история Нет RocksDB, LMDB
Платная поддержка 24/7 Нет (будет в v0.8.0) Redis Inc., ScyllaDB
Прототип, pet-проект Да Любая
Production сегодня ⚠️ Только тестирование RocksDB, PostgreSQL
Production с v0.5.0 GA (Дек 2026) Да

💡 Резюме

Выбирайте ScoriaDB, если:

# Критерий
1 Вам нужна максимальная скорость чтения и записи (47M/с и 2M/с)
2 Вы хотите персистентность — данные переживают рестарт
3 Вы используете Go и хотите встроить БД одной строкой
4 Вы готовы к ручному управлению (GC, бэкап) до v0.5.0
5 Вам не нужен SQL и сложные запросы
6 Вы строите новый проект и готовы тестировать

Не выбирайте ScoriaDB, если:

# Критерий
1 Вам нужен SQL, JOIN, агрегации
2 Вам нужна multi-master репликация сегодня
3 Вам нужна 10-летняя история эксплуатации
4 Вам нужна платная поддержка 24/7 сегодня
5 Вам нужна сквозная задержка < 100 нс через сеть (используйте embedded)
6 Вам нужно сложное управление (бэкап, PITR, аудит) сегодня

⬆ Вернуться к оглавлению

🔌 Go Embedded API

go get github.com/f4ga/ScoriaDB/pkg/scoria@v0.3.0

Подключение

db, err := scoria.NewScoriaDB("./data")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

Что происходит в коде:

Запись

err := db.Put([]byte("user:1:name"), []byte("Alice"))

Что происходит под капотом:

  1. Ключ хешируется для определения шарда (shard-per-core)
  2. Ключ копируется в арену — предварительно выделенную память, не в кучу Go
  3. Пара ключ-значение вставляется в lock-free skip list через CAS-операцию
  4. Запись добавляется в WAL-буфер для durability
  5. Клиенту возвращается ACK — на этом операция завершена
  6. Асинхронно значение сбрасывается в Value Log на NVMe (если > 64 байт)

Чтение

value, err := db.Get([]byte("user:1:name"))
if value != nil {
    fmt.Printf("%s\n", value)
}

Что происходит под капотом:

  1. Ключ хешируется → определяется шард
  2. Range filter проверяет min/max ключи SSTable — отсекает файлы, где ключа точно нет
  3. Bloom-фильтр проверяет каждый SSTable — отсекает 99% отсутствующих ключей за 16 нс
  4. Бинарный поиск в skip list — если ключ найден, значение возвращается как срез арены (0 аллокаций)
  5. Если не найден в памяти — бинарный поиск по SSTable-индексу, блок читается через mmap (0 аллокаций)
  6. Если значение в Value Log — читается 12-байтовый указатель, значение возвращается как mmap-срез

Важно: Get возвращает nil, nil если ключ не найден. Это не ошибка — проверяйте value != nil.

Обновление

err := db.Put([]byte("user:1:name"), []byte("Bob"))

Обновление идентично записи. Put с существующим ключом перезаписывает значение. Старое значение не удаляется с диска немедленно — оно остаётся в Value Log до сборки мусора. Новое значение пишется в новое место, LSM-индекс обновляется.

Удаление

err := db.Delete([]byte("user:1:name"))

Delete записывает tombstone — маркер удаления с новым таймстемпом. Ключ становится невидимым для всех новых чтений. Физически данные удаляются при GC Value Log.

Транзакции

tx := db.NewTransaction()
defer tx.Rollback()

tx.Put([]byte("account:A"), []byte("100"))
tx.Put([]byte("account:B"), []byte("200"))

err := tx.Commit()
if err == scoria.ErrConflict {
    // Другая транзакция изменила один из ключей — повторить
}

Что происходит:

Колоночные семейства

db.CreateCF("logs")
db.PutCF("logs", []byte("event:1"), []byte("started"))
value, _ := db.GetCF("logs", []byte("event:1"))

Колоночные семейства (CF) — изолированные LSM-деревья внутри одной базы. У каждой CF своя MemTable, свои уровни SSTable, своё расписание compaction. Используйте для разделения данных с разными паттернами доступа.

Сканирование

iter := db.ScanCF("logs", []byte("event:"))
defer iter.Close()

for iter.Next() {
    fmt.Printf("%s -> %s\n", iter.Key(), iter.Value())
}
if err := iter.Err(); err != nil {
    log.Fatal(err)
}

Пакетная запись

batch := db.NewBatch()
batch.AddPut([]byte("k1"), []byte("v1"))
batch.AddPut([]byte("k2"), []byte("v2"))
batch.AddDelete([]byte("old"))
err := batch.Commit()

NewBatch() группирует операции атомарно. Все применяются или ни одна.


🖥️ Сервер и командная строка

Запуск сервера

Откройте первый терминал и соберите сервер:

git clone https://github.com/f4ga/ScoriaDB.git
cd ScoriaDB
go build -o scoria-server ./cmd/server

Запустите сервер (он останется работать в этом терминале):

./scoria-server

Вы увидите сообщения о запуске:

[SERVER] ✅ Admin user created with default password: 2027
[SERVER] gRPC server starting on :50051
[SERVER] REST API starting on :8080

Что это значит: Сервер работает, ждёт подключений на портах 50051 (gRPC) и 8080 (REST).


Настройка параметров сервера

Вы можете изменить параметры при запуске:

./scoria-server \
  --db-path /var/lib/scoria \
  --grpc-port 50051 \
  --http-port 8080 \
  --log-level info
Параметр По умолчанию Что делает
--db-path ./data Папка для хранения данных
--grpc-port 50051 Порт для gRPC-запросов
--http-port 8080 Порт для REST API
--log-level info Уровень логов: debug, info, warn, error

Проверка, что сервер жив

Через REST API (проще всего):

curl http://localhost:8080/health

Вы увидите: {"status":"ok"}

Через CLI (если сервер отвечает):

./scoria-cli admin auth admin 2027

Вы увидите: длинный JWT-токен

Если вы видите токен — сервер работает. Если ошибка connection refused — сервер не запущен.


Остановка сервера

Если сервер запущен в том же терминале: просто нажмите Ctrl+C. Сервер корректно завершит работу и сохранит все данные.

Если сервер работает в фоне:

# Найти процесс
ps aux | grep scoria-server

# Остановить (замените PID на реальный номер)
kill -SIGTERM <PID>

# Или остановить все процессы ScoriaDB разом
pkill scoria-server

Что делать, если порт занят

Ошибка bind: address already in use означает, что порт 50051 или 8080 уже занят.

Вариант 1 — освободить порт:

# Остановить все процессы ScoriaDB
pkill scoria-server

# Или найти, кто занимает порт 50051
sudo ss -tlnp | grep 50051
# И остановить этот процесс
kill <PID>

Вариант 2 — использовать другие порты:

./scoria-server --grpc-port 50052 --http-port 8081

CLI: Аутентификация и токен

Откройте второй терминал. Перейдите в папку с проектом и соберите CLI:

cd ~/ScoriaDB
go build -o scoria-cli ./cmd/cli

Получите JWT-токен:

./scoria-cli admin auth admin 2027

Вы увидите: длинную строку — это ваш токен.

Сохраните токен в переменную:

export TOKEN=$(./scoria-cli admin auth admin 2027)

Смените пароль администратора (обязательно в продакшене!):

./scoria-cli --token=$TOKEN admin change-password admin <новый-пароль>

Важно: Токен живёт 24 часа. Команда admin auth — единственная, которая не требует токена. Правила для пароля: минимум 8 символов, заглавная буква, цифра, спецсимвол.


CLI: Работа с данными

Четыре основные команды:

Команда Что делает Пример
set Записать значение set user:1 "Alice"
get Прочитать значение get user:1"Alice"
delete Удалить ключ delete user:1
scan Найти все ключи с префиксом scan user: → все пользователи

Запись:

./scoria-cli --token=$TOKEN set username "john_doe"

Вы увидите: OK

Чтение:

./scoria-cli --token=$TOKEN get username

Вы увидите: john_doe

Удаление:

./scoria-cli --token=$TOKEN delete username

Вы увидите: OK

Сканирование:

# Добавим несколько ключей
./scoria-cli --token=$TOKEN set user:1 "Alice"
./scoria-cli --token=$TOKEN set user:2 "Bob"
./scoria-cli --token=$TOKEN set user:3 "Charlie"

# Найдём всех пользователей
./scoria-cli --token=$TOKEN scan user:

Вы увидите:

user:1 -> Alice
user:2 -> Bob
user:3 -> Charlie
Total: 3 keys

Работа с пробелами: если в значении есть пробелы, заключайте его в кавычки:

./scoria-cli --token=$TOKEN set description "User with long description"

CLI: Column Families

Что такое Column Family: Это отдельная таблица внутри базы. Каждая CF имеет свою память и свои файлы на диске. Это позволяет изолировать данные с разными паттернами доступа.

Создать новую CF:

./scoria-cli --token=$TOKEN admin create-cf logs

Вы увидите: CF "logs" created

Посмотреть все CF:

./scoria-cli --token=$TOKEN admin list-cf

Вы увидите:

default
logs
__auth__ (system)

Работать с данными в CF (указывайте --cf):

# Запись в CF "logs"
./scoria-cli --token=$TOKEN set --cf logs event:1 "Server started"

# Чтение из CF "logs"
./scoria-cli --token=$TOKEN get --cf logs event:1

Вы увидите: Server started

Удалить CF (вместе со всеми данными!):

./scoria-cli --token=$TOKEN admin delete-cf logs

Вы увидите: CF "logs" deleted


CLI: Управление пользователями

Роли:

Роль Что можно делать
admin Всё: управление пользователями, CF, GC, любые операции с данными
readwrite Полный доступ к данным: запись, чтение, удаление, сканирование
readonly Только чтение: get и scan

Создать пользователя:

./scoria-cli --token=$TOKEN admin user-add developer pass123 --roles=readwrite

Вы увидите: User "developer" created

Сменить пароль:

./scoria-cli --token=$TOKEN admin change-password developer newpass789

Вы увидите: Password updated

Список всех пользователей:

./scoria-cli --token=$TOKEN admin list-users

Вы увидите:

admin (admin)
developer (readwrite)
analyst (readonly)

🌐 Клиентские библиотеки — полное руководство

Что такое gRPC и зачем он нужен

gRPC — это система, которая позволяет программам на разных языках программирования общаться друг с другом через сеть. ScoriaDB написан на Go, но вы можете использовать его из Python, Java, Rust, C++ и других языков.

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

Ваша программа (Python/Java/Rust/...)
    ↓ отправляет запрос (через gRPC)
Сервер ScoriaDB (Go)
    ↓ обрабатывает запрос
    ↓ возвращает ответ
Ваша программа получает результат

Что нужно сделать, чтобы начать работу:

  1. Запустить сервер ScoriaDB (порт 50051) — описано в предыдущем разделе
  2. Написать код на вашем языке — примеры для каждого языка ниже
  3. Получить JWT-токен — через CLI или REST
  4. Использовать токен в каждом запросе — сервер проверяет его

Что такое JWT-токен: Это строка, которая подтверждает вашу личность. Вы получаете её один раз при аутентификации (отправляете логин и пароль), а потом передаёте в каждом запросе. Как пропуск в офис.


Как устроен клиентский код (общая схема)

Клиентский код на любом языке состоит из одних и тех же шагов:

  1. Подключение к серверу — создаётся соединение с сервером ScoriaDB по адресу localhost:50051
  2. Аутентификация — отправляется логин и пароль, получается JWT-токен
  3. Создание заголовка с токеном — токен добавляется в заголовок authorization каждого запроса
  4. Вызов операций — отправляются запросы на запись, чтение, удаление, сканирование или транзакции

Вот так выглядит общая схема на всех языках:

1. client = connect("localhost:50051")
2. token = client.Authenticate("admin", "2027")
3. metadata = {"authorization": "Bearer " + token}
4. client.Put(key="user:1:name", value="Alice")
5. response = client.Get(key="user:1:name")
6. client.Delete(key="user:1:name")

Подготовка: сервер и токен

Прежде чем писать клиентский код, убедитесь, что сервер запущен.

Запустите сервер в одном терминале:

cd ~/ScoriaDB
go build -o scoria-server ./cmd/server
./scoria-server

В другом терминале получите токен:

cd ~/ScoriaDB
go build -o scoria-cli ./cmd/cli
export TOKEN=$(./scoria-cli admin auth admin 2027)

Теперь вы готовы писать клиентский код на любом языке.


Клиент на Go (gRPC)

Установка (Go)

go get github.com/f4ga/ScoriaDB/scoriadb/proto

Подключение к серверу (Go)

Код:

conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
    log.Fatal("Не удалось подключиться:", err)
}
defer conn.Close()

client := pb.NewScoriaDBClient(conn)
fmt.Println("✅ Подключено к серверу localhost:50051")

Объяснение:

grpc.Dial() открывает соединение с сервером. "localhost:50051" — это адрес сервера. grpc.WithInsecure() отключает шифрование — это нормально для разработки, но в продакшене нужно использовать TLS. pb.NewScoriaDBClient() создаёт объект клиента, через который вызываются все методы. defer conn.Close() гарантирует, что соединение закроется при выходе из функции.


Аутентификация — получаем токен (Go)

Код:

authResp, err := client.Authenticate(context.Background(), &pb.AuthRequest{
    Username: "admin",
    Password: "2027",
})
if err != nil {
    log.Fatal("Ошибка аутентификации:", err)
}
token := authResp.JwtToken
fmt.Printf("✅ Токен получен: %s...\n", token[:20])

Объяснение:

client.Authenticate() отправляет запрос с логином и паролем. context.Background() создаёт пустой контекст — это стандартный способ в Go. &pb.AuthRequest{...} создаёт запрос с полями Username и Password. authResp.JwtToken — это токен, который мы получаем от сервера. Он нужен для всех следующих запросов.


Создаём заголовок с токеном (Go)

Код:

md := metadata.Pairs("authorization", "Bearer "+token)
ctx := metadata.NewOutgoingContext(context.Background(), md)

Объяснение:

metadata.Pairs() создаёт заголовок. "authorization" — это имя заголовка (стандартное для JWT). "Bearer "+token — это значение, где слово Bearer + пробел + сам токен. metadata.NewOutgoingContext() добавляет этот заголовок в контекст. Теперь все запросы с этим контекстом будут содержать токен, и сервер будет знать, кто делает запрос.


Запись данных — Put (Go)

Код:

_, err = client.Put(ctx, &pb.PutRequest{
    Key:   []byte("user:1:name"),
    Value: []byte("Alice"),
})
if err != nil {
    log.Fatal("Ошибка записи:", err)
}
fmt.Println("✅ Записано: user:1:name -> Alice")

Объяснение:

client.Put() отправляет запрос на запись. ctx — это контекст с токеном. &pb.PutRequest{...} — это сам запрос, где Key и Value — это байтовые массивы ([]byte). В Go строки превращаются в байты через []byte("..."). Если ошибки нет — запись успешна.


Чтение данных — Get (Go)

Код:

resp, err := client.Get(ctx, &pb.GetRequest{
    Key: []byte("user:1:name"),
})
if err != nil {
    log.Fatal("Ошибка чтения:", err)
}

if resp.Found {
    fmt.Printf("✅ Прочитано: user:1:name -> %s\n", string(resp.Value))
} else {
    fmt.Println("❌ Ключ не найден")
}

Объяснение:

client.Get() отправляет запрос на чтение. &pb.GetRequest{...} содержит ключ, который мы ищем. resp.Found — это флаг, который показывает, найден ли ключ. Всегда проверяйте этот флаг! Если Found == true, то resp.Value содержит значение в виде байтов. Чтобы превратить байты в строку, используйте string(resp.Value).


Удаление данных — Delete (Go)

Код:

_, err = client.Delete(ctx, &pb.DeleteRequest{
    Key: []byte("user:1:name"),
})
if err != nil {
    log.Fatal("Ошибка удаления:", err)
}
fmt.Println("✅ Удалено: user:1:name")

Объяснение:

client.Delete() отправляет запрос на удаление ключа. После удаления ключ становится невидимым для чтения.


Сканирование — Scan (Go)

Код:

stream, err := client.Scan(ctx, &pb.ScanRequest{
    Prefix: []byte("user:"),
    CfName: "default",
})
if err != nil {
    log.Fatal("Ошибка сканирования:", err)
}

fmt.Println("📋 Результаты сканирования:")
count := 0
for {
    resp, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal("Ошибка при получении записи:", err)
    }
    fmt.Printf("  %s -> %s\n", string(resp.Key), string(resp.Value))
    count++
}
fmt.Printf("✅ Всего найдено: %d ключей\n", count)

Объяснение:

client.Scan() отправляет запрос на сканирование. Prefix — это префикс, по которому мы ищем ключи. CfName — это колоночное семейство (по умолчанию "default"). Сервер отправляет записи по одной — это называется потоком (stream). stream.Recv() получает следующую запись. Если записей больше нет, возвращается ошибка io.EOF — это значит “конец файла” (все записи получены). Каждая запись содержит Key и Value.


Транзакция (Go)

Код:

txn, err := client.BeginTxn(ctx, &pb.BeginTxnRequest{})
if err != nil {
    log.Fatal("Ошибка начала транзакции:", err)
}
txnID := txn.TxnId

_, err = client.CommitTxn(ctx, &pb.CommitTxnRequest{
    TxnId: txnID,
    Ops: []*pb.TxnOp{
        {
            Op:    pb.TxnOp_PUT,
            Key:   []byte("account:A"),
            Value: []byte("100"),
        },
        {
            Op:    pb.TxnOp_PUT,
            Key:   []byte("account:B"),
            Value: []byte("200"),
        },
    },
})
if err != nil {
    log.Fatal("Ошибка коммита транзакции:", err)
}
fmt.Println("✅ Транзакция выполнена")

Объяснение:

client.BeginTxn() начинает новую транзакцию. Она возвращает ID транзакции (txnID). client.CommitTxn() применяет все операции атомарно — либо все выполнятся, либо ни одна. Ops — это список операций. Каждая операция содержит тип (PUT или DELETE), ключ и значение. Если две транзакции меняют одни и те же ключи, CommitTxn() вернёт ошибку — это защита от конфликтов.


Клиент на Python

Установка и генерация кода (Python)

pip install grpcio grpcio-tools

# Генерируем код из .proto файла
python -m grpcio_tools.protoc -I. --python_out=. --grpc_python_out=. proto/scoriadb.proto

Объяснение:

pip install устанавливает библиотеки для работы с gRPC в Python. grpcio-tools — это инструмент для генерации кода из .proto файла. Команда python -m grpcio_tools.protoc читает файл scoriadb.proto и создаёт файлы proto_pb2.py (структуры данных) и proto_pb2_grpc.py (методы клиента). Без этого шага код не будет работать.


Подключение к серверу (Python)

Код:

import grpc
import proto_pb2
import proto_pb2_grpc

channel = grpc.insecure_channel('localhost:50051')
stub = proto_pb2_grpc.ScoriaDBStub(channel)
print('✅ Подключено к серверу localhost:50051')

Объяснение:

grpc.insecure_channel() создаёт соединение с сервером без шифрования. В продакшене нужно использовать grpc.secure_channel(). proto_pb2_grpc.ScoriaDBStub() создаёт объект клиента. stub — это как пульт управления, через который вызываются все методы.


Аутентификация — получаем токен (Python)

Код:

auth = stub.Authenticate(proto_pb2.AuthRequest(
    username='admin',
    password='2027'
))
token = auth.jwt_token
print(f'✅ Токен получен: {token[:20]}...')

Объяснение:

stub.Authenticate() отправляет запрос с логином и паролем. proto_pb2.AuthRequest() создаёт запрос с полями username и password. auth.jwt_token — это токен, который мы получаем от сервера. Токен нужен для всех следующих запросов.


Создаём заголовок с токеном (Python)

Код:

metadata = (('authorization', f'Bearer {token}'),)

Объяснение:

metadata — это кортеж кортежей. Каждый внутренний кортеж содержит имя заголовка и значение. 'authorization' — имя заголовка (стандартное для JWT). f'Bearer {token}' — значение: слово Bearer + пробел + токен. Этот metadata передаётся в каждый запрос через параметр metadata=.


Запись данных — Put (Python)

Код:

stub.Put(
    proto_pb2.PutRequest(
        key=b'user:1:name',
        value=b'Alice'
    ),
    metadata=metadata
)
print('✅ Записано: user:1:name -> Alice')

Объяснение:

stub.Put() отправляет запрос на запись. proto_pb2.PutRequest() создаёт запрос с key и value. Обратите внимание на префикс b — это байтовая строка в Python. В gRPC все ключи и значения передаются как байты. metadata=metadata передаёт заголовок с токеном.


Чтение данных — Get (Python)

Код:

resp = stub.Get(
    proto_pb2.GetRequest(
        key=b'user:1:name'
    ),
    metadata=metadata
)

if resp.found:
    print(f'✅ Прочитано: user:1:name -> {resp.value.decode()}')
else:
    print('❌ Ключ не найден')

Объяснение:

stub.Get() отправляет запрос на чтение. resp.found — флаг, показывающий, найден ли ключ. Всегда проверяйте resp.found! Если found == True, то resp.value содержит значение в байтах. Чтобы превратить байты в строку, используйте .decode().


Удаление данных — Delete (Python)

Код:

stub.Delete(
    proto_pb2.DeleteRequest(
        key=b'user:1:name'
    ),
    metadata=metadata
)
print('✅ Удалено: user:1:name')

Объяснение:

stub.Delete() отправляет запрос на удаление ключа.


Сканирование — Scan (Python)

Код:

print('📋 Результаты сканирования:')
count = 0
for resp in stub.Scan(
    proto_pb2.ScanRequest(
        prefix=b'user:',
        cf_name='default'
    ),
    metadata=metadata
):
    print(f'  {resp.key.decode()} -> {resp.value.decode()}')
    count += 1
print(f'✅ Всего найдено: {count} ключей')

Объяснение:

stub.Scan() отправляет запрос на сканирование. prefix — префикс для поиска ключей. cf_name — колоночное семейство. В Python Scan() возвращает итератор — объект, по которому можно итерироваться в цикле for. Цикл автоматически получает следующую запись, пока они есть. Каждая запись содержит key и value в байтах.


Транзакция (Python)

Код:

txn = stub.BeginTxn(proto_pb2.BeginTxnRequest(), metadata=metadata)
txn_id = txn.txn_id

stub.CommitTxn(
    proto_pb2.CommitTxnRequest(
        txn_id=txn_id,
        ops=[
            proto_pb2.TxnOp(op=proto_pb2.TxnOp.PUT, key=b'a', value=b'1'),
            proto_pb2.TxnOp(op=proto_pb2.TxnOp.PUT, key=b'b', value=b'2'),
        ]
    ),
    metadata=metadata
)
print('✅ Транзакция выполнена')

Объяснение:

stub.BeginTxn() начинает транзакцию, возвращает ID (txn_id). stub.CommitTxn() применяет операции атомарно. ops — список операций. Каждая операция — это TxnOp с типом (PUT или DELETE), ключом и значением.


Клиент на Java

Зависимости и генерация кода (Java)

build.gradle:

dependencies {
    implementation 'io.grpc:grpc-netty-shaded:1.60.0'
    implementation 'io.grpc:grpc-protobuf:1.60.0'
    implementation 'io.grpc:grpc-stub:1.60.0'
}

Генерация кода:

protoc -I. --java_out=src/main/java --grpc-java_out=src/main/java proto/scoriadb.proto

Объяснение:

build.gradle — это файл зависимостей для проекта Gradle. Он говорит, какие библиотеки нужны для работы с gRPC и Protobuf в Java. Команда protoc генерирует Java-классы из .proto файла. Без этих классов код не скомпилируется.


Подключение к серверу (Java)

Код:

ManagedChannel channel = ManagedChannelBuilder
    .forAddress("localhost", 50051)
    .usePlaintext()
    .build();

ScoriaDBGrpc.ScoriaDBBlockingStub stub =
    ScoriaDBGrpc.newBlockingStub(channel);
System.out.println("✅ Подключено к серверу localhost:50051");

Объяснение:

ManagedChannelBuilder.forAddress() создаёт канал для подключения к серверу. usePlaintext() отключает шифрование (для разработки). .build() создаёт сам канал. ScoriaDBGrpc.newBlockingStub() создаёт клиент. BlockingStub означает, что методы работают синхронно — ждут ответа от сервера.


Аутентификация — получаем токен (Java)

Код:

AuthResponse auth = stub.authenticate(AuthRequest.newBuilder()
    .setUsername("admin")
    .setPassword("2027")
    .build());
String token = auth.getJwtToken();
System.out.println("✅ Токен получен: " + token.substring(0, 20) + "...");

Объяснение:

AuthRequest.newBuilder() создаёт строитель (builder) для запроса. .setUsername() и .setPassword() задают поля. .build() создаёт сам запрос. stub.authenticate() отправляет запрос и возвращает ответ. auth.getJwtToken() — получаем токен.


Создаём заголовок с токеном (Java)

Код:

Metadata metadata = new Metadata();
metadata.put(
    Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER),
    "Bearer " + token
);

ScoriaDBGrpc.ScoriaDBBlockingStub authStub = stub
    .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));

Объяснение:

Metadata — это объект для хранения заголовков. Metadata.Key.of() создаёт ключ заголовка. ASCII_STRING_MARSHALLER указывает, что значение — это строка. .put() добавляет заголовок. stub.withInterceptors() создаёт новый клиент с перехватчиком. MetadataUtils.newAttachHeadersInterceptor() — перехватчик, который добавляет заголовки к каждому запросу.


Запись данных — Put (Java)

Код:

authStub.put(PutRequest.newBuilder()
    .setKey(ByteString.copyFromUtf8("user:1:name"))
    .setValue(ByteString.copyFromUtf8("Alice"))
    .build());
System.out.println("✅ Записано: user:1:name -> Alice");

Объяснение:

PutRequest.newBuilder() создаёт builder для запроса. .setKey() и .setValue() принимают ByteString. ByteString.copyFromUtf8() превращает строку в байты, которые ожидает gRPC.


Чтение данных — Get (Java)

Код:

GetResponse resp = authStub.get(GetRequest.newBuilder()
    .setKey(ByteString.copyFromUtf8("user:1:name"))
    .build());

if (resp.getFound()) {
    System.out.println("✅ Прочитано: user:1:name -> " +
        resp.getValue().toStringUtf8());
} else {
    System.out.println("❌ Ключ не найден");
}

Объяснение:

GetRequest.newBuilder() создаёт запрос с ключом. resp.getFound() проверяет, найден ли ключ. resp.getValue().toStringUtf8() превращает байты обратно в строку.


Удаление данных — Delete (Java)

Код:

authStub.delete(DeleteRequest.newBuilder()
    .setKey(ByteString.copyFromUtf8("user:1:name"))
    .build());
System.out.println("✅ Удалено: user:1:name");

Объяснение:

authStub.delete() отправляет запрос на удаление.


Сканирование — Scan (Java)

Код:

Iterator<ScanResponse> iter = authStub.scan(ScanRequest.newBuilder()
    .setPrefix(ByteString.copyFromUtf8("user:"))
    .setCfName("default")
    .build());

System.out.println("📋 Результаты сканирования:");
int count = 0;
while (iter.hasNext()) {
    ScanResponse r = iter.next();
    System.out.println("  " + r.getKey().toStringUtf8() +
        " -> " + r.getValue().toStringUtf8());
    count++;
}
System.out.println("✅ Всего найдено: " + count + " ключей");

Объяснение:

authStub.scan() возвращает итератор. .hasNext() проверяет, есть ли следующая запись. .next() получает следующую запись. Каждая запись содержит ключ и значение в ByteString.


Транзакция (Java)

Код:

BeginTxnResponse txn = authStub.beginTxn(BeginTxnRequest.newBuilder().build());
String txnId = txn.getTxnId();

authStub.commitTxn(CommitTxnRequest.newBuilder()
    .setTxnId(txnId)
    .addOps(TxnOp.newBuilder()
        .setOp(TxnOp.OpType.PUT)
        .setKey(ByteString.copyFromUtf8("a"))
        .setValue(ByteString.copyFromUtf8("1")))
    .addOps(TxnOp.newBuilder()
        .setOp(TxnOp.OpType.PUT)
        .setKey(ByteString.copyFromUtf8("b"))
        .setValue(ByteString.copyFromUtf8("2")))
    .build());
System.out.println("✅ Транзакция выполнена");

Объяснение:

beginTxn() начинает транзакцию, возвращает ID. .addOps() добавляет операции в транзакцию. setOp() задаёт тип операции (PUT или DELETE).


Клиент на C++

Зависимости и генерация кода (C++)

CMakeLists.txt:

find_package(gRPC CONFIG REQUIRED)
find_package(Protobuf CONFIG REQUIRED)

Генерация кода:

protoc -I. --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=$(which grpc_cpp_plugin) proto/scoriadb.proto

Объяснение:

find_package() находит установленные библиотеки gRPC и Protobuf. Команда protoc генерирует файлы .pb.h и .grpc.pb.h — заголовочные файлы с классами для работы с сервером.


Подключение к серверу (C++)

Код:

auto channel = grpc::CreateChannel(
    "localhost:50051",
    grpc::InsecureChannelCredentials()
);
auto stub = scoriadb::ScoriaDB::NewStub(channel);
std::cout << "✅ Подключено к серверу localhost:50051" << std::endl;

Объяснение:

grpc::CreateChannel() создаёт канал к серверу. grpc::InsecureChannelCredentials() отключает шифрование (для разработки). scoriadb::ScoriaDB::NewStub() создаёт клиент.


Аутентификация — получаем токен (C++)

Код:

grpc::ClientContext auth_ctx;
scoriadb::AuthRequest auth_req;
auth_req.set_username("admin");
auth_req.set_password("2027");
scoriadb::AuthResponse auth_resp;
stub->Authenticate(&auth_ctx, auth_req, &auth_resp);
std::string token = auth_resp.jwt_token();
std::cout << "✅ Токен получен: " << token.substr(0, 20) << "..." << std::endl;

Объяснение:

ClientContext — объект, который хранит метаданные для запроса. auth_req.set_username() и set_password() задают поля запроса. stub->Authenticate() отправляет запрос. auth_resp.jwt_token() возвращает токен.


Запись данных — Put (C++)

Код:

grpc::ClientContext put_ctx;
put_ctx.AddMetadata("authorization", "Bearer " + token);

scoriadb::PutRequest put_req;
put_req.set_key("user:1:name");
put_req.set_value("Alice");
scoriadb::PutResponse put_resp;
stub->Put(&put_ctx, put_req, &put_resp);
std::cout << "✅ Записано: user:1:name -> Alice" << std::endl;

Объяснение:

put_ctx.AddMetadata() добавляет заголовок с токеном. put_req.set_key() и set_value() задают ключ и значение. stub->Put() отправляет запрос.


Чтение данных — Get (C++)

Код:

grpc::ClientContext get_ctx;
get_ctx.AddMetadata("authorization", "Bearer " + token);

scoriadb::GetRequest get_req;
get_req.set_key("user:1:name");
scoriadb::GetResponse get_resp;
stub->Get(&get_ctx, get_req, &get_resp);

if (get_resp.found()) {
    std::cout << "✅ Прочитано: user:1:name -> " << get_resp.value() << std::endl;
} else {
    std::cout << "❌ Ключ не найден" << std::endl;
}

Объяснение:

get_resp.found() проверяет, найден ли ключ. get_resp.value() возвращает значение в виде строки.


Удаление данных — Delete (C++)

Код:

grpc::ClientContext del_ctx;
del_ctx.AddMetadata("authorization", "Bearer " + token);

scoriadb::DeleteRequest del_req;
del_req.set_key("user:1:name");
scoriadb::DeleteResponse del_resp;
stub->Delete(&del_ctx, del_req, &del_resp);
std::cout << "✅ Удалено: user:1:name" << std::endl;

Объяснение:

stub->Delete() отправляет запрос на удаление.


Сканирование — Scan (C++)

Код:

grpc::ClientContext scan_ctx;
scan_ctx.AddMetadata("authorization", "Bearer " + token);

scoriadb::ScanRequest scan_req;
scan_req.set_prefix("user:");
scan_req.set_cf_name("default");

auto reader = stub->Scan(&scan_ctx, scan_req);
scoriadb::ScanResponse scan_resp;
std::cout << "📋 Результаты сканирования:" << std::endl;
int count = 0;
while (reader->Read(&scan_resp)) {
    std::cout << "  " << scan_resp.key() << " -> " << scan_resp.value() << std::endl;
    count++;
}
std::cout << "✅ Всего найдено: " << count << " ключей" << std::endl;

Объяснение:

stub->Scan() возвращает reader (читатель). reader->Read() читает следующую запись. Если записей больше нет, возвращает false. Каждая запись содержит key() и value().


Клиент на Rust

Зависимости (Rust)

Cargo.toml:

[dependencies]
tonic = "0.10"
prost = "0.12"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Объяснение:

tonic — это gRPC-клиент для Rust. prost — это библиотека для работы с protobuf. tokio — асинхронный рантайм, нужен для работы с tonic.


Подключение к серверу (Rust)

Код:

let mut client = ScoriaDbClient::connect("http://localhost:50051").await?;
println!("✅ Подключено к серверу localhost:50051");

Объяснение:

ScoriaDbClient::connect() создаёт клиент и подключается к серверу. .await? — это асинхронный вызов в Rust. mut — клиент должен быть изменяемым, потому что методы меняют его состояние.


Аутентификация — получаем токен (Rust)

Код:

let auth_req = tonic::Request::new(AuthRequest {
    username: "admin".into(),
    password: "2027".into(),
});
let auth_resp = client.authenticate(auth_req).await?.into_inner();
let token = auth_resp.jwt_token;
println!("✅ Токен получен: {}...", &token[..20]);

Объяснение:

tonic::Request::new() создаёт gRPC-запрос. AuthRequest — структура с полями username и password. .into() превращает строку в String. client.authenticate() отправляет запрос. .await? ждёт ответ. .into_inner() достаёт ответ из gRPC-обёртки. token — это строка JWT.


Запись данных — Put (Rust)

Код:

let mut put_req = tonic::Request::new(PutRequest {
    key: b"user:1:name".to_vec(),
    value: b"Alice".to_vec(),
});
put_req.metadata_mut().insert(
    "authorization",
    format!("Bearer {}", token).parse()?
);
client.put(put_req).await?;
println!("✅ Записано: user:1:name -> Alice");

Объяснение:

b"user:1:name".to_vec() создаёт байтовый вектор из строки. put_req.metadata_mut() даёт доступ к метаданным запроса. .insert() добавляет заголовок с токеном. format!("Bearer {}", token) создаёт строку с токеном. .parse()? превращает строку в тип MetadataValue. client.put() отправляет запрос.


Чтение данных — Get (Rust)

Код:

let mut get_req = tonic::Request::new(GetRequest {
    key: b"user:1:name".to_vec(),
});
get_req.metadata_mut().insert(
    "authorization",
    format!("Bearer {}", token).parse()?
);
let resp = client.get(get_req).await?.into_inner();

if resp.found {
    let value = String::from_utf8(resp.value)?;
    println!("✅ Прочитано: user:1:name -> {}", value);
} else {
    println!("❌ Ключ не найден");
}

Объяснение:

client.get() отправляет запрос на чтение. resp.found проверяет, найден ли ключ. String::from_utf8() превращает байты в строку (может вернуть ошибку, поэтому ?).


Удаление данных — Delete (Rust)

Код:

let mut del_req = tonic::Request::new(DeleteRequest {
    key: b"user:1:name".to_vec(),
});
del_req.metadata_mut().insert(
    "authorization",
    format!("Bearer {}", token).parse()?
);
client.delete(del_req).await?;
println!("✅ Удалено: user:1:name");

Объяснение:

client.delete() отправляет запрос на удаление.


Сканирование — Scan (Rust)

Код:

let mut scan_req = tonic::Request::new(ScanRequest {
    prefix: b"user:".to_vec(),
    cf_name: "default".to_string(),
});
scan_req.metadata_mut().insert(
    "authorization",
    format!("Bearer {}", token).parse()?
);

let mut stream = client.scan(scan_req).await?.into_inner();
println!("📋 Результаты сканирования:");
let mut count = 0;
while let Some(resp) = stream.message().await? {
    println!("  {} -> {}", String::from_utf8(resp.key)?, String::from_utf8(resp.value)?);
    count += 1;
}
println!("✅ Всего найдено: {} ключей", count);

Объяснение:

client.scan() возвращает поток (stream). stream.message().await? получает следующее сообщение. Если сообщений больше нет, возвращает None. Каждое сообщение содержит key и value в байтах.


Клиент на TypeScript / Node.js

Установка (TypeScript)

npm install @grpc/grpc-js @grpc/proto-loader

Объяснение:

@grpc/grpc-js — это gRPC-клиент для Node.js. @grpc/proto-loader — загрузчик .proto файлов.


Подключение к серверу (TypeScript)

Код:

const packageDef = protoLoader.loadSync('proto/scoriadb.proto', {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDef).scoriadb as any;

const client = new proto.ScoriaDB(
    'localhost:50051',
    grpc.credentials.createInsecure()
);
console.log('✅ Подключено к серверу localhost:50051');

Объяснение:

protoLoader.loadSync() загружает .proto файл и преобразует его в JavaScript-объект. keepCase: true сохраняет оригинальные имена полей. grpc.loadPackageDefinition() создаёт объект с определёнными сервисами. new proto.ScoriaDB() создаёт клиент.


Аутентификация — получаем токен (TypeScript)

Код:

client.Authenticate(
    { username: 'admin', password: '2027' },
    (err: any, authResp: any) => {
        if (err) {
            console.error('Ошибка аутентификации:', err);
            return;
        }
        const token = authResp.jwt_token;
        console.log(`✅ Токен получен: ${token.substring(0, 20)}...`);
        // Остальной код будет внутри этого колбэка
    }
);

Объяснение:

В Node.js gRPC методы работают с колбэками (асинхронно). Первый аргумент колбэка — ошибка, второй — ответ. authResp.jwt_token — это токен. Все следующие операции нужно выполнять внутри колбэка, потому что токен становится доступен только после ответа сервера.


Создаём заголовок с токеном (TypeScript)

Код:

const metadata = new grpc.Metadata();
metadata.add('authorization', `Bearer ${token}`);

Объяснение:

new grpc.Metadata() создаёт объект для заголовков. .add() добавляет заголовок. Bearer ${token} — значение заголовка.


Запись данных — Put (TypeScript)

Код:

client.Put(
    {
        key: Buffer.from('user:1:name'),
        value: Buffer.from('Alice'),
    },
    metadata,
    (err: any) => {
        if (err) {
            console.error('Ошибка записи:', err);
            return;
        }
        console.log('✅ Записано: user:1:name -> Alice');
    }
);

Объяснение:

Buffer.from() превращает строку в байты. metadata — это заголовок с токеном. Колбэк вызывается, когда сервер ответит.


Чтение данных — Get (TypeScript)

Код:

client.Get(
    { key: Buffer.from('user:1:name') },
    metadata,
    (err: any, resp: any) => {
        if (err) {
            console.error('Ошибка чтения:', err);
            return;
        }
        if (resp.found) {
            console.log(`✅ Прочитано: user:1:name -> ${resp.value.toString()}`);
        } else {
            console.log('❌ Ключ не найден');
        }
    }
);

Объяснение:

resp.found проверяет, найден ли ключ. resp.value.toString() превращает байты в строку.


Удаление данных — Delete (TypeScript)

Код:

client.Delete(
    { key: Buffer.from('user:1:name') },
    metadata,
    (err: any) => {
        if (err) {
            console.error('Ошибка удаления:', err);
            return;
        }
        console.log('✅ Удалено: user:1:name');
    }
);

Объяснение:

client.Delete() отправляет запрос на удаление.


Сканирование — Scan (TypeScript)

Код:

const scanStream = client.Scan(
    { prefix: Buffer.from('user:'), cfName: 'default' },
    metadata
);
console.log('📋 Результаты сканирования:');
let count = 0;
scanStream.on('data', (resp: any) => {
    console.log(`  ${resp.key.toString()} -> ${resp.value.toString()}`);
    count++;
});
scanStream.on('end', () => {
    console.log(`✅ Всего найдено: ${count} ключей`);
});
scanStream.on('error', (err: any) => {
    console.error('Ошибка сканирования:', err);
});

Объяснение:

client.Scan() возвращает поток (stream). .on('data', callback) вызывается для каждой полученной записи. .on('end', callback) вызывается, когда сервер закончил отправку. .on('error', callback) вызывается при ошибке.


🏛️ Архитектура

Общая схема

ScoriaDB — LSM-дерево с разделением ключей и значений (key-value separation, inspired by WiscKey). Ключи хранятся в lock-free skip list (MemTable) и SSTable-индексе на диске. Значения — в отдельном append-only Value Log на NVMe.

Почему разделение: В классическом LSM-дереве (RocksDB, LevelDB) compaction перезаписывает и ключи, и значения на каждом уровне. При размере значения 1 КБ и ключа 16 Б это означает, что 98% данных при compaction — значения, которые просто копируются без изменений. Это даёт Write Amplification 10-30×.

ScoriaDB хранит в SSTable только ключи и 12-байтовые указатели [offset, size, checksum] на значения в Value Log. Compaction трогает только ключи и указатели — 0.05% данных. Write Amplification снижается до 5-12× (текущая версия) с целью 1.05× в v0.5.0.


Компоненты движка

MemTable — Lock-Free Skip List

Назначение: Буфер записи в памяти. Все новые ключи попадают сюда.

Реализация:

Производительность: 5M операций вставки в секунду на одно ядро. 47M поиска в секунду при попадании в кэш L1/L2.

WAL (Write-Ahead Log)

Назначение: Гарантия durability. Все записи сначала попадают в WAL на диске, потом в MemTable. При краше данные восстанавливаются из WAL.

Реализация:

Режим 1: Group Commit + Sync (по умолчанию) GroupCommitEnabled=true, SyncMode=true

  1. Put() пишет запись в WAL-буфер в памяти и сразу возвращает ACK клиенту. Запись не ждёт fsync
  2. Фоновый воркер syncLoop срабатывает каждые GroupCommitInterval (по умолчанию 10 мс)
  3. Воркер вызывает file.Write() для всего накопленного буфера, затем file.Sync()
  4. При краше теряются записи, сделанные после последнего fsync (≤10 мс)
  5. Производительность: 2.09M PUT/с, задержка 580 нс
  6. Рекомендован для продакшен-нагрузок: пользовательские данные, контент, стандартные приложения

Режим 2: Group Commit, без fsync GroupCommitEnabled=true, SyncMode=false

  1. Аналогичен режиму 1, но syncLoop вызывает только file.Write() без file.Sync()
  2. Данные уходят в Page Cache ОС, но не гарантируются на диске немедленно. ОС сама решит, когда сбросить (обычно 5-30 секунд)
  3. Максимальная скорость: 2.15M+ PUT/с, минимальная durability
  4. Для некритичных данных: логи, метрики, кэши, аналитика в реальном времени

Режим 3: Strict Sync GroupCommitEnabled=false, SyncMode=true

  1. Групповой коммит и фоновый воркер syncLoop полностью отключаются
  2. Каждый Put() напрямую вызывает WAL.Write(), который делает file.Write() + file.Sync() синхронно
  3. ACK клиенту возвращается только после завершения file.Sync() — данные гарантированно на диске
  4. Производительность: 375K PUT/с, задержка 3.2 мкс
  5. Для финансовых транзакций, критичных счётчиков, систем учёта. Для сравнения: RocksDB в аналогичном режиме даёт ~1-2K оп/с. ScoriaDB в 187 раз быстрее

Общие характеристики WAL:

Value Log (VLog)

Назначение: Хранение значений на диске. Отделено от ключей.

Реализация:

SSTable (Sorted String Table)

Назначение: Постоянное хранение ключей и указателей на значения. Создаётся при flush MemTable на диск.

Структура файла SSTable:

[Block 0] [Block 1] ... [Block N] [Index] [Bloom Filter] [Footer]

Поиск в SSTable:

  1. Проверить Bloom-фильтр. Если ключа точно нет — возврат nil
  2. Бинарный поиск по индексу — найти блок, содержащий ключ
  3. Прочитать блок через mmap (0 системных вызовов)
  4. Бинарный поиск внутри блока — найти ключ
  5. Прочитать 12-байтовый указатель на значение в Value Log
  6. Прочитать значение из VLog через mmap — вернуть клиенту

Compaction: объединяет несколько SSTable одного уровня в один файл следующего уровня. Трогает только ключи и указатели (12 байт на запись). Значения в Value Log не перезаписываются. Write Amplification: 5-12× в текущей версии, цель 1.05× в v0.5.0.


Путь записи (детально)

Клиент вызывает Put(key, value)
│
├─ 1. Хеширование ключа
│   xxhash(key) → определение шарда (shard-per-core, 8 шардов на 8 потоков)
│
├─ 2. Копирование ключа в арену
│   arena.Allocate(len(key)) → ключ в предварительно выделенной памяти, не в куче Go
│
├─ 3. Вставка в MemTable (lock-free skip list)
│   skiplist.Insert(key, valuePtr, timestamp)
│   CAS-операция на каждом уровне skip list
│   Писатели не блокируют друг друга и читателей
│
├─ 4. Запись в WAL
│   │
│   ├─ [Если GroupCommitEnabled = true]
│   │   wal.Write(entry) → запись в WAL-буфер в памяти → ACK клиенту (580 нс)
│   │   Клиент не ждёт fsync
│   │   [async, syncLoop каждые 10 мс]:
│   │     file.Write(накопленный буфер)
│   │     └─ [Если SyncMode = true]  → file.Sync() → данные на диске
│   │     └─ [Если SyncMode = false] → без fsync → данные в Page Cache ОС
│   │
│   └─ [Если GroupCommitEnabled = false, Strict Sync]
│       wal.Write(entry) → file.Write() → file.Sync() → ACK клиенту (3.2 мкс)
│       Клиент ждёт завершения fsync. Данные гарантированно на диске.
│
└─ 5. [Асинхронно] Сброс значения в Value Log
    Если len(value) > 64 байт:
      vlog.Append(value) → последовательная запись в конец файла на NVMe
      Обновить указатель в MemTable: [offset, size, checksum]
    Иначе:
      Значение будет храниться inline в SSTable (до 64 байт)

Задержка по шагам (Group Commit + Sync, значение 16 Б): | Шаг | Время | |—–|——-| | Хеширование + шард | ~10 нс | | Копирование в арену | ~20 нс | | CAS-вставка в skip list | ~300 нс | | Запись в WAL-буфер | ~100 нс | | ACK клиенту | ~150 нс | | Итого | ~580 нс |

Задержка по шагам (Strict Sync, значение 16 Б): | Шаг | Время | |—–|——-| | Хеширование + шард | ~10 нс | | Копирование в арену | ~20 нс | | CAS-вставка в skip list | ~300 нс | | file.Write() + file.Sync() | ~2.8 мкс | | ACK клиенту | ~70 нс | | Итого | ~3.2 мкс |


Путь чтения (детально)

Клиент вызывает Get(key)
│
├─ 1. Хеширование ключа → определение шарда
│
├─ 2. Поиск в MemTable (lock-free skip list)
│   skiplist.Search(key)
│   Если найден → вернуть значение (срез арены, 0 аллокаций)
│   Время: 24.6 нс при попадании в кэш L1/L2
│
├─ 3. [Если не найден в MemTable] Поиск в SSTable на диске
│   │
│   ├─ 3a. Range Filter
│   │   Проверить min/max ключи каждого SSTable
│   │   Отсеять файлы, где ключа точно нет
│   │
│   ├─ 3b. Bloom-фильтр
│   │   Для каждого SSTable проверить Bloom-фильтр
│   │   Если ключа точно нет → следующий SSTable
│   │   Время: 16 нс на проверку
│   │   Отсеивает 99% отсутствующих ключей
│   │
│   ├─ 3c. Бинарный поиск по индексу SSTable
│   │   Найти блок, содержащий ключ
│   │   Индекс в mmap-памяти — 0 системных вызовов
│   │
│   ├─ 3d. Чтение блока через mmap
│   │   Блок уже в page cache (если недавно читали) или читается с NVMe
│   │   0 системных вызовов, 0 копирований
│   │
│   ├─ 3e. Бинарный поиск внутри блока
│   │   Найти ключ и 12-байтовый указатель на значение
│   │
│   └─ 3f. Чтение значения из Value Log
│       vlog.Read(offset, size)
│       Mmap-срез файла → вернуть клиенту
│       0 системных вызовов, 0 копирований, 1 аллокация (срез)
│
└─ 4. Возврат значения клиенту
    Если ключ не найден нигде → nil (ключа нет в базе)

Shard-per-core

Каждый поток работает со своим шардом. Шард определяется хешем ключа: shard = xxhash(key) % numShards.

Состав шарда:

Преимущества:


🧩 Возможности

ACID-транзакции (Snapshot Isolation)

Модель: Snapshot Isolation с обнаружением конфликтов при коммите.

Как работает:

  1. NewTransaction() создаёт транзакцию, фиксирует startTS (текущий счётчик времени)
  2. Все чтения внутри транзакции (tx.Get()) видят снапшот базы на момент startTS. MVCC гарантирует, что более новые версии ключей не видны
  3. Все записи (tx.Put(), tx.Delete()) накапливаются в буфере транзакции и не видны другим транзакциям
  4. tx.Commit() атомарно применяет все операции с commitTS = NextTimestamp(). Проверяет, не изменил ли кто-то те же ключи между startTS и commitTS. Если изменил — возвращает ErrConflict
  5. При ErrConflict клиент должен повторить транзакцию
for {
    tx := db.NewTransaction()
    val, _ := tx.Get([]byte("account:A"))
    tx.Put([]byte("account:A"), updateBalance(val))
    if err := tx.Commit(); err != scoria.ErrConflict {
        break // успех
    }
    // повтор
}

MVCC (Multi-Version Concurrency Control)

Формат ключа: [user_key][^timestamp], где ^timestamp — побитовая инверсия таймстемпа.

Почему инверсия: При лексикографической сортировке меньший ключ идёт раньше. Инвертированный таймстемп даёт обратный порядок: чем новее версия, тем меньше её инвертированный таймстемп, и тем раньше она идёт при поиске по префиксу.

Пример:

Поскольку 0x…37 < 0x…9B, версия ts=200 идёт раньше и будет найдена первой. Читатель с snapshotTS = 150 увидит версию ts=100 (она идёт второй) и проигнорирует ts=200.

Преимущества:

Колоночные семейства

Назначение: Изолированные LSM-деревья внутри одной базы данных.

Реализация: Каждое CF — это отдельный экземпляр LSMEngine со своей MemTable, своими SSTable-уровнями, своим Value Log и своим расписанием compaction.

db.CreateCF("logs")     // создаёт новое LSM-дерево
db.CreateCF("sessions") // ещё одно

// Запись в разные CF
db.PutCF("logs", []byte("event:1"), []byte("started"))
db.PutCF("sessions", []byte("user:1"), []byte("token"))

// Чтение из CF
val, _ := db.GetCF("logs", []byte("event:1"))

Когда использовать:

Lock-Free MemTable

Структура данных: Skip list на 16 уровнях с вероятностью повышения 1/4.

Почему lock-free:

Арена:

WAL: три режима синхронизации

Управляются двумя полями структуры WALOptions (internal/engine/options.go):

Режим GroupCommitEnabled SyncMode ops/с (16 Б) Задержка Потеря при сбое Описание
Group Commit + Sync (по умолчанию) true true 2.09M 580 нс ≤10 мс Буферизация, fsync раз в 10 мс. Рекомендован для продакшена.
Group Commit, без fsync true false 2.15M+ 570 нс ≤10 мс (Page Cache) Буферизация без fsync. Данные в Page Cache ОС. Максимальная скорость.
Strict Sync false true 375K 3.2 мкс 0 потерь Каждый Put() делает write() + file.Sync() синхронно. Данные на диске до ACK.

Код для настройки:

// По умолчанию: Group Commit + Sync
db, _ := scoria.NewScoriaDB("./data")

// Strict Sync (максимальная надёжность)
db, _ := scoria.NewScoriaDB("./data",
    scoria.WithSync(true),
    scoria.WithGroupCommitDisabled(),
)

// Group Commit, без fsync (максимальная скорость)
db, _ := scoria.NewScoriaDB("./data",
    scoria.WithGroupCommit(10*time.Millisecond),
    scoria.WithSync(false),
)

SSTable + Bloom-фильтр

SSTable: Отсортированный файл ключей и указателей на значения. Создаётся при flush MemTable на диск.

Структура:

Bloom-фильтр:

Пакетная запись (Batch)

Назначение: Атомарная запись нескольких операций с одним fsync.

batch := db.NewBatch()
batch.AddPut([]byte("k1"), []byte("v1"))
batch.AddPut([]byte("k2"), []byte("v2"))
batch.AddDelete([]byte("old"))
batch.Commit() // атомарно, один fsync на пачку

Как работает:

Value Log

Назначение: Хранение значений на диске отдельно от ключей.

Характеристики:

JWT-аутентификация

Роли: | Роль | Права | |——|——-| | admin | Всё: данные, пользователи, CF, GC | | readwrite | Чтение, запись, удаление, сканирование | | readonly | Только чтение и сканирование |

Токен: JWT с подписью HMAC-SHA256. Живёт 24 часа. Содержит логин, роли, время создания и истечения.

gRPC + REST + CLI

Автоматическая генерация клиентов из .proto файла. Поддерживаются: Go, Python, Java, C++, Rust, TypeScript, C#, Kotlin, Dart, PHP, Ruby, Swift, Objective-C. CLI для администрирования и ручных операций. REST API на порту 8080.


🗺️ Roadmap

ScoriaDB — это проект с прозрачным планом развития. Мы публикуем дорожную карту, чтобы вы могли планировать внедрение и понимать, когда появятся нужные вам возможности.

Принципы планирования:


2026 год: фундамент и production-готовность

Версия Срок Что нового Для кого
v0.3.0 ✅ Авг 2026 47M GET/s, 0 аллокаций (GET), lock-free skip list, shard-per-core, арена 64MB, mmap Pet-проекты, прототипы, энтузиасты
v0.3.1 🔄 Авг 2026 Исправление критических багов WAL (SyncMode, Flush, syncCh), CRC32, crash-тесты Все пользователи v0.3.0
v0.4.0 📅 Сен 2026 Бинарный поиск в SSTable, PUT → 0 аллокаций, TTL (Time-To-Live), флаг --no-auth для разработки Кэширование, сессии, высокие нагрузки
v0.5.0-alpha 📅 Окт 2026 Raft-кластер (3 ноды) — первая распределённая версия Кластерные системы, отказоустойчивость
v0.5.0-beta 📅 Ноя 2026 TLS, mTLS, аутентификация по файлу конфигурации, namespaces Безопасные среды, мультитенантность
v0.5.0 GA 📅 Дек 2026 Автоматическая GC Value Log, compaction только ключей, WA → 1.05×, Prometheus-метрики Продакшен с критичными данными

2027 год: платформа и enterprise

Версия Срок Что нового Бизнес-ценность
v0.6.0 📅 Q1 2027 io_uring (Linux), 100K одновременных соединений, Direct I/O, ZSTD-сжатие Снижение CPU на 30%, экономия диска в 3–5 раз
v0.7.0 📅 Q2 2027 Change Data Capture (CDC), публикация в Kafka/NATS/WebSocket, Kafka Connect Интеграция в событийные системы, стриминг
v0.8.0 📅 Q3 2027 ScoriaDB Cloud (Beta), Kubernetes Operator, Jepsen-сертификация, Terraform Provider Облачный сервис, управление в K8s, доказанная надёжность
v1.0.0 📅 Q4 2027 Row-Level Security (RLS), полный аудит (audit log), mTLS для всех соединений, SLA 99.99% Enterprise-готовность, финтех, healthcare

Ключевые вехи для бизнеса

Дата Событие Что получают клиенты
Дек 2026 v0.5.0 GA Первая версия, рекомендованная для продакшена. Автоматическая GC, WA → 1.05×, TLS
Мар 2027 Три production-кейса Доказательство в реальных условиях: AdTech, FinTech, Gaming
Июн 2027 ScoriaDB Cloud Beta Облачный сервис с бесплатным тарифом. Запуск кластера за 5 минут
Июл 2027 Jepsen-сертификация Формальное подтверждение корректности распределённой системы
Сен 2027 K8s Operator Управление кластером в Kubernetes через CRD
Дек 2027 v1.0.0 Полная enterprise-платформа с RLS, аудитом и mTLS

Почему вы можете доверять этой дорожной карте

  1. Публичность. Вы всегда видите план и статус.
  2. Честность. Мы открыто говорим об ограничениях текущей версии.
  3. Регулярность. Релизы выходят каждый месяц.
  4. Приоритеты. Мы делаем то, что нужно клиентам, а не фичи ради фич.
  5. Открытый код. Вы можете следить за разработкой в репозитории.

Как повлиять на дорожную карту

Мы открыты к обратной связи. Если вам нужна конкретная функция — создайте issue на GitHub или напишите автору.


⬆ Вернуться к оглавлению