bigywater.tech
微服务 · 36

gRPC 穿过 nginx:grpc_pass 的配置要点

nginx 怎么代理 gRPC?哪些写法会错、会错成什么样?流式调用会不会被超时误杀?一条长连接上的调用怎么在后端之间分布?消息多大会被网关挡住?这篇用 Go 的 gRPC 服务和 nginx 1.31.6 量出来。

Unavailable · 504
grpc_read_timeout 1 s 时,上游 1.5 s 才回的 Unary,client 得到的 gRPC 状态(nginx 回 HTTP 504)
50 / 50
client 只开一条连接、顺序发 100 个 Unary 经 nginx 轮询,分到 A / B 的数量;直连 A 时是 100 / 0
Unknown · 413
client_max_body_size 默认 1m 时,2 MB 的 gRPC 消息被 nginx 拒绝,Go 库得到的状态

配置要点

实测 · 本机执行输出
  配置                        | Unary 结果                                                                       | ServerStream 结果                                                                                 
  --------------------------+--------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------
  (a) http2 on + grpc_pass  | OK:A 1                                                                         | got=3 code=EOF(正常结束) elapsed_ms=306                                                             
  (b) 没开 http2 + grpc_pass  | ERR:Unavailable:connection_error:_desc_=_"error_reading_server_preface:_http 1 | got=0 code=Unavailable:connection_error:_desc_=_"error_reading_server_preface:_http elapsed_ms=0
  (c) http2 on + proxy_pass | ERR:Internal:stream_terminated_by_RST_STREAM_with_error_code:_INTERNAL_ER 1    | got=0 code=Internal:stream_terminated_by_RST_STREAM_with_error_code:_INTERNAL_ER elapsed_ms=1   
  (d) 直连上游                  | OK:A 1                                                                         | got=3 code=EOF(正常结束) elapsed_ms=305                                                             

要 http2 on 加 grpc_pass;proxy_pass 或没开 http2 都会出错,错误信息各不相同。

超时

实测 · 本机执行输出
  调用                 | Unary 结果                                                                       | ServerStream 结果                                                                                    
  -------------------+--------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------
  (a) Unary,上游 1.5 s | ERR:Unavailable:unexpected_HTTP_status_code_received_from_server:_504_(Gatew 1 | -                                                                                                  
  (b) 间隔 0.5 s × 4 条 | -                                                                              | got=4 code=EOF(正常结束) elapsed_ms=2009                                                               
  (c) 间隔 1.5 s × 2 条 | -                                                                              | got=0 code=Unavailable:unexpected_HTTP_status_code_received_from_server:_504_(Gatew elapsed_ms=1003
  grpc_read_timeout 和 proxy_read_timeout 一样,管的是“两次读到上游数据之间的间隔”:(b) 每次只隔 0.5 s,整个流 2 s 也没有超时;(a) 和 (c) 第一次读就等了 1.5 s,nginx 超时后回 HTTP 504,Go 的 gRPC 库把它翻译成 Unavailable(消息里带着 504)

grpc_read_timeout 管两次读之间的间隔,流式调用只要消息间隔短就不会被误杀;超时表现为 504 → Unavailable。

负载均衡

实测 · 本机执行输出
  配置                    | 100 个 Unary 的分布(OK:服务名 次数)
  ----------------------+---------------------------
  nginx 轮询              | OK:A 50; OK:B 50          
  nginx least_conn      | OK:A 50; OK:B 50          
  nginx ip_hash         | OK:A 100                  
  client 直连 A(没有 nginx) | OK:A 100                  
  client 到 nginx 只有一条长连接,但 nginx 对每个 gRPC 调用(HTTP/2 的每个流)独立选择上游,所以轮询时请求被平均分到两个后端——不像 client 直连时那样,长连接把所有调用钉在一个后端上(见 gRPC 系列里 client 侧负载均衡的陷阱)

nginx 对每个流独立选上游,一条长连接上的调用也会被分开。

消息大小

实测 · 本机执行输出
  配置                          | 0.5 MB | 2 MB                                                                       | 6 MB                                                                      
  ----------------------------+--------+----------------------------------------------------------------------------+---------------------------------------------------------------------------
  默认(client_max_body_size 1m) | OK:A 1 | ERR:Unknown:unexpected_HTTP_status_code_received_from_server:_413_(Reque 1 | ERR:Unknown:unexpected_HTTP_status_code_received_from_server:_413_(Reque 1
  client_max_body_size 10m    | OK:A 1 | OK:A 1                                                                     | OK:A 1                                                                    
  client_max_body_size 0      | OK:A 1 | OK:A 1                                                                     | OK:A 1                                                                    
  grpc_pass 的请求体也受 client_max_body_size 限制(默认 1m);超限时 nginx 回 HTTP 413,Go 的 gRPC 库把它翻译成 Unknown(消息是 unexpected HTTP status code received from server: 413),不是 ResourceExhausted 之类一眼看得出原因的错误

client_max_body_size 也限制 gRPC 的消息,超限时 client 看到的是 Unknown。

分步演示

gRPC 穿过 nginx
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。nginx 是 Homebrew 的 1.31.6(listen 加 http2 on,明文 h2c,grpc_pass grpc://);gRPC 服务和 client 是 Go 1.27.1 + google.golang.org/grpc v1.84.0,服务是 gRPC 系列里的 Demo(Unary / ServerStream);状态码的翻译是 Go gRPC 库的行为,别的语言的库可能翻译成不同的 gRPC 状态;每个场景只测一次
    • 没有验证:gRPC over TLS(grpcs://)、ALPN;双向流(Bidi);gRPC-Web;grpc_next_upstream(重试);健康检查(grpc.health.v1)穿过 nginx;grpc_buffer_size;其它语言(Java、Python)客户端对 504 / 413 的翻译
    京ICP备2026067502号-1京公网安备11010502063487号