Новинка Виртуальный VDS сервер в Нидерландах от 390р
Выберите продукт

gRPC‑Web на практике: проксирование фронта к gRPC‑сервисам через Envoy

Если бэкенд говорит на gRPC, а браузер — нет, помогает gRPC‑Web и Envoy. Разбираем архитектуру, минимальный конфиг, CORS, таймауты и стриминг, TLS/HTTP2, ретраи и health‑check, логи и отладку. Пошаговые примеры и типовые ловушки, чтобы быстрого запуска в прод без сюрпризов.
gRPC‑Web на практике: проксирование фронта к gRPC‑сервисам через Envoy

gRPC — удобный RPC‑протокол для быстрой и типобезопасной связи сервисов. Но браузеры не умеют в родной gRPC поверх HTTP/2 с трейлерами. Для этого существует gRPC‑Web: тонкая надстройка, которая упаковывает запрос/ответ в HTTP, понятный фронтенду, и обратно. На практике самое удобное место для такой трансляции — прокси Envoy. Он принимает запросы с фронта (HTTP/1.1 или HTTP/2), выполняет CORS, таймауты, ретраи, логирование и отправляет чистый gRPC в backend по HTTP/2.

Архитектура и поток данных

Базовая схема:

  • Frontend обращается к Envoy по HTTP/1.1 или HTTP/2, используя клиентскую библиотеку gRPC‑Web.
  • В Envoy установлен HTTP‑фильтр grpc_web, который транскодирует gRPC‑Web в нативный gRPC, и обратно.
  • Далее Envoy проксирует трафик в backend‑кластер по HTTP/2, используя настройки балансировки, ретраев и health‑check.

Ключевое: downstream (браузер → Envoy) может быть HTTP/1.1, а upstream (Envoy → сервис) — обязательно HTTP/2 для полноценной поддержки gRPC.

Минимальный конфиг Envoy для gRPC‑Web

Ниже — рабочий скелет envoy.yaml для локального старта (порт 8080), с HTTP‑фильтрами gRPC‑Web и CORS, и проксированием к бэкенду на 50051.

static_resources:
  listeners:
  - name: listener_http
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          codec_type: AUTO
          access_log:
          - name: envoy.access_loggers.stdout
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
              log_format:
                text_format: "[%START_TIME%] %DOWNSTREAM_REMOTE_ADDRESS% \"%REQ(:METHOD)% %REQ(:PATH)%\" %RESPONSE_CODE% grpc=%RESP(grpc-status)% %RESPONSE_FLAGS% %DURATION%ms\n"
          route_config:
            name: local_route
            virtual_hosts:
            - name: grpc_web_host
              domains: ["*"]
              routes:
              - match:
                  prefix: "/api"
                route:
                  cluster: backend_grpc
                  timeout: 0s
                typed_per_filter_config:
                  envoy.filters.http.cors:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy
                    allow_origin_string_match:
                    - safe_regex:
                        google_re2: {}
                        regex: "^https://app.example.com$"
                    allow_methods: "POST, OPTIONS"
                    allow_headers: "content-type,x-grpc-web,x-user-agent,grpc-timeout,authorization"
                    expose_headers: "grpc-status,grpc-message"
                    allow_credentials: false
                    max_age: "86400"
          http_filters:
          - name: envoy.filters.http.grpc_web
          - name: envoy.filters.http.cors
          - name: envoy.filters.http.router

  clusters:
  - name: backend_grpc
    type: STRICT_DNS
    connect_timeout: 2s
    lb_policy: ROUND_ROBIN
    http2_protocol_options: {}
    load_assignment:
      cluster_name: backend_grpc
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 50051
    health_checks:
    - timeout: 1s
      interval: 5s
      unhealthy_threshold: 2
      healthy_threshold: 2
      grpc_health_check: {}

admin:
  address:
    socket_address:
      address: 127.0.0.1
      port_value: 9901

Комментарии к конфигу:

  • codec_type: AUTO позволяет приём и HTTP/1.1, и HTTP/2 от фронта.
  • http_filters: порядок важен — сначала grpc_web, потом cors, затем router.
  • timeout: 0s на маршруте отключает общий таймаут — полезно для длительных server‑streaming ответов. Для unary‑запросов лучше ставить явный таймаут.
  • http2_protocol_options: {} на кластере включает HTTP/2 к бэкенду, без него gRPC работать не будет.
  • В cors указываем точное происхождение, заголовки и методы. allow_credentials: false при звёздочке в Origin — обязательное ограничение стандарта; если нужны куки/авторизация — явно перечисляйте домены. Хорошая практическая памятка по заголовкам — «CORS: шпаргалка для Nginx и Apache»: как правильно выставлять CORS.

