Взаимодействия и интеграции: где ломается кажущаяся простота

Статья архитектурно-аналитической серии на примере лунной базы: как аналитика и архитектура по-разному смотрят на синхронные и асинхронные взаимодействия, REST, события, команды и очереди — и где простая на схеме интеграция оказывается дорогой

На диаграмме интеграция почти всегда выглядит просто: стрелка от одного блока к другому. Но именно стрелки чаще всего и врут. За одной линией «A вызывает B» может скрываться синхронный запрос с таймаутом, очередь с гарантией доставки, поток событий или команда, которую нельзя потерять. И от того, что именно стоит за стрелкой, зависит, переживёт ли система реальную жизнь.

В прошлой статье мы научились показывать систему на разных уровнях C4. На container-уровне между блоками появляются связи — и здесь начинается следующая большая тема: чем эти связи являются на самом деле. Открываем вторую арку серии — про реализацию и эксплуатацию — и начинаем с взаимодействий, потому что именно тут «простая» архитектура чаще всего и ломается.

Разные связи между модулями базы и Землёй: синхронные, асинхронные, с задержкой

В статье

Почему стрелка на схеме — самая опасная деталь

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

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

Пока эти вопросы не заданы, «интеграция нарисована», но не спроектирована. Хуже того, разные роли читают одну и ту же стрелку по-разному: аналитик видит «данные передаются», а инженер эксплуатации — «ещё одна точка отказа в три часа ночи».

Паспорт стрелки: контракт взаимодействия

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

  • Тип (семантика) — запрос данных, команда или событие;
  • Синхронность — sync или async;
  • SLA / timeout — сколько ждём ответа, что считаем отказом;
  • Retry — повторяем ли, сколько раз, с каким backoff;
  • Idempotency — безопасен ли повтор;
  • Ordering — важен ли порядок сообщений;
  • Delivery guarantee — at-most-once / at-least-once / exactly-once. Важно: exactly-once — это обычно свойство всей схемы обработки (at-least-once + идемпотентность/дедупликация), а не магическая гарантия транспорта;
  • TTL / срок актуальности — когда сообщение перестаёт быть валидным;
  • Owner и версионирование — кто владеет контрактом и как его менять без поломки потребителей;
  • Failure mode — что происходит при недоступности получателя.

Дальше в статье мы по сути расшифровываем поля этого паспорта.

Синхронное против асинхронного: разные обещания

Разница не в технологии, а в том, какое обещание даёт взаимодействие.

  • Синхронное — «я жду ответа сейчас». Просто рассуждать, но вызывающая сторона завязана на доступность и скорость получателя. Если получатель недоступен — операция падает здесь и сейчас.
  • Асинхронное — «я отправил, ответ придёт позже (или не придёт сразу)». Устойчивее к недоступности, но добавляет сложность: очереди, порядок, дубликаты, отслеживание состояния.

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

REST, события, команды, очереди — что это на самом деле

За модными словами стоят разные модели взаимодействия. Полезно держать их различия явно:

Сначала важно не путать механизм взаимодействия и смысл. REST, gRPC, очередь сообщений — это как передаём: механизмы взаимодействия, причём разных уровней абстракции (REST и gRPC — протоколы и стили API поверх сети; очередь — модель обмена через посредника-брокер). Объединять их в один «транспорт» неточно. Запрос данных, команда, событие — это что передаём по смыслу (семантика). Один и тот же REST-вызов может нести и запрос данных, и команду — поэтому смотреть нужно на семантику, а не на технологию.

Тип (семантика) Обещание Когда уместен Риск
Запрос данных (обычно sync) «дай мне это сейчас» стабильный канал, нужен немедленный ответ падает при недоступности получателя
Команда (sync или async) «сделай это» (намерение) действие нельзя потерять; часто async, если выполнимо позже идемпотентность, отслеживание, срок актуальности
Событие (обычно async) «вот что произошло» (факт) уведомить многих, слабая связанность порядок и дубликаты, eventual consistency
Очередь / буфер (механизм доставки) «доставлю, когда смогу» пики нагрузки, ненадёжный канал рост задержки, переполнение, backpressure

Ключевое различие, которое аналитик обязан удерживать: команда — это намерение, событие — это факт. «Отключить нагрузку» (команда) и «нагрузка отключена» (событие) — разные вещи с разной семантикой, разными гарантиями и разными последствиями при потере. Синхронность здесь — отдельная ось: команда бывает и синхронной, и асинхронной, важнее её смысл «сделай это».

Как аналитик и архитектор смотрят на интеграцию по-разному

Обе роли смотрят на одну стрелку, но видят разное — и именно стык их взглядов делает интеграцию рабочей:

  • Аналитик спрашивает: что это за взаимодействие по смыслу? Это запрос данных, приказ к действию или уведомление? Что критично потерять, а что нет? Кто инициатор и кто отвечает за результат?
  • Архитектор спрашивает: какие гарантии доставки нужны? Где таймауты, ретраи, идемпотентность? Что происходит при недоступности? Где backpressure?

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

