Поиск событий — это архитектурный шаблон; это не серебряная пуля

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

Цель этой статьи — помочь вам понять, что источник событий — это всего лишь шаблон, который может иметь несколько вариантов реализации в зависимости от предметных областей и контекстов.

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

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

Примечание. Если вы никогда не слышали об источниках событий, я рекомендую вам прочитать хорошее введение в другом месте, прежде чем читать эту статью.

Введение

На то, как я смотрю на архитектуру в целом, сильно повлияла работа Кристофера Александера о шаблонах и языке шаблонов, которая представляет собой философский взгляд на шаблоны и их значение в архитектуре. Вот цитата из книги:

«Каждый паттерн представляет собой наше текущее лучшее предположение о том, какое расположение физической среды будет работать для решения представленной проблемы. Эмпирические вопросы сосредоточены на проблеме — происходит ли это и ощущается ли оно так, как мы его описываем? — и решение — решает ли предлагаемая нами схема проблему? А звездочки обозначают степень нашей веры в эти гипотезы.

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

По сути, мы не должны путать шаблоны с жесткой стратегией реализации — шаблоны не являются конкретными решениями класса проблем!

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

Однако прагматичный архитектор будет черпать необходимое вдохновение только из всех известных паттернов (а не только паттернов поиска событий) и подходит к решению проблемы с мышлением новичка.

Цель не в том, чтобы реализовать идеальный шаблон. Вместо этого нужно создать элегантное и максимально простое решение, не увязая в ненужных догмах о самом шаблоне.

Является ли Event Sourcing новой идеей?

Хотя «источник событий» был придуман «Грегом Янгом», этот шаблон можно заметить в различных других системах для решения некоторых интересных проблем.

Например, базы данных использовали аналогичный шаблон для обеспечения аварийного восстановления, резервного копирования, репликации и гарантии долговечности.

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

Все обновления базы данных обычно выполняются в соответствии со следующими шагами:

  1. Добавить изменение (транзакцию) в файл WAL
  2. Сохраните файл WAL (для надежности)
  3. Обновите кэши страниц (в памяти), которые представляют таблицы и структуры данных индексов.

Как вы заметили, все изменения в таблицах и индексах выполняются только в памяти. Это хорошо для производительности, поскольку обновления таблиц и индексов обычно обходятся дорого, особенно в среде с высокой степенью параллельности. Даже одно обновление индекса может потребовать полной реструктуризации структуры индекса B-Tree, что приведет к множественным обновлениям файловой системы.

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

Обновления файлов WAL дешевы, так как это всегда только операция добавления. Есть несколько специальных оптимизированных системных вызовов (Direct IO), которые позволяют выполнять очень быстрые операции только добавления в файловую систему.

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

Каждый раз, когда контрольная точка сбрасывает кэши страниц на диск, она помечает конкретную запись в файле WAL как текущую «контрольную точку», указывая, что все транзакции до этой точки сохраняются на диске и, следовательно, устойчивы.

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

Если база данных перезапускается после сбоя, она читает файл WAL и воспроизводит все записи после текущей контрольной точки одну за другой, чтобы восстановить любые потерянные изменения из-за сбоя!

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

  • Файл WAL подобен «потоку событий» в приложении, основанном на событиях.
  • Текущее состояние базы данных можно получить, применив (или воспроизведя) файл WAL, точно так же, как вы восстанавливаете свои агрегаты из потока событий.
  • Файл WAL можно рассматривать как источник достоверной информации, точно так же, как потоки событий выступают в качестве источника достоверной информации в системе, основанной на событиях.
  • Записи WAL передаются по сети для создания реплик базы данных, поскольку различные потребители используют потоки событий для построения сложных проекций и моделей чтения.
  • Контрольные точки играют ту же роль, что и сводные снимки.

Я сравнил источник событий с реализацией WAL, чтобы подчеркнуть и повторить, что шаблоны могут иметь несколько вариантов реализации. Не существует «идеальной» реализации источника событий, и вам не следует пытаться ее создать! Используйте шаблон в качестве руководства для создания собственного решения проблемы. Будь проще!

