open atlas
↑ К треку
Go с нуля до senior GO · 09 · 03

Gateway и REST: аннотации google.api.http, пределы транскодинга, gRPC-Web и альтернатива ConnectRPC

grpc-gateway компилирует аннотации google.api.http в прокси, переводящий JSON REST в protobuf-RPC, — стандартная топология «внутри gRPC, снаружи REST». Стриминг и маппинг ошибок — шершавые края, браузерам нужен прокси из-за трейлеров, ConnectRPC сворачивает стек в обычный HTTP.

GO Senior ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Публичный API-gateway умер в 14:07, а gRPC-бэкенд за ним почти ничего не заметил. Миграция оставила блокировку на одной таблице Postgres, и RPC заказов уехал с 40 мс до 90 секунд. Gateway — сгенерированный grpc-gateway, задеплоенный восемь месяцев назад — пересылал каждый REST-запрос как gRPC-вызов без дедлайна: его никто не настроил, и каждый вызов фактически ждал вечно. Каждый входящий запрос парковал горутину на стриме, который ответит через полторы минуты; при 30 запросах в секунду это 2 700 новых запаркованных горутин в минуту, каждая держит буферы запроса и HTTP/2-стрим. RSS рос одиннадцать минут, потом OOM-киллер снёс gateway — все эндпоинты, включая совершенно здоровые и сам health-check. Одна медленная таблица превратилась в полный аутедж публичного API, потому что слой перевода между двумя культурами таймаутов — REST-клиентами, которые просто вешают трубку, и gRPC-серверами, которые ждут дедлайн, — был настроен без обеих.

Аннотации компилируются в прокси

Прежде чем писать рукописный REST-адаптер для gRPC-сервиса, спросите себя: что произойдёт, когда появится шестидесятый RPC? Именно этот вопрос делает генерируемый подход первым кандидатом для изучения.

Внешние потребители не поставят protoc. Стандартный ответ оставляет gRPC внутри и компилирует REST-фасад из того же контракта — аннотации google.api.http на каждом RPC:

import "google/api/annotations.proto";

service Orders {
  rpc GetOrder(GetOrderRequest) returns (Order) {
    option (google.api.http) = { get: "/v1/orders/{order_id}" };
  }
  rpc CreateOrder(CreateOrderRequest) returns (Order) {
    option (google.api.http) = { post: "/v1/orders", body: "*" };
  }
}

Правила привязки механические: переменные шаблона пути вроде order_id привязываются к одноимённому полю сообщения запроса; для GET каждое оставшееся скалярное поле становится принимаемым query-параметром; body: "*" отображает весь JSON-боди на сообщение запроса при записи. Из этого protoc-gen-grpc-gateway генерирует реверс-прокси — runtime.ServeMux, который сам по себе просто http.Handler, так что применимо всё из юнита про HTTP: оборачивайте в middleware, монтируйте под путём, ограничивайте по времени.

gw := runtime.NewServeMux()
// Дозванивается до gRPC-бэкенда; JSON на входе, protobuf по HTTP/2, JSON на выходе.
err := orderspb.RegisterOrdersHandlerFromEndpoint(ctx, gw, "orders:50051", dialOpts)
// Строка, которой не хватило в крючке, — никогда не проксируй без бюджета:
srv := &http.Server{Addr: ":8080", Handler: withTimeout(gw, 5*time.Second)}

На каждый запрос gateway делает: декодирует JSON в сгенерированную структуру запроса, вызывает RPC через настоящее клиентское gRPC-соединение (интерсепторы и клиентская балансировка прилагаются), кодирует ответ обратно в JSON. Есть и in-process-вариант — RegisterOrdersHandlerServer, вызывающий вашу серверную реализацию напрямую без дозвона; он экономит хоп, но обходит клиентские интерсепторы и статистику, поэтому большинство продакшен-схем держат настоящее соединение. Те же аннотации питают protoc-gen-openapiv2: публичный OpenAPI-документ генерируется из контракта, а не дрейфует от него.

Викторина

Для rpc ListOrders(ListOrdersRequest) с аннотацией get: /v1/orders в сообщении запроса есть поля page_size и customer_id. Как REST-клиент их передаст?

Что отказывается переводиться: стримы, ошибки, дедлайны

Транскодинг честен с unary-вызовами и уклончив со всем остальным. Серверный стриминг технически работает: gateway отдаёт NDJSON-чанки (Newline-Delimited JSON — последовательность независимых JSON-объектов, разделённых переводом строки), каждый завёрнут в конверт result — это не JSON-массив, что ломает любого клиента, зовущего response.json() в ожидании одного объекта. Клиентский и bidi-стриминг не отображаются на request/response-REST вовсе; если они нужны браузеру, эндпоинту нужен другой дизайн — вебсокеты, gRPC-Web или Connect, а не аннотация gateway.

Ошибкам нужна таблица перевода, потому что два мира не согласны, что такое статус. Gateway поставляет дефолтный маппинг — InvalidArgument в 400, NotFound в 404, PermissionDenied в 403, Unavailable в 503, DeadlineExceeded в 504, Internal и Unknown в 500 — и кладёт в тело JSON google.rpc.Status. В день, когда публичному API понадобится собственный конверт ошибок, вы ставите runtime.WithErrorHandler и владеете маппингом сами; сделайте это осознанно один раз, а не по эндпоинту за раз.

