На диаграмме интеграция почти всегда выглядит просто: стрелка от одного блока к другому. Но именно стрелки чаще всего и врут. За одной линией «A вызывает B» может скрываться синхронный запрос с таймаутом, очередь с гарантией доставки, поток событий или команда, которую нельзя потерять. И от того, что именно стоит за стрелкой, зависит, переживёт ли система реальную жизнь.
В прошлой статье мы научились показывать систему на разных уровнях C4. На container-уровне между блоками появляются связи — и здесь начинается следующая большая тема: чем эти связи являются на самом деле. Открываем вторую арку серии — про реализацию и эксплуатацию — и начинаем с взаимодействий, потому что именно тут «простая» архитектура чаще всего и ломается.
В статье
- Почему стрелка на схеме — самая опасная деталь
- Паспорт стрелки: контракт взаимодействия
- Синхронное против асинхронного: разные обещания
- REST, события, команды, очереди — что это на самом деле
- Как аналитик и архитектор смотрят на интеграцию по-разному
- Лунная база: сценарий потери связи
- Где простота превращается в дорогую сложность
- Короткий checklist по взаимодействиям
- Что дальше
Почему стрелка на схеме — самая опасная деталь
Стрелка кажется самой невинной частью диаграммы. Но за ней прячется набор решений, каждое из которых влияет на надёжность:
- ждёт ли вызывающая сторона ответа — или продолжает работу;
- что происходит, если получатель недоступен;
- можно ли потерять это сообщение — или нельзя ни при каких условиях;
- допустим ли повтор (идемпотентность) — или повтор опасен;
- сколько ждать, прежде чем считать вызов неудачным.
Пока эти вопросы не заданы, «интеграция нарисована», но не спроектирована. Хуже того, разные роли читают одну и ту же стрелку по-разному: аналитик видит «данные передаются», а инженер эксплуатации — «ещё одна точка отказа в три часа ночи».
Паспорт стрелки: контракт взаимодействия
Практический приём — завести для каждой значимой стрелки паспорт взаимодействия (контракт). Это не бюрократия, а ровно тот набор полей, который превращает линию на схеме в спроектированную интеграцию:
- Тип (семантика) — запрос данных, команда или событие;
- Синхронность — 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
Одна и та же база, один момент времени — но четыре взаимодействия с четырьмя разными контрактами. Если бы все они были спроектированы как обычные синхронные вызовы, потеря связи обрушила бы и жизнеобеспечение, и телеметрию.
Где простота превращается в дорогую сложность
Самые дорогие сюрпризы появляются там, где async-взаимодействие выбрали, но не достроили. Типичные ловушки:
- Потерянные команды. Async без гарантии доставки и идемпотентности теряет приказы — а в критичной системе это отказ.
- Дубликаты. Очередь доставляет «хотя бы один раз», значит обработчик обязан быть идемпотентным, иначе одна команда исполнится дважды.
- Порядок. События пришли не в том порядке — и состояние разъехалось. Нужно либо упорядочивание, либо модель, устойчивая к перестановке.
- Невидимый backpressure. Очередь телеметрии переполнилась во время аварии — именно тогда, когда данные нужнее всего. Приоритизация и сброс некритичного должны быть спроектированы заранее.
- Скрытая синхронность. Цепочка async-вызовов, где каждый ждёт следующего, по сути синхронна — со всеми её рисками, но без её простоты.
Вывод: async не «бесплатно надёжнее». Он переносит сложность из «момента отказа» в «модель взаимодействия». Эту сложность нужно осознанно спроектировать, а не получить случайно.
Короткий checklist по взаимодействиям
Перед тем как принять стрелку на схеме за готовую интеграцию, проверьте:
- Это запрос данных, команда (намерение) или событие (факт)?
- Синхронно или асинхронно — и почему именно так?
- Что произойдёт, если получатель недоступен?
- Можно ли потерять это сообщение? Если нет — какая гарантия доставки?
- Идемпотентен ли обработчик при повторе?
- Важен ли порядок сообщений?
- Что с backpressure при пике или аварии?
- Какой таймаут и что считается отказом?
- Есть ли correlation / trace id для сквозной диагностики?
- Как инициатор узнаёт, что сообщение выполнено или отклонено?
- Есть ли TTL / срок актуальности у сообщения?
- Кто владеет контрактом и как он версионируется?
- Где хранится состояние выполнения — у инициатора, у получателя или в отдельном процессе?
Если на эти вопросы нет ответа, интеграция нарисована, но не спроектирована.
Что дальше
Взаимодействия неизбежно поднимают следующий вопрос: что значит «данные согласованы», когда части системы общаются асинхронно и с задержкой. Следующая статья серии — согласованность данных: где нужна строгая, а где достаточно «в итоге» — про то, как выбирать уровень консистентности под реальные сценарии, а не по привычке.
Комментарии