Рынок баз данных суров, особенно для новичков: сложно запустить новый продукт и отличиться от старожилов, ещё сложнее удержать долгосрочные позиции. На этой неделе SpacetimeDB выпустила версию 2.0 своей базы данных с необычным подходом к продвижению: слегка сюрреалистичное (мемное) видео, где конкурентов высмеивают, предлагая им «выпить слёз конкурента», и набор бенчмарков, которые выглядят слишком хорошо, чтобы быть правдой (и действительно ими не являются), причём эти бенчмарки тоже издеваются над другими базами данных. Рядом с «большими проигравшими» этого бенчмарка красуется забавная лупа — нужно как следует приглядеться, чтобы увидеть, насколько они плохи. Мило.

Слёзы конкурентов! Ха-ха! Эти ребята отстой! Ха-ха!

Стоит сразу признать: подобный стиль выглядит неприятно. Но в продукте есть интересные технические идеи, которые заслуживают честного разбора.

Бенчмарки

Одна из типичных ошибок новичков на рынке баз данных — вера в то, что можно выиграть за счёт «лучшей производительности». На практике такого почти не случается. Немногочисленные компании, построившие устойчивый бизнес на базах данных, побеждают за счёт качественной, честной технической работы, которая говорит сама за себя. Наличие бенчмарков, конечно, помогает — но только если сами бенчмарки представляют собой качественную, честную техническую работу.

У SpacetimeDB бенчмарки не относятся ни к тому, ни к другому. В них немало технических изъянов в самой методике измерений. Существует альтернативный набор бенчмарков, где SpacetimeDB показывает себя куда хуже на фоне конкурентов.

Но фундаментальная проблема этих бенчмарков в другом — они нечестны. И причина понятна: продукт SpacetimeDB устроен принципиально иначе, чем у конкурентов, и это делает соблазн написать подобные бенчмарки крайне сильным. Продукт занимает другой сегмент рынка баз данных, а сравнивается с системами, построенными на совершенно других компромиссах. Сравнение выглядит эффектно, но оно нечестно.

Похожая история случилась несколько лет назад в PlanetScale, где велась работа над MySQL-расширением для векторного поиска по схожести. У реализации были очень конкретные цели: она была полностью транзакционной, а векторные данные хранились на диске и управлялись буферными пулами MySQL. Это принципиально отличалось от более простых подходов вроде pgvector, использующих HNSW и требующих, чтобы граф схожести помещался целиком в память. Это был совсем другой продукт с другими компромиссами. И было очень заманчиво взять инстанс EC2 с 32 ГБ RAM, залить в базу 64 ГБ векторных данных, а затем повторить то же самое на Postgres с pgvector. Та же машина, тот же датасет, те же запросы! Но PlanetScale обрабатывает десятки тысяч запросов в секунду, а pgvector тратит больше 3 секунд на один запрос, потому что граф HNSW постоянно вытесняется на диск и обратно.

Показать это в бенчмарке было очень соблазнительно — «мы в 10000 раз быстрее pgvector!». Но это нечестно. Да, машина та же, датасет тот же, запросы те же — но это не одно и то же по сути. Такие бенчмарки в PlanetScale решили не публиковать; вместо этого вышел технический разбор реализации без нечестных сравнений, который был хорошо принят сообществом.

Для победы на этом рынке не нужны «БЕЗУМНЫЕ БЕНЧМАРКИ». Нужна прочная техническая работа и внятное описание компромиссов и ограничений своего продукта. Хороший пример — Turbopuffer. Их бенчмарки не впечатляют, особенно на фоне конкурентов. В их документации больше строк о том, чего база данных не умеет, чем о том, что она умеет. Но все знают: если ваш кейс укладывается в их модель, это лучший продукт для поиска на рынке — с большим отрывом от конкурентов. Они не пьют слёзы конкурентов, а просто тихо забирают их клиентов.