Дедлайны — это крючок. REST-клиенты не шлют grpc-timeout; они просто вешают трубку, а повесивший трубку HTTP-клиент не отменяет автоматически вышестоящий RPC, если контекст запроса не прокинут насквозь, — и даже тогда неотвечающий бэкенд держит горутину, пока что-то её не ограничит. Gateway уважает входящий заголовок Grpc-Timeout, если он есть (от публичных клиентов — почти никогда), и поддерживает дефолт через runtime.DefaultContextTimeout или обычные таймауты http.Server/middleware. Правило, которое спасло бы крючок: прокси перед gRPC обязан порождать дедлайны, а не только пересылать их, потому что его публичные вызыватели не пришлют бюджет никогда.

Почему это работает

Почему прокси генерируют из аннотаций, а не пишут маленький REST-слой руками? Ответ даёт счёт эндпоинтов: на пяти рукописный выигрывает гибкостью. На шестидесяти рукописный слой — это шестьдесят возможностей дрейфа между REST-документацией и реальностью proto, шестьдесят самодельных маппингов ошибок и команда, обновляющая три места на каждое поле. Сгенерированный прокси переносит ревью API в .proto-файл — который прошлый урок уже загейтил через buf breaking, — а OpenAPI выпадает из тех же аннотаций бесплатно.

Выбор топологии: gateway, gRPC-Web или Connect

Типовая продакшен-топология теперь видна целиком: сервисы говорят друг с другом на gRPC — скомпилированные контракты, распространяемые дедлайны, стримы, — а публичный край держит сгенерированный gateway, говорящий на REST и JSON с теми, кому .proto-файл в руки не дашь. Внутренние команды получают строгий контракт; внешние потребители — curl, OpenAPI-документацию и middleware с API-ключами на обычном http.Handler.

Браузеры всё усложняют. Браузерный fetch не умеет читать HTTP/2-трейлеры — а gRPC кладёт grpc-status ровно туда, — поэтому нативный браузерный gRPC невозможен независимо от поддержки HTTP/2. gRPC-Web (протокол-прослойка, перекладывающий статус из трейлеров в тело или заголовки) — обходной протокол: статус переезжает в тело или заголовки, а прокси (фильтр grpc_web у Envoy или gateway) переводит в настоящий gRPC. Работает — ценой ещё одной движущейся детали на каждом браузерном пути.

ConnectRPC — честная современная альтернатива всему этому бутерброду: один сервер говорит на трёх протоколах на одном порту — gRPC для существующих внутренних клиентов, gRPC-Web для браузеров без прокси и протокол Connect, где unary-вызов — обычный HTTP POST с JSON-телом, который можно дёрнуть curl-ом, а ошибки живут в теле ответа, не в трейлерах. Тот же .proto, другой сгенерированный рантайм. Размен — возраст экосистемы: у grpc-gateway десятилетие продакшен-шрамов, Connect моложе и беднее инструментами — но для новой Go-системы, которой нужны браузеры и curl-доступность, он вычёркивает gateway и Envoy-фильтр из архитектурной диаграммы.

Какой бы край вы ни выбрали, protobuf-first-поток оказывает тихое проектное давление, которое стоит назвать вслух: аннотации заставляют формулировать каждую операцию как путь ресурса и глагол, с явными сообщениями запроса и ответа. Команды, начинающие с REST, часто пропускают это моделирование и отращивают суп из глаголов; команды, компилирующие REST из контракта, получают ресурсную дисциплину, навязанную тулчейном.

Викторина

Почему браузер не может быть нативным gRPC-клиентом даже по HTTP/2?

Вспомните перед уходом
  1. 01
    Проследи REST-вызов через grpc-gateway, назвав правила привязки и две участвующие таблицы перевода.
  2. 02
    Сравни grpc-gateway, gRPC-Web и ConnectRPC как края для одного Go-бэкенда — что решает каждый и какой ценой?
Итог

Паттерн gateway замыкает арку юнита: тот же .proto, чьи теги вы версионируете, теперь компилирует собственный публичный край. Аннотации google.api.http объявляют REST-маппинги на каждый RPC — переменные пути привязываются к полям сообщения по имени, оставшиеся скаляры становятся query-параметрами в GET, а body со звёздочкой отображает JSON-тела при записи. protoc-gen-grpc-gateway превращает это в реверс-прокси, который остаётся просто http.Handler: JSON декодируется в сгенерированную структуру запроса, вызов идёт через настоящее клиентское gRPC-соединение, ответ кодируется обратно, а protoc-gen-openapiv2 выпускает публичную документацию из тех же аннотаций — документация не может уйти от контракта. У транскодинга есть края: серверные стримы приходят как JSON с конвертом result, разделённый переводами строк, а не массив; клиентский и bidi-стриминг не отображаются вовсе; ошибки пересекают осознанную таблицу — NotFound в 404, Unavailable в 503, DeadlineExceeded в 504, — заменяемую один раз глобально через WithErrorHandler. Дороже всего операционный урок: REST-клиенты не шлют таймаут, поэтому gateway обязан порождать дедлайны через DefaultContextTimeout или серверный middleware — неограниченный прокси превращает одну медленную таблицу бэкенда в кучу горутин и полный публичный аутедж. Браузеры добавляют жёсткое ограничение: fetch не читает трейлеры, где живёт grpc-status, поэтому браузерным путям нужен gRPC-Web за фильтром Envoy — или ConnectRPC, отдающий gRPC, gRPC-Web и curl-доступный Connect-JSON с одного порта и убирающий прокси целиком, в обмен на более молодую экосистему. Стандартная топология стоит: нативный gRPC внутри, скомпилированный перевод на краю и ресурсное моделирование, навязанное самим контрактом. Теперь, когда вы проектируете сервис с gRPC внутри и REST снаружи, первое, что вы настроите на gateway, — дедлайн: ещё до того, как первый внешний запрос доберётся до бэкенда.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.