惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
Recent Announcements
Recent Announcements
月光博客
月光博客
B
Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
The GitHub Blog
The GitHub Blog
Last Week in AI
Last Week in AI
罗磊的独立博客
NISL@THU
NISL@THU
酷 壳 – CoolShell
酷 壳 – CoolShell
P
Proofpoint News Feed
H
Help Net Security
L
LINUX DO - 最新话题
MongoDB | Blog
MongoDB | Blog
雷峰网
雷峰网
The Hacker News
The Hacker News
Apple Machine Learning Research
Apple Machine Learning Research
I
Intezer
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Know Your Adversary
Know Your Adversary
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Secure Thoughts
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
C
CERT Recently Published Vulnerability Notes
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News and Events Feed by Topic
F
Full Disclosure
人人都是产品经理
人人都是产品经理
Blog — PlanetScale
Blog — PlanetScale
Recorded Future
Recorded Future
T
Threat Research - Cisco Blogs
博客园 - 三生石上(FineUI控件)
S
Securelist
T
The Exploit Database - CXSecurity.com
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
Security Archives - TechRepublic
Security Archives - TechRepublic
C
Check Point Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
V
Visual Studio Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tor Project blog
博客园 - 司徒正美
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Хотелось пополнить резюме, а написала LSM-движок с MVCC, снапшотами и Value Log на чистом Go
Екатерина Годулян · 2026-05-06 · via Все публикации подряд на Хабре

Средний

14 мин

8.9K

Введение: Это было не так случайно, почти

Я хотела разобраться, почему LSM‑движки такие быстрые. Мои попытки получить циферки «получше» были слишком наивными. С каждой тысячей записей читать становилось всё тяжелее ждать когда же тест закончится.

И тут возможно проблема в том что я не знала многих вещей, возможно, потому что я не думала даже о том, что данные должны быть каким-то способом организованы. Не в свою защиту но бывшему Беку на фастапи это не пригождалось. Правда, я потом подумала о слишком многом.

Если вы писали свое хранилище — даже простую очередь или кэш — вы сталкивались с тем же. Мой опыт поможет не повторять мои ошибки. В статье расскажу: из каких частей состоит мой LSM‑движок и чьи решения я подсмотрела для своего «лего» 0

Оглавление


Архитектура ScoriaDB: как я собирала конструктор

Я не изобретала велосипед это по сути конструктор, содержащий в себе множество готовых решений. Посмотрев, как устроены LevelDB, RocksDB, BadgerDB, TiKV, я взяла у них некоторые идеи. Получился гибрид.

Архитектура базы данных

Архитектура базы данных

MemTable: почему всё‑таки B‑tree, а не skip list (и когда это станет проблемой)

Мне нужна была отсортированная in‑memory структура. Для чего? Чтобы быстро делать скан по префиксу и потом сбрасывать эти отсортированные данные в SSTable (на диск).

Вот как я сравнивала варианты:

  • Хеш‑мапа? нет, она не сохраняет порядок ключей, скан невозможен.

  • sync.Map? Тоже нет, не упорядочена, под write‑heavy нагрузкой меня лично не устроили цифры.

  • Skip list — Хм, это золотой стандарт LSM (LevelDB, RocksDB, BadgerDB), lock‑free или хотя бы с минимальными блокировками. Но корректная реализация конкурентного skiplist в Go — задача, которая может затянуться и отвлечь от реализации mvp.

  • B‑tree — взяла github.com/google/btree. Стабильный, есть copy‑on‑write, быстро и просто использовать.

Поясню еще один момент, глобальный мьютекс на всю MemTable (реализация B‑tree) означает, что при интенсивной записи все горутины выстраиваются в очередь. Для первого релиза я осознанно пошла на это, выиграв время для реализации других частей. В планах на v0.3.0 — замена на lock‑free skip list с собственным управлением памятью. Это будет необходимо, чтобы база держала конкурентную нагрузку.

SSTable и Bloom‑фильтр

Когда MemTable переполняется, она сбрасывает все в Sstable. Ниже его структура:

