Pull to refresh

Comments 20

Игрался с Кафкой и пришёл к выводу, что ES имеет смысл разбить на глобальный и локальный. Глобальный — единый аудит лог, из которого можно "накармливать" новые сервисы или сервисы, в которых обнаружен баг, приведший к порче данных. А вот для локальных задач типа получить стейт сущности с таким-то ид, кафка как-то не очень

Спасибо за статью! Но как по мне, некоторые утверждения из статьи достаточно странные. Например

Я уверен, все согласятся с тем, что плохо, когда разные ограниченные контексты совместно используют данные в (реляционной) базе данных из-за возникающей связанности. Но чем это отличается от Event Sourcing? Ничем

Мне кажется между этими вещами вообще нет ничего общего. Я не понимаю как публикуемые события из контекста и вообще события могут привести к подобной связности через базу.

Если вы используете Event Sourcing глобально, то вы раскрываете свой уровень хранения.

Здесь используется утверждение, но оно не обосновывается. Каким образом публикуя событие я раскрываю свой уровень хранения?

Так же в статье используются две одинаковые картинки.

События в EventSourcing это по факту (единственное*) хранилище. Кроме событий, вы больше ничего не храните и текущее состояние данных это результат воспроизведения всех событий по одному.


Например, ваш event store для сервиса пользователей выглядит следующим образом:


  • Событие1: ФИО пользователя Х изменено на "Иванов Иван Иваныч"
  • Событие2: Возрас пользователя Х изменен на 25

И тут мы решаем в этом сервисе изменить ФИО на фамилию, имя отчество, и исправить опечатку "возрас" на "возраст"
Локально код поменяли, старые события не трогаем (это же хистори, не принято), поэтому сделали беквард компатибл, и поехали стали слать новые события
Тут в наш event store попадает


  • Событие 3: Пользователь Х. Возраст 30, Фамилия Сидоров

С точки зрения сервиса пользователей, всё в порядке, все поля ожидаемые.


Но как к этому событию должен отнестись сторонний сервис? Он ничего ни про "фамилию", ни про "возраст" не знает. И не сможет распарсить новый формат событий


Каким образом публикуя событие я раскрываю свой уровень хранения?

События = ваш способ хранения данных. Сохраняя события (и ничего кроме них), и потом выставляя эти же самые события наружу, вы полностью связываете своё хранилище и API


Но надо признать, что подобная проблема есть и у многих REST/CRUD/ORM приложений. Завели entity, и она одновременно используется как для внутреннего хранения, так и для REST API. С одной стороны удобно, с другой стороны уже нельзя просто так взять и что-то оптимизировать в работе с БД.


* очень часто дополнительно к событиям делают materialized view/snapshot/cache, для увеличения производимости. Но все они могут быть выкинуты и построены заново исходя из очереди событий.

Предлагаю провести мысленный эксперимент. У вас есть микросервис из заголовка статьи. Он подписался на событие изменения реквизитов пользователя. И получает его, скажем, в виде JSON на свой REST-endpoint ровно в том объеме, в каком он это понимает. Что он знает о единственности хранилища и вообще о хранении? С каких пор в нормальных реализациях event-driven архитектур он знает о чём-то большем чем брокер сообщений? Или мы говорим о EventSourcing, с которым мы работаем в state-driven стиле?

ровно в том объеме, в каком он это понимает

не совсем понял эту фразу. Как можно понимать объём?


С каких пор в нормальных реализациях event-driven архитектур он знает о чём-то большем чем брокер сообщений?

Он знает структуру данных в сообщении (какие поля должны быть).


И эта структура та же самая, что и используется сервисом-отправителем внутри своего собственного event store


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

не совсем понял эту фразу. Как можно понимать объём?

