Как стать автором
Обновить

Интеграция: синхронное, асинхронное и реактивное взаимодействие, консистентность и транзакции

Время на прочтение15 мин
Количество просмотров85K
Всего голосов 18: ↑17 и ↓1+16
Комментарии11

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

Разве деление на «асинхронное» и «реактивное» взаимодействие по терминологии общепринято? Если да, то можно ссылку на первоисточники?
Что значит «общепринятое?» Это — разные способы взаимодействия, и в статье это описано. Исторически сначала было разделение на синхронное и асинхронное, оно есть во многих учебниках, потому что синхронное — это удаленный вызов, а асинхронное — это посылка сообщения, которое когда-нибудь обработается. И если у вас канал обращения однонаправленный, как обычно при клиент-серверном взаимодействии, и при межсерверном — тоже, то никакой ответ или callback невозможен, надо опрашивать. Потом появилась возможность вызова callback при межсерверных взаимодействиях, и появился шаблон реактивного взаимодействия. При этом писали в коде это вручную, и это было тяжело. Появился реактивный манифест, на который я ссылаюсь в статье. И он — именно про реактивное взаимодействие, а не про любую асинхронную обработку сообщений. И уже потом появились промисы, и async/await, которые позволяют оборачивать асинхронное взаимодействие удобно, при этом разработчик часто не понимает, что там внутри происходит. Async/await и их аналоги в принципе могут скрывать оба способа взаимодействия, в зависимости от возможностей канала или драйвера реализации протокола, который под ними лежит.

И возвращаясь к вопросы про «общепринятое». Если это означает «вошедшее в учебники», то для IT оно — не слишком интересно, потому что учебники часто фиксируют замшелую историю. Технологии IT развиваются гораздо быстрее, чем пишут учебники. И многие новые конструкции — не осмыслены теоретиками и учебников нет. Включая мультипарадигмальные языки — в C# это есть с 2012, а теории — нет. Реактивное программирование еще моложе.
Насчет асинхронных распределенных транзакций, у меня меня нет надежного решения для сервисной архитектуры.
Что если использовать привязку к родителю и callback для обновления статуса?
  1. инициатор открывает транзакцию, вызывает нужные 'дочерние' сервисы (Srvc1,2,n) и передает в них parent id
  2. дочерний привязывается к parent transaciton id, чтоб использовать в случае повторного вызова с этим-же parentID или запросом на откат
  3. при вызове 'дочернего' сервиса, дочерний сразу возвращает родителю свой child transaction id
  4. по завершению транзакции в дочернем сервисе, в родителький, через callback endpoint, передается подтверждение об успешном окончании транзакции

Диаграмма
image
Так и не понял проблемы(в рамках схемы). Вроде как автор почти прямо и говорит(или прямо в ранних статьях), что нужно как-то идентифицировать действие(что по схеме является транзакцией от инициатора, если я правильно понял). Если делать как говорит автор, то нужно добавить обработку отказов в автоматическом и ручном виде. И, вероятно, должен быть ещё один вызов к инициатору после 4 пункта, опциональный(к примеру, покупка товара произошла, а доставка не справилась).

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

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

Сдается мне вы только что описали хореографическую сагу…
На мой взгляд, хватает единственного ключа, который должен поставлять инициатор, это шаблон идемпотентных операций. Но дальше надо действительно аккуратно разбираться с обработкой ошибок и с тем, а зачем, собственно. нужны подтверждения и в какой момент они должны приходить. Например, почему подтверждения инициатору — когда сервис-1 обработал, а не когда обработал сервис-2 (а также 3 и 4 в более сложных случаях).

А с обработкой ошибок ситуации:
1. Что делает Сервис-1, если Сервис-2 не смог обработать (в разных вариантах)
2. Что делает Сервис-1, если он не дождался ответа от Сервис-2, при этом могло быть утеряно прямое сообщение или ответ.
3. Что делает инициатор во всех этих случаях.

