gRPC — это высокопроизводительный RPC-фреймворк от Google, который работает поверх HTTP/2 и становится стандартом де-факто для межсервисного взаимодействия. Но как только вы пытаетесь пустить gRPC-трафик через прокси, сразу возникают проблемы: большинство классических прокси-серверов не умеют работать с HTTP/2 и долгоживущими стриминговыми соединениями. В этой статье разберём, как правильно настроить gRPC через прокси — от выбора архитектуры до конкретных конфигураций Nginx, Envoy и HAProxy.
Почему gRPC плохо работает с обычными прокси
Чтобы понять проблему, нужно разобраться в том, как gRPC устроен под капотом. Протокол использует HTTP/2 в качестве транспортного слоя, и это кардинально отличает его от привычного REST поверх HTTP/1.1. Большинство корпоративных прокси, межсетевых экранов и балансировщиков нагрузки проектировались в эпоху HTTP/1.1 и просто не умеют обрабатывать мультиплексированные потоки HTTP/2.
Вот конкретные проблемы, с которыми вы столкнётесь при попытке пустить gRPC через классический HTTP-прокси:
- Downgrade до HTTP/1.1. Многие прокси автоматически понижают версию протокола. gRPC требует HTTP/2 — без него соединение просто не установится, а клиент получит ошибку
UNAVAILABLE. - Разрыв долгоживущих соединений. gRPC активно использует серверный и двунаправленный стриминг. Прокси с агрессивными таймаутами (особенно AWS ELB Classic, некоторые версии Squid) обрывают соединения, которые не передают данные дольше 60 секунд.
- Проблемы с Content-Type. gRPC использует заголовок
Content-Type: application/grpc. Прокси, не знающий этого типа, может отклонить запрос или некорректно буферизовать тело. - Trailers (трейлеры). gRPC использует HTTP/2 trailers для передачи статуса завершения вызова. HTTP/1.1 прокси не поддерживают trailers — информация о статусе будет потеряна.
- Буферизация тела. Некоторые прокси буферизуют всё тело ответа перед передачей клиенту. Для стриминговых gRPC-вызовов это означает, что клиент не получит ни одного сообщения, пока стрим не завершится.
Ключевой вывод:
Для работы gRPC через прокси нужен прокси с поддержкой HTTP/2 end-to-end или специальный режим туннелирования. Классический HTTP-прокси без доработки конфигурации не подойдёт.
HTTP/2 туннелирование: как это работает
Существует два принципиально разных подхода к проксированию gRPC-трафика, и важно понимать разницу между ними, чтобы выбрать правильное решение для своей архитектуры.
Подход 1: HTTP/2 end-to-end (рекомендуется)
Прокси понимает HTTP/2 и устанавливает HTTP/2-соединение как с клиентом, так и с бэкендом. Он может анализировать отдельные стримы, применять балансировку на уровне запросов, добавлять заголовки и выполнять TLS-терминацию. Это самый функциональный вариант — так работают Envoy, Nginx (начиная с версии 1.13.10) и gRPC-aware балансировщики в облачных провайдерах.
Подход 2: TCP-туннелирование (CONNECT)
Прокси не анализирует HTTP/2 трафик, а просто создаёт прозрачный TCP-туннель через метод CONNECT. Клиент устанавливает TLS-соединение напрямую с бэкендом через туннель. Прокси видит только зашифрованный поток байт. Этот метод проще в настройке, но лишает вас возможностей балансировки на уровне gRPC-запросов и добавления заголовков.
| Характеристика | HTTP/2 end-to-end | TCP CONNECT туннель |
|---|---|---|
| Балансировка по запросам | ✅ Да | ❌ Нет (только по TCP) |
| TLS-терминация на прокси | ✅ Да | ❌ Нет |
| Добавление заголовков | ✅ Да | ❌ Нет |
| Сложность настройки | Средняя | Низкая |
| Поддержка стриминга | ✅ Полная | ✅ Полная |
| Observability (метрики) | ✅ Детальная | ❌ Только TCP |
Настройка Nginx как gRPC-прокси
Nginx поддерживает проксирование gRPC начиная с версии 1.13.10 (февраль 2018 года). Для работы необходим модуль ngx_http_grpc_module, который входит в стандартную сборку. Важно: Nginx поддерживает HTTP/2 на стороне клиента (фронтенд), но на стороне бэкенда использует HTTP/2 только для gRPC — обычный HTTP upstream работает по HTTP/1.1.
Базовая конфигурация gRPC-прокси на Nginx
server {
listen 443 ssl http2;
server_name grpc.example.com;
# TLS-сертификаты
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# Современные TLS-параметры
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
# Директива grpc_pass вместо proxy_pass
grpc_pass grpc://grpc_backend;
# Таймауты для долгоживущих стримов
grpc_read_timeout 3600s;
grpc_send_timeout 3600s;
grpc_connect_timeout 5s;
# Передача реального IP клиента
grpc_set_header X-Real-IP $remote_addr;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
upstream grpc_backend {
server backend-1:50051;
server backend-2:50051;
server backend-3:50051;
# Keepalive для HTTP/2 соединений
keepalive 32;
}
Обратите внимание на несколько критически важных деталей. Во-первых, используется директива grpc_pass, а не proxy_pass — это разные модули с разным поведением. Во-вторых, таймауты grpc_read_timeout и grpc_send_timeout установлены в 3600 секунд (1 час) — это важно для серверных стримов, которые могут работать долго без передачи данных. В-третьих, keepalive 32 в upstream позволяет переиспользовать HTTP/2 соединения с бэкендами.
Конфигурация для незашифрованного gRPC (grpc://)
server {
listen 80 http2;
server_name grpc-internal.example.com;
location / {
grpc_pass grpc://127.0.0.1:50051;
# Обработка ошибок gRPC
error_page 502 = /error502grpc;
}
location = /error502grpc {
internal;
default_type application/grpc;
add_header grpc-status 14;
add_header content-length 0;
return 204;
}
}
Блок error502grpc — важная деталь: при недоступности бэкенда он возвращает корректный gRPC-статус UNAVAILABLE (14) вместо HTTP 502, который gRPC-клиент не сможет корректно обработать.
Envoy Proxy: лучший выбор для gRPC в микросервисах
Envoy создавался Lyft именно для микросервисной архитектуры, и поддержка gRPC в нём реализована на самом глубоком уровне. Он понимает Protocol Buffers, умеет транскодировать gRPC в REST, собирает детальные метрики по каждому RPC-методу и является основой для service mesh решений — Istio, AWS App Mesh и других. Если вы строите серьёзную микросервисную архитектуру, Envoy — это стандарт индустрии.
Базовая конфигурация Envoy для gRPC
static_resources:
listeners:
- name: grpc_listener
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: grpc_proxy
codec_type: HTTP2
route_config:
name: grpc_routes
virtual_hosts:
- name: grpc_services
domains: ["*"]
routes:
- match:
prefix: "/com.example.UserService"
route:
cluster: user_service
timeout: 30s
retry_policy:
retry_on: "reset,connect-failure,retriable-status-codes"
num_retries: 3
retriable_status_codes: [14]
- match:
prefix: "/com.example.OrderService"
route:
cluster: order_service
timeout: 60s
http_filters:
- name: envoy.filters.http.grpc_stats
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_stats.v3.FilterConfig
emit_filter_state: true
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: user_service
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: user_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: user-service
port_value: 50051
Ключевые преимущества этой конфигурации: маршрутизация по gRPC-сервисам (префикс пути соответствует полному имени сервиса в формате package.ServiceName), автоматические ретраи при статусе UNAVAILABLE и сбор gRPC-метрик через фильтр grpc_stats.
gRPC-Web транскодинг в Envoy
Одна из killer-фич Envoy — встроенный транскодер gRPC-Web. Браузеры не поддерживают gRPC напрямую (из-за ограничений Fetch API на HTTP/2 трейлеры), поэтому используется протокол gRPC-Web. Envoy может автоматически конвертировать gRPC-Web запросы от браузера в обычный gRPC для бэкенда:
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
HAProxy и gRPC: конфигурация с балансировкой нагрузки
HAProxy поддерживает gRPC начиная с версии 1.9.2. Он работает на уровне TCP или HTTP/2, умеет балансировать gRPC-трафик и выполнять health checks. HAProxy — хороший выбор, если вы уже используете его в инфраструктуре и хотите добавить поддержку gRPC без введения нового компонента.
global
maxconn 50000
log stdout format raw local0
defaults
log global
timeout connect 5s
timeout client 3600s
timeout server 3600s
frontend grpc_frontend
bind *:443 ssl crt /etc/haproxy/certs/server.pem alpn h2,http/1.1
mode http
option http-use-htx
default_backend grpc_servers
backend grpc_servers
mode http
balance leastconn
option http-use-htx
# Health check через gRPC Health Checking Protocol
option httpchk GET /grpc.health.v1.Health/Check
http-check expect status 200
server grpc1 10.0.0.1:50051 check ssl verify none
server grpc2 10.0.0.2:50051 check ssl verify none
server grpc3 10.0.0.3:50051 check ssl verify none
Обратите внимание на несколько важных настроек. alpn h2,http/1.1 в директиве bind указывает, что HAProxy принимает как HTTP/2, так и HTTP/1.1 соединения через TLS ALPN negotiation. timeout client 3600s и timeout server 3600s — критически важные параметры для долгоживущих gRPC-стримов. Алгоритм leastconn предпочтительнее roundrobin для gRPC, поскольку стриминговые соединения могут быть долгоживущими и неравномерно нагружать бэкенды.
TLS-терминация и сквозное шифрование для gRPC
gRPC по умолчанию предполагает использование TLS — это часть спецификации. На практике в микросервисной архитектуре возникает вопрос: где выполнять TLS-терминацию и как организовать шифрование между компонентами? Существует три основных паттерна.
Паттерн 1: TLS-терминация на прокси (Edge TLS)
Прокси принимает зашифрованный трафик от клиентов, расшифровывает его и передаёт на бэкенды по незашифрованному каналу (или с отдельным TLS). Это самый распространённый подход в корпоративных сетях. Бэкенды могут использовать grpc.Insecure() для упрощения конфигурации.
Паттерн 2: Сквозное шифрование (mTLS)
Mutual TLS (mTLS) — стандарт для service mesh. Каждый сервис имеет собственный сертификат, и при каждом соединении обе стороны проверяют сертификаты друг друга. Это обеспечивает аутентификацию сервисов и шифрование трафика внутри кластера. Именно так работает Istio с Envoy sidecar-прокси.
# Go: gRPC-сервер с mTLS
import (
"crypto/tls"
"crypto/x509"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func createMTLSCredentials() credentials.TransportCredentials {
cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
caCert, _ := os.ReadFile("ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
}
return credentials.NewTLS(tlsConfig)
}
// Создание сервера с mTLS
creds := createMTLSCredentials()
server := grpc.NewServer(grpc.Creds(creds))
Паттерн 3: TLS Passthrough
Прокси работает на уровне TCP и не расшифровывает трафик — просто перенаправляет зашифрованные байты на бэкенд. Это самый простой вариант с точки зрения прокси, но он лишает возможности анализировать gRPC-трафик, добавлять заголовки или выполнять балансировку на уровне запросов.
Балансировка нагрузки для gRPC-стримов: особенности и решения
Балансировка нагрузки для gRPC — нетривиальная задача, и вот почему. В HTTP/1.1 каждый запрос — это отдельное TCP-соединение (или соединение из пула), и балансировщик легко распределяет запросы по бэкендам. В HTTP/2 одно TCP-соединение мультиплексирует множество потоков — если балансировщик работает на уровне TCP, все потоки одного соединения попадут на один бэкенд.
Для gRPC это означает: если клиент установил одно HTTP/2 соединение и отправляет через него 100 RPC-запросов, при TCP-балансировке все 100 запросов обработает один инстанс бэкенда. Остальные бэкенды будут простаивать. Решение — балансировка на уровне HTTP/2 стримов (L7 балансировка).
Алгоритмы балансировки для gRPC
| Алгоритм | Подходит для gRPC | Комментарий |
|---|---|---|
| Round Robin | ✅ Да | Хорош для унарных RPC с примерно одинаковым временем обработки |
| Least Connection | ✅ Лучший выбор | Учитывает активные стримы, равномерно распределяет нагрузку |
| Random | ⚠️ Условно | Работает при большом количестве запросов, неравномерен при малом |
| IP Hash | ❌ Плохо | Привязывает клиента к одному бэкенду, нет смысла при L7 |
| Pick First (gRPC built-in) | ❌ Не для продакшена | Все запросы идут на первый доступный сервер |
Клиентская балансировка в gRPC
gRPC поддерживает клиентскую балансировку нагрузки — клиент сам решает, на какой сервер отправить запрос. Это позволяет обойти проблему TCP-мультиплексирования. Для service discovery используется DNS с несколькими A-записями или специальные резолверы (Consul, etcd). Пример настройки клиентской балансировки в Go:
import (
"google.golang.org/grpc"
"google.golang.org/grpc/balancer/roundrobin"
)
// Клиент с round-robin балансировкой через DNS
conn, err := grpc.Dial(
"dns:///grpc-service.internal:50051",
grpc.WithDefaultServiceConfig(
`{"loadBalancingConfig": [{"round_robin":{}}]}`
),
grpc.WithTransportCredentials(creds),
)
// Клиент с least-connection (доступен в gRPC >= 1.58)
conn, err := grpc.Dial(
"dns:///grpc-service.internal:50051",
grpc.WithDefaultServiceConfig(
`{"loadBalancingConfig": [{"least_request":{}}]}`
),
grpc.WithTransportCredentials(creds),
)
Внешние прокси для gRPC: когда и зачем они нужны в микросервисах
Помимо внутренних прокси-компонентов (Nginx, Envoy, HAProxy), в микросервисной архитектуре иногда возникает необходимость использовать внешние прокси-серверы — например, для маршрутизации трафика через конкретные регионы, обхода ограничений сети или изоляции исходящих соединений. Рассмотрим основные сценарии.
Сценарий 1: Геораспределённые микросервисы
Если ваши микросервисы расположены в разных регионах (например, часть в Европе, часть в США), и вам нужно контролировать, через какой IP-адрес устанавливаются gRPC-соединения между регионами, внешние прокси могут помочь организовать предсказуемую маршрутизацию. Для таких задач подходят прокси дата-центров — они обеспечивают стабильные IP-адреса и высокую скорость соединения, что критично для gRPC с его чувствительностью к задержкам.
Сценарий 2: Изоляция исходящего трафика
В некоторых корпоративных окружениях весь исходящий трафик должен проходить через корпоративный прокси. Для gRPC-клиентов, которым нужно обращаться к внешним gRPC-сервисам (например, Google Cloud APIs, которые используют gRPC), это создаёт сложности. Решение — настройка CONNECT-туннеля через корпоративный прокси.
Пример настройки gRPC-клиента для работы через HTTP CONNECT прокси в Go:
import (
"net"
"net/http"
"golang.org/x/net/proxy"
"google.golang.org/grpc"
)
// Использование SOCKS5 прокси для gRPC
proxyDialer, _ := proxy.SOCKS5(
"tcp",
"proxy.example.com:1080",
&proxy.Auth{User: "user", Password: "pass"},
proxy.Direct,
)
conn, err := grpc.Dial(
"grpc-service.example.com:443",
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return proxyDialer.Dial("tcp", addr)
}),
grpc.WithTransportCredentials(creds),
)
Сценарий 3: Тестирование и разработка
При разработке микросервисов часто нужно тестировать поведение сервисов из разных сетевых окружений — проверить, как сервис отвечает при высокой задержке, или протестировать geo-зависимую логику. Для таких задач удобны резидентные прокси, которые позволяют имитировать запросы из конкретных регионов с реальными IP домашних пользователей.
Типичные ошибки при настройке gRPC через прокси и их устранение
Разберём наиболее частые проблемы, с которыми сталкиваются разработчики при настройке gRPC через прокси, и конкретные способы их решения.
Ошибка 1: "transport: received the unexpected content-type"
Симптом:
Клиент получает ошибку transport: received the unexpected content-type "text/html; charset=utf-8"
Причина: Прокси вернул HTML-страницу с ошибкой (например, 502 Bad Gateway) вместо gRPC-ответа. gRPC-клиент не умеет обрабатывать HTML и выдаёт эту ошибку.
Решение: Убедитесь, что бэкенд доступен. Добавьте обработку gRPC-ошибок на уровне прокси (как показано в примере Nginx выше с блоком error502grpc).
Ошибка 2: Соединение обрывается через 60-120 секунд
Симптом:
Стриминговые gRPC-соединения неожиданно закрываются с ошибкой UNAVAILABLE: transport is closing через примерно одинаковый интервал времени.
Причина: Прокси закрывает idle-соединения по таймауту. Классические значения: AWS ELB — 60 секунд, Nginx по умолчанию — 60 секунд.
Решение 1: Увеличьте таймауты на прокси (как показано в примерах выше).
Решение 2: Настройте keepalive на стороне gRPC-клиента:
import "google.golang.org/grpc/keepalive"
kaParams := keepalive.ClientParameters{
Time: 30 * time.Second, // Пинг каждые 30 секунд
Timeout: 10 * time.Second, // Ждать ответа 10 секунд
PermitWithoutStream: true, // Пинговать даже без активных RPC
}
conn, err := grpc.Dial(
"grpc-service:50051",
grpc.WithKeepaliveParams(kaParams),
grpc.WithTransportCredentials(creds),
)
Ошибка 3: HTTP/2 не согласовывается (ALPN failure)
Симптом:
Ошибка transport: failed to dial: context deadline exceeded или no application protocol
Причина: Прокси или промежуточное оборудование не поддерживает ALPN (Application-Layer Protocol Negotiation) или не включён h2 в списке поддерживаемых протоколов.
Решение: Убедитесь, что в конфигурации TLS прокси явно указан протокол h2: alpn h2,http/1.1 (HAProxy) или listen 443 ssl http2 (Nginx).
Ошибка 4: Стриминг зависает — данные не приходят
Симптом:
Серверный стрим работает на бэкенде (видно в логах), но клиент не получает сообщения до завершения стрима.
Причина: Прокси буферизует ответ и отправляет его клиенту только после завершения. Типичная проблема для прокси, настроенных с proxy_buffering on.
Решение для Nginx:
location / {
grpc_pass grpc://backend;
# Отключить буферизацию для gRPC стримов
grpc_buffer_size 0;
# Или для обычного proxy_pass:
proxy_buffering off;
proxy_cache off;
}
Чек-лист диагностики gRPC через прокси
✅ Чек-лист: диагностика gRPC-прокси
- Прокси поддерживает HTTP/2 (проверьте через
curl --http2 -v) - ALPN h2 включён в TLS-конфигурации прокси
- Таймауты клиента и сервера установлены не менее 300 секунд
- Буферизация ответов отключена для gRPC-эндпоинтов
- Настроен gRPC keepalive на клиенте (Time: 30s, Timeout: 10s)
- Ошибки бэкенда возвращаются как gRPC-статусы, а не HTTP-коды
- Health check использует gRPC Health Checking Protocol
- Балансировка работает на L7 (HTTP/2 стримы), а не L4 (TCP)
Заключение
Настройка gRPC через прокси требует понимания ключевых отличий HTTP/2 от HTTP/1.1: мультиплексирование потоков, долгоживущие соединения, HTTP/2 trailers и ALPN-согласование. Классические HTTP-прокси без специальной настройки с gRPC не работают — нужен либо L7-прокси с поддержкой HTTP/2 (Nginx 1.13.10+, Envoy, HAProxy 1.9.2+), либо TCP CONNECT туннелирование.
Для продакшн-среды рекомендуется Envoy — он создавался именно для микросервисной архитектуры, имеет нативную поддержку gRPC, умеет собирать детальные метрики по каждому RPC-методу и является основой большинства service mesh решений. Nginx — хороший выбор, если вы уже используете его как API Gateway и хотите добавить gRPC-поддержку без введения нового компонента. HAProxy подойдёт, если критически важна производительность и вы уже знакомы с его конфигурацией.
Независимо от выбранного прокси, три правила остаются неизменными: увеличьте таймауты для долгоживущих стримов, отключите буферизацию ответов и настройте keepalive на клиенте. Эти три настройки решают 80% проблем с gRPC через прокси.
Если в вашей архитектуре gRPC-сервисы должны взаимодействовать через внешние сети или вам нужна маршрутизация трафика через конкретные регионы, обратите внимание на прокси дата-центров — они обеспечивают стабильные IP-адреса, низкую задержку и высокую пропускную способность, что особенно важно для gRPC с его бинарным протоколом и чувствительностью к latency.