┌─────────────┐
│   Header    │  ← magic, версия, контрольная сумма заголовка
├─────────────┤
│ Block index │  ← массив «последний ключ блока → смещение»
├─────────────┤
│Bloom filter │
├─────────────┤
│  Block 1    │  ← данные + CRC32 блока
│  Block 2    │
│  ...        │
└─────────────┘
  • Блочный индекс — массив, который для каждого блока SSTable хранит его последний ключ и смещение в файле. Позволяет быстро найти нужный блок бинарным поиском, не читая весь файл.

  • Префиксное сжатие — способ экономить место в SSTable: если ключи имеют общий префикс (например, user:100, user:101), то общая часть хранится один раз, а различающиеся суффиксы — отдельно.

  • Bloom‑фильтр — вероятностная структура, которая быстро говорит «ключа точно нет», отсекая лишние чтения с диска. Ложноположительные срабатывания возможны, но они лишь приводят к холостому поиску в блоке, не нарушая корректности.

  • Контрольные суммы — каждый блок снабжён CRC32. При чтении блока я проверяю сумму, чтобы обнаружить битые данные на диске. Без этого можно было бы молча получить мусор, если файл повреждён.

WriteBatch: атомарные группы операций

Идея из RocksDB. Если нужно записать несколько ключей атомарно — либо все, либо ничего. В ScoriaDB WriteBatch сериализуется в один буфер и пишется в WAL одной записью. При восстановлении применяется всё до маркера конца. Если обнаружен обрыв — операции батча не применяются.

Схема движения данных при записи

Схема движения данных при записи

WiscKey и Value Log: большие значения без амплификации записи

Классический LSM страдает амплификацией записи : значение в 1 МБ копируется при каждом компакшене. Десять раз и уже 10 МБ записи, а двадцать — 20 МБ. У Badger я подсмотрела реализацию Value Log (WiscKey‑стиль). Значения >64 байт выносятся в отдельный файл, а в LSM‑дереве хранятся только указатели.

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

mmap и почему zero‑copy в Go — не то, чем кажется

Я использую mmap для отображения файла VLog в память. Какие проблемы возникают:

  • После syscall.Munmap (например, при закрытии БД) слайсы, которые ссылаются на этот регион, становятся висячими указателями, то есть любое обращение к ним вызывает SIGSEGV.

  • Без контроля времени жизни таких слайсов легко получить аварию: горутина читает по старому слайсу, пока основной поток закрыл файл и сделал Munmap.

Текущая реализация копирует данные из mmap‑области в новую кучу, думаю это безопасно и одно лишнее копирование не сыграет. Настоящий zero‑copy требовал бы системы подсчёта ссылок на регионы mmap и гарантии, что никто не держит слайс после закрытия VLog. Пока это остаётся в планах на v0.3.0.

MVCC и инвертированная метка

MVCC (Multi‑Version Concurrency Control) даёт каждой записи версию с временной меткой commitTS. Транзакция получает startTS и видит только версии с commitTS <= startTS. Главное: писатели никогда не блокируют читателей. Если транзакция обнаруживает конфликт (кто‑то записал ключ после её начала), она возвращает ошибку, и вызов должен повторить транзакцию — обычно в цикле с несколькими попытками.

Инвертированная метка — трюк, который я подсмотрела в TiKV. Обычно timestamp растёт, и новые записи лежат в конце дерева. Причем хранится не сам timestamp, а его побитовое отрицание (^uint64(now) в Go — это инвертирование всех битов). Благодаря этому свежие записи (с большим commitTS) оказываются в начале итератора. Итератор по умолчанию идёт от новых к старым.

Column Families: зачем они нужны на самом деле

Идея из Bigtable и Cassandra. Независимые LSM‑деревья внутри одной базы. У каждого CF свои настройки: размер MemTable, политика компакшена, а в будущем — TTL.

Реализовано виде map[string]*Engine и общего WAL для атомарности между CF. Без общего WAL при сбое часть CF могла остаться в обновлённом состоянии, а другая — нет. Это была одна из первых грубых ошибок, которую я нашла вообще аж при разработке анализатора логов на этой бд.

Leveled Compaction: классика

Классическая схема из LevelDB: уровни L0 → L1 → L2 → L3. На L0 файлы могут пересекаться по ключам, на L1 и ниже — нет.

На схеме показан процесс компакшена уровня 0 в ScoriaDB (v0.1.0). Три файла уровня 0, которые могут пересекаться по ключам, сливаются в один новый файл уровня 1 без пересечений. Для слияния используется multi‑way merge с min‑heap. При этом учитываются активные снапшоты (через minActiveSnapshotTS), чтобы не удалять версии, нужные долгим транзакциям. После компакшена уровень 0 очищается.

На схеме показан процесс компакшена уровня 0 в ScoriaDB (v0.1.0). Три файла уровня 0, которые могут пересекаться по ключам, сливаются в один новый файл уровня 1 без пересечений. Для слияния используется multi‑way merge с min‑heap. При этом учитываются активные снапшоты (через minActiveSnapshotTS), чтобы не удалять версии, нужные долгим транзакциям. После компакшена уровень 0 очищается.

