Что не так с REST

REST — де-факто стандарт для веб-API. Удобно, понятно, работает везде. Но у него есть фундаментальные ограничения, которые начинают болеть, когда нагрузка растёт или архитектура усложняется.

Первая проблема — текстовый формат. JSON хорош для чтения глазами, но тяжёл для машины. Сериализация и десериализация занимают время и CPU. При тысячах запросов в секунду это ощутимо.

Вторая — HTTP/1.1. Большинство REST API работают на нём. Head-of-line blocking никуда не девается: один зависший запрос тормозит очередь.

Третья — отсутствие строгой схемы. REST с JSON позволяет слать что угодно. Поле переименовали на бэкенде — фронтенд сломался. Автоматической валидации на уровне протокола нет.

gRPC решает все три проблемы сразу.

Что такое gRPC

gRPC — фреймворк удалённого вызова процедур от Google, открытый в 2016 году. Под капотом три вещи:

  • Protocol Buffers (Protobuf) — бинарный формат сериализации
  • HTTP/2 — мультиплексирование, сжатие заголовков, server push
  • Строгая схема.proto файл описывает все методы и типы данных

Идея простая: вместо «отправь POST на /users с JSON телом» ты вызываешь метод CreateUser(user) как обычную функцию в своём коде, а gRPC сам разбирается, как передать данные по сети.

Protobuf против JSON: конкретные цифры

Разница между Protobuf и JSON ощутимая. Цифры из реальных бенчмарков:

  • Размер сообщения: Protobuf в среднем в 3–5 раз меньше JSON
  • Скорость сериализации: Protobuf быстрее в 5–7 раз
  • Скорость десериализации: Protobuf быстрее в 3–6 раз

Пример. Объект пользователя с 10 полями в JSON занимает около 200 байт. Тот же объект в Protobuf — 50–70 байт. При миллионе запросов в день это разница в гигабайтах трафика.

Почему Protobuf такой компактный? Он не хранит названия полей — только числовые идентификаторы. Никаких фигурных скобок, кавычек, двоеточий. Только данные.

HTTP/2: что это меняет

gRPC работает только на HTTP/2. Это не ограничение, а источник преимуществ.

Мультиплексирование. Несколько запросов по одному TCP-соединению без очередей. REST на HTTP/1.1 страдает от head-of-line blocking: если один запрос завис, остальные ждут. HTTP/2 этого лишён.

Сжатие заголовков. HTTP заголовки повторяются в каждом запросе. HTTP/2 кешируует их через HPACK — ощутимая экономия при частых запросах.

Стриминг. В REST для потока данных приходится изощряться: long polling, SSE, WebSocket. В gRPC стриминг — встроенная возможность на уровне протокола.

Четыре типа взаимодействия

Это одно из главных отличий от REST. gRPC поддерживает четыре режима:

Unary — классика: один запрос, один ответ. Аналог обычного HTTP-запроса.

Server streaming — клиент отправляет один запрос, сервер отвечает потоком. Удобно для экспорта больших данных или live-обновлений.

Client streaming — клиент шлёт поток, сервер отвечает один раз. Пример: загрузка файла по частям.

Bidirectional streaming — оба конца шлют потоки независимо. Идеально для чатов, игр, realtime-систем.

В REST для bidirectional streaming нужен WebSocket, который стоит отдельно от архитектуры API. В gRPC это просто ещё один тип метода.

.proto файл: схема как контракт

Схема в gRPC — не документация, а исполняемый код. Файл .proto описывает сервис:

syntax = "proto3";

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);
  rpc CreateUser (CreateUserRequest) returns (User);
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  bool is_active = 4;
}

message GetUserRequest {
  int32 id = 1;
}

Из этого файла генерируется клиентский и серверный код для Go, Python, Java, Node.js, C#, Ruby — практически любого популярного языка. Не нужно вручную писать HTTP-клиент, парсить JSON, следить за совместимостью полей.

Если ты изменил тип поля или удалил его — компилятор сразу скажет, какой код сломается. С REST/JSON так не получится без дополнительного инструментария вроде OpenAPI и codegen.

Где gRPC реально выигрывает

Не везде. gRPC не серебряная пуля. Вот случаи, где он даёт измеримый выигрыш:

Микросервисы. Когда у тебя 20+ сервисов, которые активно общаются между собой, gRPC снижает latency и нагрузку на сеть. Именно так его используют Google, Netflix, Dropbox внутри своей инфраструктуры.

Высокая нагрузка. Если сервис обрабатывает десятки тысяч запросов в секунду, экономия на сериализации становится заметной. Реальные кейсы показывают снижение CPU-нагрузки на 20–40% по сравнению с REST/JSON.

Мобильные клиенты. Меньший размер сообщений — меньше трафика. Для мобильных приложений на нестабильном соединении это важно. Square и Lyft перешли на gRPC для критичных запросов именно по этой причине.

