返回博客

通过代理在微服务架构中使用gRPC:HTTP/2隧道设置的完整指南

详细指南,介绍如何在微服务架构中通过代理服务器设置gRPC流量,包括HTTP/2隧道、负载均衡和TLS终止。

📅2026年8月7日
```html

gRPC是Google开发的高性能RPC框架,基于HTTP/2,并成为微服务之间交互的事实标准。但一旦你尝试通过代理传输gRPC流量,就会立即遇到问题:大多数传统的代理服务器无法处理HTTP/2和长连接流。在本文中,我们将讨论如何正确配置gRPC通过代理——从架构选择到Nginx、Envoy和HAProxy的具体配置。

为什么gRPC与普通代理配合不好

要理解这个问题,首先需要了解gRPC的底层结构。该协议使用HTTP/2作为传输层,这使其与基于HTTP/1.1的传统REST有着根本的区别。大多数企业级代理、防火墙和负载均衡器都是在HTTP/1.1时代设计的,根本无法处理HTTP/2的多路复用流。

以下是您在尝试通过传统HTTP代理传输gRPC时可能遇到的具体问题:

  • 降级到HTTP/1.1。许多代理会自动降级协议版本。gRPC需要HTTP/2——没有它,连接根本无法建立,客户端将收到错误UNAVAILABLE
  • 长连接中断。gRPC积极使用服务器端和双向流。具有激进超时的代理(尤其是AWS ELB Classic和某些版本的Squid)会中断超过60秒未传输数据的连接。
  • Content-Type问题。gRPC使用头部Content-Type: application/grpc。不识别此类型的代理可能会拒绝请求或错误地缓冲主体。
  • 尾部(Trailers)。gRPC使用HTTP/2尾部传递调用完成状态。HTTP/1.1代理不支持尾部——状态信息将丢失。
  • 主体缓冲。某些代理在将响应传递给客户端之前会缓冲整个响应主体。对于流式gRPC调用,这意味着客户端在流结束之前不会收到任何消息。

关键结论:

要通过代理使用gRPC,需要支持HTTP/2端到端的代理或特殊的隧道模式。未经配置的传统HTTP代理不适用。

HTTP/2隧道:它是如何工作的

对于gRPC流量的代理,有两种根本不同的方法,理解它们之间的区别对于选择适合您架构的解决方案至关重要。

方法1:HTTP/2端到端(推荐)

代理理解HTTP/2,并与客户端和后端建立HTTP/2连接。它可以分析单独的流,在请求级别应用负载均衡,添加头部并执行TLS终止。这是功能最强大的选项——Envoy、Nginx(从1.13.10版本开始)以及云服务提供商中的gRPC感知负载均衡器都是如此工作。

方法2:TCP隧道(CONNECT)

代理不分析HTTP/2流量,而是通过CONNECT方法创建透明的TCP隧道。客户端通过隧道直接与后端建立TLS连接。代理只看到加密的字节流。此方法设置更简单,但会失去在gRPC请求级别进行负载均衡和添加头部的能力。

特性 HTTP/2端到端 TCP CONNECT隧道
按请求负载均衡 ✅ 是 ❌ 否(仅限TCP)
代理上的TLS终止 ✅ 是 ❌ 否
添加头部 ✅ 是 ❌ 否
设置复杂性 中等
流支持 ✅ 完全 ✅ 完全
可观察性(指标) ✅ 详细 ❌ 仅限TCP

将Nginx配置为gRPC代理

Nginx从1.13.10版本开始支持gRPC代理(2018年2月)。需要使用ngx_http_grpc_module模块,该模块包含在标准构建中。重要的是:Nginx在客户端(前端)支持HTTP/2,但在后端仅对gRPC使用HTTP/2——普通HTTP上游使用HTTP/1.1。

Nginx上的gRPC代理基本配置

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;

    # HTTP/2连接的保持活动
    keepalive 32;
}

请注意几个关键细节。首先,使用的是grpc_pass指令,而不是proxy_pass——这两个模块的行为不同。其次,grpc_read_timeoutgrpc_send_timeout设置为3600秒(1小时)——这对于可能长时间不传输数据的服务器流至关重要。第三,keepalive 32在上游允许重用与后端的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),而不是gRPC客户端无法正确处理的HTTP 502。

Envoy Proxy:微服务中gRPC的最佳选择

Envoy是为微服务架构而创建的,支持gRPC的深度实现。它理解Protocol Buffers,能够将gRPC转码为REST,收集每个RPC方法的详细指标,并且是服务网格解决方案(如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_stats过滤器收集gRPC指标。

Envoy中的gRPC-Web转码

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从1.9.2版本开始支持gRPC。它在TCP或HTTP/2层工作,能够对gRPC流量进行负载均衡并执行健康检查。如果您已经在基础设施中使用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

    # 通过gRPC健康检查协议进行健康检查
    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协商。timeout client 3600stimeout server 3600s是长连接gRPC流的关键参数。leastconn算法比roundrobin更适合gRPC,因为流式连接可能是长期的,并且对后端的负载不均匀。

TLS终止与端到端加密

gRPC默认假定使用TLS——这是规范的一部分。在微服务架构中,出现了一个问题:在哪里执行TLS终止以及如何在组件之间组织加密?主要有三种模式。

模式1:代理上的TLS终止(边缘TLS)

代理接受来自客户端的加密流量,解密后通过未加密的通道(或单独的TLS)传递给后端。这是企业网络中最常见的方法。后端可以使用grpc.Insecure()来简化配置。

模式2:端到端加密(mTLS)

互信TLS(mTLS)是服务网格的标准。每个服务都有自己的证书,并且在每次连接时,双方都会验证对方的证书。这确保了服务的身份验证和集群内部流量的加密。这就是Istio与Envoy侧车代理的工作方式。

# Go: 带mTLS的gRPC服务器
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透传

代理在TCP层工作,不解密流量——只是将加密的字节转发到后端。这是从代理的角度来看最简单的选项,但它失去了分析gRPC流量、添加头部或在请求级别进行负载均衡的能力。

gRPC流的负载均衡:特点与解决方案

gRPC的负载均衡并不是一项简单的任务,原因如下。在HTTP/1.1中,每个请求都是一个单独的TCP连接(或来自连接池的连接),负载均衡器可以轻松地将请求分配给后端。在HTTP/2中,一个TCP连接可以多路复用多个流——如果负载均衡器在TCP层工作,则同一连接的所有流都将发送到同一后端。

对于gRPC来说,这意味着:如果客户端建立了一个HTTP/2连接并通过它发送100个RPC请求,则在TCP负载均衡的情况下,所有100个请求都将由一个后端实例处理。其他后端将处于空闲状态。解决方案是对HTTP/2流进行负载均衡(L7负载均衡)。

gRPC的负载均衡算法

算法 适合gRPC 备注
轮询 ✅ 是 适合处理时间大致相同的单一RPC
最少连接 ✅ 最佳选择 考虑到活动流,均匀分配负载
随机 ⚠️ 有条件 在请求数量较多时有效,数量较少时不均匀
IP哈希 ❌ 不好 将客户端绑定到一个后端,在L7中没有意义
优先选择(gRPC内置) ❌ 不适用于生产 所有请求都发送到第一个可用的服务器

gRPC中的客户端负载均衡

gRPC支持客户端负载均衡——客户端自行决定将请求发送到哪个服务器。这可以绕过TCP多路复用的问题。服务发现使用DNS与多个A记录或特殊解析器(如Consul、etcd)。以下是Go中客户端负载均衡的示例配置:

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/balancer/roundrobin"
)

// 通过DNS的轮询负载均衡客户端
conn, err := grpc.Dial(
    "dns:///grpc-service.internal:50051",
    grpc.WithDefaultServiceConfig(
        `{"loadBalancingConfig": [{"round_robin":{}}]}`
    ),
    grpc.WithTransportCredentials(creds),
)

// 使用最少连接的客户端(在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:地理分布的微服务

如果您的微服务位于不同的区域(例如,一部分在欧洲,一部分在美国),并且您需要控制gRPC连接通过哪个IP地址在区域之间建立,外部代理可以帮助组织可预测的路由。对于这种任务,适合使用数据中心代理——它们提供稳定的IP地址和高速连接,这对于对延迟敏感的gRPC至关重要。

场景2:隔离出站流量

在某些企业环境中,所有出站流量必须通过企业代理。对于需要访问外部gRPC服务的gRPC客户端(例如,使用gRPC的Google Cloud APIs),这会造成困难。解决方案是通过企业代理配置CONNECT隧道。

以下是Go中配置gRPC客户端通过HTTP CONNECT代理的示例:

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:测试与开发

在开发微服务时,通常需要测试来自不同网络环境的服务行为——检查服务在高延迟下的响应,或测试地理依赖逻辑。对于这种任务,方便使用住宅代理,它们允许模拟来自特定区域的请求,使用真实的家庭用户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

原因:代理根据超时关闭空闲连接。经典值:AWS ELB——60秒,Nginx默认——60秒。
解决方案1:增加代理的超时(如上面的示例所示)。
解决方案2:在gRPC客户端侧配置keepalive:

import "google.golang.org/grpc/keepalive"

kaParams := keepalive.ClientParameters{
    Time:                30 * time.Second, // 每30秒发送一次ping
    Timeout:             10 * time.Second, // 等待响应10秒
    PermitWithoutStream: true,             // 即使没有活动RPC也要ping
}

conn, err := grpc.Dial(
    "grpc-service:50051",
    grpc.WithKeepaliveParams(kaParams),
    grpc.WithTransportCredentials(creds),
)

错误3:HTTP/2不协商(ALPN失败)

症状:

错误transport: failed to dial: context deadline exceededno application protocol

原因:代理或中间设备不支持ALPN(应用层协议协商)或未在支持的协议列表中启用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代码
  • 健康检查使用gRPC健康检查协议
  • 负载均衡在L7(HTTP/2流)上工作,而不是L4(TCP)

结论

通过代理配置gRPC需要理解HTTP/2与HTTP/1.1之间的关键区别:流的多路复用、长连接、HTTP/2尾部和ALPN协商。未经特殊配置的传统HTTP代理无法与gRPC配合使用——需要支持HTTP/2的L7代理(Nginx 1.13.10+、Envoy、HAProxy 1.9.2+)或TCP CONNECT隧道。

对于生产环境,建议使用Envoy——它是专为微服务架构设计的,具有原生gRPC支持,能够收集每个RPC方法的详细指标,并且是大多数服务网格解决方案的基础。如果您已经将Nginx用作API网关并希望添加gRPC支持而不引入新组件,Nginx是一个不错的选择。如果性能至关重要且您已经熟悉HAProxy的配置,HAProxy也适合使用。

无论选择哪个代理,三个规则始终不变:增加长连接流的超时,禁用响应缓冲,并在客户端上配置keepalive。这三项设置解决了80%的通过代理的gRPC问题。

如果您的架构中的gRPC服务需要通过外部网络进行交互,或您需要通过特定区域路由流量,请考虑使用数据中心代理——它们提供稳定的IP地址、低延迟和高带宽,这对于gRPC的二进制协议和对延迟的敏感性尤为重要。

```