Мы привыкли считать JSON «медленным» текстовым форматом, удобным исключительно для людей. Мы используем его для REST API, конфигурационных файлов и веб-интерфейсов, но как только речь заходит о высоконагруженных базах данных, мы сразу же смотрим в сторону Protocol Buffers, FlatBuffers или проприетарных бинарных форматов.Но что, если взглянуть на JSON не с человеческой стороны, а со стороны машины?Если убрать тяжелые слои абстракций (такие как рефлексия Go и сетевые оверхеды), стандартный JSON оказывается удивительно плотным, прозрачным и невероятно быстрым в обработке. На его основе можно построить встраиваемый Key-Value движок, который выполняет чтение за 16 наносекунд, а поиск — менее чем за 0.5 миллисекунды на базе в миллион записей. Погрузиться в наносекунды и код
Мы привыкли считать JSON «медленным» текстовым форматом, удобным исключительно для людей. Мы используем его для REST API, конфигурационных файлов и веб-интерфейсов, но как только речь заходит о высоконагруженных базах данных, мы сразу же смотрим в сторону Protocol Buffers, FlatBuffers или проприетарных бинарных форматов.
Но что, если взглянуть на JSON не с человеческой стороны, а со стороны машины?
Если убрать тяжелые слои абстракций (такие как рефлексия Go и сетевые оверхеды), стандартный JSON оказывается удивительно плотным, прозрачным и невероятно быстрым в обработке. На его основе можно построить встраиваемый Key-Value движок, который выполняет чтение за 16 наносекунд, а поиск — менее чем за 0.5 миллисекунды на базе в миллион записей.
В этой статье мы разберем, как это реализовано в MakoDB (безсерверной NoSQL СУБД на Go, работающей через memory-mapped файлы).
Поскольку MakoDB хранит и обрабатывает данные в виде «сырых» JSON-документов, вот показатели задержки (latency) и пропускной способности под параллельной нагрузкой в 32 потока:
Операция | Задержка (ns/op) | Пропускная способность | Аллокации памяти | Описание |
|---|---|---|---|---|
Get | 16.78 ns | ~60 000 000 ops/sec | 0 B/op | Параллельное чтение (Lock-Free) |
Query | 27.97 ns | ~35 000 000 ops/sec | 0 B/op | Выборка полей из JSON без десериализации |
Put | 484.50 ns | ~2 060 000 ops/sec | 0 B/op | Параллельная запись (16 шардов) |
Search | 71.88 μs | ~14 000 ops/sec | 600 KB/op | Полнотекстовый поиск AND (1000 совпадений) |
MakoDB поставляется в виде встраиваемой библиотеки (daemonless). Вы просто импортируете её в проект на Go, открываете файл базы данных и начинаете работать.
// Открываем или создаем базу: путь, количество шардов, макс. размер файла, buckets на шард
db, err := makodb.OpenSharded("mydb.db", 16, 15*1024*1024*1024, 6250000)
if err != nil {
log.Fatalf("Failed to open DB: %v", err)
}
defer db.Close()
// 1. Запись документа
key := "user:101"
jsonDoc := []byte(`{"name":"Mako","age":25,"city":"Ocean","role":"admin"}`)
err := db.Put(key, jsonDoc)
// 2. Чтение документа (Lock-Free)
val, err := db.Get("user:101")
log.Printf("Document: %s", string(val))
MakoDB интегрирован с парсером silentjson, который умеет вытягивать конкретные поля прямо из сырых байт JSON в памяти без парсинга всего документа. Это позволяет делать до 35 000 000 запросов в секунду:
type UserAgeQuery struct {
Name string `json:"name"`
Age int `json:"age"`
}
var result UserAgeQuery
// Проецирует только указанные поля напрямую из JSON-байтов
err := db.Query("user:101", &result)
MakoDB поддерживает индексацию текстовых полей документов и быстрое пересечение списков совпадений (AND-запросы):
docID := "doc:456"
bodyText := "Mako is an extremely fast memory-mapped JSON database"
_ = db.Put(docID, []byte(`{"id":"doc:456","body":"`+bodyText+`"}`))
_ = db.Index(docID, bodyText)
// Поиск возвращает список ID подходящих документов
matches, _ := db.Search("mako database") // Вернет ["doc:456"]
Никакой магии, только симпатия к аппаратному обеспечению (mechanical sympathy). MakoDB отображает файлы базы данных напрямую в виртуальное адресное пространство вашего приложения через системный вызов mmap.
Для процессора вся база данных выглядит просто как огромный непрерывный массив байт в оперативной памяти. Операции чтения Get происходят напрямую из системного страничного кэша (Page Cache) ОС за наносекунды, минуя сетевой оверхед и контекст-свитчи потоков, которые неизбежны в клиент-серверных базах (Redis, PostgreSQL).
Поскольку это бессерверная библиотека и данные хранятся как чистый JSON, несколько процессов (например, написанных на Go, C++, Python или PHP) могут одновременно отображать одни и те же файлы базы в свою память и безопасно читать/писать данные.
+-----------------------------------------------------------------+
| BFF на Go/C++ |
| [ Буфер записи в памяти ] [ Двухпроходное слияние ] |
+-------------------------------+---------------------------------+
|
| (Отображение в память mmap / SHM)
v
+-----------------------------------------------------------------+
| MakoDB Storage (Key-Value) |
| Документы: |
| - "tx:101" -> {"id":101,"country":"Germany","cost":12.5} |
| Индексы: |
| - "sort:cost" -> tx:15,tx:101,tx:88... |
| - "idx:country:germany" -> tx:4,tx:101,tx:502... |
+-----------------------------------------------------------------+
При поиске и пагинации сортировка «на лету» — это бутылочное горлышко. Мы храним отсортированные списки ID на диске (sort:total_profit).
Но если новые транзакции записываются в базу в реальном времени, перестраивать индекс на 1 000 000 записей при каждой записи было бы катастрофически медленно. Для решения этой проблемы мы реализовали гибридную схему (LSM-style):
Путь записи: Новые записи сохраняются в MakoDB и параллельно добавляются во временный буфер в оперативной памяти бэкенда (recentTransactions). Запись происходит мгновенно (~500 нс).
Путь чтения: Движок запросов на лету объединяет отсортированный дисковый индекс и отсортированный буфер в памяти, используя двухпроходный алгоритм слияния (merge-sort).
Ленивые сравнения: Чтобы объединять потоки без загрузки всей базы, бэкенд запрашивает значения сравнения из MakoDB только для элементов, попадающих в текущую страницу пагинации (обычно 50 штук).
Когда буфер в памяти заполняется (например, при достижении 50 элементов) или по нажатию кнопки в UI, мы сбрасываем (flush) его на диск. Для этого мы делаем бинарный поиск места вставки для каждой новой записи в существующий индекс:
Бинарный поиск в массиве из 1 000 000 элементов требует максимум 20 сравнений.
Для пачки из 50 новых записей: точечных
Get запросов к базе.
С учетом скорости MakoDB весь сброс и перезапись индекса на диске занимают менее 5 миллисекунд!
В безсерверных архитектурах, где база данных отображается прямо в память процессов приложения, падение процесса в момент записи — это главный источник повреждения данных и взаимных блокировок (deadlocks). Мы заложили два механизма защиты от таких аварий:
Если один из процессов захватил блокировку на запись в MakoDB и внезапно «умер» (например, по kill -9 или из-за паники), в стандартных мьютексах блокировка осталась бы активной навсегда, заблокировав все остальные процессы. В MakoDB используется RobustShmMutex:
При захвате блокировки в shared-memory записывается Process ID (PID) владельца.
Если другой процесс пытается записать данные и видит, что мьютекс занят, он опрашивает ОС (через системный вызов OpenProcess на Windows или сигнал 0 на Unix), жив ли владелец PID.
Если процесс-владелец мертв, блокировка принудительно сбрасывается и «крадется» новым процессом. База продолжает работать в штатном режиме.
Что будет, если бэкенд упадет, когда в оперативной памяти накоплено 49 транзакций из 50, которые еще не сбросились в дисковый индекс?
Сами транзакции не пропадут — они уже записаны в MakoDB под ключами tx:<id> (запись одного документа занимает всего 480 нс). Пострадает только актуальность индексов.
При запуске бэкенд считывает последний проиндексированный ID на диске (из индекса sort:id).
Затем он сканирует базу на наличие неиндексированных документов: пробует прочитать tx:<maxID+1>, tx:<maxID+2> и т.д.
Если такие записи обнаружены, сервер автоматически загружает их в буфер и проводит слияние с индексами на диске при старте. Данные полностью восстанавливаются без участия администратора!
При пиковых нагрузках (например, резком наплыве заказов) синхронная запись в дисковый индекс привела бы к просадке производительности.
Использование гибридного буфера выполняет роль амортизатора вспышек активности:
Все пиковые всплески записи гасятся в памяти бэкенда со скоростью записи в RAM (~500 нс на документ).
Диск нагружается равномерно: сброс индекса на диск происходит пачками (по 50 элементов) в фоновом режиме. Это снижает количество операций перезаписи блоков на SSD и выравнивает нагрузку на систему ввода-вывода (I/O).
Мы собрали все компоненты демо-приложения (СУБД MakoDB, генератор 1M транзакций, поисковый движок и веб-интерфейс) в один полностью автономный исполняемый файл с помощью go:embed.
Вы можете скачать бинарник под свою систему, запустить локально и проверить скорость работы:
Релизы демо-сервера: github.com/GenshIv/makodb-demo
Быстрый парсер JSON: github.com/GenshIv/silentjson
Буду рад обсудить архитектуру в комментариях! Если вам показалось, что есть слабые места, давайте обсудим.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Ты не найдёшь эту ошибку. Потому что её нет в твоём коде. Как Self-describing API спасает от чужих рефакторингов | 5 | 8 | 07-07-2026 |
| 2 | Тап по тысяче точек за O(log n): QuadTree и сферическая геометрия в гео-соцсети | 0 | 7 | 28-06-2026 |
| 3 | Нейросеть-автопилот вместо 400 Playwright-тестов | 0 | 7 | 07-07-2026 |
| 4 | Как желание быстрее читать чужой код превратилось в войну с недетерминизмом LLM | 0 | 5 | 28-06-2026 |
| 5 | [Перевод] Структуры данных на практике. Глава 16: Фильтры Блума и вероятностные структуры данных | 0 | 7 | 28-06-2026 |
| 6 | Когда может пригодиться экзотика в ООП: миксины/трейты/аспекты | 0 | 5 | 08-07-2026 |
| 7 | Токенная диета для ИИ-агентов: Caveman, Ponytail и Headroom | 0 | 7 | 07-07-2026 |
| 8 | [Перевод] Паттерны доступа к данным, которые выбесят ваш процессор | 0 | 7 | 03-07-2026 |
| 9 | Свой VPN на Rust: как я спорил с сетью, TLS и самим собой | 7 | 8 | 27-06-2026 |
| 10 | Как Яндекс меня кинул на фестивале для будущих яндексоидов, или Хроники одного YoungCon | -2 | 3 | 29-06-2026 |