Потоковые данные. Realtime-уведомления, live-трекинг, чаты — bidirectional streaming из коробки.

Полиглот-архитектуры. Когда у тебя Go на бэкенде, Python для ML, Java для легаси — gRPC генерирует клиентов для всех языков из одного .proto файла. Контракт единый.

Где REST лучше

Честно: для большинства обычных веб-проектов REST остаётся лучшим выбором.

Браузеры. gRPC не работает в браузере напрямую. Нужен gRPC-Web — проксирующий слой. Это дополнительная сложность. Для публичных API, которые используют браузеры, REST проще.

Читаемость. JSON можно открыть в браузере, в curl, в Postman. Бинарный Protobuf — нет. Дебаггинг сложнее.

Экосистема. REST/JSON поддерживают все: CDN, кеши, прокси, шлюзы. gRPC требует специфической поддержки на каждом уровне стека.

Простые проекты. Если у тебя CRUD для блога или лендинга — gRPC только добавит сложности без заметного выигрыша. Переходить стоит тогда, когда производительность стала измеримой проблемой, а не абстрактным «хотим быстрее».

Как выглядит gRPC в коде

Покажу на примере Go — один из самых популярных языков для gRPC бэкендов.

Серверная сторона:

type userServer struct {
    pb.UnimplementedUserServiceServer
    db *sql.DB
}

func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, err := s.db.GetUserByID(ctx, req.Id)
    if err != nil {
        return nil, status.Errorf(codes.NotFound, "user %d not found", req.Id)
    }
    return &pb.User{
        Id:       user.ID,
        Name:     user.Name,
        Email:    user.Email,
        IsActive: user.IsActive,
    }, nil
}

Клиентская сторона:

conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
client := pb.NewUserServiceClient(conn)

user, _ := client.GetUser(ctx, &pb.GetUserRequest{Id: 42})
fmt.Println(user.Name)

Никаких URL, никакого JSON, никакого парсинга. Просто вызов метода.

Коды ошибок в gRPC

REST использует HTTP статус-коды: 200, 404, 500. gRPC использует собственные коды — их 16 штук, и они более семантичны:

  • OK — успех
  • NOT_FOUND — ресурс не найден
  • PERMISSION_DENIED — нет прав
  • INVALID_ARGUMENT — неверные параметры
  • DEADLINE_EXCEEDED — превышен таймаут
  • UNAVAILABLE — сервис недоступен

DEADLINE_EXCEEDED прямо говорит, что запрос превысил таймаут — в REST для этого нет стандартного кода, используют 408 или 504 по-разному. К коду можно прикрепить детали ошибки: например, список невалидных полей через google.rpc.ErrorInfo.

Инструменты для работы с gRPC

grpcurl — curl для gRPC. Позволяет вызывать методы из командной строки:

grpcurl -d '{"id": 42}' localhost:50051 UserService/GetUser

grpc-gateway — генерирует REST/JSON шлюз поверх gRPC сервиса. Отличное решение, когда нужно поддерживать и браузерных клиентов, и эффективное внутреннее взаимодействие.

Evans — интерактивный gRPC клиент с REPL-интерфейсом. Удобен для исследования незнакомого сервиса.

Buf — инструмент для управления .proto файлами: линтинг, обнаружение ломающих изменений. Для серьёзных проектов незаменим.

Практические советы

Несколько вещей, которые сэкономят время:

Версионируй через пакеты, не через URL. В REST принято /api/v2/users. В gRPC добавь пакет: package userservice.v2. Это стандарт.

Устанавливай deadline на каждом запросе. gRPC поддерживает контексты с таймаутами — обязательно их задавай. Зависший downstream иначе заблокирует upstream по цепочке.

Не меняй номера полей в Protobuf. Числовой идентификатор поля — его постоянный ID. Удалил поле? Зарезервируй номер через reserved. Иначе получишь несовместимость бинарных данных.

TLS обязателен в продакшене. gRPC поддерживает Mutual TLS — взаимную аутентификацию клиента и сервера. Для внутренних сервисов это хорошая практика.

Server reflection — только для дев-окружения. Reflection позволяет инструментам вроде grpcurl автоматически видеть доступные методы. Удобно в разработке, но раскрывает API в продакшене — выключай.

Когда пора переходить

Конкретные признаки:

  • Сервис обрабатывает больше 5 000 запросов в секунду и REST latency стала узким местом
  • Количество внутренних микросервисов перевалило за 10–15 и inter-service трафик ощутим
  • Нужен настоящий bidirectional streaming
  • Команда работает на разных языках и синхронизация API-контрактов превратилась в боль

Если ни один из этих пунктов не про тебя — скорее всего, REST тебе достаточно. Не усложняй без причины.

Когда проект дорастает до точки, где архитектура API становится критичной, в REEXY помогут спроектировать и реализовать нужный подход — будь то gRPC, REST или гибридный шлюз. Подробности на r3xy.ru.