Как я исследовал стоимость банковского 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 превратили проигрыш в выигрыш.