least_conn对grpc长连接“失效”是因为它仅在新建tcp连接时生效,而grpc基于http/2复用单连接发送多stream,后续请求不再触发后端重选;需配合grpc_pass、客户端round_robin策略及合理超时缓冲配置才能实现真正请求级负载均衡。

gRPC长连接在Nginx七层代理下负载不均,根本不是Nginx没转发,而是它根本没机会在请求级做分发——连接复用让所有stream卡死在同一个后端节点上。
为什么least_conn对gRPC长连接“失效”
gRPC基于HTTP/2,客户端默认复用单个TCP连接发送多个stream。Nginx的least_conn只在新建连接时生效,一旦连接建立,后续所有stream都复用该连接,不会重新选后端。结果就是:一个Go客户端建连到10.0.1.5:8080,之后几百个RPC调用全打过去,其他节点空闲。
- 验证方法:在Nginx access_log中添加
$upstream_addr和$connection_requests,观察同一客户端IP的多条日志——若$connection_requests持续增长(如1→92→176),但$upstream_addr始终固定,说明连接未重分配 - 关键误区:以为配置了
least_conn就万事大吉;实际上它只管“连接建立”,不管“stream分发” - 更隐蔽的问题:
ip_hash或hash $remote_addr必须禁用,否则会强制绑定IP,加剧倾斜
grpc_pass必须配合upstream,且不能用proxy_pass
Nginx对gRPC的支持依赖原生HTTP/2处理路径,proxy_pass走的是HTTP/1.x兼容逻辑,会降级为短连接或错误转发。只有upstream + grpc_pass组合才能启用真正的gRPC流式代理能力。
- 正确写法:
upstream oap_cluster { server 10.0.1.5:8080; server 10.0.1.6:8080; least_conn; keepalive 32; },然后在location里用grpc_pass grpc://oap_cluster; - 错误写法:
proxy_pass http://10.0.1.5:8080;——这会让gRPC请求被当成普通HTTP处理,TLS握手失败、HEADERS帧丢失、流中断 - 注意前缀:
grpc://可省略,但grpcs://不可省;若后端启用了TLS,必须显式写grpcs://,否则明文传输
Go客户端必须主动启用round_robin策略
Nginx只能控制连接入口,但gRPC客户端(尤其是Go)默认使用pick_first策略:DNS解析出多个A记录,它只取第一个IP建连,其余IP彻底闲置。这是K8s+Headless Service场景下负载不均的最常见根因。
- Go客户端需显式配置:
grpc.Dial("dns:///my-svc.default.svc.cluster.local:8080", grpc.WithResolvers(dns.NewBuilder()), grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin": {}}]}`)) - 验证DNS是否返回多IP:
dig +short my-svc.default.svc.cluster.local,输出应为至少两个Pod IP;若只有一个,检查Service是否为clusterIP: None - 禁用DNS缓存:Go默认不刷新DNS,需设置
grpc.WithDialer自定义拨号器,或通过GRPC_GO_REQUIRE_HANDSHAKE=0环境变量规避握手阻塞
超时与缓冲参数不调,双向流必断
gRPC双向流(如实时日志推送、心跳保活)可能持续数分钟。Nginx默认grpc_read_timeout是60秒,空闲超过该时间就会关闭连接,导致Go服务端收到RST,客户端报rpc error: code = Unavailable desc = transport is closing。
- 必须设置:
grpc_read_timeout 300s;、grpc_send_timeout 300s;、grpc_connect_timeout 10s; - 缓冲区要够大:
grpc_buffer_size 256k;,避免大trace数据被截断(尤其SkyWalking等APM场景) - 关键开关:
reset_timedout_connection off;——否则Nginx会在超时后主动kill看似空闲实则活跃的流 - 健康检查要配实:
max_fails=3 fail_timeout=30s,配合proxy_next_upstream error timeout http_502 http_503,快速剔除僵死Pod
真正难的不是配参数,而是意识到:Nginx只管连接入口,gRPC流分发的主控权其实在客户端手里。不改客户端的LB策略,光调Nginx,永远只是在修漏水的桶底。











