Зачем нужен event sourcing?
Если идет работа с реальными людьми, то нужно сохранять все действия для последующего “разбора полетов”. Для многих это основная бизнес-цель event sourcing.
Если посмотреть в будущее, то, другими словами, примерно все бизнес-приложения рано или позже полностью или частично перейдут на эту архитектуру. Почему тогда существующие решения массово не используют? Это все-таки требует дополнительного проектирования и кода, но при постоянном увеличении требований к ПО это уже сейчас по идее окупается.
Есть дополнительные плюсы и минусы, но они вторичны (влияют на выбор реализации, а не концепции).
Стоит отметить, что это достаточно революционное отклонение от обычной архитектуры. Другими словами, потребуется дополнительное обучение программистов и новые лучшие практики. Для примера негативного опыта можно почитать, например, этот тред: https://news.ycombinator.com/item?id=13339972
По теории можно прочитать предыдущую заметку “Классический event sourcing”.
Основные проблемы
- Eventual Consistency или старые данные возвращаются при чтении.
- Как разбираться с ошибками, которые случились где-то по середине обработки?
- Медленное построение онлайн проекций (когда событий много в агрегате)
- При хранении событий в обычной БД c автоинкрементом иногда теряются событий при обработке, т.к. БД под нагрузкой выделяет блоки автоинкрементных id, которые параллельно сохраняются, а обработчик событий обычно ищет по последним id.
Предлагаемое решение
Для решения этих проблем предполагается следующие изменение архитектуры относительно классической:
- Вводится понятие “основной проекции” – это проекция, расположенная в той же БД, что и хранилище событий.
- Основная проекция должна содержать все данные, необходимые для работы команд, встроенные проекторы основной БД и реакторов. Соответственно, при необходимости получения данных они берутся из основной проекции, а не из хранилища событий.
- Хранилище событий используется для получения истории и при необходимости перестроения основной проекции (полное или частичное).
- Команда, сохранение события и встроенные проекторы основной БД работают в одной транзакции.
- Реакторы работают в отдельной транзакции.
- Живые проекции не рекомендуются (только для специфичных случаев с учетом потенциального наличия задержки). Фактически нет такого понятия как класс агрегата или агрегат в памяти. Идет работа на уровне команд и событий, а агрегат как id только для транзакций и параллельности.
- Если требуется ограниченная асинхронность, то можно прикрутить рассылку событий через NATS (или другую очередь).
Как решаются проблемы?
- Нет eventual consistency, т.к. идет классическая работа с данными.
- Обработка ошибок опять же классическая – если все пошло не так, то транзакция откатывается, а клиент получает сообщение об ошибке.
- Нет медленной обработки событий при получении данных, т.к. для этого, в основном, используется основная проекция.
- Нет проблемы с диапазонами автоинкрементов, т.к. при обработке событий мы не полагаемся на эти id.
Что теряется?
- Полностью асинхронный подход (который потенциально облегчает масштабирование) – его можно применить, но уже частично.
- Распределенность – ее нет из коробки, нужно думать отдельно
- Масштабирование – ее нет из коробки, нужно думать отдельно
В целом, исходя из “принципа простоты”, они нам обычно не нужны. Если же все же нужны, то, вероятно, стоит использовать классический подход к event sourcing.
Кто-то даже может сказать, что это совсем не Event sourcing, если у нас нет агрегата в памяти, который вычисляет свое состояние по событиям. Но у нас есть агрегат в базе данных, который создан по событиям (чем не память, только не оперативная). Так что это все-таки Event sourcing, но я довольно сильно рад, что не классический, т.к. классический имеет слишком много ограничений для обычных приложений. Я бы сказал, что предлагаемый подход не event driven (EVA), но event sourcing.
Как итог, идет отказ от разделения базы событий и основной проекции. За счет этого решаются проблемы распределенности и асинхронности (их нет). При масштабировании решения я бы скорее использовал партиционирование решения, чем асинхронность и отдельные сервисы. Но, это, понятно, зависит от специфики решения: в разных случаях могут быть разные ответы.
С одной стороны, ничего сверхестественного в подходе нет. С другой, какого-то описания и термина для такого подхода не встречал. Предлагаю использовать Inline event sourcing (встроенный event sourcing), т.к. он основывается на встроенной проекции.