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_timeout和grpc_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 3600s和timeout 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 exceeded或no 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的二进制协议和对延迟的敏感性尤为重要。
```