gRPC vs REST vs GraphQL: что выбрать аналитику при проектировании
gRPC vs REST — это не холивар из курилки, где побеждает самый громкий разработчик. Это выбор инструмента под задачу. Представьте: вы проектируете интеграцию, и вам нужно описать, как два сервиса будут общаться. Вы пишете ТЗ, согласовываете с командой — а через неделю выясняется, что REST, который вы заложили, не тянет стриминг, а JSON съедает 40% времени на сериализацию. Переделывать контракты через месяц после старта разработки — удовольствие ниже среднего.
В этом посте мы разберём, когда брать REST, когда gRPC, а когда GraphQL — и, что важнее, как аналитику описать выбор в документации так, чтобы архитектор не переделывал его на первой же встрече. Если вы пока плаваете в основных принципах REST API — сначала туда, потом возвращайтесь.
Что скрывается за аббревиатурами
Три протокола — три философии. Они решают одну задачу (передать данные между клиентом и сервером), но подходят к ней с совершенно разных сторон.
REST (Representational State Transfer)
Архитектурный стиль, построенный вокруг ресурсов. Каждый ресурс — это URL, с которым работают через HTTP-методы: GET /orders/42, POST /orders, DELETE /orders/42. Тело ответа — почти всегда JSON. Нет строгого контракта: сервер может добавить поле в ответ, клиент его проигнорирует. И сервер может убрать поле — клиент упадёт (но это уже другая история).
REST — это не протокол, а набор соглашений. Нет единого стандарта, нет компилятора, который проверит контракт. Что хорошо для гибкости — плохо для предсказуемости.
Аналогия: ресторан с меню. Каждое блюдо (ресурс) — отдельная строка. Вы говорите официанту «принеси борщ» (GET), «запиши в счёт компот» (POST), «убери салат» (DELETE). Официант понимает вас, потому что вы оба знаете правила ресторана. Но если шеф-повар сегодня заменил борщ на рассольник, не предупредив официанта — вы получите не то, что заказывали.
gRPC (gRPC Remote Procedure Call)
Высокопроизводительный RPC-фреймворк от Google. Работает поверх HTTP/2, данные сериализуются в бинарный формат Protobuf (Protocol Buffers). Главное отличие от REST: строгий контракт. Вы пишете .proto-файл, описываете сервисы и сообщения, компилятор генерирует клиентский и серверный код на всех поддерживаемых языках. Компилятор не даст клиенту вызвать метод с неправильной сигнатурой.
Protobuf — бинарный формат сериализации. Меньше JSON в 3–10 раз по размеру, быстрее на сериализацию/десериализацию. Жёсткая типизация:
int32,string,repeated,oneof— компилятор проверяет всё.
Аналогия: курьерская служба с унифицированными накладными. Вы заполняете бланк строго по полям: отправитель, получатель, вес, габариты. На складе не нужно «догадываться», что написано в графе «особые отметки» — все поля формализованы. Это быстрее, чем писать от руки, но если cargo-форма не предусматривает поле «хрупкое» — вы его не добавите без перевыпуска формы.
GraphQL
Язык запросов к API, разработанный Facebook. Один эндпоинт (обычно /graphql), клиент сам описывает в теле запроса, какие поля и связи ему нужны. Сервер возвращает ровно то, что запросили — ни байтом больше. Это решает проблему over-fetching (REST отдаёт 30 полей, мобилке нужны 3) и under-fetching (чтобы собрать страницу, нужно сделать 5 запросов в разные эндпоинты).
Схема GraphQL — это строго типизированное описание всех доступных данных и операций. Клиент не может запросить поле, которого нет в схеме. Но в отличие от Protobuf, схема не компилируется в код — она живёт на сервере и отдаётся через introspection.
Аналогия: шведский стол. Вы берёте поднос и сами набираете ровно то, что хотите съесть. Хотите только горячее и десерт? Пожалуйста. Хотите пять видов салата и ни одного супа? Без проблем. Но если на раздачу пришло 500 человек одновременно — начинается давка (N+1 проблема GraphQL).
Матрица выбора: диаграмма принятия решений
Спойлер: нет единственного правильного ответа. Есть контекст, который диктует выбор. Вместо того чтобы пересказывать сценарии словами, я собрал дерево решений — пробегитесь по нему глазами, когда в следующий раз будете спорить с командой.
На диаграмме жёлтые ромбы — вопросы, которые аналитик должен задать (себе и команде) до начала проектирования. Зелёные узлы — сценарии, где GraphQL или gRPC дают ощутимый выигрыш. Синие — выбор в пользу REST. Обратите внимание: REST занимает большую часть финальных ответов. Это не случайность — для 70% интеграций REST действительно оптимален. Но есть 30%, где неправильный выбор бьёт по скорости разработки, latency и масштабируемости. Про эти 30% — дальше.
Таблица сравнения: REST vs gRPC vs GraphQL
Цифры и факты, разложенные по полочкам. Не для холивара — для ТЗ.
| Критерий | REST (JSON/HTTP) | gRPC (Protobuf/HTTP/2) | GraphQL |
|---|---|---|---|
| Формат данных | JSON (текстовый) | Protobuf (бинарный) | JSON (текстовый) |
| Транспорт | HTTP/1.1, HTTP/2 | HTTP/2 (обязателен) | HTTP/1.1, HTTP/2 |
| Контракт | OpenAPI (опционально, на доброй воле) | .proto-файл (обязателен, компилятор проверяет) | Schema (обязательна, introspection) |
| Совместимость | Любой HTTP-клиент, curl | Генерируемые клиенты, curl не прокатит | Любой HTTP-клиент, POST-запросы |
| Скорость сериализации | Средняя (JSON-парсинг) | Высокая (бинарный, до 7x быстрее JSON) | Зависит от реализации (обычно JSON) |
| Размер сообщения | Большой (текстовые ключи повторяются) | Компактный (3–10x меньше JSON) | Управляемый (клиент решает, но оверхед ключей остаётся) |
| Streaming | Только server-sent events (костыль) | Нативный: unary, server, client, bidirectional | Subscription (через WebSocket) |
| Over-fetching / Under-fetching | Частая проблема | Нет проблемы (жёсткий контракт) | Решён из коробки |
| Кэширование | HTTP-кэш (ETag, Cache-Control) | Не предусмотрен на уровне протокола | Требует ручного слоя |
| Версионирование | /v1/, /v2/ в URL или заголовках | Поля с номерами в Protobuf, обратная совместимость | @deprecated в схеме, эволюция без версий |
| Браузерная поддержка | Нативная | Требует gRPC-web прокси | Нативная (POST-запросы) |
| Кривая входа | Низкая | Высокая (Protobuf, генерация кода, HTTP/2) | Средняя (схема, резолверы, N+1) |
Важно: «меньше» и «быстрее» в таблице — это объективные цифры, но они не означают «лучше». Если ваша интеграция передаёт 50 заказов в час — вам без разницы, Protobuf там или JSON, быстрее на 3 миллисекунды или на 20. Таблица — инструмент для осознанного выбора, а не для доказательства, что «gRPC — это круто».
Когда выбирать REST
REST — это дефолт. С него стоит начинать, если нет явных причин для другого протокола. Сценарии, где REST оптимален:
- Публичное API. Партнёры, сторонние разработчики, open API. Вы не можете заставить всех внешних потребителей генерировать клиент из
.proto. Они хотят curl, Postman и документацию на Swagger. Подробнее про последний пункт — в разборе API Gateway, который часто становится точкой входа для публичного REST. - Веб-приложение с браузерным клиентом. Браузер нативно работает с HTTP и JSON. gRPC требует прокси (grpc-web), GraphQL требует поднятия отдельного сервера или библиотеки. Лишние компоненты — лишние точки отказа.
- Низкая и средняя нагрузка. До ~500 RPS REST не узкое место. Проблемы начинаются, когда вам нужно гонять сотни тысяч сообщений в секунду между сервисами — но это уже не про REST.
- Команда без опыта в gRPC/GraphQL. Если никто не работал с Protobuf — внедрение gRPC затянется на недели. Кривая входа реально высокая. За это время можно написать три REST-интеграции и пойти пить кофе.
- HTTP-кэширование критично. REST работает с CDN, прокси и браузерным кэшем из коробки. В gRPC и GraphQL с этим сложнее — требует дополнительной прослойки.
Простая ментальная модель: если вы проектируете API, которое будут дёргать люди (а не только машины) — REST. Если у вас стандартный CRUD (Create, Read, Update, Delete) — REST. Если вам нужно, чтобы интеграция заработала завтра, а не через месяц — REST.
Когда выбирать gRPC
Вот тут начинается интересное. gRPC даёт выигрыш в трёх сценариях — и проигрывает везде, где эти сценарии не критичны.
Межсервисное взаимодействие с высоким RPS
Внутри кластера, где Order Service вызывает Payment Service 50 000 раз в секунду — JSON превращается в бутылочное горлышко. Парсинг строк, повторяющиеся ключи ("orderId":, "customerId":), текстовое представление чисел — всё это на высоких нагрузках съедает CPU и сетевое время.
Protobuf-сообщение с теми же данными на 60–80% меньше. Умножьте на 50 000 запросов в секунду — разница превращается в десятки мегабит в секунду на ровном месте. А если добавить HTTP/2 с мультиплексированием (один TCP-соединение на множество параллельных запросов) — REST на HTTP/1.1 выглядит как велосипед рядом с болидом.
Стриминг данных
REST — это синхронный запрос-ответ. Отправил GET, получил JSON, соединение закрыто. Это нормально, пока вам не нужно:
- Server streaming: сервис аналитики запрашивает отчёт, а бэкенд отправляет порции данных по мере готовности — клиент начинает обрабатывать первую порцию, не дожидаясь финала.
- Client streaming: IoT-датчик отправляет показания потоком, сервер агрегирует и раз в минуту сохраняет результат.
- Bidirectional streaming: голосовой ассистент посылает аудиопоток на сервер, сервер в реальном времени возвращает текст распознавания.
В REST всё это — костыли. Server-sent events для server streaming, чанкованные upload для client streaming, WebSocket для bidirectional. В gRPC все три режима — нативные: одна строчка stream в .proto-файле.
Строгий контракт с кодогенерацией
Когда сервер и клиент пишутся в одном проекте (или хотя бы в одной компании), жёсткий контракт .proto — это подарок. Компилятор генерирует типизированные клиенты и серверные заготовки на Go, Java, Python, C++, Kotlin, Rust и ещё десятке языков. Ошибка в названии поля или типе всплывает на этапе компиляции, а не в проде в три часа ночи.
Аналитику проще всего представить это так: .proto-файл — это контракт, который невозможно нарушить случайно. REST с OpenAPI даёт похожий эффект, но только если команда дисциплинированно поддерживает спеки, генерирует клиентов и не правит JSON руками «по-быстрому» (то есть почти никогда).
Когда выбирать GraphQL
GraphQL решает одну конкретную боль: клиентам нужно разное. Мобильному приложению нужны 2 поля из сущности «заказ», веб-кабинету — 15 полей, а админке — вообще другая агрегация. В REST вы либо делаете три разных эндпоинта (и плодите сущности), либо отдаёте все 30 полей всем подряд.
Сценарии GraphQL
- Мобильное приложение с десятком экранов. Каждый экран хочет свой набор данных. GraphQL позволяет фронтендеру собрать ровно то, что нужно для конкретного экрана — без правок на бэке и без «API для страницы 7».
- Агрегация данных из нескольких источников. GraphQL-сервер может быть надстройкой над REST-сервисами: один запрос от клиента, сервер ходит в три микросервиса, собирает ответ, возвращает клиенту. Правда, внутри вы получаете классическую N+1 проблему и dataloader как обязательный инструмент.
- Публичное API с непредсказуемыми запросами. Партнёры хотят дёргать ваши данные как угодно, и вы не можете предугадать, какие поля им понадобятся через полгода. GraphQL даёт им гибкость без вашего участия.
Когда GraphQL избыточен
- У вас один клиент. Если клиент ровно один — вся гибкость GraphQL не нужна. REST с нормальным контрактом решит задачу быстрее.
- Простые CRUD-операции. GraphQL ради
GET /orders/42— это как брать такси до соседнего подъезда. Можно, но зачем. - Нет экспертизы в команде. N+1 проблема, кэширование (не HTTP-кэш!), complexity analysis для защиты от тяжелых запросов, пермишны на уровне полей — GraphQL тащит за собой хвост инфраструктурных задач, которые в REST решаются проще.
Как аналитику описывать контракты для каждого протокола
Хватит теории — давайте к документам, которые вы понесёте команде.
REST: OpenAPI-спек
openapi: 3.0.0info: title: Orders API version: 1.0.0paths: /orders: post: summary: Создать заказ requestBody: content: application/json: schema: $ref: '#/components/schemas/CreateOrderRequest' responses: '201': description: Заказ создан content: application/json: schema: $ref: '#/components/schemas/Order'components: schemas: CreateOrderRequest: type: object required: [customerId, items] properties: customerId: type: integer items: type: array items: $ref: '#/components/schemas/OrderItem'Аналитику достаточно описать сущности и эндпоинты — разработчик допишет технические детали (авторизацию, коды ошибок). Главное — не уйти в «а потом допишем». OpenAPI без кодогенерации — это просто красивый YAML. С кодогенерацией — это половина серверного кода из коробки.
gRPC: Protobuf-контракт
syntax = "proto3";
package orders;
service OrderService { rpc CreateOrder (CreateOrderRequest) returns (Order); rpc GetOrder (GetOrderRequest) returns (Order); rpc StreamOrders (OrderFilter) returns (stream Order);}
message CreateOrderRequest { int64 customer_id = 1; repeated OrderItem items = 2; string currency = 3; // ISO 4217}
message OrderItem { string sku = 1; int32 quantity = 2; int64 price = 3; // в минимальных единицах валюты (копейки)}
message Order { int64 order_id = 1; int64 customer_id = 2; repeated OrderItem items = 3; int64 total_amount = 4; string currency = 5; string status = 6;}Номера полей (= 1, = 2) — это не для красоты. Protobuf идентифицирует поля по номерам, а не по именам. Поэтому вы можете переименовать customer_id в buyer_id — обратная совместимость сохранится. Но если поменяете номер поля — предыдущие клиенты сломаются. Это первое, что аналитик должен объяснить команде: номера полей — часть контракта навсегда.
GraphQL: схема
type Query { order(id: ID!): Order orders(filter: OrderFilter, first: Int, after: String): OrderConnection}
type Mutation { createOrder(input: CreateOrderInput!): CreateOrderPayload}
type Order { id: ID! customer: Customer items: [OrderItem!]! totalAmount: Int currency: String status: OrderStatus}
input CreateOrderInput { customerId: ID! items: [OrderItemInput!]! currency: String = "RUB"}
enum OrderStatus { CREATED CONFIRMED PAID SHIPPED CANCELLED}Что важно зафиксировать аналитику: схема GraphQL — это не просто список полей. Это контракт, по которому клиент может построить любой валидный запрос. А вы должны гарантировать, что любой валидный запрос отработает. Поле order(id: ID!) — ок. Но если вы добавили orders(filter: ...) без first и без ограничения по complexity — ждите клиента, который запросит orders { items { ... } } на миллион записей и уронит базу (примечательно, что обычно это происходит в пятницу вечером).
Что писать в ТЗ: чеклист для аналитика
При проектировании интеграции зафиксируйте в requirements-документе ответы на эти вопросы — на первой же встрече с архитектором это сэкономит полчаса спора:
- Кто потребитель? Браузер / мобильное приложение / другой микросервис / внешний партнёр.
- Ожидаемый RPS и размер сообщений. 10 запросов в час или 10 000 в секунду — выбор протокола будет разным.
- Нужен ли стриминг? Server, client или bidirectional. Если да — gRPC почти без альтернативы.
- Критична ли latency? Если клиент ждёт ответа и каждая миллисекунда на счету (high-frequency trading, real-time bidding) — Protobuf против JSON даёт измеримый выигрыш.
- Кто контролирует клиента и сервер? Одна команда — gRPC. Разные команды — REST. Внешние партнёры — только REST (и GraphQL как опция).
- Нужна ли обратная совместимость? Если клиенты обновляются раз в полгода (мобильные приложения) — версионирование REST или
@deprecatedв GraphQL обязательны. Если клиент и сервер деплоятся вместе —.protoс номерами полей решает задачу. - Экспертиза команды. Если никто не знает Protobuf — внедрение gRPC займёт не день и не неделю. Учитывайте это в сроках.
Не надо писать «использовать gRPC» и умывать руки. Опишите, почему именно gRPC, на какие сценарии, с каким контрактом. Иначе через месяц разработчик перепишет на REST «потому что так быстрее» — и будет по-своему прав.
Заключение
gRPC vs REST vs GraphQL — это не вопрос «что лучше». Это вопрос «что лучше для конкретной интеграции». Если у вас публичное API и внешние партнёры — REST, без вариантов. Если внутренний обмен данными с сотнями тысяч RPS — gRPC. Если мобильное приложение с десятком экранов и разные наборы данных — GraphQL.
Большинство проектов, на которых я был, использовали смесь: REST для внешнего API (через шлюз), gRPC для синхронного межсервисного общения, Kafka для асинхронных событий. И это не «сложная архитектура ради архитектуры». Это когда каждый инструмент на своём месте — и никакой холивар в курилке этого не отменяет.
PS. Парадокс в том, что выбор протокола — на 70% аналитическая задача и на 30% техническая. Но в реальности её почти всегда делегируют разработчикам, которые выбирают то, с чем работали последние три года. Если вы как аналитик придёте на встречу с матрицей из этой статьи — поверьте, архитектор скажет спасибо. Или как минимум перестанет закатывать глаза.