Примечание. Если вы заинтересованы в изучении других систем, которые имеют вариант этого шаблона, вы можете изучить некоторые реализации блокчейна, такие как Ethereum.

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

Но прежде чем мы перейдем к вопросам, давайте рассмотрим очень простую эталонную реализацию, которую мы можем использовать в качестве примера.

Пример: эталонная реализация

Примечание. Если вы предпочитаете читать код, вот реализация Golang описанного ниже подхода, который я создал несколько лет назад — https://github.com/yehohanan7/flux.

Диаграмма выше представляет собой простую реализацию «системы управления заказами» с использованием шаблона поиска событий. Все обновления агрегата заказов будут следовать аналогичной последовательности шагов, как показано ниже:

  1. Получить заказ из репозитория, используя порядок id.
    — репозиторий будет получать все события заказа
    — репозиторий регидратирует агрегат с извлеченными событиями (т. е. применит события для построения текущее состояние заказа.)
    — репозиторий возвращает гидратированный агрегат заказа.
  2. Клиент выполняет команды на возвращенном агрегате, который обычно генерирует новые события, хранящиеся в агрегате.
  3. Клиент сохраняет обновленный агрегат (с вновь сгенерированными событиями) через репозиторий заказов.
  4. Репозиторий добавляет новые события в хранилище событий «условно» — т. е. в приведенном выше примере он добавляет событие только в том случае, если последнее событие для определенного заказа (order123) все еще является событием с версией 1. Это гарантирует, что заказ не будет обновлен какой-либо другой транзакцией между моментом, когда текущая транзакция считывает события и выполняет команды. Этот метод называется оптимистическим параллелизмом, который обычно обеспечивает лучшую общую производительность ваших систем.
  5. Если репозиторий обнаруживает конфликт при добавлении новых событий, т. е. другая транзакция уже добавила новое событие, репозиторий возвращает клиенту ошибку «конфликта».
  6. Клиент, в случае возникновения конфликта, может снова повторить весь процесс.
  7. Но если конфликта не обнаружено, транзакция считается успешной.

Часто задаваемые вопросы по внедрению

Нужны ли мне сводные снимки?

Снимок агрегата — это оптимизация, поэтому, пожалуйста, избегайте преждевременной оптимизации!

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

В примере с управлением заказами нельзя ожидать, что будут сгенерированы тысячи событий, связанных с конкретным заказом. Обычно у вас может быть от 10 до 20 событий.

Даже если вы считаете, что ваш агрегат имеет длительный жизненный цикл и может иметь до 100 или 200 событий, в реализации моментальных снимков все равно нет необходимости!

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

Например, если вы используете Postgres в качестве хранилища событий, убедитесь, что вы разделили свои таблицы событий так, чтобы все события агрегата хранились в одном разделе!

Если вы используете хранилище ключей и значений, такое как DynamoDB, в качестве хранилища событий, вам следует выбрать ключ секции и ключ диапазона, чтобы события располагались в одном узле, чтобы обеспечить эффективное чтение.

Однако, если вы действительно считаете, что ваш агрегат может иметь тысячи событий, у вас есть несколько вариантов моментального снимка состояния агрегата.

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

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

Кроме того, вы можете лениво обновлять снимок, т. е. сохранять текущее состояние агрегата только тогда, когда размер событий достигает максимального количества порогов. При таком подходе каждый раз, когда вам нужно текущее состояние агрегата, вы должны получать как моментальный снимок, так и все новые события, добавленные после создания моментального снимка, и гидратировать агрегат.

Где я должен выполнять побочные эффекты?

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

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

Но распространенное заблуждение состоит в том, что публикация события в системе обмена сообщениями, такой как Kafka, также считается побочным эффектом, что вызывает серьезную озабоченность. Если это не будет сделано должным образом, вы получите несогласованное состояние, как показано в следующем примере:

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

