Транспорт бэкенда

GraphQL: гибкие запросы данных

В 2015 году Facebook открыл GraphQL после 3 лет использования внутри. К тому времени он уже обслуживал всё мобильное приложение с 1+ миллиардом пользователей. Проблема, которую он решал, знакома каждому: мобильный экран нуждается в данных из 5 разных REST-эндпоинтов - это 5 round-trip'ов, 5 JSON-ответов с лишними полями, и жёсткая зависимость от того что решил вернуть сервер.

  • **GitHub API v4** - полностью на GraphQL, заменил REST v3 для сложных запросов
  • **Shopify Storefront API** - GraphQL позволяет кастомизировать витрины без новых эндпоинтов
  • **Twitter API** - GraphQL для внутренних сервисов, где разные клиенты нуждаются в разных данных

Schema и система типов

В 2012 году Facebook переписывал мобильное приложение с HTML5 на нативное и столкнулся с проблемой: сотни REST-эндпоинтов, каждый возвращает фиксированную структуру. Мобильным экранам нужны разные данные - один экран берёт 3 поля из 5 разных эндпоинтов. Результат: лишние HTTP-запросы и over-fetching. Решение, которое они придумали - **GraphQL**: клиент сам описывает что именно ему нужно.

Основа GraphQL - **Schema Definition Language (SDL)**. Схема описывает все доступные типы и операции - это контракт API. В отличие от REST, где форма ответа определяется сервером, в GraphQL форма ответа определяется запросом клиента.

**! в типах:** `String!` - non-nullable (никогда не будет null). `[Post!]!` - массив non-nullable, и каждый элемент non-nullable. `[Post]` - может быть null, и элементы могут быть null. Клиент знает гарантии заранее.

Тип поля объявлен как `tags: [String!]`. Какие значения возможны?

Queries и Mutations

GraphQL знает три типа операций. **Query** - чтение, аналог GET. **Mutation** - изменение данных, аналог POST/PUT/DELETE. **Subscription** - подписка на события в реальном времени. Главная особенность: клиент точно указывает какие поля ему нужны - сервер возвращает ровно то, что запрошено.

**Batching mutations:** несколько mutations в одном запросе выполняются **последовательно** (в отличие от queries, которые могут выполняться параллельно). Это гарантирует порядок для операций с состоянием.

Клиент запрашивает поля `id, name` из типа User, у которого есть ещё 10 полей. Что вернёт GraphQL?

N+1 проблема и DataLoader

GraphQL создаёт коварную проблему производительности. Запрос 100 постов с авторами выглядит невинно, но внутри происходит следующее: 1 запрос для списка постов, затем 100 запросов - по одному для каждого автора. 101 запрос к БД вместо 2. Это **N+1 проблема** - она незаметна при разработке (маленькие данные) и убивает production.

**DataLoader и кеш:** DataLoader по умолчанию кеширует результаты в рамках одного запроса. `userLoader.load('u_5')` второй раз - возвращает из кеша без нового запроса к БД. Для mutation-запросов нужно явно очищать: `loader.clear('u_5')`.

DataLoader накапливает IDs и делает batch-запрос. Когда именно он отправляет batch?

Subscriptions: реальное время

Query и Mutation - это запрос-ответ. **Subscription** - это долгоживущее соединение: клиент подписывается на событие, сервер пушит данные каждый раз когда оно происходит. Технически subscriptions работают поверх WebSocket, хотя протокол остаётся GraphQL.

**Когда не нужны Subscription:** polling каждые 5-10 секунд проще и надёжнее для данных, которые меняются редко. Subscriptions нужны для реального реального времени - чат, collaborative editing, live-метрики. Каждый WebSocket держит ресурсы - 10 000 подписчиков = 10 000 открытых соединений.

GraphQL Subscription в production с несколькими инстанциями сервера. Почему нужен внешний PubSub (Redis)?

Apollo Federation: распределённая схема

Когда микросервисов становится много, единая GraphQL-схема превращается в монолит: все команды редактируют один файл, деплои блокируют друг друга. **Apollo Federation** решает это иначе: каждый сервис владеет своей частью схемы, Federation Gateway объединяет их в единый граф на лету.

**Rover CLI:** инструмент для работы с Federation. `rover subgraph publish` регистрирует обновлённую схему сервиса в Apollo Schema Registry - Gateway подхватывает изменение без рестарта. Схемы всех сервисов версионируются централизованно.

GraphQL всегда быстрее REST, потому что клиент получает только нужные поля

GraphQL решает over-fetching, но без DataLoader создаёт N+1 проблему. REST с правильными эндпоинтами может быть быстрее для конкретного use case

Гибкость GraphQL требует более сложной серверной архитектуры. Простой REST-эндпоинт с оптимизированным SQL-запросом выиграет у наивной GraphQL-реализации

В Federation клиент запрашивает User с его Posts. Сколько HTTP-запросов сделает Gateway к subgraph-сервисам?

GraphQL: ключевые идеи

  • SDL-схема - единый контракт API; клиент определяет форму ответа, не сервер
  • Query (чтение), Mutation (изменение), Subscription (real-time) - три типа операций
  • N+1 проблема - resolver для каждого элемента списка делает отдельный запрос к БД
  • DataLoader - батчинг и дедупликация запросов через event loop tick boundary
  • Federation - каждый микросервис владеет частью схемы, Gateway объединяет их

Связанные темы

GraphQL живёт в экосистеме синхронных протоколов и дополняет REST там, где нужна гибкость.

  • REST — Классический подход, с которым сравнивается GraphQL
  • WebSocket — Транспорт для GraphQL Subscriptions
  • Очереди сообщений — Альтернатива Subscriptions для event-driven архитектуры

Вопросы для размышления

  • В каких случаях GraphQL создаёт больше проблем чем решает по сравнению с REST?
  • Как DataLoader влияет на транзакционность при чтении - могут ли два .load() вызова в одном batch получить несогласованные данные?
  • Как Federation меняет процесс разработки API в команде из 5+ сервисов?

Связанные уроки

  • net-22-http-headers
GraphQL: гибкие запросы данных

0

1

Войти