Web over gRPC:
какую технологию
выбрать
Эдгар Сипки // Sipki Tech
Эдгар Сипки
- В Go 8 лет
- Евангелист gRPC && OpenSource
- Консалтинг по архитектуре и proto
- Founder в EasyP
Я не
НЕ ГЕНИЙ
НЕ МИЛЛИАРДЕР
НЕ ФИЛАНТРОП
Просто человек, который много и больно щупал gRPC в проде.
О чём поговорим
- 01Преимущества gRPC
- 02Проблемы в WEB
- 03Все попытки завоевать WEB
- 04K6 и прямой батл между фреймворками
- 05Выводы
ПРЕИМУЩЕСТВА
gRPC
DOC FIRST
FOREVER
Контракт — единственный источник правды. Сначала .proto, потом всё остальное.
Stream данных из коробки
service Storage {
rpc Get (GetRequest) returns (stream Chunk);
rpc Store (stream Chunk) returns (StoreResponse);
rpc Sync (stream Event) returns (stream Event);
}- Server-stream, client-stream, bidi — одной строкой в контракте
- HTTP/2 multiplexing бесплатно
Компактность данных
{
"id": 42,
"user":"edgar",
"ok": true
}08 2A 12 05 65 64 67 61 72 18 01
~34% меньше на ровном месте, без gzip.
Валидация в контракте
message CreateUserRequest {
string email = 1 [(buf.validate.field).string.email = true];
string name = 2 [(buf.validate.field).string.min_len = 2];
int32 age = 3 [(buf.validate.field).int32 = { gte: 18, lte: 120 }];
}Один контракт → один набор правил → ноль расхождений между сервисами.
Почему «почти»
version: v2 deps: - buf.build/grpc-ecosystem/grpc-gateway - buf.build/bufbuild/protovalidate plugins: - name: go - name: go-grpc - name: validate-go
Экосистема собирается руками. EasyP закрывает этот зазор.
ПРОБЛЕМЫ
В WEB
О чём поговорим
- Преимущества gRPC
- Проблемы в WEB
- Все попытки завоевать WEB
- K6 и прямой батл между фреймворками
- Выводы
Почему он не покорил
весь мир?
Покорил, но не WEB
WEB — ?
А что в WEB
- REST развивался десятки лет
- HTTP/2 в браузере есть, но fetch «руками» писать нельзя
- Trailers, framing, flow-control — недоступны клиентскому коду
- Монополия OpenAPI
OpenAPI
доминирует
Для каждого языка, каждого редактора и каждой CI — есть готовый плагин.
Экосистема OpenAPI
- Кодогенераторы под все языки
- Mock-сервера, контракт-тесты
- UI: Swagger, Redoc, Stoplight, Scalar...
- API-gateway, документация, миграции
ВСЕ ПОПЫТКИ
ЗАВОЕВАТЬ WEB
О чём поговорим
- Преимущества gRPC
- Проблемы в WEB
- Все попытки завоевать WEB
- K6 и прямой батл между фреймворками
- Выводы
Сдались без боя?
gRPC-Gateway
gRPC-Web
Twirp
ConnectRPC
gRPC‑Gateway
Обманка REST
Снаружи — обычный JSON-over-HTTP/1.1.
Внутри — всё тот же gRPC-сервер.
Магия аннотаций
service Users {
rpc GetUser(GetUserRequest) returns (User) {
option (google.api.http) = {
get: "/v1/users/{id}"
};
}
}Один proto — три файла
модели и gRPC-клиент
HTTP-обёртка
OpenAPI-схема
Один код — два интерфейса
- Бизнес-логика пишется один раз
- Доступна и по gRPC, и по HTTP/JSON
- Frontend счастлив, backend не дублирует код
Interceptors живут
Auth, logging, tracing — всё работает как в обычном gRPC.
Серверы можно разделять
Gateway
HTTP/1.1, TLS, кэш, rate-limit
gRPC Server
HTTP/2, бизнес-логика
Стримы через WebSockets
mux := runtime.NewServeMux()
proxy := wsproxy.WebsocketProxy(mux)
http.ListenAndServe(":8080", proxy)Server-streaming → WebSocket. Костыль, но работает.
Файлы — больно
func handleBinaryFileUpload(w http.ResponseWriter, r *http.Request) {
// 1. парсим multipart руками
// 2. собираем proto-сообщение
// 3. бьём на чанки
// 4. дёргаем gRPC client напрямую
// ...прощай, кодоген
}Multipart живёт мимо кодогенерации.
Снаружи это REST.
Это не gRPC.
Итог: gRPC‑Gateway
+ Плюсы
- Адаптер REST ↔ gRPC
- Простая интеграция
- Streams → WebSocket
- Swagger из коробки
- Полноценные интерсепторы
− Минусы
- HTTP-логика просачивается в proto
- Файлы — вручную
- Это всё ещё не gRPC снаружи
gRPC‑Web
«It is currently impossibleto implement the HTTP/2 gRPC spec in the browser.»
— gRPC-Web design doc, Google, 2016
Прокси посередине
Без прокси не запустится: HTTP/2 framing в браузере недоступен.
Frontend узнаёт про «ещё один прокси».
Client-streaming — нет
- Unary — ok
- Server-streaming — ok
- Client-streaming — не поддержан
- Bidi-streaming — не поддержан
Full-duplex обещают через WebTransport
QUIC + HTTP/3 в браузере → полноценные стримы.
Но это в будущем, и не для всех браузеров.
В итоге придётся переезжать
+ Сейчас
- Стандартный proto-контракт
- Кодоген TypeScript
− Потом
- WebTransport — другой клиент
- Прокси отвалится не сразу
- Двойная миграция в проде
Итог: gRPC‑Web
- Reverse proxy обязателен
- Файлы — не из коробки
- Полноценных streams нет
- Frontend не любит лишний прокси
Twirp
Два транспорта — одна схема
Content-Type: application/protobuf <binary>
Content-Type: application/json
{ "id": "42" }Один сервис — HTTP/1.1 + gRPC
Без прокси, без gateway. Один бинарь.
Контроль HTTP-методов
func (s *Server) GetUser(ctx context.Context, r *GetUserRequest) (*User, error) {
twirp.SetHTTPResponseHeader(ctx, "Cache-Control", "max-age=60")
return s.repo.Find(ctx, r.Id)
}Streams отрезали сразу
// linter rules RPC_NO_CLIENT_STREAMING RPC_NO_SERVER_STREAMING
@spenczar — «we don't support streaming, by design.»
Свой Interceptor
type Interceptor func(next Method) Method type Method func(ctx context.Context, req any) (any, error)
Совместимости с gRPC-интерсепторами нет.
Нет swagger.
Документация генерируется только через сторонние плагины. Frontend остаётся без типов из коробки.
Это не gRPC.
Совсем.
Но плюсы же будут?
«Сделаем простой не-gRPC поверх HTTP»
«Но плюсы же будут?»
…
«Плюсы же будут??»
Итог: Twirp
- Это не gRPC
- Файлы — не из коробки
- Streams — запрещены
- Frontend остался без OpenAPI
- Нет интерсепторов от gRPC
- Twitch проиграл битву за внимание
ConnectRPC
Большие надежды
grpcurl -plaintext \
-d '{"id":42}' \
localhost:8080 users.Users/Getcurl -X POST \
-H "Content-Type: application/json" \
-d '{"id":42}' \
http://localhost:8080/users.Users/Get«Будет как gRPC, но без прокси» — знакомо звучит.
Один Go-сервер — три протокола
- Connect (свой JSON/proto over HTTP)
- gRPC
- gRPC-Web
В одном бинаре, на одном порту.
Контроль HTTP
func (s *Server) GetAvatar(ctx context.Context, req *connect.Request[GetAvatarRequest]) (*connect.Response[Avatar], error) {
res := connect.NewResponse(&Avatar{...})
res.Header().Set("Cache-Control", "max-age=604800")
return res, nil
}Bidi-stream
снова не работает
в браузере.
// déjà vu, gRPC-Web style
Снова свой интерсептор
type UnaryFunc func(ctx context.Context, req AnyRequest) (AnyResponse, error)
type Interceptor interface {
WrapUnary(UnaryFunc) UnaryFunc
}Наработки gRPC интерсепторов — на свалку.
Эволюция или деградация
Эволюция
- gRPC выходит в WEB
- Поддерживает разные протоколы
- Современный Go API
Деградация
- Выкинули наработки сообщества
- Привязка к Buf-экосистеме
- По сути — Twirp v2
Итог: ConnectRPC
- Самый универсальный по протоколам
- Bidi-stream в браузере всё равно нет
- Свой собственный API для интерсепторов
- Жёсткая привязка к Buf
Все четыре в одной таблице
| gRPC‑Gateway | gRPC‑Web | Twirp | ConnectRPC | |
|---|---|---|---|---|
| Это gRPC? | нет (HTTP) | почти | нет | почти |
| Прокси нужен? | нет | да | нет | нет |
| Streams | через WS | server only | нет | server / client |
| Файлы | руками | нет | руками | через HTTP |
| OpenAPI | есть | нет | нет | плагин |
| Интерсепторы gRPC | да | да | свои | свои |
| Привязка к вендору | нет | Twitch | Buf |
НЕ БОЙТЕСЬ ПИСАТЬ
OPEN SOURCE
Один человек — одна экосистема
Yuki Yugui Sonoda
Google team
Twitch team
Akshay Shah & co
Битва не проиграна
- gRPC‑Gateway — лидер, если нужен REST-фасад
- gRPC‑Web — ставка на будущее (WebTransport)
- Twirp — жив, но без массы
- ConnectRPC — для тех, кто уже на Buf
K6 И ПРЯМОЙ
БАТЛ
Но кто же
быстрее?
// готовых сравнений в интернете нет
K6 как инструмент
Нагрузка
HTTP/1, HTTP/2, gRPC, WebSocket из коробки
Browser API
Реальный Chromium для e2e-сценариев
Chaos
Fault injection и network throttling
Стенд
Больше лучше — отдача
| Payload | gRPC‑Gateway | ConnectRPC | gRPC‑Web | Twirp |
|---|---|---|---|---|
| 30b | ~39 800 | ~47 600 | ~48 100 | ~49 000 |
| 2kb | ~27 800 | ~32 300 | ~29 500 | ~31 700 |
| 1mb | ~7 700 | ~9 600 | ~7 200 | ~10 100 |
На малом payload Twirp чуть впереди, на крупном — Twirp / ConnectRPC.
Больше лучше — отдача (потюнили)
| Payload | gRPC‑Gateway | ConnectRPC | gRPC‑Web | Twirp |
|---|---|---|---|---|
| 30b | ~48 100 | ~47 600 | ~48 100 | ~49 000 |
| 2kb | ~32 300 | ~32 400 | ~29 500 | ~31 700 |
| 1mb | ~9 900 | ~9 600 | ~7 200 | ~10 100 |
После тюнинга — разница в пределах погрешности.
ВЫВОДЫ
О чём поговорили
- Преимущества gRPC
- Проблемы в WEB
- Все попытки завоевать WEB
- K6 и прямой батл между фреймворками
- Выводы
Что выбрать
Нужен REST-фасад
→ gRPC‑Gateway
Готовы к WebTransport
→ gRPC‑Web
Минимализм без streams
→ Twirp
Стек на Buf
→ ConnectRPC
Производительность — не критерий. Все четверо в одной весовой категории.