sipki.online
// доклад · GolangConf 2024

Web over gRPC:
какую технологию
выбрать

Эдгар Сипки // Sipki Tech

// speaker.bio

Эдгар Сипки

@zergslaw
  • В Go 8 лет
  • Евангелист gRPC && OpenSource
  • Консалтинг по архитектуре и proto
  • Founder в EasyP
// disclaimer.txt

Я не

01

НЕ ГЕНИЙ

02

НЕ МИЛЛИАРДЕР

03

НЕ ФИЛАНТРОП

Просто человек, который много и больно щупал gRPC в проде.

// agenda

О чём поговорим

  1. 01Преимущества gRPC
  2. 02Проблемы в WEB
  3. 03Все попытки завоевать WEB
  4. 04K6 и прямой батл между фреймворками
  5. 05Выводы
// chapter 01

ПРЕИМУЩЕСТВА
gRPC

// philosophy

DOC FIRST
FOREVER

Контракт — единственный источник правды. Сначала .proto, потом всё остальное.

// streams

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 бесплатно
// payload size

Компактность данных

JSON
{
  "id":  42,
  "user":"edgar",
  "ok":  true
}
65 B
PROTO
08 2A
12 05 65 64 67 61 72
18 01
43 B

~34% меньше на ровном месте, без gzip.

// protovalidate

Валидация в контракте

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 }];
}

Один контракт → один набор правил → ноль расхождений между сервисами.

// easyp.yaml

Почему «почти»

version: v2
deps:
  - buf.build/grpc-ecosystem/grpc-gateway
  - buf.build/bufbuild/protovalidate
plugins:
  - name: go
  - name: go-grpc
  - name: validate-go

Экосистема собирается руками. EasyP закрывает этот зазор.

// chapter 02

ПРОБЛЕМЫ
В WEB

// agenda

О чём поговорим

  • Преимущества gRPC
  • Проблемы в WEB
  • Все попытки завоевать WEB
  • K6 и прямой батл между фреймворками
  • Выводы

Почему он не покорил
весь мир?

// platforms

Покорил, но не WEB

Swift
Android
C++
Python
Ruby
Go
Dart
Java
Node.js
Kotlin

WEB — ?

// web reality

А что в WEB

  • REST развивался десятки лет
  • HTTP/2 в браузере есть, но fetch «руками» писать нельзя
  • Trailers, framing, flow-control — недоступны клиентскому коду
  • Монополия OpenAPI

OpenAPI
доминирует

Для каждого языка, каждого редактора и каждой CI — есть готовый плагин.

// openapi tools registry

Экосистема OpenAPI

1548
tools registered
  • Кодогенераторы под все языки
  • Mock-сервера, контракт-тесты
  • UI: Swagger, Redoc, Stoplight, Scalar...
  • API-gateway, документация, миграции
// chapter 03

ВСЕ ПОПЫТКИ
ЗАВОЕВАТЬ WEB

// agenda

О чём поговорим

  • Преимущества gRPC
  • Проблемы в WEB
  • Все попытки завоевать WEB
  • K6 и прямой батл между фреймворками
  • Выводы
// challengers

Сдались без боя?

01

gRPC-Gateway

02.04.2015 · Yuki Yugui Sonoda
02

gRPC-Web

03.11.2016 · Google
03

Twirp

16.01.2018 · Twitch
04

ConnectRPC

01.09.2019 / 02.2022 · Buf
// 02.04.2015

gRPC‑Gateway

// idea

Обманка REST

Снаружи — обычный JSON-over-HTTP/1.1.
Внутри — всё тот же gRPC-сервер.

client — HTTP/JSON → gateway— gRPC → server
// google.api.http

Магия аннотаций

service Users {
  rpc GetUser(GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/v1/users/{id}"
    };
  }
}
// protoc output

Один proto — три файла

users.pb.go

модели и gRPC-клиент

users.pb.gw.go

HTTP-обёртка

users.swagger.json

