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

#microservices #eventual-consistency

#микросервисы #конечная согласованность

Вопрос:

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

В двух словах: если клиент исторически выполняет последующие синхронные вызовы REST для вашей системы, что вы делаете, когда последующие вызовы могут возвращать неожиданные результаты после выполнения вызовов для разных микросервисов (из-за возможной согласованности)?

Проблема

Предположим, у вас есть монолитное приложение, которое предоставляет REST API. Допустим, есть два модуля A и B, которые вы хотите преобразовать в микросервисы. Объекты, которые поддерживает B, могут ссылаться на объекты, которые поддерживает A (например, A поддерживает студентов, а B поддерживает классы). В монолитной ситуации модули просто ссылаются на одну и ту же базу данных, но в ситуации с микросервисами каждый из них имеет свою собственную базу данных и обменивается данными с помощью асинхронных сообщений. Таким образом, их базы данных в конечном итоге согласованы друг с другом.

Некоторые существующие сторонние клиентские приложения нашего API используются для первого (синхронного) вызова конечной точки, принадлежащей модулю A, и после того, как этот первый вызов возвращается, немедленно (т. Е. Через несколько мс) вызывается конечная точка в модуле B как часть их рабочего процесса (например, создание учащегося и ввод его в класс а). В новой ситуации это приводит к проблеме: когда происходит второй вызов, модуль B может еще не знать об изменениях в модуле A. Таким образом, существующий рабочий процесс клиентского приложения может нарушиться. (Например, модуль B может ответить: ученик, которого вы пытаетесь поместить в класс, не существует, или он учится не в том году.)

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

Простая диаграмма, описывающая ситуацию

Вопрос

Существует ли наилучшая практика или общепринятый набор опций для смягчения этой проблемы? (Я составил пример ученика / класса, не зацикливайтесь на его особенностях. :))

Что мы можем придумать

  • Просто сообщаю разработчикам этих клиентов: с этого момента вы должны реализовать механизм повторных попыток для каждой конечной точки, которую вы вызываете. Недостаток кажется очевидным.
  • Внедрите шлюз API, который ожидает, пока B не будет готов. Недостаток: существует множество возможных рабочих процессов (с участием большего количества модулей от А доЯ), которые потребовали бы этого, поэтому шлюз может стать довольно сложным.
  • Каким-то образом создайте «сеанс» для клиента, который отслеживает, какие запросы он выполнял последовательно. Тогда B может выяснить, следует ли ему ждать сообщения от A, или он может даже обновить свое состояние, просто просмотрев точный запрос, который клиент отправил A.

Существуют ли лучшие методы? Что было бы наиболее подходящим?

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

Ответ №1:

Существует ли наилучшая практика или общепринятый набор опций для смягчения этой проблемы?

ДА. Вы не можете разбить каждый метод на отдельный микросервис с собственным репозиторием.

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

Комментарии:

1. Возможно, тогда я могу сформулировать суть своего вопроса в следующих терминах: что, если у меня нет варианта использования для вызова B сразу после A, но я обнаруживаю (после разделения A и B), что какой-то другой потребитель моего API, по-видимому , имеет такой вариант использования? В более общем плане, если я разделю свою монолитную систему на модули в соответствии с моими собственными вариантами использования, как мне иметь дело с другими потребителями, которые «случайным образом» (как мне кажется) вызывают разные части моей системы и ожидают, что это будет продолжать работать? Является ли ответ «вы не можете», или для этого есть «обходной путь наилучшей практики»?

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

3. Я еще немного уточню «незнакомцев», на случай, если это изменит ответ. 🙂 Наше приложение в основном состоит из серверной части с общедоступным API и нескольких интерфейсных приложений. Многие клиенты взаимодействуют с нашей системой исключительно через один из наших интерфейсов, но любой может напрямую вызвать наш API, чтобы интегрировать нашу систему в свою собственную. Это также большая часть клиентской базы, и именно это вызывает проблему. Эти интеграции не были созданы с учетом возможной согласованности. Мы хотели бы разделить наши микросервисы, сведя к минимуму необходимость перезаписи на стороне клиента.

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

Ответ №2:

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

В этой замечательной статье о миграции monolith на микросервисы Жамак Дехгани также рассматривает проблему несоответствия данных:

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

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

Кроме того, в статье освещается действительно проницательный способ увидеть несоответствие данных в отношении бизнес-процессов:

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

Вот как я вижу эту проблему:

  • Это правда, что хранилища между микросервисами A и B обновляются асинхронно, но какова точная задержка этого рабочего процесса обновления? Если мы говорим о 1-2 секундах, то несоответствие может быть воспринято пользователями вообще. В противном случае система должна быть масштабирована для поддержки этого (или даже более низкого) порога задержки.
  • Вы можете отслеживать события несоответствия — когда пользователь пытается извлечь данные, которых нет в хранилище, поскольку они находятся в процессе обновления, и масштабировать свою систему на основе этого.
  • Суть в том, что это может помочь определить необходимость такой гарантии согласованности, а затем применить подходящее обходное решение.

Комментарии:

1. Спасибо за ваше объяснение и ссылку на статью! Читая ваши пункты, я понимаю, что не смог подчеркнуть один важный аспект моего вопроса: я не думал о человеческих клиентах нашего приложения. Вы совершенно правы, что они, вероятно, не будут воспринимать задержку в 1-2 секунды. Мой вопрос касается программных клиентов, которые запускают запрос к модулю A и B в быстрой последовательности (так быстро, как позволяет их программирование и информационная структура). Я соответствующим образом отредактирую вопрос.

2. Спасибо за разъяснения. В этом случае, я считаю, что с этой проблемой проще справиться, создав потоки повторных попыток (компенсирующие операции) или, в крайнем случае, используя одинаковое хранилище между службами A и B

3. Просто хотел отметить, что, хотя статья размещена на martinfowler.com , на самом деле это было написано Жамаком Дехгани.

4. Спасибо за эту заметку, я обновил ответ