gRPC 以 HTTP/2 为传输底座,用 Protobuf 定义接口,天然支持多路复用与双向流式,已经成为微服务内部通信的事实标准。但 gRPC 的协议特性与普通 HTTP/1.1 差异很大:它依赖 HTTP/2 的帧与流、依赖 trailers 传递状态、依赖长连接维持多路复用。这意味着传统的 proxy_pass 配置往往无法直接代理 gRPC 流量,必须使用 Nginx 专门的 grpc_pass 指令族。本文从协议原理出发,逐层拆解 Nginx 代理 gRPC 的配置要点与生产陷阱。
一句话总结: gRPC 代理的核心是让 Nginx 用 HTTP/2 与上游通信,并正确透传 trailers、超时与流控语义,而不是简单地把字节流转发过去。
1. gRPC 与 HTTP/2 上游的基本原理
一句话总结: gRPC 把请求映射为 HTTP/2 的流,状态码放在 trailers 而非 header,因此代理层必须理解帧与流的边界。
gRPC 的报文结构是:请求体是一段 5 字节前缀加 Protobuf 负载,前缀里的 1 字节表示是否压缩、4 字节表示消息长度;响应体同样如此。一次 RPC 对应 HTTP/2 的一条流,content-type 固定为 application/grpc。关键差异在于状态码不在 header 里:gRPC 的最终状态放在 HTTP trailers 的 grpc-status 与 grpc-message 字段中,HTTP 层的 200 只表示「连接可用」,真正的业务错误码藏在 trailer。
# 观察一次 gRPC 调用在 HTTP/2 层的真实形态
grpcurl -plaintext -v localhost:50051 helloworld.Greeter/SayHello
# 输出中可以看到 response headers、response trailers 两段
# trailers 里包含 grpc-status: 0 表示成功
因为状态在 trailers,代理层必须原样透传 trailer 帧。Nginx 的 grpc_pass 就是为此设计的:它会自动把上游的 trailer 转发给客户端,而 proxy_pass 会把 trailer 丢弃,导致客户端永远拿不到 grpc-status,只能靠超时判断失败。这就是为什么 gRPC 流量不能直接复用 HTTP 代理配置。
另一个原理差异是多路复用。HTTP/1.1 的一条 TCP 连接同一时刻只能承载一个请求,因此 proxy_pass 时代上游连接池按「并发请求数」估算即可。HTTP/2 的一条连接可以并发承载成百上千条流,gRPC 上游连接池的规模需求骤降,但单条连接的流量与内存压力上升。理解这一点,才能正确设置上游连接数与并发流上限。
2. grpc_pass 与 HTTP/2 上游配置
一句话总结: 用 grpc_pass 替代 proxy_pass,客户端侧开启 HTTP/2,上游侧用 grpcs 走 TLS,并显式声明 grpc 的 Host 头。
最小可用的 gRPC 代理配置如下,它监听 443 端口并接受 HTTP/2 客户端,转发到明文 gRPC 上游:
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
location /helloworld.Greeter/ {
# 关键:用 grpc_pass 而不是 proxy_pass
grpc_pass grpc://grpc_backend;
# gRPC 要求 Host 头与上游一致,否则部分框架会拒绝
grpc_set_header Host $host;
grpc_set_header X-Real-IP $remote_addr;
}
}
upstream grpc_backend {
server 10.0.0.11:50051;
server 10.0.0.12:50051;
# gRPC 长连接必须开启 keepalive,否则每条 RPC 都要重建 HTTP/2 连接
keepalive 32;
}
grpc_pass 的协议前缀有两个:grpc:// 表示明文 HTTP/2(h2c),grpcs:// 表示带 TLS 的 HTTP/2。上游启用 TLS 时还要配合 grpc_ssl_certificate 与 grpc_ssl_trusted_certificate 完成双向校验:
location /secure.Greeter/ {
grpc_pass grpcs://secure_backend;
# 上游校验客户端证书时,Nginx 需要出示自己的证书
grpc_ssl_certificate /etc/nginx/certs/client.crt;
grpc_ssl_certificate_key /etc/nginx/certs/client.key;
grpc_ssl_trusted_certificate /etc/nginx/certs/ca.crt;
grpc_ssl_verify on;
}
2.1 客户端侧 HTTP/2 的开启条件
一句话总结: Nginx 只有在 TLS + ALPN 协商到 h2 时才接受 gRPC,明文端口需要显式声明 http2 才能接收 h2c。
浏览器与 gRPC 客户端都要求 HTTP/2,而 Nginx 的 HTTP/2 在 1.25 之前必须由 listen ... ssl 隐式开启,1.25 之后改为显式 http2 on;。明文 gRPC(不带 TLS)在服务网格内部很常见,此时必须在 listen 50051; 之后显式写 http2 on;,否则 Nginx 会按 HTTP/1.1 解析帧而直接报错:
server {
listen 50051;
http2 on; # 明文端口必须显式开启,否则 h2c 请求被拒绝
location / {
grpc_pass grpc://127.0.0.1:50052;
}
}
如果客户端用 HTTP/1.1 发起请求(例如 curl 不带 --http2-prior-knowledge),Nginx 会返回 426 或直接解析失败。生产上建议在网关入口做一次探测:命中 content-type: application/grpc 但协议不是 HTTP/2 的请求直接返回 400,避免这类请求进入上游造成难以定位的协议错误。
3. trailers 与流式传输
一句话总结: 单向流、服务端流、双向流都靠 HTTP/2 的 DATA 帧承载,代理层不能缓冲整个响应体,否则流式体验会被破坏。
gRPC 支持四种调用模式:一元(Unary)、服务端流(Server Streaming)、客户端流(Client Streaming)、双向流(Bidirectional Streaming)。后三种都依赖 HTTP/2 的数据帧持续推送。代理层如果开启了响应缓冲,就会把流式响应攒成完整报文再下发,实时性完全丧失:
location /stream.Events/ {
grpc_pass grpc://grpc_backend;
# 关闭缓冲:让数据帧一到达就转发给客户端
grpc_buffering off;
# 上游响应头一到就下发给客户端,不等响应体
grpc_read_timeout 3600s;
grpc_send_timeout 3600s;
# 大消息场景放宽单帧限制
grpc_buffer_size 16k;
}
grpc_buffering off 是流式场景的关键。Nginx 默认会缓冲上游响应以提升吞吐,但对长连接推送来说,缓冲意味着延迟累积。同时必须把 grpc_read_timeout 与 grpc_send_timeout 从默认的 60s 放大:双向流可能空闲数分钟才推送下一条消息,60 秒的超时会把正常空闲连接直接掐断。
trailers 的透传还需要注意 grpc_set_header 与 TE: trailers 头。部分 gRPC 实现要求客户端声明 TE: trailers,Nginx 在转发时会自动补上,但如果前面还有一层代理(比如 ingress)把该头过滤掉了,就会出现 grpc-status 丢失的现象。排查时先确认链路上的每一跳都保留 TE 与 content-type 头。
# 抓包确认 trailer 帧是否真的透传到了客户端
sudo tcpdump -i any -A 'tcp port 50051' | grep -A2 grpc-status
4. 超时、重试与流控
一句话总结: gRPC 的超时要按调用类型分别设置,重试只对幂等的一元调用安全,流控则要靠 grpc_buffer_size 与并发流上限配合。
超时是 gRPC 代理最容易踩坑的地方。一元调用通常几百毫秒完成,而双向流可能持续数小时,两者显然不能用同一组超时。推荐按 location 区分:
# 一元调用:短超时快速失败
location /api.Unary/ {
grpc_pass grpc://grpc_backend;
grpc_connect_timeout 3s;
grpc_read_timeout 10s;
grpc_send_timeout 10s;
}
# 长连接流式:长超时或干脆不设
location /api.Stream/ {
grpc_pass grpc://grpc_backend;
grpc_connect_timeout 3s;
grpc_read_timeout 3600s;
grpc_send_timeout 3600s;
}
grpc_next_upstream 控制重试行为。默认只对连接失败与超时重试,且 gRPC 的重试语义比 HTTP 更危险:一个已经发送了部分消息的流被重试,会导致上游收到重复消息。因此只有一元、幂等的调用才适合开启更激进的重试:
location /api.ReadOnly/ {
grpc_pass grpc://grpc_backend;
# 连接错误与超时可换一台上游重试
grpc_next_upstream error timeout http_502 http_503;
grpc_next_upstream_tries 2;
grpc_next_upstream_timeout 5s;
}
流控方面,grpc_buffer_size 决定 Nginx 读取上游响应时的缓冲区大小。缓冲区越小,内存占用越低,但大消息会被拆成更多次读写;缓冲区越大,单连接内存越高。对于传输大 payload(比如文件分片)的 gRPC 接口,建议把缓冲区调到 16k~64k 并做压测验证。同时 http2_max_concurrent_streams 限制了客户端单连接能并发多少条流,网关层通常需要放宽到 128 以上,避免客户端因流被拒而不断重连。
5. 健康检查与连接复用
一句话总结: gRPC 上游的健康检查要用 HTTP/2 探测而非 TCP 探测,keepalive 池必须开启并定期清理空闲连接。
gRPC 上游「端口开着」不等于「服务可用」——HTTP/2 连接建立后,如果后端业务线程池耗尽,新流会被拒绝。因此健康检查应该用 gRPC 官方的健康检查协议,而不是简单的 TCP 探测。Nginx 开源版只支持被动健康检查,用 grpc_next_upstream 把失败的上游临时摘除:
upstream grpc_backend {
server 10.0.0.11:50051 max_fails=3 fail_timeout=10s;
server 10.0.0.12:50051 max_fails=3 fail_timeout=10s;
keepalive 64;
keepalive_timeout 60s;
keepalive_requests 1000;
}
max_fails=3 fail_timeout=10s 表示连续失败 3 次后摘除 10 秒。这个组合对 gRPC 要格外小心:gRPC 的失败往往表现为 trailer 里的非零 grpc-status,而 Nginx 的被动检查只统计传输层失败与 HTTP 5xx,业务层的 grpc-status: 14(UNAVAILABLE)并不会触发摘除。因此应用层的健康状态需要额外的主动探测,可以用 Nginx Plus 的 health_check,或者用 sidecar 定期调用健康检查接口并动态更新 upstream。
连接复用是 gRPC 代理的性能关键。HTTP/2 的多路复用让上游连接数需求很低,但代价是空闲连接会被中间设备(NAT、防火墙)悄悄回收。keepalive_timeout 60s 让 Nginx 在 60 秒空闲后主动关闭连接,比等待对端 RST 更可控:
# 观察上游连接复用情况:ESTAB 数量应远小于 QPS
ss -tn state established '( dport = :50051 )' | wc -l
如果发现每次请求都新建连接(TIME_WAIT 大量堆积),通常是因为 keepalive 指令漏写、或者 location 里用了 grpc_pass 直接指向 IP 而没有经过 upstream 块。keepalive 只在 upstream 块内生效,直接写 grpc_pass grpc://10.0.0.11:50051 会绕过连接池。
6. 与普通 HTTP 代理的关键差异
一句话总结: gRPC 代理在指令族、状态码位置、连接语义、缓冲策略四个方面都与 HTTP 代理不同,混用会导致静默失败。
把差异整理成一张对照表,能避免大部分配置错误:
| 维度 | HTTP 代理(proxy_pass) | gRPC 代理(grpc_pass) |
|---|---|---|
| 上游协议 | HTTP/1.1 或 HTTP/2 | 强制 HTTP/2(h2c 或 h2) |
| 状态码位置 | 响应头 | HTTP trailers(grpc-status) |
| 超时指令 | proxy_read_timeout | grpc_read_timeout |
| 请求头设置 | proxy_set_header | grpc_set_header |
| 重试指令 | proxy_next_upstream | grpc_next_upstream |
| 缓冲指令 | proxy_buffering | grpc_buffering |
| 上游连接 | 每请求一条或 keepalive 池 | 依赖 HTTP/2 多路复用 |
最容易犯的错误是「指令混用」:在 grpc_pass 的 location 里写 proxy_read_timeout,Nginx 不会报错但该指令完全不生效,超时仍然是默认 60 秒。类似地,proxy_set_header Host $host 对 gRPC 也无效,必须写 grpc_set_header。这类错误在测试环境往往表现正常(因为响应快、没触发超时),上线后遇到慢调用才暴露。
另一个差异是错误码映射。HTTP 代理下,上游 502 会被 proxy_next_upstream 捕获;而 gRPC 上游崩溃时,客户端看到的是 HTTP/2 的 RST_STREAM 或 grpc-status: 14,Nginx 的日志里记录的是 upstream prematurely closed connection 之类的信息。排查 gRPC 故障时,必须同时看 Nginx 的 error_log 与客户端侧的 grpc-status,两边对齐才能定位是网络层还是应用层的问题。
# 为 gRPC 单独准备日志格式,记录 grpc 特有字段
log_format grpc '$remote_addr "$request" $status '
'upstream=$upstream_addr '
'rt=$request_time urt=$upstream_response_time';
server {
listen 50051;
http2 on;
access_log /var/log/nginx/grpc.log grpc;
location / { grpc_pass grpc://grpc_backend; }
}
7. 生产实践与可观测性
一句话总结: gRPC 网关的生产化需要补齐指标采集、协议感知的日志与灰度路由,才能把「能跑通」提升到「能运营」。
gRPC 的可观测性与 HTTP 有明显差别:一次 RPC 的耗时由 HTTP 层耗时与业务耗时共同构成,而业务耗时体现在 trailer 的 grpc-status 上。Nginx 的 $upstream_response_time 只统计到响应体结束,对于流式调用它记录的是「最后一条消息到达」的时间,未必等于业务处理完成时间。因此长连接场景要结合客户端的 span 一起分析。
指标采集方面,除了通用的连接数、QPS、P99,gRPC 网关还应关注:活跃流数量、grpc-status 非零比例、上游连接复用率、trailer 透传失败次数。这些指标可以通过 stub_status 拿到连接维度数据,再配合 OpenResty 的 lua-resty 在日志阶段解析 trailer:
# OpenResty 场景:在 log_by_lua 中记录 grpc-status
log_by_lua_block {
local status = ngx.var.upstream_http_grpc_status
if status and status ~= "0" then
ngx.log(ngx.WARN, "grpc failed status=", status)
end
}
灰度发布是 gRPC 网关的另一个高频需求。由于 gRPC 的请求路径是 包名.服务名/方法名,可以直接用 location 前缀做服务级路由,再用 split_clients 做流量切分:
# 按 5% 流量切到新版本
split_clients "${remote_addr}${request_uri}" $grpc_version {
5% "v2";
* "v1";
}
upstream grpc_v1 { server 10.0.1.11:50051; keepalive 32; }
upstream grpc_v2 { server 10.0.2.11:50051; keepalive 32; }
map $grpc_version $grpc_upstream {
v1 grpc_v1;
v2 grpc_v2;
}
server {
listen 50051;
http2 on;
location / {
grpc_pass grpc://$grpc_upstream;
}
}
灰度期间要特别关注 trailers 的完整性:新旧版本的 gRPC 框架版本可能不同,个别老版本在 trailers 的编码上存在兼容问题,表现为客户端偶尔收不到 grpc-status 而超时。上线前用 grpcurl 对新旧两个 upstream 各跑一轮用例,确认 trailer 行为一致。
gRPC 网关的容量规划要跳出「按 QPS 估算」的思维。HTTP/2 多路复用意味着单条连接承载的并发流数才是关键指标,http2_max_concurrent_streams、上游 keepalive 数量与业务线程池三者要匹配:流上限设得比线程池大,多余的流只会排队;设得比连接数承载力小,则会浪费复用能力。压测时用 ghz 这类 gRPC 专用压测工具,按并发流数而非并发连接数施压,才能测出真实瓶颈。
8. 总结
| 环节 | 要点 |
|---|---|
| 协议原理 | gRPC 基于 HTTP/2 流,状态码在 trailers 而非 header |
| 基本配置 | grpc_pass 替代 proxy_pass,客户端开 HTTP/2,上游用 grpc/grpcs |
| 流式传输 | grpc_buffering off,超时按调用类型分别设置 |
| 超时重试 | grpc_next_upstream 只对幂等一元调用开启 |
| 健康检查 | 端口探测不够,需应用层主动探测 |
| 连接复用 | upstream keepalive 必须显式开启,否则 TIME_WAIT 堆积 |
| 与 HTTP 差异 | 指令族、状态码位置、缓冲策略全面不同,不可混用 |
| 生产运营 | 采集 grpc-status 指标,灰度路由按服务方法切分 |
Nginx 代理 gRPC 的难点不在语法,而在协议语义的透传:trailers 必须原样转发、流式不能缓冲、超时要按调用类型区分、连接池要靠 keepalive 维持。把这四点做对,Nginx 就能成为一个稳定、低延迟的 gRPC 网关,同时保留限流、鉴权、灰度等接入层能力。掌握 gRPC 代理之后,接入层的下一道防线是应用层攻击检测,接下来看看如何在 Nginx 中集成 WAF 与 ModSecurity。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。