Я удалил Datomic из проекта.
В апреле 2023 Datomic стал бесплатным, и я загорелся идеей добавить его в проект, т.к. была потребность в транзакционной БД и кэшировании данных в приложении. За эти 2 года он так и не прижился у нас. Расскажу в этой заметке немного про его внутренности и недостатки.
Чтобы понимать мою экспертную экспертность в вопросе
Т.е. я глубже всех понимаю дизайн и внутреннее устройство Datomic OnPrem.
Изначально я рассматривал датомик для
Датомик хранит данные тройками [entity attribute value]
.
entity
тут как раз номер - идентификатор сущности.
Узнать подробности о структуре битов можно из статьи Datomic Entity Id and Datom Internals.
Для нас тут важно, что независимо от партиции на t-counter выделяется 42 бита. Этот счетчик монотонно растет при создании новой сущности.
Давайте прикинем на сколько этого хватит
2^42 / 5000TPS / 60 / 60 / 24 / 365 = 27 years
.
Т.е. мы можем коммитить 27 лет 5000 транзакций в секунду, где создается только одна сущность - сама транзакция.
Т.к. датомик создает сущность, описывающую транзакцию.
В реальности же на одну транзакцию будет создание пары сущностей.
Вроде бы числа получаются большие, но недостаточно.
Например, если мы выполняли транзакцию, которая не изменила данные, то счетчик все равно увеличится, т.к. датомик записывает время выполнения транзакции, а значит создает сущность транзакции. Это важно понимать при выполнении “upsert”.
Казалось бы, если не хватает одной базы данных, сделай по базе данных на каждый год или месяц.
Хотя Датомик и позволяет делать запросы одновременно к нескольким БД, число этих БД должно быть заранее известно:
(d/q '[:find ,,,
:in $1 $2 ,,,
:where
[$1 ,,,]
[$2 ,,,] ,,,]
(d/db conn1)
(d/db conn2)
,,,)
Т.е. не получится взять все базы данных, которые каждый год добавляются, и
(d/q '[:find $db
:in [$db ...] ,,, ;; так нельзя :'(
:where
[$db ,,,] ,,,
[(d/db conn1)
(d/db conn2)
,,,])
В сообществе появился термин Decanting
- переливание данных из одного инстанса Datomic в новый
путем чтения лога транзакций и фильтрацией ненужных записей.
Это нужно, когда история занимает слишком много данных, или нужно удалить много данных.
Возвращаясь к утечке t-counter
- декантирование может помочь, но только если entid оставались приватными,
а в качестве публичных ключей использовался UUID или что-то похожее.
Т.е. при декантировании мы могли бы изменять entid сущностей.
Одной из целей внедрения Datomic в мой проект, были как раз Int64 идентификаторы. В документации даже писали, что не стоит их делать публичными, но теперь я осознаю эту рекомендацию.
Для некоторых проектов важно иметь возможность надежно удалить данные. В datomic есть api для этого. Но там есть много нюансов, в том числе связанных с производительностью. Например удаление может остановить транзакции.
Вместо удаления обычно используют декантирование. Класс!
Может показаться, раз у нас БД как значение и мы можем применять d/filter
, то проблем с multitenancy нет.
Но это не так. В документации или сообществе нет никаких наработок в этой области.
Нельзя просто взять и изолировать данные разных клиентов. Нужно довольно глубоко погружаться во внутренности датомика, чтобы сделать безопасное и удобное решение.
От себя замечу, что если можно ограничить счетчик клиентов сверку в полмиллиона (524288), то можно разделять их по разным implicit partitions. Но это именно счетчик, а не количество активных клиентов, этот счетчик так же может утекать.
Есть EAVT, AEVT, AVET, VAET. Для поиска по значению атрибута будет использоваться AVET. И это одна из причин, почему все говорят, что датомик медленный. Он не медленный, просто нужна экспертиза, чтобы учитывать раскладку данных в индексе.
Например, у нас есть Платформа приложения (java, ruby, ….) и Имя приложения. И зная их, мы хотим найти Идентификатор приложения (пусть будет UUID). Кроме того, они содержатся в аккаунтах, но оставим это за скобками.
Нам нужно выбрать, какой атрибут поставить первым в :where
: Платформу или Имя.
Нужно выбрать Имя, как самый высоко кардинальный (селективный) атрибут.
Но что, если и платформ тоже много, и одно Имя встречается во многих Платформах?
Тогда мы выберем много лишних данных, что снизит скорость запроса.
Решение этой проблемы - Composite tuples. С их помощью мы сможем положить в один атрибут и Платформу и Имя, тем самым увеличив кардинальность множества значений.
Вот пример Composite tuples:
{:db/ident :my-proj/account+platform+name
:db/valueType :db.type/tuple
:db/tupleAttrs [:my-proj/account :my-proj/platform :my-proj/name]
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
У этого есть проблемы.
Нужно учитывать это при создании схемы. А если мы не учли, то нужен процесс миграции, чтобы
добавить этот атрибут существующим сущностям.
А это куда сложнее, чем CREATE INDEX CONCURRENTLY
.
Я так и не придумал зачем нужен этот индекс. Если кто-то знает - расскажите.
Проблема в том, что из-за него данные хранятся 2 раза: в EAVT и в AEVT.
Transaction function это функция, которая выполняется в транзакторе. Она принимает базу, параметры и возвращает tx-data.
Проблема в том, что другой вызов функции в той же транзакции не увидит изменений первой функции. Например это делает сложным инкремент атрибута. Вместо двух вызовов функции в транзакции, мы или должны сделать 2 транзакции, или вызывать функцию один раз, но передать ей двойку как аргумент.
У датомика есть дурацкая проблема с количеством тредов, отведенных на health-check endpoint, внутри контейнера. ping-concurrency
Почему я должен разбираться в этом?
Нет официального docker-image. Нет конфигурации через env.
Мне нужно хранить мало данных. Очень часто клиентам не нужна отказоустойчивость и мы запускаем один инстанс приложения.
Но я не понимаю, почему нельзя запустить transactor и приложение в одном процессе, а данные сохранять в H2.
Это самая очевидная проблема. Довольно часто хочется зайти в консоль и посмотреть таблички. Но тут нет ни консоли ни табличек.
Бывает, что БД живет дольше, чем код приложения. Т.е. приложение устаревает, пишут новое приложение, а БД остается.
С Датомиком это почти наверняка не сработает.
Я не знаю истории, и предпосылок появления Датомика. Но за эти 10 лет сервера стали очень мощными. Если вы не “Яндекс”, то вы очень долго сможете расти вертикально.
Многие БД позволяют потоково читать данные. И это решение, если вам нужно сделать отчет по всем данным, что не помещается в память приложения.
Т.е. масштабирование чтения (Peer) в датомике теряет актуальность.
В Датомике есть in-memory или дисковый (Valcache) кэш.
И важно отметить, что если вы берете SQL DB + Cache, то все ваши запросы становятся key-value, вы теряете QL.
А у датомика кэшируются части B+ tree индекса, что оставляет полноценные запросы.
Я не делал полноценные бенчмарки, но select count(*) ...
в Postgres в докере в виртуалке
с большим отрывом обгонял подобный запрос на полностью закэшированной в Valcache бд в Датомике.
Да, сравнение никакое, но заставляет задуматься.
С другой стороны, а часто ли нужно делать консистентный поиск по диапазонам в кэше?
В Датомике такая поддержка полнотекста, что считай, что ее нет.
В итоге нужно подключать стороннее решение, разбираться, как настроить репликакцию и т.д. Готовых решений нет.
Добавление новой БД полностью убивает все преимущества Датомика.
В Датомике не стоит хранить большие строки, а тем более их индексировать. И документация рекомендует положить их “куда-то еще”.
Если датомик использует другие базы данных для хранения сегментов, то почему бы не сделать api для хранения BLOB?
Вы не можете сделать свой тип атрибута.
Конечно, я слышал, что вроде бы можно как-то подлезть в fressian
.
java.time
уже много лет доступно, но команда не торопится добавлять поддержку этих новых типов.
Если вы знаете примеры успешного использования Datomic - поделитесь, пожалуйста, опытом.