Как я исследовал стоимость банковского BFF GQL vs REST

2/7. Почему "обычный GraphQL" сначала проиграл REST почти в 3 раза

Продолжаю историю исследования банковского BFF — Backend for Frontend, серверного слоя под клиентское приложение.

Первый эксперимент был честным и неприятным.

Я взял GraphQL — язык запросов к данным — и провел через обычный runtime, то есть стандартную среду исполнения:

HTTP-запрос -> контроллер -> проверки -> parse -> validate -> graphql.execute -> resolver -> сборка ответа -> JSON.

И получил ожидаемый, но важный результат: GraphQL baseline — исходная неоптимизированная версия — проиграл REST примерно в 3 раза по RPS, то есть requests per second, запросам в секунду.

Почему?

Потому что GraphQL в обычном виде делает много полезной, но дорогой работы: - парсит запрос; - валидирует схему; - строит дерево полей; - запускает общий executor; - проходит resolver path; - создает дополнительные allocations, то есть выделения памяти; - тащит runtime overhead — накладные расходы среды исполнения.

Для публичного универсального GraphQL это нормально.

Но в банковском BFF у меня был другой сценарий: операции заранее известны. Фронтенд не должен присылать произвольные запросы.

Контракт должен пройти проверку до публикации.

Security review — проверка безопасности — должен понимать, какая поверхность API доступна.

Для mutation, то есть операции изменения данных, нужны CSRF — защита от подделки межсайтового запроса — и idempotency key, ключ идемпотентности, чтобы не получить double-spend, повторное списание.

Для subscription, то есть потоковой подписки, нужен SSE — Server-Sent Events, серверные события по HTTP, а не обязательно WebSocket, двустороннее постоянное соединение.

И вот ключевой вывод: проблема была не в GraphQL как инструменте.

Проблема была в том, что я использовал его как runtime для произвольного исполнения, хотя мой сценарий был контрактным.

Тогда архитектура перевернулась.

GraphQL стал не публичной ручкой, а языком описания клиентской потребности. А публичной поверхностью стал manifest allowlist — манифест разрешенных операций.

Каждая операция получила hash-route — маршрут с хешем: - query, операция чтения, идет через HTTP GET; - mutation, операция изменения данных, идет через HTTP POST; - subscription, потоковая подписка, идет через SSE; - неизвестный hash получает 403; - introspection, самоописание GraphQL-схемы, выключено.

То есть клиент больше не "спрашивает что угодно".

Он вызывает только опубликованную операцию из контракта.

И тогда появился главный инженерный вопрос: если операция уже известна, зачем каждый раз запускать полный GraphQL runtime?

Ответ на этот вопрос дал самый большой прирост.

В следующем посте расскажу, как hash-route и fast path превратили проигрыш в выигрыш.