Не объём, а "в объёме" — стандартный речевой оборот. Смотрите корпус русского языка (http://ruscorpora.ru/) или словари. В данном случае речь идёт об ubiquitous language и том его подмножестве (https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D1%91%D0%BC_%D0%BF%D0%BE%D0%BD%D1%8F%D1%82%D0%B8%D1%8F), которое известно потребителю.


И эта структура та же самая, что и используется сервисом-отправителем внутри своего собственного event store

Да боже упаси. Если вы гоняете одни и те же супер-структуры по своей системе, то не важно, ES под ней или CRUD, делаете ли вы split сложных запросов или конструируете комплексную view-model, у вас будут проблемы. Прелесть event-driven архитектур как раз в том, что вам проще организовать цепочку SRP-компонент (частью чего является и микросервис). Сложный запрос занырнул в брокер и всплыл у микросервиса уже в виде специфичного для него атомарного.


Приведу простой пример. Практически во всех промышленных СУБД используется WAL, который по природе есть Events Source. Именно его replay используется для формирования состояния (в том числе и на других серверах при репликации через log shipping). И хотя данные всех таблиц (а также DDL-операции) сидят там, вам не нужно знание об организации WAL, вы работаете с таблицами/коллекциями/KV-словарями.

Не объём, а "в объёме" — стандартный речевой оборот.

понял, спасибо, прошу прощения!


Да боже упаси

так и статья о том же. "События Event Sourcing не должны выдаваться наружу во внешний мир!"
Важный момент, тут речь не о запросах (я про "Сложный запрос занырнул в брокер"), а о событиях. Внешние события определяются отправителем и от получателя не зависят (т.к. отправителю все равно, сколько получателей есть, кто они, и есть ли вообще).


В мысленном эксперименте выше про "событие изменения реквизитов пользователя": кто должен подгонять это событие под "ровно в том объеме, в котором он ожидает"? Отправитель? Или вы же говорите о промежуточном слое трансляторов, типа Apache Camel?

так и статья о том же. "События Event Sourcing не должны выдаваться наружу во внешний мир!"

Нельзя взять отдельное предложение из текста и сказать, что статья о нём. Сокрытие реализации от внешнего мира — это SOLID-принципы и с этим стоит согласиться. Можно частично согласиться с соседним утверждением "Event Sourcing — это локальное решение", поскольку может иметь смысл в микро-сервисной архитектуре локализовать источники данных. Но это паттерн "Database per service", он одинаков, храним мы историю или снапшоты. Причем, скорее всего, у сервиса будут закэшированы именно снапшоты, а не события. Но из этих полутора утверждений не получается вывод в заголовке "Почему Event Sourcing — это антипаттерн для взаимодействия микросервисов".


Внешние события определяются отправителем и от получателя не зависят (т.к. отправителю все равно, сколько получателей есть, кто они, и есть ли вообще).

Нет. У вас есть ubiquitous language на котором говорят и отправитель и получатель. Он может быть ограничен диалектом некоторого bounding context, но такой формулы, что отправитель определяет язык — нет. Ssrk ojrfimqe lkravbe ktub. Понимаете, в чем суть? Адресуя сообщение сюда я не могу выдумывать языки какие мне заблагорассудится, а должен соблюдать определённые интерфейсы. Использовать русский/английский, в контексте данного обсуждения придерживаться по возможности ООП и DDD и т.п. Если я начну продвигать свои мысли на финском с примерами на конкатенативных языках, то не буду понят.


Адресуется абстрактному получателю, за которым могут прятаться самые разные читатели, которые сделают самые разные выводы. Поэтому здесь на первый план выходит дизайн ubiquitous language и это касается не только ES. То есть я начинаю проводить сегрегацию (ISP-Принцип) событий, чтобы моим микро-сервисам (SRP-принцип), было проще их понимать (DIP-принцип) и эволюционировать (OCP-принцип). Скажем, событие UserCreated сегрегируется на интерфейсы Timestamp+AuthTicket+UserState. Брокер раскидает их по соответствующим сервисам, которые установят время, авторизуют, зарегистрируют изменение состояния. И это опять же, не только про ES.


В мысленном эксперименте выше про "событие изменения реквизитов пользователя": кто должен подгонять это событие под "ровно в том объеме, в котором он ожидает"? Отправитель? Или вы же говорите о промежуточном слое трансляторов, типа Apache Camel?

Может быть Camel, топологии Kafka, роутеры Rabbit MQ, могут быть отдельные consumer-producer. Эта схема может работать вообще без message queue/event bus. Скажем у вас есть конвеншен по описанию JSON где свойства первого уровня определяют интерфейсы (у нас подобный инструмент отлично работал на ELK). Или вы используете IoC с дженериками и/или рефлексию для VM-языков вроде C#/Java. Варианты есть на все случаи жизни.

Нельзя взять отдельное предложение из текста и сказать, что статья о нём

значит каждый из нас понимает статью, event sourcing и event driven архитектуры по-разному. По-моему статья именно об этом.


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


В любом случае спасибо за разговор и буду очень признателен, если ещё поделитесь ссылочками на статьи/презентации об архитектуре, про которую вы говорите. Учиться тому, что не знаешь или не понимаешь, всегда полезно

Как я понимаю речь о том, что сервис, как реакцию на какой-нибудь HTTP POST /users, сохраняет в евентлоге какое-то UserCreated и его же паблишит в MQ. На GET /users/1 он читает все User*ed с userId=1, агрегирует их и отдаёт плюс-минус в том же формате. Все четыре шейпа (входящий пост, ответ на гет, запись в евентлоге и паблишинг в MQ) практически одинаковы и обусловлены прежде всего способом хранения в евентлоге. Вот это, кажется, и называется в посте раскрытием деталей хранения.

Это нарушение ISP и DIP ещё на этапе дизайна, мы даже до архитектуры не дошли. UserCreated должны быть представлен совокупностью интерфейсов и работать все потребители должны с ними. Например, я хочу, чтобы у меня был один сервис, который отвечает за простановку правильного Created timestamp (используя правильный источник времени, нормализуя должным образом). Независимо от того, User это или Order. Значит я выделю какой-нибудь Lifetime-интерфейс и работать буду с ним. Точно такая же организация у меня была бы и в случае прямолинейного CRUD решения, когда нужно проставить User.Created.


Точно также у меня будет какой-то интерфейс UserState, который будет реализовываться как UserCreated так и UserUpdated и может каким-то UserMigrated/UserImported/etc и работать я буду исключительно по этому интерфейсу.

UserCreated обычно на уровне инфраструктуры какой-нибудь json или подобная форма представления, хорошо если какая-нибудь примитивная схема без абстракций где-то сбоку, а может тупл, где опять же сбоку описано, что первые четыре байта это unsigned int BE id и т. д.. В языке потребителя и/или производителя событий может вообще не быть классов и интерфейсов, а информацию регулярками из тела достают.


Но речь не о том, а том что у вас на уровне архитектуры завязано, что формат везде одинаковый. Разобьёте вы его или нет у себя внутри на классы и интерфейсы, но физически ходить будет один и тот же json по сети и он же на диске храниться.

В языке потребителя и/или производителя событий может вообще не быть классов и интерфейсов, а информацию регулярками из тела достают.

В языке producer/consumer интерфейсы как раз и описываются. Они есть основа их диалога по определению этого слова. ООП-языки являются просто реализацией данной фундаментальной концепции, равно как и вертикальные партиции в РБД или другие конвенции. И регулярные выражения здесь не исключение. Если вы хотите хороший дизайн, то с своём регулярном языке вы предусмотрите такую грамматику, которая обеспечит работу с интерфейсами.


UserCreated обычно на уровне инфраструктуры какой-нибудь json или подобная форма представления…
Разобьёте вы его или нет у себя внутри на классы и интерфейсы, но физически ходить будет один и тот же json по сети и он же на диске храниться.

Вот сейчас у меня под капотом топология на Kafka, которая партиционирует аспект UserState в отдельный KStream, огружает в Avro и т.п. А вот сейчас я заменил со стриминговой на реляционную модель скажем с Postgres+PGQ. И вы дажа не заметите, как произошла эта миграция. Нигде не было JSON. Нигде UserCreated не хранился в исходном виде, но концепция ES работает, у меня на руках полная история. Щёлкнул пальцем и у меня Kafka соберёт события в KTable или Postgres в материализованное представление и получу гибридную модель events+snapshot. Но вы так и не узнали о том, что произошло за занавесом интерфейса producer/consumer.

Что вы имеете в виду под словом интерфейс? Какая-нибудь json schema им является?

Интерфейс — это абстракция взаимодействия, inter- + face. Мы её описываем постоянно. JSON Schema, DDL, сигнатуры функций, структуры, легион способов. Самый удачный для сохранения принципа SSOT — спецификация (здравствуй ubiquitous language), чтобы не создавать зависимостей от способов реализации (как в случае с сериализацией в JSON/JSON Schema).

Тут, по-моему, нечёткая терминология. "Публикация события" может быть как просто помещением его в короткоживущую очередь, в которой события удаляются как только обработаны всеми заинтересованными наблюдателями, так и помещение его в публично доступный лог событий, который будет источником правды для всей системы. В первом случае всё нормально для микросервисной архитектуры, а вот во втором это действительно сродни выставлению наружу базы данных сервиса вместо API сервиса и даже хуже. Хуже потому что все клиенты должны поддерживать все версии всех сообщений, касающихся интересующих их сущностей.

Вы правы, что возможны два варианта, но в контексте статьи об Event Sources, всё же подразумевается именно второй вариант (лог событий). На картинке это Event Sourcing Event
А первый вариант автор предлагает как решение проблемы. Соответственно на картинке это Domain Event

Картинку поправил. Благодарю за замечание!

Чем в вашем понимании отличается Event Sourcing от Службы с открытым протоколом?

Перевод немного хромает и из-за этого трудно понять о чем речь.


I still argue that Domain Events are a perfect fit for the communication between Bounded Contexts, but these events shall not correspond to the events used for Event Sourcing.

Тут еще нужна оговорка о том, какие данные сервис отдает публично, иначе все равно может понадобится ввести дополнительное событие и будет ровно та же ситуация, что и в начале.

Sign up to leave a comment.