Возвращаясь к SpacetimeDB и их бенчмаркам: у них действительно очень отличное от конкурентов предложение. Это база данных «всё в одном» плюс сервер приложений, где вы разворачиваете инстанс базы данных, а код вашего приложения выполняется прямо внутри неё. Идея довольно интересная — можно сказать, это похоже на хранимые процедуры в реляционной базе, но с лучшим developer experience. Справедливо. И на этой основе действительно можно построить жизнеспособный продукт.

Но стоит признать: это имеет очень мало общего с мультирегиональными, высокодоступными распределёнными базами данных, с которыми продукт себя сравнивает. Если код вашего приложения выполняется внутри базы данных, а у конкурентов отдельное приложение вынуждено делать сетевой запрос на каждый запрос к БД — конечно, вы будете впереди в бенчмарках, измеряющих QPS. Но честны ли такие бенчмарки? Действительно ли такое сравнение стоит показывать потенциальным клиентам, оценивающим техническое предложение?

Едва ли это стоит подчёркивать. Доступ к данным в памяти быстрее доступа к данным по сети — и для доказательства этого факта построен целый тестовый стенд. Как потенциального пользователя это совсем не впечатляет. С точки зрения маркетинга было бы куда интереснее показать, насколько быстр доступ к данным в памяти, а затем объяснить, какими компромиссами эта скорость достигается.

Похоже, на сайте проекта такого технического разбора нет. Что ж, разберём это здесь.

Хранилище

Есть несколько причин, почему SpacetimeDB показывает такую высокую производительность записи в опубликованных синтетических бенчмарках. Очевидная причина — логика приложения выполняется локально, рядом с базой данных, и запись в хранилище может быть исключительно эффективной именно поэтому. Эффективность дополнительно усиливается другими приёмами (например, батчингом записей), но чтобы достичь показанных цифр производительности, приходится идти на компромисс где-то ещё: хранилище данных — полностью в памяти, что сильно отличает систему от традиционных РСУБД.

Хорошая новость: записи в хранилище в памяти линеаризуемы. Плохая новость: доказывать линеаризуемость систем обычно приходится долго и мучительно, но здесь TLA+ не понадобился — доказательство тривиально. Потому что система, по сути, представляет собой хеш-таблицу с блокировкой перед ней.

Схема реализации хранилища

Звучит как преувеличение, но это довольно точное описание архитектуры движка хранения. Зафиксированное состояние всей базы данных в инстансе SpacetimeDB обёрнуто в единый Read-Write Mutex. Все операции записи выполняются последовательно, что и является тривиальным доказательством линеаризуемости. Две записи не могут произойти одновременно, значит, не могут конфликтовать или создавать гонку. Но и чтение, и запись тоже не могут произойти одновременно!

Что происходит при слишком большом потоке записей? Голодают ли читатели? Строить хранилище на единственной глобальной блокировке с семантикой read/write — вполне валидный технический выбор. Возможно, немного спорно называть такую конструкцию «базой данных». Но если делать ставку именно на такой подход, если эта блокировка обеспечивает контроль конкурентности для всей базы данных целиком, нужна очень явная, настраиваемая семантика приоритизации читателей и писателей — чтобы сервер оставался отзывчивым при любой нагрузке.

В данном случае поведение — это деталь реализации, нигде толком не определённая и не объяснённая. Мьютекс — это готовый parking_lot::RWMutex из крейта parking_lot. У него есть eventual fairness — «в конечном счёте справедливость», то есть читатели в конце концов получат блокировку, даже при высокой интенсивности записи. Правда, с произвольной задержкой до 0,5 мс. Крейт parking_lot — это Rust-порт оригинального WTF::Lock из WebKit. Этот чейнджсет 2024 года показывает, как там была реализована eventual fairness — стоит почитать, там хорошие наблюдения о конкуренции за мьютекс. Считайте это передышкой от текущего разбора. А теперь вернёмся к хеш-таблице и блокировке.

Так что же происходит в системе во время записи? Да что угодно. Пока удерживается глобальная блокировка, среда выполнения Wasmtime исполняет «редьюсеры» (произвольный пользовательский код, скомпилированный в WebAssembly). Пока редьюсер выполняется, ни один другой редьюсер не может выполниться и записать в базу. Никакой другой код не может даже прочитать данные из базы. Официальная документация честно предупреждает, что редьюсеры «не могут выполнять HTTP-запросы». Да, конечно нет. Критическая секция для всех записей в этой базе данных эксклюзивна и сериализована, и в ней выполняется произвольный пользовательский код. Лучше не делать HTTP-запросы посреди неё.

