bigywater.tech
gRPC · 16

客户端负载均衡的陷阱:长连接让流量不均衡

gRPC 的一条 HTTP/2 连接可以复用很久。当连接在均衡器后面建立之后,新增的后端会不会分到流量?请求量不同的客户端会怎样?这篇用一个自己写的四层均衡器逐项验证。

[300 300 0]
加入第 3 个后端后,已有的 6 个客户端再发 600 次请求,新后端收到的次数
[1100 200]
4 个客户端(1000/100/100/100 次)经按连接均衡的均衡器分到 2 个后端的请求数
0 个失败 / 49 条连接
MaxConnectionAge = 1 s 时,连接定期重建、均衡自动恢复,整个过程的失败数与连接数

为什么会不均衡

实测 · 本机执行输出
  2 个后端时,6 个客户端各发 100 次:各后端收到 [300 300];均衡器接受的连接 6 条
  加入第 3 个后端后,原来的 6 个客户端再各发 100 次:各后端收到 [300 300 0];均衡器接受的连接仍是 6 条
  再新建 3 个客户端各发 100 次:各后端收到 [100 100 100];均衡器接受的连接 9 条

HTTP/2 在一条连接上复用很多请求。四层均衡器只在建立连接时选后端,之后这条连接上所有请求都去同一个后端;新增后端后,只有新建的连接才会分到它,已有的连接继续待在原后端——扩容之后新后端闲着就是典型表现。

请求量差异

实测 · 本机执行输出
  4 个客户端轮流被分到 2 个后端(每个后端 2 条连接),但请求数:各后端收到 [1100 200](后端 0 占 85%)

按连接均衡并不等于按请求均衡:一条连接上跑多少请求由客户端决定,忙的客户端把它所在的后端压垮,而别的后端很闲。

客户端负载均衡

实测 · 本机执行输出
  1000+100+100+100 次调用,各后端收到 [421 420 421]

客户端拿到全部后端地址、自己做 round_robin,则每个请求轮流发给各后端,与客户端忙闲无关。要求客户端能拿到地址列表并及时更新(第 15 篇)。

MaxConnectionAge

实测 · 本机执行输出
  第 1 秒内各后端收到的次数 [135 135]
  第 2 秒内各后端收到的次数 [133 131 0]
  第 3 秒内各后端收到的次数 [100 94 82]
  第 4 秒内各后端收到的次数 [87 88 89]
  第 5 秒内各后端收到的次数 [89 91 90]
  第 6 秒内各后端收到的次数 [90 89 91]
  第 7 秒内各后端收到的次数 [89 87 88]
  第 8 秒内各后端收到的次数 [91 91 88]
  整个过程中失败的调用 0 个;均衡器共接受 49 条连接

服务端让连接活一段时间就发 GOAWAY 并关闭(带宽限期),客户端重新连接,均衡器重新分配。设置过短会增加建连开销,过长则恢复均衡慢;读源码时看到服务端给这个值加了 ±10% 的随机抖动,避免所有连接同时断开。

分步演示

长连接均衡
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。“四层均衡器”是我写的简易 TCP 转发(按连接轮流选后端,不解析 HTTP/2),只模拟“按连接分配”这一类行为,并不代表某个具体产品;第四节每个后端的 MaxConnectionAge 都是 1 秒,是为了在实验里很快看到效果,生产环境通常用分钟级;MaxConnectionAge 的 ±10% 抖动读自 http2_server.go
    • 没有验证:真实的 Kubernetes ClusterIP / kube-proxy / 云厂商四层均衡器的行为(不同产品是否按连接、是否有连接复用);七层(HTTP/2 感知)均衡器按请求分配的行为;least_request 等策略;MaxConnectionAge 过短时的建连开销(第 40 篇);客户端 round_robin 时地址列表更新的及时性(第 15 篇测过 DNS)
    京ICP备2026067502号-1京公网安备11010502063487号