Запуск локально

docker run --rm -p 8080:8080 -p 9901:9901 -v $PWD/envoy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy:v1.31-latest

После старта Envoy доступен на 8080. Admin‑интерфейс — на 9901 (держите открытым только локально или за ACL).

Frontend: вызов gRPC‑Web

На фронте используем официальный клиент. Пример вызова unary‑метода:

import { EchoServiceClient } from "./generated/echo_grpc_web_pb";
import { EchoRequest } from "./generated/echo_pb";

const client = new EchoServiceClient("https://api.example.com");

const req = new EchoRequest();
req.setMessage("hello");

client.echo(req, {}, (err, resp) => {
  if (err) {
    console.error("gRPC-Web error", err.code, err.message);
    return;
  }
  console.log("response:", resp.getMessage());
});

Важно помнить специфику gRPC‑Web:

  • Браузер почти всегда получает HTTP‑код 200, а статус операции в grpc-status и grpc-message. Обрабатывайте ошибки на уровне клиента.
  • Поддерживается unary и server‑streaming. Client‑streaming и bidi‑streaming в gRPC‑Web недоступны.
  • Режимы сериализации: бинарный (application/grpc-web) и текстовый (grpc-web-text, base64). Предпочтителен бинарный для производительности.

CORS без боли

Чаще всего проблемы именно в CORS. Минимум, что нужно разрешить для gRPC‑Web:

  • POST и OPTIONS методы;
  • заголовки: content-type, x-grpc-web, x-user-agent, grpc-timeout, плюс ваши авторизационные (authorization или cookie);
  • в expose_headers вернуть grpc-status и grpc-message — иначе клиент не увидит статус.

Проверка preflight:

curl -i -X OPTIONS -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type,x-grpc-web" http://localhost:8080/api/your.Service/Method

Таймауты, ретраи и стриминг

По умолчанию Envoy может иметь маршрутный таймаут (например, 15 секунд), что ломает долгие стримы. Для stream‑методов используйте timeout: 0s на маршруте или выносите их в отдельный префикс/вирт‑хост с особыми значениями. Для unary‑методов задавайте ограничение через grpc-timeout на клиенте и маршрутный timeout в Envoy, например 5–10 секунд.

route:
  cluster: backend_grpc
  timeout: 10s
  retry_policy:
    retry_on: 5xx,connect-failure,refused-stream,reset
    num_retries: 2
    per_retry_timeout: 2s

Ретраи включайте только для идемпотентных методов, иначе рискуете получить двойную запись. Для стриминга ретраи обычно выключают.

Фрагменты конфигурации Envoy и access‑логи для gRPC‑Web.

TLS и HTTP/2: нюансы

Для браузера чаще используется HTTPS, а к бэкенду — HTTP/2 с TLS или h2c (HTTP/2 без TLS) внутри приватной сети. Важные моменты:

  • Upstream должен уметь HTTP/2: в кластере включён http2_protocol_options.
  • При h2c укажите соответствующую схему подключения к upstream. Для TLS — настройте доверенный CA и SNI, если требуется. Подбор и выпуск сертификатов упростят SSL-сертификаты. Также может пригодиться путеводитель по HTTPS и HSTS: настройка HTTPS с HSTS.
  • На downstream можно оставить codec_type: AUTO, чтобы клиент поднимал HTTP/2 по ALPN при необходимости.

Ограничения размеров и буферизация

Большие сообщения могут упираться в лимиты либо на сервере gRPC, либо в Envoy. Для Envoy есть настройки HTTP‑менеджера и кластера, ограничивающие размер заголовков и тела. Если передаёте большие payload‑ы, проверьте:

  • лимиты на стороне фреймворка gRPC сервиса (max receive/send message size);
  • маршрутные таймауты и буферизацию в Envoy;
  • правила защиты upstream (WAF, прокси), которые могут резать бинарные данные.

Здоровье и балансировка

Envoy умеет выполнять gRPC health‑check через стандартный grpc.health.v1.Health. Это быстрее и точнее HTTP‑пинга. Для балансировки используйте ROUND_ROBIN и при необходимости включайте outlier detection, чтобы выкидывать сбойные инстансы.

outlier_detection:
  consecutive_5xx: 5
  interval: 10s
  base_ejection_time: 30s
  max_ejection_percent: 50

Логи и метрики

Для диагностики пригодится расширенный формат access‑логов с gRPC‑статусом, временем и флагами:

access_log:
- name: envoy.access_loggers.stdout
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
    log_format:
      text_format: "[%START_TIME%] %REQ(:METHOD)% %REQ(:PATH)% status=%RESPONSE_CODE% grpc=%RESP(grpc-status)% dur=%DURATION%ms bytes=%BYTES_SENT% flags=%RESPONSE_FLAGS%\n"