Есть небольшая лазейка: на сервере можно использовать «процедуры». На момент релиза этой недели они всё ещё в бете (документация предупреждает, что API может измениться в будущем). Они позволяют выполнять дорогой код, включая HTTP-запросы, и это хорошо. Внутри процедуры можно открыть транзакцию, которая снова захватывает глобальный мьютекс и не допускает никаких других одновременных записей и чтений из базы — так что коммитить её нужно очень быстро, иначе вся система застопорится.

С чтением похожая история. Предполагается, что оно должно происходить через «views» — эквивалент редьюсеров только для чтения. Поскольку они захватывают блокировку на чтение глобального мьютекса, несколько views могут выполняться одновременно, но во время их выполнения запись в базу невозможна. Как и редьюсеры, views — это произвольный пользовательский код, скомпилированный в WebAssembly.

Долговечность (durability)

Очевидное следствие архитектуры с единственным мьютексом — необходимость минимизировать работу в критическом пути транзакции. HTTP-запросы исключены по определению. Но и другие «дорогие» операции, которые обычно выполняют РСУБД — например, сохранение транзакции на диск — здесь тоже под вопросом.

Схема конвейера обеспечения долговечности

Эта полностью in-memory база данных всё же опирается на Write Ahead Log, но WAL не коммитится на диск как часть транзакции записи. WAL асинхронен и периодически сбрасывается на диск в фоне (по умолчанию — каждые 50 мс).

Можно ли сделать это полностью консистентным? Ограничения «дизайна с единственным мьютексом» серьёзно усложняют задачу: WAL никогда не может писаться синхронно (это полностью застопорило бы все остальные записи и чтения в приложении). При чтении система предлагает опцию с своеобразной семантикой. Флаг withConfirmedReads позволяет чтениям возвращать только те данные, что уже синхронизированы на диск: сервер «засыпает», пока не увидит, что записи WAL для результата запроса действительно сброшены на диск. Это может занять до 50 мс — довольно много для одного запроса. Не самое эргономичное поведение, но предполагается, что база данных рассчитана на «в основном эфемерные» данные, и среднему запросу такая строгая гарантия консистентности не нужна.

Всё это сильно напоминает атмосферу MongoDB образца 2011 года. Во многом буквально. Команда Mongo когда-то выпустила довольно слабую базу данных с очень впечатляющими бенчмарками, а затем под давлением интернет-сообщества (см. «MongoDb is Web Scale») реализовала нормальный движок хранения. Они приобрели WiredTiger — действительно серьёзный движок хранения. Спустя пятнадцать лет это серьёзная и жизнеспособная компания на рынке баз данных. И тем не менее многие технические специалисты, помнящие ранние времена Mongo, до сих пор отказываются использовать её в продакшене или рекомендовать её. Их информация устарела: современная Mongo — серьёзная работающая база данных. Но плохая техническая репутация цепляется надолго, а иногда и навсегда.

Отсюда напрашивается важный вывод: если в 2026 году запускать продукт базы данных, представляющий собой хеш-таблицу с единственной блокировкой, делать это стоит тихо. Срезать углы при запуске продукта — рабочий подход (сам бы так делать не стал, но у MongoDB это сработало с большим успехом). Но как только продукт начнёт набирать обороты, придётся срочно расплачиваться и за технический, и за репутационный долг. А маркетинговое видео с лазерами и «бутылкой слёз» только усложняет эту задачу.

Компромиссы

Технический выбор, позволяющий SpacetimeDB показывать хорошие результаты в специфических бенчмарках (то есть в бенчмарках, измеряющих скорость записи приложения в базу данных, при том что приложение и есть база данных), рассмотрен выше. Этот выбор нигде заранее не объясняется в документации, и, к сожалению, вытекающие из него компромиссы тоже нигде явно не перечислены.