OpenAPI-схема

Один код — два интерфейса

  • Бизнес-логика пишется один раз
  • Доступна и по gRPC, и по HTTP/JSON
  • Frontend счастлив, backend не дублирует код
// request flow

Interceptors живут

CLIENTGATEWAYINTERCEPTORSSERVER

Auth, logging, tracing — всё работает как в обычном gRPC.

// deployment

Серверы можно разделять

Machine 1

Gateway

HTTP/1.1, TLS, кэш, rate-limit

Machine 2

gRPC Server

HTTP/2, бизнес-логика

// streams hack

Стримы через WebSockets

mux := runtime.NewServeMux()
proxy := wsproxy.WebsocketProxy(mux)
http.ListenAndServe(":8080", proxy)

Server-streaming → WebSocket. Костыль, но работает.

// edge case

Файлы — больно

func handleBinaryFileUpload(w http.ResponseWriter, r *http.Request) {
  // 1. парсим multipart руками
  // 2. собираем proto-сообщение
  // 3. бьём на чанки
  // 4. дёргаем gRPC client напрямую
  // ...прощай, кодоген
}

Multipart живёт мимо кодогенерации.

Снаружи это REST.
Это не gRPC.

// grpc-gateway · summary

Итог: gRPC‑Gateway

+ Плюсы

  • Адаптер REST ↔ gRPC
  • Простая интеграция
  • Streams → WebSocket
  • Swagger из коробки
  • Полноценные интерсепторы

− Минусы

  • HTTP-логика просачивается в proto
  • Файлы — вручную
  • Это всё ещё не gRPC снаружи
// 03.11.2016 · Google

gRPC‑Web

«It is currently impossibleto implement the HTTP/2 gRPC spec in the browser.»

— gRPC-Web design doc, Google, 2016

// architecture

Прокси посередине

BROWSERPROXY (Envoy)gRPC BACKEND

Без прокси не запустится: HTTP/2 framing в браузере недоступен.

что???

Frontend узнаёт про «ещё один прокси».

// limitations

Client-streaming — нет

  • Unary — ok
  • Server-streaming — ok
  • Client-streaming — не поддержан
  • Bidi-streaming — не поддержан
// roadmap · 2023+

Full-duplex обещают через WebTransport

QUIC + HTTP/3 в браузере → полноценные стримы.
Но это в будущем, и не для всех браузеров.

// migration

В итоге придётся переезжать

+ Сейчас

  • Стандартный proto-контракт
  • Кодоген TypeScript

− Потом

  • WebTransport — другой клиент
  • Прокси отвалится не сразу
  • Двойная миграция в проде
// grpc-web · summary

Итог: gRPC‑Web

  • Reverse proxy обязателен
  • Файлы — не из коробки
  • Полноценных streams нет
  • Frontend не любит лишний прокси
// 16.01.2018 · Twitch

Twirp

// protocol

Два транспорта — одна схема

POST /twirp/users.UserService/GetUser
Content-Type: application/protobuf

<binary>
POST /twirp/users.UserService/GetUser
Content-Type: application/json

{ "id": "42" }

Один сервис — HTTP/1.1 + gRPC

HTTP/1.1 + gRPCServer

Без прокси, без gateway. Один бинарь.

// twirp api

Контроль 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)
}
// twitchtv/twirp · issue · 2018-01-29

Streams отрезали сразу

// linter rules
RPC_NO_CLIENT_STREAMING
RPC_NO_SERVER_STREAMING

@spenczar — «we don't support streaming, by design.»

// custom interceptors

Свой Interceptor

type Interceptor func(next Method) Method
type Method func(ctx context.Context, req any) (any, error)

Совместимости с gRPC-интерсепторами нет.

Нет swagger.

Документация генерируется только через сторонние плагины. Frontend остаётся без типов из коробки.

Это не gRPC.
Совсем.

// meme · anakin & padmé

Но плюсы же будут?

anakin

«Сделаем простой не-gRPC поверх HTTP»