Есть несколько способов справиться с этой ситуацией. Рассмотрим два таких метода.

Kafka Bridge: если вы используете DynamoDB в качестве хранилища событий, вы можете использовать потоки DynamoDB для прослушивания всех новых событий и публикации их на других платформах потоковой передачи, а не в рамках транзакции, как показано ниже:

Есть также аналогичные варианты, если вы используете Postgres в качестве хранилища событий. Вы можете взглянуть на Debezium, систему с открытым исходным кодом, которая позволяет вам передавать изменения БД так же, как DynamoDB.

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

Шаблон исходящих сообщений. Этот популярный шаблон обычно используется для обновления транзакции и публикации сообщения атомарно.

В шаблоне исходящих событий вы публикуете событие вне транзакции и обновляете состояние published: true/false в хранилище событий, чтобы указать, было ли событие опубликовано успешно. Если вам не удается опубликовать событие, у вас может быть фоновое задание, которое регулярно синхронизирует хранилище событий с Kafka, повторно отправляя все события, которые не были опубликованы.

Шаблон исходящих сообщений v2: он аналогичен предыдущему шаблону исходящих сообщений, за исключением того, что вместо того, чтобы загрязнять хранилище событий состоянием published:true, вы можете использовать отдельную таблицу в качестве исходящего. Это также позволит вам разделить таблицу исходящих сообщений по состоянию (published: true против published: false), что даст вам прирост производительности для фонового задания, чтобы всегда оптимально запрашивать меньший раздел.

Можно ли использовать Postgres в качестве хранилища событий?

Абсолютно! Но у вас должна быть хорошая стратегия секционирования, чтобы обеспечить оптимальное чтение событий агрегата.

Рекомендуется иметь одну таблицу на агрегат. Например, order_events , customer_events и т.д.

Рассмотрите возможность создания составного индекса (aggregate_id, version), который обеспечит эффективное извлечение всех событий для агрегата в отсортированном порядке версии.

Выберите стратегию разбиения, которая больше подходит для вашей системы. Например, вы можете разделить order_events по месяцам, если большая часть ваших прочтений относится к недавним заказам. Это поможет вам быстро получить последний заказ, потому что у вас всегда будет очень тонкий раздел. Это также позволяет вам отделять очень старые разделы и архивировать их, чтобы в долгосрочной перспективе размер вашей базы данных был достаточно небольшим.

Можно ли использовать базы данных NoSQL в качестве хранилища событий?

В большинстве случаев лучше использовать очень хорошую базу данных NoSQL в качестве хранилища событий. Раньше я использовал DynamoDB в качестве хранилища событий.

DynamoDB позволяет вам выбрать ключ разделить и отсортировать, как описано здесь. Это основано на том, какой агрегат id и version я использовал в качестве ключей раздела и сортировки соответственно.

Эта стратегия работала лучше всего, потому что все события определенного агрегата будут храниться в одном узле и сортироваться по версии!

Вы можете разработать аналогичную стратегию в большинстве современных баз данных типа «ключ-значение», таких как Cassandra, CockroachDB и т. д.

Это относительно лучший вариант по сравнению с Postgres, поскольку вы можете горизонтально масштабировать хранилище событий, не беспокоясь об архивировании.

Могу ли я использовать Kafka в качестве магазина событий?

Хотя Kafka может показаться естественным средством для хранения агрегированных событий, это не база данных, в которой вы можете запрашивать или обновлять события транзакционно.

Тем не менее, вы по-прежнему можете рассматривать Kafka как «источник правды» (при условии, что вы правильно настроите свою политику хранения), но вам все равно потребуется база данных OLTP для обеспечения транзакционных обновлений вашего агрегата и возможности эффективного запроса событий.

Есть также несколько операционных проблем, связанных с Kafka, о которых я расскажу в следующем вопросе.

Как насчет Кафки против Пульсара?

Pulsar — ​​превосходная технология по сравнению с Kafka. Если вы крупная организация с миллиардами событий, Pulsar должен быть вашим выбором, особенно если вы хотите рассматривать события как источник правды и сохранять все события в долгосрочной перспективе!

