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

Статья архитектурно-аналитической серии на примере лунной базы: как аналитика и архитектура по-разному смотрят на синхронные и асинхронные взаимодействия, 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-вызов может нести и запрос данных, и команду — поэтому смотреть нужно на семантику, а не на технологию.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Что дальше

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

См. также

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

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

Комментарии