Следите за %RESPONSE_FLAGS% (например, UH, UF, DC) и %RESPONSE_CODE_DETAILS% — по ним проще понять источник обрыва или таймаута.

Поток: HTTP/1.1 → gRPC‑Web → Envoy → gRPC по HTTP/2.

Отладка типовых проблем

  • CORS: preflight не проходит. Проверьте, что OPTIONS попадает в маршрут и что фильтр CORS включён до роутера. Убедитесь, что allow_headers содержит все заголовки клиента.
  • На фронте всегда 200, но «не работает». Смотрите grpc-status и grpc-message в ответе, а не HTTP‑код.
  • Стриминг обрывается через N секунд. Проверяйте маршрутный timeout и stream_idle_timeout в HTTP‑менеджере/кластерных настройках. Для стримов поставьте timeout: 0s.
  • Периодические RST_STREAM. Это может быть ретрай по refused-stream или лимит на потоках upstream. Проверьте max_concurrent_streams на сервере и в http2_protocol_options.
  • Большие сообщения режутся. Увеличьте лимиты в сервисе и проверьте политики безопасности между Envoy и backend.

Полезные команды

# Проверка preflight
curl -i -X OPTIONS -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type,x-grpc-web" http://localhost:8080/api/your.Service/Method

# Unary запрос в gRPC-Web (payload.bin содержит сериализованный protobuf)
curl -i -H "Content-Type: application/grpc-web" -H "X-Grpc-Web: 1" -H "Origin: https://app.example.com" --data-binary @payload.bin http://localhost:8080/api/your.Service/Method

# Проверка gRPC upstream напрямую (без Envoy)
grpcurl -d '{"message":"hello"}' -plaintext 127.0.0.1:50051 your.Service/Echo

Производительность и устойчивость

  • Keepalive: включите HTTP/2 keepalive к upstream, чтобы быстрее выявлять обрывы и избегать «залипших» соединений.
  • Пулы соединений: регулируйте конкаренси через max_requests_per_connection и max_concurrent_streams, чтобы не душить сервер.
  • Ретраи: ограничивайте количество и используйте только для безопасных операций. Настройте per_retry_timeout и джиттер задержек на стороне клиента.
  • Лимиты: добавьте circuit breakers, чтобы защищать backend при всплесках.
circuit_breakers:
  thresholds:
  - priority: DEFAULT
    max_connections: 1024
    max_pending_requests: 1024
    max_requests: 2048
    max_retries: 3

Безопасность

  • Всегда ограничивайте allow_origin конкретными доменами. Для мультидоменов используйте безопасные паттерны string match.
  • Если используете cookie‑аутентификацию, учитывайте SameSite и включайте allow_credentials с точным списком доменов.
  • Сегментируйте API‑префикс (например, /api) отдельным виртуальным хостом с жёсткими маршрутными правилами.
  • Логируйте и анонимизируйте чувствительные заголовки в access‑логах.

Деплой и инфраструктура

Envoy удобно размещать на отдельном инстансе рядом с фронтом или как edge‑прокси. Для гибкости и изоляции поднимайте его на VDS и держите конфигурацию в Git. Внешний TLS заверните на Envoy, а внутрь сети ведите h2c или mTLS по требованиям. При необходимости используйте менеджмент‑плэйн с централизованной конфигурацией.

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды

Чек‑лист перед продом

  • Работают unary и streaming методы, время и статус корректны на фронте.
  • CORS проходит preflight, выставлены expose_headers для grpc-status/grpc-message.
  • Для stream‑маршрутов timeout: 0s или нужные увеличенные таймауты.
  • Upstream по HTTP/2, включён health‑check и балансировка.
  • Ретраи только для идемпотентных методов, ограничены по времени и количеству.
  • Достаточные лимиты размеров сообщений, настроены circuit breakers.
  • Логи содержат gRPC‑статус, флаги и время, алёрты повешены на ошибки/дедлайны.
  • TLS настроен на внешнем фронте, сертификаты и цепочки валидны.

Итоги

Envoy снимает ключевые барьеры между браузерным frontend и gRPC backend, предоставляя готовый мост gRPC‑Web, CORS, балансировку и наблюдаемость. С правильными таймаутами, ретраями и health‑check получается надёжный слой, который прозрачно транскодирует трафик, масштабируется вместе с сервисами и не требует костылей на фронте. Начните с минимального конфига, явно задайте CORS и таймауты, а дальше подкручивайте производительность и отказоустойчивость под ваш трафик.

Поделиться статьей

Вам будет интересно

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...