padmé

«Но плюсы же будут?»

anakin

padmé

«Плюсы же будут??»

// twirp · summary

Итог: Twirp

  • Это не gRPC
  • Файлы — не из коробки
  • Streams — запрещены
  • Frontend остался без OpenAPI
  • Нет интерсепторов от gRPC
  • Twitch проиграл битву за внимание
// 01.09.2019 · Buf · 02.2022

ConnectRPC

// déjà vu

Большие надежды

grpcurl -plaintext \
  -d '{"id":42}' \
  localhost:8080 users.Users/Get
curl -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 control

Контроль 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

// interceptors v2

Снова свой интерсептор

type UnaryFunc func(ctx context.Context, req AnyRequest) (AnyResponse, error)
type Interceptor interface {
  WrapUnary(UnaryFunc) UnaryFunc
}

Наработки gRPC интерсепторов — на свалку.

// vendor view

Эволюция или деградация

Эволюция

  • gRPC выходит в WEB
  • Поддерживает разные протоколы
  • Современный Go API

Деградация

  • Выкинули наработки сообщества
  • Привязка к Buf-экосистеме
  • По сути — Twirp v2
// connectrpc · summary

Итог: ConnectRPC

  • Самый универсальный по протоколам
  • Bidi-stream в браузере всё равно нет
  • Свой собственный API для интерсепторов
  • Жёсткая привязка к Buf
// matrix

Все четыре в одной таблице

gRPC‑GatewaygRPC‑WebTwirpConnectRPC
Это gRPC?нет (HTTP)почтинетпочти
Прокси нужен?нетданетнет
Streamsчерез WSserver onlyнетserver / client
Файлырукаминетрукамичерез HTTP
OpenAPIестьнетнетплагин
Интерсепторы gRPCдадасвоисвои
Привязка к вендорунетGoogleTwitchBuf
// chapter 03.5

НЕ БОЙТЕСЬ ПИСАТЬ
OPEN SOURCE

// stars · github

Один человек — одна экосистема

gRPC-Gateway

Yuki Yugui Sonoda

18.3K
gRPC-Web

Google team

8.7K
Twirp

Twitch team

7.2K
ConnectRPC

Akshay Shah & co

3.0K
// take

Битва не проиграна

  • gRPC‑Gateway — лидер, если нужен REST-фасад
  • gRPC‑Web — ставка на будущее (WebTransport)
  • Twirp — жив, но без массы
  • ConnectRPC — для тех, кто уже на Buf
// chapter 04

K6 И ПРЯМОЙ
БАТЛ

Но кто же
быстрее?

// готовых сравнений в интернете нет

// k6.io

K6 как инструмент

01

Нагрузка

HTTP/1, HTTP/2, gRPC, WebSocket из коробки

02

Browser API

Реальный Chromium для e2e-сценариев

03

Chaos

Fault injection и network throttling

// rig

Стенд

32 GB
memory
8
threads
NUC
Intel NUC
// throughput · req/sec · больше = лучше

Больше лучше — отдача

PayloadgRPC‑GatewayConnectRPCgRPC‑WebTwirp
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.

// throughput · после тюнинга gateway

Больше лучше — отдача (потюнили)

PayloadgRPC‑GatewayConnectRPCgRPC‑WebTwirp
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

После тюнинга — разница в пределах погрешности.

// chapter 05

ВЫВОДЫ

// agenda

О чём поговорили

  • Преимущества gRPC
  • Проблемы в WEB
  • Все попытки завоевать WEB
  • K6 и прямой батл между фреймворками
  • Выводы
// take aways

Что выбрать

Нужен REST-фасад

gRPC‑Gateway

Готовы к WebTransport

gRPC‑Web

Минимализм без streams

Twirp

Стек на Buf

ConnectRPC

Производительность — не критерий. Все четверо в одной весовой категории.

// fin

Спасибо.
Вопросы?

// telegram
@zergslaw
// site
sipki.online