| 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 языках.
Что нужно: 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
Что происходит при запуске:
./data (если её нет) и инициализирует LSM-движок__auth__ для хранения пользователейadmin, пароль 202750051 и REST-сервер на порту 8080Откройте второй терминал. Перейдите в папку с проектом и соберите 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 работает без сервера и без аутентификации:
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 использует систему пользователей с ролями для контроля доступа. Это работает как в любой серьёзной БД: каждый, кто подключается, должен представиться, и система решает, что ему можно делать.
Пользователь — это учётная запись, под которой кто-то подключается к базе. У каждого пользователя есть:
admin, app_backend, analyst)Где хранятся пользователи: В системной колоночной семействе __auth__. Она создаётся автоматически при первом запуске сервера. Вы не видите её в списке CF, но она есть.
Роль — это набор разрешений. В ScoriaDB три роли:
| Роль | Что можно делать | Кому давать |
|---|---|---|
admin |
Всё: управление пользователями (создавать, удалять, менять пароли), управление CF (создавать, удалять), запуск GC, любые операции с данными (чтение, запись, удаление, сканирование) | Только самым доверенным. Минимум 1 администратор всегда должен быть |
readwrite |
Полный доступ к данным: чтение, запись, удаление, сканирование. НЕ может управлять пользователями и CF | Сервисы и приложения, которые работают с данными |
readonly |
Только чтение: get и scan. НЕ может изменять или удалять данные |
Аналитики, отчёты, системы мониторинга |
При первом запуске сервер автоматически создаёт администратора:
Логин: admin
Пароль: 2027
Роль: admin
⚠️ Это временный пароль. Обязательно смените его в продакшене!
Когда нужно: Сразу после первого запуска. Обязательно в продакшене.
Как сделать:
./scoria-cli --token=$TOKEN admin change-password admin <новый-пароль>
Пример:
./scoria-cli --token=$TOKEN admin change-password admin Sc0ri@DB_2026!
Требования к паролю:
Когда нужно: У вас есть микросервис или приложение, которое должно читать и писать данные.
Как сделать:
./scoria-cli --token=$TOKEN admin user-add app_backend securePass123 --roles=readwrite
Результат: Появится пользователь app_backend с ролью readwrite. Теперь этот сервис может подключаться к БД и работать с данными.
Почему это правильно: Сервис не должен иметь прав администратора. Если его взломают, злоумышленник не сможет управлять пользователями или удалять CF.
Когда нужно: Аналитик или дашборд должны смотреть данные, но не менять их.
Как сделать:
./scoria-cli --token=$TOKEN admin user-add analyst readOnlyPass789 --roles=readonly
Результат: Появится пользователь analyst с ролью readonly. Он может выполнять только get и scan. Любая попытка put или delete будет отклонена.
Почему это правильно: Аналитик не должен случайно или намеренно изменить данные. Это защита от ошибок и злоумышленников.
Когда нужно: У вас крупная команда, и нужно, чтобы несколько человек могли управлять базой.
Как сделать:
./scoria-cli --token=$TOKEN admin user-add backup_admin backupPass456 --roles=admin
Результат: Появится второй администратор. Если первый потеряет пароль или уйдёт из компании, второй сможет управлять базой.
Когда нужно: Проверить, кто есть в системе, или найти забытого пользователя.
Как сделать:
./scoria-cli --token=$TOKEN admin list-users
Пример вывода:
admin (admin)
app_backend (readwrite)
analyst (readonly)
backup_admin (admin)
Когда нужно: Пользователь забыл пароль, пароль скомпрометирован, или нужно обновить пароль по политике безопасности.
Как сделать:
./scoria-cli --token=$TOKEN admin change-password app_backend newSecurePass456
Важно: Это может сделать только администратор. Сам пользователь не может сменить свой пароль (пока что, в следующих версиях появится).
Проблема: Вы забыли пароль от admin или потеряли токен.
Решение: На данный момент восстановление пароля невозможно. Восстановите базу из бэкапа или удалите папку ./data и перезапустите сервер — тогда администратор создастся заново.
rm -rf ./data
./scoria-server
⚠️ Это удалит ВСЕ данные! Делайте только если есть бэкап.
Когда нужно: Пользователь уволился, сервис больше не используется, или нужно сократить количество учётных записей.
Как сделать:
./scoria-cli --token=$TOKEN admin user-delete analyst
Важно: Нельзя удалить последнего администратора. Всегда должен быть хотя бы один admin.
Что такое токен: Это строка, которую сервер выдаёт после успешной аутентификации. Она подтверждает, что вы — тот, за кого себя выдаёте.
Как он выглядит:
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 |
Такой пользователь не существует | Проверьте логин, создайте пользователя |
admin для приложений.Что это: Value Log растёт с каждым обновлением и удалением. Старые значения остаются на диске. GC собирает их и освобождает место.
Когда запускать: При заполнении диска > 80%.
Команда:
./scoria-cli --token=$TOKEN admin gc
Что происходит:
Сколько времени занимает: Зависит от объёма данных. Для 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
Если вы использовали 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 позволяет настроить поведение под вашу задачу. Выбирайте профиль в зависимости от того, что для вас важнее: скорость, надёжность или работа с большими данными.
Для кого: Высоконагруженные системы, где важна скорость записи.
Что делает: Включает групповой коммит с 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 нс.
Для кого: Финансовые системы, критичные данные, где потеря недопустима. Платежи, балансы, критичные счётчики.
Что делает: Полностью отключает групповой коммит (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().
Для кого: Хранение изображений, видео, документов, эмбеддингов.
Что делает: Значения больше указанного порога сразу идут в Value Log, а не в MemTable.
Код:
db, _ := scoria.NewScoriaDB("./data",
scoria.WithValueLogThreshold(1024), // 1 КБ порог
)
Результат: PUT 64 КБ — 19 800 оп/с, 1.24 ГБ/с.
Для кого: Стандартные приложения, где есть и чтение, и запись.
Код:
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 как компонента. |
Измеряет, как меняется скорость записи при росте размера значения. Режим: 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 ГБ/с.
| База данных | 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 не перезаписываются.
Измеряет полную задержку от клиента до сервера и обратно через 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 секунд. В отчёте указана медиана.
Что измеряет каждый бенчмарк:
GetWithTS(key, MaxUint64) в цикле. Измеряет скорость поиска в lock-free skip list с попаданием в L1/L2 кэш. 0 аллокаций в куче — значение возвращается как срез арены.GroupCommitEnabled=true, SyncMode=true. Полный путь: хеш ключа → копирование в арену → CAS-вставка в skip list → добавление в WAL-буфер → ACK клиенту. fsync не вызывается синхронно — фоновый воркер syncLoop сбрасывает буфер и делает fsync раз в 10 мс. Это режим по умолчанию.GroupCommitEnabled=true, SyncMode=false. Отличается от предыдущего отсутствием file.Sync() в syncLoop. Данные уходят в Page Cache ОС, но не гарантируются на диске. Максимальная скорость, минимальная durability.GroupCommitEnabled=false, SyncMode=true. Групповой коммит полностью отключён. Каждый Put() напрямую вызывает WAL.Write(), который делает file.Write() + file.Sync() синхронно. ACK возвращается только после fsync. Данные гарантированно на диске.[offset, size, checksum] читается из SSTable, затем значение читается из VLog через mmap. Ноль системных вызовов, ноль копирований, 1 аллокация (срез mmap-памяти).Источники для сравнения: Данные конкурентов взяты из официальных бенчмарков (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 решает конкретные задачи, где скорость, персистентность и эффективность важнее 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.
Задача
База данных внутри микросервиса, без отдельного процесса, без сети, без аутентификации.
Проблема
| Решение | Проблема |
|---|---|
| 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-кэши, платформы обработки медиа.
С выходом кластерной версии 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 — это специализированный инструмент, а не серебряная пуля. Вот случаи, когда стоит посмотреть в другую сторону.
Проблема
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 пока не для вас. Если у вас есть время на тестирование и вы готовы участвовать в развитии — добро пожаловать.
Проблема
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 get github.com/f4ga/ScoriaDB/pkg/scoria@v0.3.0
db, err := scoria.NewScoriaDB("./data")
if err != nil {
log.Fatal(err)
}
defer db.Close()
Что происходит в коде:
NewScoriaDB("./data") открывает базу в директории ./data. Если директории нет — создаёт её и все необходимые файлы: WAL, Value Log, место под SSTabledefer db.Close() гарантирует, что при выходе из функции все ожидающие записи будут сброшены на диск, файловые дескрипторы закрыты, блокировки сняты. Без этого данные могут быть поврежденыerr := db.Put([]byte("user:1:name"), []byte("Alice"))
Что происходит под капотом:
value, err := db.Get([]byte("user:1:name"))
if value != nil {
fmt.Printf("%s\n", value)
}
Что происходит под капотом:
Важно: 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 {
// Другая транзакция изменила один из ключей — повторить
}
Что происходит:
NewTransaction() создаёт транзакцию с Snapshot Isolation. Все чтения внутри транзакции видят снапшот базы на момент начала транзакцииtx.Put() добавляет операцию в буфер транзакции — невидимо для других транзакцийtx.Commit() атомарно применяет все операции. Если другая транзакция изменила те же ключи — возвращает ErrConflictdefer tx.Rollback() гарантирует откат при ошибке или панике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)
}
ScanCF(cf, prefix) возвращает итератор по ключам с заданным префиксомiter.Next() продвигает итератор, возвращает false когда ключи закончилисьiter.Key() и iter.Value() возвращают текущую записьdefer iter.Close() и проверяйте iter.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:
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 символов, заглавная буква, цифра, спецсимвол.
Четыре основные команды:
| Команда | Что делает | Пример |
|---|---|---|
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"
Что такое 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
Роли:
| Роль | Что можно делать |
|---|---|
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 — это система, которая позволяет программам на разных языках программирования общаться друг с другом через сеть. ScoriaDB написан на Go, но вы можете использовать его из Python, Java, Rust, C++ и других языков.
Как это выглядит в реальности:
Ваша программа (Python/Java/Rust/...)
↓ отправляет запрос (через gRPC)
Сервер ScoriaDB (Go)
↓ обрабатывает запрос
↓ возвращает ответ
Ваша программа получает результат
Что нужно сделать, чтобы начать работу:
Что такое JWT-токен: Это строка, которая подтверждает вашу личность. Вы получаете её один раз при аутентификации (отправляете логин и пароль), а потом передаёте в каждом запросе. Как пропуск в офис.
Клиентский код на любом языке состоит из одних и тех же шагов:
localhost:50051authorization каждого запросаВот так выглядит общая схема на всех языках:
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 get github.com/f4ga/ScoriaDB/scoriadb/proto
Код:
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() гарантирует, что соединение закроется при выходе из функции.
Код:
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 — это токен, который мы получаем от сервера. Он нужен для всех следующих запросов.
Код:
md := metadata.Pairs("authorization", "Bearer "+token)
ctx := metadata.NewOutgoingContext(context.Background(), md)
Объяснение:
metadata.Pairs() создаёт заголовок. "authorization" — это имя заголовка (стандартное для JWT). "Bearer "+token — это значение, где слово Bearer + пробел + сам токен. metadata.NewOutgoingContext() добавляет этот заголовок в контекст. Теперь все запросы с этим контекстом будут содержать токен, и сервер будет знать, кто делает запрос.
Код:
_, 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("..."). Если ошибки нет — запись успешна.
Код:
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).
Код:
_, err = client.Delete(ctx, &pb.DeleteRequest{
Key: []byte("user:1:name"),
})
if err != nil {
log.Fatal("Ошибка удаления:", err)
}
fmt.Println("✅ Удалено: user:1:name")
Объяснение:
client.Delete() отправляет запрос на удаление ключа. После удаления ключ становится невидимым для чтения.
Код:
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.
Код:
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() вернёт ошибку — это защита от конфликтов.
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 (методы клиента). Без этого шага код не будет работать.
Код:
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 — это как пульт управления, через который вызываются все методы.
Код:
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 — это токен, который мы получаем от сервера. Токен нужен для всех следующих запросов.
Код:
metadata = (('authorization', f'Bearer {token}'),)
Объяснение:
metadata — это кортеж кортежей. Каждый внутренний кортеж содержит имя заголовка и значение. 'authorization' — имя заголовка (стандартное для JWT). f'Bearer {token}' — значение: слово Bearer + пробел + токен. Этот metadata передаётся в каждый запрос через параметр metadata=.
Код:
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 передаёт заголовок с токеном.
Код:
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().
Код:
stub.Delete(
proto_pb2.DeleteRequest(
key=b'user:1:name'
),
metadata=metadata
)
print('✅ Удалено: user:1:name')
Объяснение:
stub.Delete() отправляет запрос на удаление ключа.
Код:
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 в байтах.
Код:
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), ключом и значением.
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 файла. Без этих классов код не скомпилируется.
Код:
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 означает, что методы работают синхронно — ждут ответа от сервера.
Код:
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() — получаем токен.
Код:
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() — перехватчик, который добавляет заголовки к каждому запросу.
Код:
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.
Код:
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() превращает байты обратно в строку.
Код:
authStub.delete(DeleteRequest.newBuilder()
.setKey(ByteString.copyFromUtf8("user:1:name"))
.build());
System.out.println("✅ Удалено: user:1:name");
Объяснение:
authStub.delete() отправляет запрос на удаление.
Код:
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.
Код:
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).
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 — заголовочные файлы с классами для работы с сервером.
Код:
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() создаёт клиент.
Код:
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() возвращает токен.
Код:
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() отправляет запрос.
Код:
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() возвращает значение в виде строки.
Код:
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() отправляет запрос на удаление.
Код:
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().
Cargo.toml:
[dependencies]
tonic = "0.10"
prost = "0.12"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Объяснение:
tonic — это gRPC-клиент для Rust. prost — это библиотека для работы с protobuf. tokio — асинхронный рантайм, нужен для работы с tonic.
Код:
let mut client = ScoriaDbClient::connect("http://localhost:50051").await?;
println!("✅ Подключено к серверу localhost:50051");
Объяснение:
ScoriaDbClient::connect() создаёт клиент и подключается к серверу. .await? — это асинхронный вызов в Rust. mut — клиент должен быть изменяемым, потому что методы меняют его состояние.
Код:
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.
Код:
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() отправляет запрос.
Код:
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() превращает байты в строку (может вернуть ошибку, поэтому ?).
Код:
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() отправляет запрос на удаление.
Код:
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 в байтах.
npm install @grpc/grpc-js @grpc/proto-loader
Объяснение:
@grpc/grpc-js — это gRPC-клиент для Node.js. @grpc/proto-loader — загрузчик .proto файлов.
Код:
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() создаёт клиент.
Код:
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 — это токен. Все следующие операции нужно выполнять внутри колбэка, потому что токен становится доступен только после ответа сервера.
Код:
const metadata = new grpc.Metadata();
metadata.add('authorization', `Bearer ${token}`);
Объяснение:
new grpc.Metadata() создаёт объект для заголовков. .add() добавляет заголовок. Bearer ${token} — значение заголовка.
Код:
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 — это заголовок с токеном. Колбэк вызывается, когда сервер ответит.
Код:
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() превращает байты в строку.
Код:
client.Delete(
{ key: Buffer.from('user:1:name') },
metadata,
(err: any) => {
if (err) {
console.error('Ошибка удаления:', err);
return;
}
console.log('✅ Удалено: user:1:name');
}
);
Объяснение:
client.Delete() отправляет запрос на удаление.
Код:
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.
Назначение: Буфер записи в памяти. Все новые ключи попадают сюда.
Реализация:
Get() возвращает срез памяти арены напрямую, без аллокацийПроизводительность: 5M операций вставки в секунду на одно ядро. 47M поиска в секунду при попадании в кэш L1/L2.
Назначение: Гарантия durability. Все записи сначала попадают в WAL на диске, потом в MemTable. При краше данные восстанавливаются из WAL.
Реализация:
wal.log в директории с даннымиWALOptions (internal/engine/options.go):
GroupCommitEnabled — включает групповой коммит (буферизацию записей в памяти и фоновый воркер syncLoop)SyncMode — включает вызов file.Sync() после сброса буфера или каждой записиРежим 1: Group Commit + Sync (по умолчанию)
GroupCommitEnabled=true, SyncMode=true
Put() пишет запись в WAL-буфер в памяти и сразу возвращает ACK клиенту. Запись не ждёт fsyncsyncLoop срабатывает каждые GroupCommitInterval (по умолчанию 10 мс)file.Write() для всего накопленного буфера, затем file.Sync()Режим 2: Group Commit, без fsync
GroupCommitEnabled=true, SyncMode=false
syncLoop вызывает только file.Write() без file.Sync()Режим 3: Strict Sync
GroupCommitEnabled=false, SyncMode=true
syncLoop полностью отключаютсяPut() напрямую вызывает WAL.Write(), который делает file.Write() + file.Sync() синхронноfile.Sync() — данные гарантированно на дискеОбщие характеристики WAL:
Назначение: Хранение значений на диске. Отделено от ключей.
Реализация:
vlog.db. Значения пишутся последовательно в конец файла — максимальная пропускная способность NVMeWithValueLogThreshold()[offset: 8 байт, size: 2 байта, checksum: 2 байта CRC16]. Указатель хранится в SSTable вместо самого значенияGet() возвращает срез mmap-памяти напрямую — 0 системных вызовов, 0 копирований, 1 аллокация (срез)WithDirectIO()admin gc. Сканирует LSM-дерево, находит все живые ключи, копирует живые значения в новый VLog, удаляет старый. Автоматический GC появится в v0.5.0Назначение: Постоянное хранение ключей и указателей на значения. Создаётся при flush MemTable на диск.
Структура файла SSTable:
[Block 0] [Block 1] ... [Block N] [Index] [Bloom Filter] [Footer]
Поиск в SSTable:
nilCompaction: объединяет несколько 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 = xxhash(key) % numShards.
Состав шарда:
Преимущества:
Модель: Snapshot Isolation с обнаружением конфликтов при коммите.
Как работает:
NewTransaction() создаёт транзакцию, фиксирует startTS (текущий счётчик времени)tx.Get()) видят снапшот базы на момент startTS. MVCC гарантирует, что более новые версии ключей не видныtx.Put(), tx.Delete()) накапливаются в буфере транзакции и не видны другим транзакциямtx.Commit() атомарно применяет все операции с commitTS = NextTimestamp(). Проверяет, не изменил ли кто-то те же ключи между startTS и commitTS. Если изменил — возвращает ErrConflictErrConflict клиент должен повторить транзакцию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 // успех
}
// повтор
}
Формат ключа: [user_key][^timestamp], где ^timestamp — побитовая инверсия таймстемпа.
Почему инверсия: При лексикографической сортировке меньший ключ идёт раньше. Инвертированный таймстемп даёт обратный порядок: чем новее версия, тем меньше её инвертированный таймстемп, и тем раньше она идёт при поиске по префиксу.
Пример:
"user:1" + ^uint64(100) = ... (в HEX: …FFFFFFFFFFFFFF9B)"user:1" + ^uint64(200) = ... (в HEX: …FFFFFFFFFFFFFF37)Поскольку 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"))
Когда использовать:
Структура данных: Skip list на 16 уровнях с вероятностью повышения 1/4.
Почему lock-free:
Put() использует CAS (Compare-And-Swap) для вставки узлов на каждом уровнеGet() читает указатели атомарно, без блокировокАрена:
Get() возвращает срез арены напрямую — 0 аллокаций в кучеУправляются двумя полями структуры WALOptions (internal/engine/options.go):
GroupCommitEnabled — включает групповой коммит (буферизацию и фоновый syncLoop)SyncMode — включает file.Sync() после сброса буфера или каждой записи| Режим | 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: Отсортированный файл ключей и указателей на значения. Создаётся при flush MemTable на диск.
Структура:
Bloom-фильтр:
WithBloomBitsPerKey(10) (по умолчанию)Назначение: Атомарная запись нескольких операций с одним fsync.
batch := db.NewBatch()
batch.AddPut([]byte("k1"), []byte("v1"))
batch.AddPut([]byte("k2"), []byte("v2"))
batch.AddDelete([]byte("old"))
batch.Commit() // атомарно, один fsync на пачку
Как работает:
Commit() записывает все операции в WAL одной записьюНазначение: Хранение значений на диске отдельно от ключей.
Характеристики:
vlog.db[offset: 8B, size: 2B, checksum: 2B CRC16]Get() возвращает срез mmap-памяти, 0 системных вызововРоли:
| Роль | Права |
|——|——-|
| admin | Всё: данные, пользователи, CF, GC |
| readwrite | Чтение, запись, удаление, сканирование |
| readonly | Только чтение и сканирование |
Токен: JWT с подписью HMAC-SHA256. Живёт 24 часа. Содержит логин, роли, время создания и истечения.
Автоматическая генерация клиентов из .proto файла. Поддерживаются: Go, Python, Java, C++, Rust, TypeScript, C#, Kotlin, Dart, PHP, Ruby, Swift, Objective-C. CLI для администрирования и ручных операций. REST API на порту 8080.
ScoriaDB — это проект с прозрачным планом развития. Мы публикуем дорожную карту, чтобы вы могли планировать внедрение и понимать, когда появятся нужные вам возможности.
Принципы планирования:
| Версия | Срок | Что нового | Для кого |
|---|---|---|---|
| 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-метрики | Продакшен с критичными данными |
| Версия | Срок | Что нового | Бизнес-ценность |
|---|---|---|---|
| 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 |
Мы открыты к обратной связи. Если вам нужна конкретная функция — создайте issue на GitHub или напишите автору.