Ранас Мукминов – Оркестрация ИИ-агентов. Claude Opus 4.7 (страница 100)
Брокер (broker), или шина (bus) — посредник, принимающий публикации и доставляющий их подписчикам. Брокер — это и есть тот компонент, который материализует развязку: благодаря ему публикующий не обращается к подписчикам напрямую. Но он же — общая для всех инфраструктура, и его свойства определяют свойства всей координации: какие гарантии доставки он даёт, сохраняет ли порядок, буферизует ли события для отставших подписчиков, что делает при перегрузке. Брокер — это маршрутизатор данной модели коммуникации, перенесённый с адресной доставки на доставку по подписке, и, как всякий общий посредник, он — кандидат на роль единой точки отказа (см. главу 75).
Подписка (subscription) — заявленный получателем интерес к теме. Подписка определяет, что подписчик получает все события темы (или их отфильтрованное подмножество) и с какой семантикой: получает ли он только события, опубликованные после момента подписки, или также накопленные ранее; сохраняется ли его позиция в потоке между отключениями; что происходит с событиями, пришедшими, пока подписчик был недоступен. Эти свойства подписки — не деталь реализации брокера, а часть контракта, от которой зависит корректность подписчика.
1. Подписчик объявляет брокеру интерес к теме (subscribe).
2. Публикующий отправляет событие в тему (publish), не зная подписчиков.
3. Брокер принимает событие и определяет множество текущих подписчиков темы.
4. Брокер доставляет событие каждому подписчику (с той семантикой,
которую брокер гарантирует: порядок, повторы, потери).
5. Подписчик реагирует на событие независимо от других подписчиков
и независимо от того, как отреагируют они.
6. Подписчик может отписаться (unsubscribe), и тогда перестаёт получать
события темы; публикующего это не затрагивает.
Принципиальное отличие от адресного цикла главы 27 — в шагах 2 и 3. В адресной модели отправитель указывает получателя, и маршрутизатор переводит адрес в исполнителя. В pub/sub отправитель указывает тему, а множество получателей вычисляет брокер в момент доставки, исходя из текущих подписок. Поэтому одно и то же действие публикующего может достичь нуля, одного или многих подписчиков — в зависимости от того, кто подписан в этот момент, — и публикующий об этом не знает. Это и есть развязка по числу получателей.
Точнее всего pub/sub описывается через три независимые оси развязки, и полезно различать их, потому что архитектурные выгоды и риски привязаны к разным осям.
Развязка по пространству (адресная). Публикующий не знает идентичности и адресов подписчиков. Это даёт изменчивость состава: добавление и удаление подписчиков не затрагивает публикующих. Цена — потеря явной адресности: невозможно адресовать событие конкретному получателю, остаётся только опубликовать в тему и надеяться, что нужный подписан.
Развязка по времени. Публикующий и подписчик не обязаны быть активны одновременно. Если брокер буферизует события, подписчик может обработать их позже, когда снова станет доступен. Это та самая асинхронность из главы 24: событие переживает момент публикации и ждёт получателя. Цена — буфер, который где-то хранится и может переполниться, и неопределённость, насколько «позже» подписчик догонит поток.
Развязка по синхронизации (по потоку управления). Публикующий не блокируется в ожидании обработки события подписчиками; опубликовав, он продолжает работу. Подписчики реагируют в своём темпе. Это развязывает скорости источника и потребителей, но порождает обратное давление (backpressure): если источник публикует быстрее, чем подписчики успевают обрабатывать, очередь растёт, и систему надо явно защищать от этого роста.
Три оси ортогональны: можно иметь развязку по пространству без развязки по времени (брокер не буферизует — событие теряется, если подписчик не активен) и наоборот. Зрелая событийная модель сознательно выбирает положение по каждой оси, а не получает его случайно как побочный эффект выбора брокера.
Событийная модель — не «лучше» и не «хуже» адресной; это другой размен. Уместность определяется характером связи между источником информации и её потребителями.
Один источник, много потребителей, состав которых изменчив. Если о факте должны узнать несколько агентов, и их набор меняется со временем, адресная рассылка заставляет источник вести и обновлять список получателей — связанность, которая растёт с числом подписчиков. Pub/sub снимает её: источник публикует, потребители подписываются сами. Это каноническая ниша pub/sub.
Реакция, а не запрос-ответ. Pub/sub уместен, когда взаимодействие — это уведомление о факте с последующей независимой реакцией, а не запрос, требующий ответа конкретного исполнителя. Если отправителю нужен ответ именно от того, кому он отправил (запрос-ответ, см. главу 24), pub/sub плохо подходит: он развязывает отправителя от получателя, а запрос-ответ эту связь, наоборот, требует. Попытка построить запрос-ответ поверх двух тем («запросы» и «ответы») возможна, но воспроизводит адресность поверх развязки и обычно проигрывает прямому адресному вызову (см. главу 24 о корреляции запроса и ответа).
Слабая связанность важнее точной доставки. Pub/sub выгоден там, где ценность развязки состава перевешивает потребность точно знать, кто и когда обработал событие. Если же для корректности необходимо подтверждение обработки конкретным получателем, развязка pub/sub становится помехой: её приходится «чинить» дополнительными каналами подтверждения, и выгода теряется.
Сводно — когда pub/sub уместен и когда адресная коммуникация проще:
Признак взаимодействия | Pub/sub уместен | Адресная коммуникация лучше
Число получателей | много или заранее неизвестно | один или явно перечисленные
Состав получателей | изменчивый, расширяемый | стабильный, известный
Характер сообщения | уведомление о факте | команда или запрос конкретному
Нужен ли ответ источнику | нет, реакция независима | да, запрос-ответ
Что важнее | развязка состава | точная адресная доставка и подтверждение
Связь источник-потребитель | многие-ко-многим, неявная | один-к-одному, явная
Если большинство строк тяготеет к правой колонке, pub/sub добавит неявную связанность через тему и потерю адресности, ничего не дав взамен. Это вариант антипаттерна из главы 7: шину берут «ради развязки» там, где прямой адресный вызов был бы проще, надёжнее и наблюдаемее. Особенно опасна подмена запроса-ответа двусторонним pub/sub: она маскирует синхронную по смыслу связь под асинхронную развязку и оставляет систему без понятной точки, где ответ ожидается.
Подписка кажется простой — «получать события темы», — но именно её устройство определяет, сколько лишнего проходит через рой и насколько чувствительна система к изменению схемы событий.
Есть два принципиальных способа задать интерес подписчика, и они дают разные разменом.
Подписка на тему (topic-based). Подписчик объявляет интерес к именованному каналу и получает все его события. Гранулярность интереса равна гранулярности тем: чтобы подписаться на узкое подмножество, нужна отдельная тема. Это просто, предсказуемо и дёшево для брокера — ему достаточно знать множество подписчиков каждой темы. Цена — либо грубая фильтрация (подписчик получает лишнее и отсеивает сам), либо размножение тем под каждое подмножество интересов.
Подписка по содержанию (content-based). Подписчик задаёт предикат над содержимым события — «события завершения, где статус равен ошибке», — и брокер доставляет только то, что предикату удовлетворяет. Это точнее: подписчик не получает лишнего. Но это дороже для брокера, который теперь обязан вычислять предикаты на каждом событии, и опаснее по связанности: предикат подписчика зависит от внутренней структуры события глубже, чем имя темы, и потому сильнее ломается при изменении схемы.
На практике для роя агентов выбор смещён к подписке на тему с тщательно спроектированной гранулярностью тем — потому что для подписчика-на-модели обработка каждого лишнего события стоит вызова модели, а значит, реальных денег и латентности (об этом ниже). Грубая фильтрация, при которой агент получает нерелевантное событие и тратит вызов модели, чтобы понять, что оно нерелевантно, — это прямой и часто недооценённый расход. Поэтому гранулярность тем для агентов важнее, чем для дешёвых детерминированных подписчиков: тема должна быть достаточно узкой, чтобы подписчик не платил за разбор лишнего.
Темы часто организуют иерархически — task.completed, task.failed, artifact.ready — и допускают подписку по шаблону (task.), охватывающую группу тем. Это удобно, но создаёт скрытый риск: подписчик на task. автоматически начинает получать события новой темы task.escalated, добавленной позже, о которой он ничего не знает и к обработке которой не готов. Wildcard-подписка превращает добавление темы в неявное изменение поведения всех, кто подписан по шаблону. Это та же скрытая связанность через схему, что и в blackboard (см. главу 11): расширение набора тем — операция, затрагивающая существующих подписчиков, и относиться к ней нужно как к изменению контракта (см. главу 22 о версионировании и совместимости).
Подписчик зависит не от публикующего, а от схемы событий темы. Эта зависимость — настоящий контракт между ними, хотя ни одна сторона не знает другую поимённо. Схема события (см. главу 25) здесь — то же, чем была схема доски в blackboard: публичный интерфейс, на который завязаны многие, и менять его нужно строже, чем адресный контракт двух агентов, потому что число и идентичность зависящих неизвестны. Добавление необязательного поля обычно безопасно; переименование, удаление или смена смысла поля потенциально ломает каждого подписчика темы, и кто именно сломается, отправитель не видит — он не знает подписчиков. Поэтому эволюция схемы событий в pub/sub требует тех же приёмов, что эволюция любого публичного контракта: версионирование, период совместимости, недопустимость молчаливого изменения смысла поля.