Как показано на иллюстрации выше, Pulsar «действительно» горизонтально масштабируется для вычислений и хранения.

Когда вы добавляете новые узлы в кластер Pulsar, он немедленно распределяет темы и разделы по узлам, т. е. Pulsar разбивает ваши разделы на более мелкие фрагменты для репликации и распределения по кластеру.

Однако когда вы добавляете новые узлы в кластер Kafka, он не распределяет темы и разделы, то есть Kafka не разбивает ваш раздел на более мелкие фрагменты, как Pulsar. Kafka полностью сохраняет раздел на одном узле (и полностью реплицирует его на несколько других узлов).

Еще одной привлекательной особенностью Pulsar является Усталое хранилище, где вы можете перемещать старые события в более дешевое хранилище (холодное хранение), предоставляя клиентам беспрепятственный доступ к ним! Это может быть удобно в среде, основанной на событиях, чтобы не архивировать/удалять очень старые события.

Кроме того, Apache Pulsar предоставляет однократную семантику, если вы обнаружите в ней потребность, но в большинстве случаев она вам не понадобится, если вы правильно спроектируете свои системы.

Apache Pulsar также утверждает, что имеет лучшую производительность по сравнению с Kafka. Я бы посоветовал вам прочитать их подробное сравнение, если вам интересно.

Примечание. Единственным недостатком Pulsar является то, что сообщество не такое зрелое и широкое, как у Kafka, но я думаю, что это скоро изменится, когда больше людей поймут ценность Pulsar.

Могу ли я использовать eventstoredb в качестве хранилища событий?

Eventstoredb выглядит как база данных, созданная специально для поиска событий. Я не пробовал и никогда не чувствовал в этом необходимости.

Тем не менее, несколько вопросов, которые вы должны задать, прежде чем попробовать эту базу данных:

  • Это кластеризованная/реплицированная база данных?
  • Как он масштабируется (как для вычислений, так и для хранения) по горизонтали?
  • Поддерживает ли он усталое хранилище?
  • Могу ли я легко архивировать старые события?
  • Оптимизирован ли он для чтения или записи?
  • Могу ли я выбирать между согласованностью и доступностью?
  • Каковы эксплуатационные расходы?
  • Есть ли какие-либо накладные расходы на техническое обслуживание?

Я не смог найти официальный документ от eventstoredb для изучения деталей реализации. Я буду обновлять эту статью с подробностями моих выводов.

Как выполнить эволюцию схемы событий?

Это большая тема и требует отдельной статьи. Если вы используете библиотеку сериализации с обратной совместимостью, такую ​​как Apache Avro или google protobuf, для сериализации ваших событий, вы можете избежать большинства проблем с эволюцией схемы. Вот хороший пост в блоге об эволюции схемы от martin kleppman.

Нужна ли мне платформа для поиска событий?

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

Но если вы разрабатываете архитектуру для крупной организации, я настоятельно рекомендую вам потратить некоторое время на проектирование вашей системы на основе вашей проблемной области и максимально упростить ее.

Я также рекомендую вам изучить некоторые реализации с открытым исходным кодом, чтобы получить идеи и использовать их в качестве эталонной реализации!

Краткое содержание

  • Поиск событий — это просто архитектурный шаблон; это не серебряная пуля.
  • Если нет веских оснований для использования источника событий, лучше его не использовать.
  • Не пытайтесь реализовать источник событий догматически, будьте очень прагматичны, сохраняя свою архитектуру как можно более простой.
  • В большинстве случаев вам не нужен готовый фреймворк, вам нужна только «эталонная реализация».
  • Будьте готовы столкнуться с некоторыми непредвиденными проблемами на своем пути, но всегда находите прагматичное решение, которое необходимо только для текущей проблемы. Остерегайтесь случайных сложностей, закрадывающихся в вашу архитектуру!

Удачный поиск событий :-).