Назад к блогу

gRPC через прокси в микросервисной архитектуре: полное руководство по настройке HTTP/2 туннелирования

Подробное руководство по настройке gRPC-трафика через прокси-серверы в микросервисной архитектуре — от HTTP/2 туннелирования до балансировки нагрузки и TLS-терминации.

📅7 августа 2026 г.

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.