Шаблоны для поддержания согласованности в распределенной системе источников событий?

12

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

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

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

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

Теперь мы вошли в противоречивое глобальное состояние, потому что нарушено бизнес-правило (есть два разных пользователя с одним и тем же именем пользователя).

В традиционной системе стилей СУБД N - сервера <-> 1 база данных используется в качестве центральной точки синхронизации, которая помогает предотвратить такие несоответствия.

Мой вопрос: как источники событий обычно подходят к этой проблеме? Они просто последовательно обрабатывают все команды (например, ограничивают количество процессов, которые могут записать в хранилище, до 1)?

Оливье Лалонд
источник
1
Контролируется ли такое ограничение кодом или это ограничение БД? N событий могут или не могут быть обработаны-обработаны в последовательности ... N событий могут проходить проверки в одно и то же время, не отсекая друг друга. Если порядок имеет значение, вам нужно будет синхронизировать проверку. Или использовать очередь для постановки в очередь событий, делайте dispatch'em последовательно
Laiv
@ Правильно. Для простоты я предположил, что базы данных не существует, все состояние хранится в памяти. Последовательная обработка определенных типов команд через очередь была бы вариантом, но кажется, что это может быть сложно решить, какие команды могут причинно влиять на другие, и я, вероятно, в конечном итоге поместил бы все команды в одну очередь, что равносильно наличию одних команд обработки процесса. Например, если у меня есть пользователь, добавляющий комментарий к сообщению в блоге, «удалить пользователя», «приостановить пользователя», «удалить сообщение в блоге», «отключить комментарии к сообщению в блоге» и т. Д. Должны все находиться в одной очереди.
Оливье Лалонд
1
Я согласен с вами, работать с очередями или семафорами не просто. Ни для работы с параллелизмом, ни с шаблонами событий-источников. Но в основном все решения заканчиваются системным трафиком событий. Однако это интересная парадигма. Существуют также внешние кэши, ориентированные на кортежи, такие как Redis, которые могут помочь в управлении этим трафиком между узлами, например, кэширование последнего состояния объекта или его обработка в данный момент. Общие кэши довольно распространены в подобных разработках. Это может показаться сложным, но не сдаваться ;-) это довольно интересно
Laiv

Ответы:

6

В традиционной системе стилей СУБД N - сервера <-> 1 база данных используется в качестве центральной точки синхронизации, которая помогает предотвратить такие несоответствия.

В системах с источником событий «хранилище событий» выполняет ту же роль. Для объекта, полученного из события, ваша запись - это добавление ваших новых событий к определенной версии потока событий. Таким образом, как и при параллельном программировании, вы можете получить блокировку этой истории при обработке команды. Для систем, основанных на событиях, чаще используется более оптимистичный подход - загрузить предыдущую историю, вычислить новую историю, а затем сравнить и заменить. Если какая-то другая команда также записала в этот поток, то сравнение и обмен не удаются. Оттуда вы либо повторно запустите свою команду, либо откажетесь от своей команды, или, возможно, даже включите свои результаты в историю.

Конфликт становится основной проблемой, если все N серверов с их командами M пытаются записать в один поток. Обычный ответ здесь состоит в том, чтобы выделить историю для каждого источника событий в вашей модели. Таким образом, пользователь (Боб) будет иметь отличную историю от пользователя (Алиса), и запись в один не будет блокировать записи в другой.

Мой вопрос: как источники событий обычно подходят к этой проблеме? Они просто обрабатывают каждую команду последовательно?

Грег Янг о проверке набора

Существует ли элегантный способ проверки уникальных ограничений на атрибуты объекта домена без перемещения бизнес-логики на уровень обслуживания?

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

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

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

VoiceOfUnreason
источник
1) Спасибо за предложения и рекомендации. Когда вы говорите «сравнить и поменять», вы имеете в виду, что процесс во время сохранения события обнаружит новые события, поступившие с момента начала обработки команды? Я предполагаю, что для этого потребуется хранилище событий, которое поддерживает семантику сравнения и обмена, верно? (например, «записывать это событие только и только если последнее событие имеет идентификатор X»)?
Оливье Лалонд
2) Мне также нравится идея принимать временные несоответствия и исправлять их в конечном итоге, но я не уверен, как бы я надежно кодировал для этого ... может быть, есть специальный процесс, который последовательно проверяет события и создает события отката при обнаружении что-то пошло не так? Благодарность!
Оливье Лалонд
(1) Я бы сказал «новая версия истории», а не «новые события», но у вас есть идея; замените историю, только если она та, которую мы ожидаем.
VoiceOfUnreason
(2) Да. Это немного логики, которая читает события из хранилища в пакетах, и в конце пакета передает отчет об исключении («у нас слишком много пользователей с именем Боб») или отправляет команды, чтобы компенсировать проблему (при условии, что правильный ответ вычислимо без вмешательства человека).
VoiceOfUnreason
2

Похоже, вы могли бы реализовать бизнес-процесс ( sagaв контексте Domain Driven Design) для регистрации пользователя, где с пользователем обращаются как с CRDT.

Ресурсы

  1. https://doc.akka.io/docs/akka/current/distributed-data.html http://archive.is/t0QIx

  2. "CRDT с распределенными данными Akka" https://www.slideshare.net/markusjura/crdts-with-akka-distributed-data, чтобы узнать о

    • CmRDTs - операционные CRDT
    • CvRDTs - CRTD на основе состояния
  3. Примеры кода в Scala https://github.com/akka/akka-samples/tree/master/akka-sample-distributed-data-scala . Может быть, «корзина» наиболее подходит.

  4. Экскурсия по кластеру Акка - Распределенные данные Акки https://manuel.bernhardt.io/2018/01/03/tour-akka-cluster-akka-distributed-data/
SemanticBeeng
источник