Сначала была заглушка, потом я сделала multi‑way merge с min‑heap. В планах — добавлять ограничение размеров уровней, чтобы избежать write stall, когда входящая запись опережает компакшн.

Manifest: журнал метаданных

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

Я взяла идею из RocksDB: все изменения состава файлов пишутся в журнал MANIFEST. Сейчас каждая запись — это VersionEdit в JSON (с последующим fsync). Это удобно для отладки, но не для продакшена . В v0.2.0 планирую перейти на компактный бинарный формат и пакетную запись изменений.

Пример записи:

{
  "level": 1,
  "add": ["sstable_000123.sst"],
  "delete": ["sstable_000045.sst"]
}

Fail‑safe VLog: восстановление после повреждения

Если во время записи в Value Log случился сбой, файл может повредиться (magic mismatch — специальная сигнатура в начале). Я сделала как в BadgerDB: при открытии проверяю magic. Если не совпадает — переименовываю в .corrupt, создаю новый, восстанавливаю данные из WAL. Записи с указателями на старый VLog пропускаются (они потеряны), но база остаётся работоспособной.

VFS‑абстракция для тестирования

Чтобы тестировать сбои диска, я абстрагировала все файловые операции через интерфейс VFS:

type VFS interface {
    Open(name string) (File, error)
    Create(name string) (File, error)
    Rename(old, new string) error
    Remove(name string) error
}

В production — DefaultVFS (обёртка над os), в тестах — мок, который эмулирует ошибки записи, задержки или отказы. Правда, здесь есть тонкость: для mmap нужен реальный файловый дескриптор (*os.File), а не абстрактный vfs.File. Поэтому в тестах, где я подменяю файловую систему моком, VLog не сможет использовать mmap — и мой механизм fail‑safe VLog проверить на моках пока не выйдет. Это ограничение я планирую исправить в v0.3.0, например, сделав mmap опциональным или заменив его на pread в тестовом режиме.

Чистый Go без cgo и gRPC как у etcd

RocksDB требует C++ и cgo — сложная сборка, проблемы с кросс‑компиляцией. BadgerDB — чистый Go, но без встроенного сервера.

Я сделала ставку на чистый Go: go build одной командой, никаких внешних зависимостей, работает на всех платформах. А gRPC‑сервер (как в etcd и TiKV) даёт строгую типизацию, HTTP/2 и клиенты на 12+ языках. REST и WebSocket я добавила скорее ради эксперимента; в идеальном мире серверный слой должен быть отдельным процессом или включаться флагом сборки.

Что пошло не так: реальные проблемы и их решения

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

1. WAL без синхронизации → данные улетали в буфер, а не на диск

Как было: самый первый WAL писался через f.Write(). Данные уходили в буфер операционной системы и при перезагрузке просто исчезали.

Как обнаружилось: провела стресс‑тест: записала 10 000 ключей, убила процесс через kill -9, перезапустила базу… пусто.

Исправление: добавила f.Sync() после каждой записи батча. Теперь данные действительно сохраняются.

Цена вопроса: замеры на Intel Core i3‑1215U:

Бенчмарк

Итераций

Время на операцию

BenchmarkPut_WithoutSync-8

5000

300 456 ns/op

BenchmarkPut_WithSync-8

1000

1 523 400 ns/op

Запись замедлилась в 5 раз. Но теперь после kill -9 данные не теряются.

Обратите внимание: это тесты одиночных записей с честным fsync на диск. В синтетическом тесте без реальной синхронизации задержка ~1 мкс — но такие цифры не отражают надёжность.

Что дальше: в v0.2.0 добавлю Group Commit — буферизирую несколько записей и делаю один fsync на группу. Должно стать и быстро, и надёжно.

2. Гонка в генераторе меток (и как её нашёл ‑race)

Было: генератор меток был просто lastTS++. Без блокировок.

И когда запустила 100 горутин, которые одновременно вызывали NextTimestamp(). Начали появляться дубликаты меток, MVCC ломался, тесты падали нестабильно.

Как нашла: включила go test -race. Без него я бы искала этот баг неделю.

Исправление (первое):

// Было
func (g *TimestampGenerator) Next() uint64 {
    g.lastTS++
    return g.lastTS
}

// Стало
func (g *TimestampGenerator) Next() uint64 {
    return atomic.AddUint64(&g.lastTS, 1)
}

