Серверная или клиентская агрегация данных в API: какой компромисс определяет выбор?
Серверная агрегация уменьшает сложность клиентов и позволяет централизованно управлять тайм-аутами, кэшированием и правилами доступа, но создаёт дополнительный слой с веерными вызовами к зависимостям. Клиентская агрегация снижает связанность агрегирующего сервера и может лучше использовать локальный кэш, однако переносит сетевую сложность, обработку частичных отказов и согласование контрактов на каждый клиент.
Выбор определяется прежде всего тем, где должна находиться сложность композиции и кто способен надёжнее контролировать задержку, отказоустойчивость и безопасность.
В распределённых системах данные одного пользовательского экрана часто принадлежат нескольким сервисам. По мере появления мобильных, веб- и внешних клиентов возникла необходимость решить, должен ли каждый клиент самостоятельно обращаться к этим сервисам или получать готовое представление через специальный слой.
Так появились распространённые подходы API-композиции и BFF — Backend for Frontend. Они позволяют адаптировать набор данных под конкретный тип клиента, не заставляя его знать внутреннюю структуру сервисов.
Допустим, экран заказа требует сведения о самом заказе, профиле клиента, доставке и доступных скидках. При клиентской агрегации приложение выполняет несколько запросов, самостоятельно координирует их, решает, что делать при частичном отказе, и преобразует разные форматы ответов.
При серверной агрегации один запрос клиента запускает несколько внутренних вызовов. Это упрощает внешний контракт, но агрегатор становится критическим узлом: его задержка зависит от зависимостей, а ошибка в планировании веерных запросов может усилить нагрузку на всю систему.
Неверный выбор приводит либо к слишком умным и хрупким клиентам, либо к перегруженному агрегатору, который скрывает проблемы зависимостей и становится новым монолитом.
Серверную агрегацию выбирают, когда:
Клиентскую агрегацию выбирают, когда клиенты действительно имеют разные сценарии композиции, способны корректно выполнять параллельные запросы и могут независимо обновляться. Она также подходит, когда объединение данных не является стабильной бизнес-операцией, а представляет собой экранную логику конкретного приложения.
У серверного агрегатора должна быть собственная политика тайм-аутов и общий дедлайн запроса. Вызовы к зависимостям обычно выполняют параллельно, но ограничивают число одновременных операций, чтобы один входящий запрос не создавал неконтролируемый веер нагрузки.
Агрегатор должен явно разделять обязательные и необязательные данные. Отказ сервиса доставки может сделать ответ непригодным, если без него нельзя оформить заказ, но не обязательно должен ломать выдачу профиля. Это решение фиксируют в контракте, а не оставляют неявным поведением кода.
Нужно учитывать согласованность: данные от разных сервисов могут быть получены в разные моменты времени и не образовывать единую транзакционную версию. Если требуется строго согласованный снимок, простая агрегация запросов не решает проблему — понадобится специальная модель данных или согласованный источник представления.
Серверная агрегация увеличивает связанность агрегатора с внутренними API и может стать узким местом. Её смягчают кэшированием безопасных данных, ограничением размера ответа, защитой от каскадных отказов и независимым масштабированием агрегатора.
Клиентская агрегация уменьшает число посредников, но усложняет выпуск изменений: изменение внутреннего сервиса может потребовать обновления нескольких клиентов. Кроме того, нельзя полагаться на клиентскую проверку доступа — каждый сервис всё равно обязан авторизовать обращение к своим данным.
Мобильное приложение должно показать карточку заказа. Рассматривались два варианта: мобильный клиент сам вызывает четыре сервиса либо отдельный BFF возвращает подготовленную карточку.
Клиентская схема давала гибкость и позволяла переиспользовать ответы в локальном кэше, но увеличивала число сетевых запросов, расход батареи и объём логики в приложении. Обработка частичного отказа также различалась бы между версиями клиентов.
Серверная схема добавляла BFF и его веерные вызовы, зато позволяла задать единый дедлайн, параллельно запросить зависимости и вернуть согласованный для экрана формат. Был выбран BFF: данные доставки сделали обязательными для экрана оформления, а рекомендации — необязательными. Это упростило клиентов и позволило централизованно наблюдать задержки и ошибки каждой зависимости; при этом BFF не получил права напрямую изменять чужие данные.
Обычно агрегатор отвечает за композицию представления, а не за владение бизнес-правилами. Он может преобразовать формат, скрыть необязательное поле или объединить ответы, но правило вроде проверки доступности скидки должно принадлежать сервису, владеющему соответствующей бизнес-областью. Иначе агрегатор превращается в скрытый оркестратор с дублированными правилами и сложными зависимостями.
Нужно оценивать не только число внешних запросов, но и их произведение: один запрос к агрегатору может порождать несколько запросов к каждому сервису. Применяют общий дедлайн, лимиты параллелизма, кэширование, групповые методы чтения и отказ от необязательных данных при перегрузке. Если зависимость не выдерживает получившийся поток, проблема не решается только увеличением числа экземпляров агрегатора.
Нет, не по умолчанию. Последовательные или параллельные чтения из разных сервисов могут увидеть разные версии данных, поэтому ответ является лишь составным представлением на некоторый интервал времени. Если бизнесу нужен согласованный снимок, следует использовать материализованное представление, версионирование данных или другой явно спроектированный механизм согласования; сам факт нахождения вызовов внутри одного HTTP-запроса такой гарантии не создаёт.