Но вообще можно взять какие-нибудь схемы распределенных транзакций Oracle на техническом уровне и в них разобраться. Там же внизу — асинхронный обмен сообщениями.
Вся суть использования асинхронности и задач(Task в .Net) — каждая задача внутри себя(или во время своего выполнения) должна быть самодостаточна. Придерживание этого правила решает большую часть описанных проблем, даже с legacy. В сложных случаях достаточно добавить свои обработчики ожидания, которые могут работать хоть через опрос, хоть через обратный вызов, с очередями или ещё с чем, в зависимости от задач и условий работы.

Вообще, это странное для меня определение асинхронного. По мне так, асинхронное такое же, как синхронное, без очередей, т.е. это как раз реактивное(в статье). А указанное здесь асинхронное — одна из реализаций асинхронной очереди задач. Вероятно сказывается, что в C# стандартная сейчас асинхронность снаружи выглядит как реактивное(в статье), а изнутри может быть выполнено различными способами, в т.ч. и как асинхронное(в статье).
На мой взгляд, асинхронное без очередей не бывает. Потому что если мы посылаем сообщение, его кто-то должен поймать, а этот кто-то может быть занят в моменте — мы можем посылать быстрее. Значит должно быть место, где необработанные сообщения накапливаются — очередь. Считать, что обработчиков всегда больше, чем потенциальных отправителей и кто-то поймает — оптимизм.

Самодостаточность — это хорошо, но сложно. Собственно, большие монолиты и вырастают из того, что в систему затягивают все больше функционала, потому что он связан с другим. Типичный пример — работа с остатками на складах или денежными. Они могут быть нужны большому количеству сервисов обработки документов при чем самых разных (разных типов заказов, сделок и так далее), и обработка документов не может быть завершена, пока он не изменит остатки (не будет зарезервирован товар для заказа или отгрузка товара, не будет переведена оплата или заблокированы деньги под сделку и так далее)…
Я согласен, что очередь так или иначе будет реализована. Но это не имеет значение на уровне реализации логики. Т.к. само управление очередью часто на порядки быстрее, по сравнению со временем задачи сервиса. А вот когда управление этой очередью будет очень сложным и затратным по времени выполнения, тогда уж стоит беспокоится. Но пока я такого даже близко не встречал.
Дело не в сложности/скорости управления очередью. А в том, что сообщение некоторое время в этой очереди лежит, пока не будет выбрано. И если мы оцениваем разные сценарии обработки сообщений, то надо оценивать, в том числе, скорость выбора сообщений из очереди против скорости поступления туда новых сообщений, а также задержки при обработке на ожидания от других сервисов. И размеры накапливаемых очередей. Потому что если сервис A обращается к сервису Б и ожидает ответ — то время обработки им сообщения включает время работы сервиса Б. Если он не ожидает ответа, а выбирает из очереди следующее сообщение, то скорость повышается. Но дальше там вопрос — насколько, он актуален, если после ответа Б надо еще некоторый набор действий сделать. В том числе, интересны вопросы с порядком обработки, например, если ответы пришли не в том порядке, что исходные сообщения — это может иметь какие-то эффекты. А также вопросы устойчивости при падении отдельных экземпляров сервисов — где лежат очереди, сохранятся ли они при падении, не останется ли в них сообщений, которых некому выбирать (например, если устроено так, что ответ обрабатывает именно тот тред, который посылал запрос) и так далее…

С тем же размером очереди вопрос не праздный, например, в одной из наших систем шел относительно равномерный и небольшой поток документов, в пределах 100-200 в минуту, но были выделенные моменты, когда в очередь за пару минут сваливалось 20к документов. Разбор такой очереди занимал примерно полчаса, это в принципе устраивало по бизнесу, но было две проблемы (а) основной поток желательно было разбирать с более высоким приоритетом, а не после разбора этой плюхи, (б) система управления очередью в установленной конфигурации не слишком уверенно работала с такими размерами очереди, а существенно ее усиливать ради получаса в сутки полагали недешевым удовольствием. В общем, пришлось несколько повозиться с решением. И еще мониторить внутренние очереди, потому что такой пик — он же и дальше распространялся…
Зарегистрируйтесь на Хабре, чтобы оставить комментарий