Но этого оказалось мало. При перезапуске базы счётчик lastTS сбрасывался в 1, и новые транзакции могли получить метки, которые уже были меньше уже записанных в SSTable. Это ломало изоляцию снапшотов между запусками.

Окончательное исправление: теперь я сохраняю последнее выданное значение в Manifest (отдельной записью), а при открытии базы загружаю его и продолжаю инкремент. Метки уникальны даже между сессиями.

Бонус: я добавила кэш последних версий(lastCommitCache), чтобы проверять конфликты за O(1) без полного сканирования LSM.

3. Value Log без CRC32 → мусор вместо значений

Я писала в VLog и магическое число, и CRC32 каждого значения. Но при чтении проверяла только магическое число, а CRC32 нет.

Как обнаружилось: после очередного краша VLog повредился (магическое число не совпало). Движок создал новый пустой файл, а старые указатели из WAL вели в пустоту. База молча возвращала мусор вместо значений.

Исправление:

  • При открытии проверяю магическое число. Если не совпадает — переименовываю файл в .corrupt, создаю новый, восстанавливаю данные из WAL.

  • В методе Get перед возвратом значения сверяю CRC32 блока с записанным.

4. Компакшн, который убивал снапшоты

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

В процессе разработки транзакций появились долгие транзакции. Они смотрели на свой startTS, пытались найти нужную версию, а их нет! Потому что компакшн решил, что ее надо убрать.

Решение: добавила глобальный минимум активных снапшотов (minActiveSnapshotTS), атомарную переменную. В компакшене перед удалением версии теперь проверяется: если commitTS этой версии больше, чем minActiveSnapshotTS, то оставляем версию.

5. WriteBatch терял атомарность

Раньше: каждая операция из батча писалась в WAL отдельной записью, тем самым пытаясь экономить на сериализации (и как оказалось неизвестсно зачем)

Как проявилось: тесты на восстановление после сбоя показали, что если процесс умирал после записи 3 операций из 10, при повторном запуске применялись только эти 3.

Теперь: сериализую весь WriteBatch в один буфер, записываю одной записью в WAL с маркерами начала и конца. При восстановлении: если есть маркер конца — применяю всё; если нет — отбрасываю целиком.

6. Column Families теряли атомарность между CF

Как было: у каждого CF был свой WAL. Мне казалось, так проще и изолированнее

К чему привело: когда батч обновлял два CF, произошёл сбой, после того, как WAL первого CF записался (и даже, возможно, выполнил fsync), но до того, как второй CF начал свою запись. В итоге первый CF сохранил изменения, а второй — нет. Атомарность между CF была потеряна.

Исправление: сделала общий WAL для всех CF. WriteBatch сериализуется в одну запись с идентификаторами CF. При восстановлении теперь читается только один поток и точно известно, куда применять каждую операцию.

7. ScanCF возвращал пустой итератор вместо ошибки

Что было до: в публичном API ScanCF я возвращала пустой итератор, если запрошенный Column Family не существовал.

Как выяснилось на практике: в демо‑проекте Scorix (анализатор логов) импортёр забыл создать CF raw. ScanCF молча возвращал пустой результат. Пользователь видел «No logs found» и не понимал, почему.

Исправление: создала errorIterator, который при первом вызове Next() или Err() возвращает ошибку CF not found. Сигнатура метода не изменилась, но пользователь наконец видит причину.

8. Manifest без fsync → база забывала свежие SSTable

Что было: VersionEdit писался в JSON‑файл, но не вызывал fsync. Всё работало…

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

Исправление: добавила f.Sync() после каждой записи в Manifest.

И ещё один fsync, замедляющий компакшн.

9. SSTable не проверял CRC блоков

Исходная наивность: я проверял только заголовок SSTable, считая, что если заголовок цел — то и всё остальное в порядке.

Как проявилось: в тестах с повреждённым файлом (симуляция битого диска) база продолжала читать блоки и возвращала мусор.

Исправление: добавила CRC32 на каждый блок SSTable. При чтении блока проверяю контрольную сумму. Если не сошлась — блок считается повреждённым.


Что уже работает, а что ещё нет

Стабильно работает (v0.1.0)

Компонент

Статус

Примечание

LSM‑движок

MemTable, SSTable (с CRC), Bloom, компакшн

Value Log + mmap

CRC32, fail‑safe, копирование в кучу (zero‑copy будет позже)

WAL + fsync + recovery

После kill -9 восстанавливается

MVCC + Snapshot Isolation

Атомарный генератор, персистентность lastTS