Кратко пройдёмся по ним: это не распределённая система, и у неё есть жёсткий предел масштабируемости и доступности. Можно развернуть «кластер SpacetimeDB» — то есть основной инстанс и несколько реплик с eventually consistent репликацией (акцент на eventually consistent: WAL асинхронен, репликация тоже, здесь достаточно места для сбоев) — но вся система упирается в CPU и объём RAM машины, на которой развёрнут основной инстанс. Нужно достаточно CPU не только для выполнения всех запросов базы данных, но и для выполнения всей логики приложения, поскольку приложение живёт внутри базы. Нужно достаточно RAM, чтобы уместить все данные базы в памяти. SpacetimeDB вообще не опирается на диск как на постоянное хранилище — она лишь сбрасывает на диск WAL (и периодически снапшоты, ускоряющие восстановление из WAL при перезапуске). Если датасет вырастет больше объёма RAM, база данных (и приложение — это одно и то же) откажет. Единственный вариант масштабирования здесь — вертикальный: покупка более мощной машины.

Эти компромиссы сами по себе абсолютно валидны. Но они явно позиционируют SpacetimeDB как «более мощный Redis», а не как «более производительную реляционную базу данных». Странно, почему авторы выбрали сравнивать себя именно со вторым.

Сценарии использования

Изначальная версия SpacetimeDB разрабатывалась как бэкенд для MMORPG (реальной игры, доступной в Steam прямо сейчас). Это выглядит вполне справедливо — все технические решения базы данных подходят под этот сценарий. Можно асинхронно сбросить на диск запись WAL о том, что игрок xXxPussyHunter420xXx получил лут [Thunderfury, Blessed Blade of the Windseeker]. Задержка в 50 мс здесь вполне допустима. Если инстанс упадёт именно в этот момент, игрок расстроится, но переживёт.

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

Маркетинговая страница проекта теперь утверждает, что «LLM продвигаются гораздо дальше со SpacetimeDB, потому что она берёт на себя всю персистентность, логику, деплой и синхронизацию в реальном времени в едином цельном бэкенде». Тоже вполне разумный выбор — agentic-программирование сейчас в центре внимания, и построить базу данных под LLM выглядит разумной идеей. Но если честно, для этого сценария выбраны, пожалуй, худшие из возможных технических решений.

Вся суть SpacetimeDB в том, что производительность и доступность и приложения, и базы данных на 100% зависят от коротких фрагментов пользовательского кода, которые не могут выполнять операции с побочными эффектами или блокировками — потому что этот код компилируется в WebAssembly и выполняется виртуальной машиной внутри критической секции, сериализующей все записи и чтения в хранилище приложения. Отсутствие побочных эффектов или блокировок не может быть проверено системой типов и зависит от конкретного WASM-байткода, сгенерированного JIT-компилятором во время выполнения. Любая ошибка внутри такой критической секции, любая операция, способная застопорить её под нагрузкой, скорее всего, проявится только в продакшене и деградирует производительность всего приложения — вероятнее всего, вплоть до проблем с доступностью.

Это далеко не идеальная среда для программирования силами LLM. lol

При всём этом, продукт представляется перспективным, и из его текущего состояния можно извлечь уроки. Возможно, в версии 3 SpacetimeDB эти уроки будут учтены, и получится более устойчивая и дружелюбная к LLM база данных — где код приложения изолирован и может выполняться сколько угодно долго, не влияя на другой код, работающий рядом, даже при серьёзных ошибках реализации; где транзакции могут выполняться сколько нужно, не задевая производительность других транзакций; где они автоматически ограничиваются по времени, если LLM не предоставила оптимальный план запроса. Возможно, появится система, гораздо более устойчивая к сбоям, но с гораздо менее «впечатляющей производительностью»; возможно, система окажется тривиально распределённой, чтобы ИИ-агенту не приходилось самому проектировать распределённую систему; возможно, релиз обойдётся без нелепых бенчмарков и с большим количеством технических подробностей.

Вот за таким продуктом действительно стоило бы последить.