Лунная база: сценарий потери связи

Вернёмся к сквозному сценарию серии: связь с Землёй пропала на 40 минут, а энергобюджет просел. Посмотрим, как разные взаимодействия ведут себя в этот момент.

  • Критичная операция жизнеобеспечения — должна исполняться локально, синхронно, без выхода за пределы базы. Любая зависимость от Земли здесь недопустима.
  • Команда от наземного центра — приходит как намерение и должна буферизоваться, но важно, где именно. Пока связь разорвана, команда, созданная на Земле, физически не может попасть в очередь на базе — она копится в наземном outbox / relay и уходит на базу только после восстановления канала. Очередь на самой базе имеет смысл лишь для команды, которая была доставлена до обрыва, но отложена к исполнению. В обоих случаях у команды нужен срок актуальности (TTL / valid-until) и сверка с текущим состоянием: приказ «снизить нагрузку», отданный 40 минут назад, может быть уже вреден.
  • Телеметрия на Землю — поток событий, который копится в очереди и догоняет наземный контур после восстановления связи; критичные сигналы нельзя терять без явной политики деградации — буфер с приоритетами и осознанный сброс некритичного при переполнении.
  • Аналитический запрос с Земли — может просто не выполниться: это не критично, повтор позже.
flowchart TD crew["Экипаж"] -->|sync, локально| life["Жизнеобеспечение"] ground["Наземный центр"] -.->|команда во время обрыва| gout["Наземный outbox / relay"] gout -.->|после восстановления связи| dec["Контур локальных решений базы"] life -->|события: буфер с приоритетом| tbuf["Очередь телеметрии"] tbuf -.->|догоняет после связи| ground ground -.->|аналитический запрос: можно потерять| dec

flowchart TD
  crew["Экипаж"] -->|sync, локально| life["Жизнеобеспечение"]
  ground["Наземный центр"] -.->|команда во время обрыва| gout["Наземный outbox / relay"]
  gout -.->|после восстановления связи| dec["Контур локальных решений базы"]
  life -->|события: буфер с приоритетом| tbuf["Очередь телеметрии"]
  tbuf -.->|догоняет после связи| ground
  ground -.->|аналитический запрос: можно потерять| dec
Взаимодействия базы при потере связи: что синхронно, что буферизуется, что отбрасывается

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

Где простота превращается в дорогую сложность

Самые дорогие сюрпризы появляются там, где async-взаимодействие выбрали, но не достроили. Типичные ловушки:

  • Потерянные команды. Причина — отсутствие гарантии доставки (at-least-once, ретраи, подтверждения): без неё приказ может просто исчезнуть, а в критичной системе потерянная команда это отказ.
  • Опасные повторы. Другая, отдельная причина — отсутствие идемпотентности. Очередь доставляет «хотя бы один раз», поэтому один и тот же приказ может прийти дважды; если обработчик не идемпотентен, команда исполнится повторно — что в критичной системе не менее опасно, чем потеря.
  • Порядок. События пришли не в том порядке — и состояние разъехалось. Нужно либо упорядочивание, либо модель, устойчивая к перестановке.
  • Невидимый backpressure. Очередь телеметрии переполнилась во время аварии — именно тогда, когда данные нужнее всего. Приоритизация и сброс некритичного должны быть спроектированы заранее.
  • Скрытая синхронность. Цепочка async-вызовов, где каждый ждёт следующего, по сути синхронна — со всеми её рисками, но без её простоты.

Вывод: async не «бесплатно надёжнее». Он переносит сложность из «момента отказа» в «модель взаимодействия». Эту сложность нужно осознанно спроектировать, а не получить случайно.

Короткий checklist по взаимодействиям

Перед тем как принять стрелку на схеме за готовую интеграцию, проверьте:

  1. Это запрос данных, команда (намерение) или событие (факт)?
  2. Синхронно или асинхронно — и почему именно так?
  3. Что произойдёт, если получатель недоступен?
  4. Можно ли потерять это сообщение? Если нет — какая гарантия доставки?
  5. Идемпотентен ли обработчик при повторе?
  6. Важен ли порядок сообщений?
  7. Что с backpressure при пике или аварии?
  8. Какой таймаут и что считается отказом?
  9. Есть ли correlation / trace id для сквозной диагностики?
  10. Как инициатор узнаёт, что сообщение выполнено или отклонено?
  11. Есть ли TTL / срок актуальности у сообщения?
  12. Кто владеет контрактом и как он версионируется?
  13. Где хранится состояние выполнения — у инициатора, у получателя или в отдельном процессе?

Если на эти вопросы нет ответа, интеграция нарисована, но не спроектирована.

Что дальше

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

См. также

Обсуждение в Telegram

Присоединиться →

Комментарии