Интерактивные транзакции

С повторными попытками

Column Families

Атомарность между CF через общий WAL

gRPC / REST / CLI

JWT, роли (admin, readwrite, readonly)

Docker + CI/CD

Одна команда — и база работает

Стресс‑тесты

20+ прогонов с -race, включая конфликтный тест

Что пока на костылях или в планах

  • MemTable: B‑tree с глобальным мьютексом → замена на lock‑free skip list (v0.3.0).

  • WAL: Group Commit (v0.2.0).

  • Manifest: бинарный формат (v0.2.0).

  • Value Log GC: ручной режим есть (scoria gc), автоматический (битовая карта, инкрементальный) → v0.3.0.

  • Ограничение размеров уровней в компакшенеv0.3.0.

  • TTL для записейv0.2.0.

  • Raft, шардирование, распределённые транзакции, Sorted Sets, JSON‑документы → после v1.0 (без точных дат).

Что я вынесла из этого проекта

  1. Не нужно боятся переписывать. Три переписывания компакшена, два — WAL, один серьёзный рефакторинг MVCC. Мне становилось страшно, что останется какая‑то глубокая ошибка после этих изменений, ведь до этого я максимально старалась избежать того чтобы пришлось прям кардинально что‑то менять.

  2. Тесты с -race — с первого дня. Гонку в генераторе меток я нашла бы за день, а не за неделю, если бы включила -race раньше. В моём CI это стало обязательно, до недавних пор я об этом не думала.

  3. fsync — правда полезная штука. Запись замедлилась в 5 раз (всего лишь), но теперь после kill -9 данные сохраняются. Позже Group Commit амортизирует все затраты, сохраняя надежность.

  4. LSM — это намного сложнее, чем просто скинуть на диск. Bloom‑фильтр отсекает лишние чтения. Блочный индекс и CRC блоков — ещё пара мелочей, но без них данные либо теряются, либо ищутся вечность. Префиксное сжатие экономит память. Value log в помощь LSM. В сумме эти мелочи и делают базу данных быстрой и надёжной.

  5. Самое сложное — заставить всё работать вместе. MemTable работает, SSTable работает, WAL работает. Каждый по отдельности — отлично. Но когда они начинают взаимодействовать при компакшне и восстановлении, вылезают такие грабли, о которых ты и не догадывалась. Интеграционные и стресс‑тесты — вот где настоящая боль и настоящие открытия.


Попробовать у себя

ScoriaDB открыта под лицензией Apache 2.0.

# Клонировать
git clone https://github.com/f4ga/ScoriaDB.git
cd ScoriaDB

# Быстрый старт через Docker
docker compose -f deployments/docker-compose.yml up --build
# Сервер на :50051 (gRPC) и :8080 (HTTP)
# Логин/пароль при первом запуске: admin/admin

# Или собрать вручную
go build -o scoria-server ./cmd/server
go build -o scoria-cli ./cmd/cli
./scoria-server &
TOKEN=$(./scoria-cli admin auth admin admin)
./scoria-cli --token "$TOKEN" set hello world
./scoria-cli --token "$TOKEN" get hello

# Запустить все тесты (обязательно с -race!)
go test -race ./...

Демонстрация CLI <3

Небольшая история любви

Небольшая история любви

Документация

Полная документация ScoriaDB доступна в папке [docs/] репозитория на GitHub c подробным описанием всех команд CLI, Column Families, транзакций, админ прав и примеров интеграции. Спойлер: В клишке намного больше команд чем на скрине, достаточно вызывать команду help или почитать документацию на GitHub Pages.

Заключение

ScoriaDB начиналась как строчка в резюме, а стала для меня причиной написать об этом свою статью. Сейчас это рабочий LSM-движок с MVCC, транзакциями, Column Families и собственным Value Log — всё на чистом Go, без внешних зависимостей, с gRPC‑сервером и CLI из коробки. Он не заменит Redis в специфичных для него вещах, но для embedded‑сценариев, локального хранения, анализа логов или учебных проектов, уже сейчас, более чем готов.

Пока я собирала материал для статьи, в голову пришло ещё несколько идей, и между делом появились новые коммиты, исправления и даже фичи. Проект живёт, родмара расписана на несколько версий, кстати, issues открыты, жду ваших предложений!


Исходники на GitHub

Я прочитаю все комментарии, хотелось бы услышать есть ли у кого-то опыт написания хранилищ :3

А вы когда‑нибудь начинали проект чисто для резюме, а получалось что‑то большее? Писали ли документацию ?