bigywater.tech
gRPC · 38

连接池:多少个 ClientConn 合适

什么时候要自己维护一个 ClientConn 池?池开多大?怎么选连接?预热有用吗?一条连接对服务端有多贵?这篇用服务端放在另一个进程的方式量出这些数字。

8152 → 34221
服务端并发上限 100、handler 10 ms 时,连接池从 1 条到 4 条的请求/秒
1150 → 1437
并发名额不够用时,轮流与“在途最少”策略的请求/秒
约 48 KiB
服务端每条空闲连接的内存(堆 38.4 + 栈 9.2)与 3 个 goroutine

什么时候需要池

实测 · 本机执行输出
  1 条连接:   8152 请求/秒(理论上限 10000);p50 36418 µs、p99 200046 µs
  2 条连接:  16577 请求/秒(理论上限 20000);p50 22859 µs、p99 71192 µs
  4 条连接:  34221 请求/秒(理论上限 40000);p50 11490 µs、p99 14944 µs
  8 条连接:  34243 请求/秒(理论上限 80000);p50 11476 µs、p99 14638 µs

一条连接同时能跑的流有上限(服务端的 MaxConcurrentStreams)。并发超过它,或者单连接的 CPU/带宽成了瓶颈,才需要多条连接。上限约等于连接数 × 每连接并发上限 ÷ 处理时间。

怎么选连接

实测 · 本机执行输出
  轮流        1150 请求/秒;p50 1692 µs、p99 226370 µs
  随机        1198 请求/秒;p50 2705 µs、p99 221361 µs
  在途最少      1437 请求/秒;p50 6531 µs、p99 217064 µs

轮流、随机、在途最少三种策略会让吞吐与延迟分布不同。在途最少让名额利用更均匀,吞吐更高,但不是所有指标都更好。

预热

实测 · 本机执行输出
  没有预热                   200 个并发调用:p50 4172 µs、p99 7036 µs、最慢 7671 µs;总耗时 7.8 ms
  Connect() 并等 200 ms    200 个并发调用:p50 5443 µs、p99 6388 µs、最慢 6822 µs;总耗时 7.2 ms

NewClient 是惰性的。本机回环上预热看不出收益;在真实网络、尤其有 TLS 握手时才可能有明显差别,需要实测。

连接的成本

实测 · 本机执行输出
  基线 2 个 goroutine;1000 条空闲连接后 3002 个(每条连接 +3.0);堆 +38.4 KiB/条;goroutine 栈 +9.2 KiB/条
  关闭这 1000 条连接之后:2 个 goroutine

服务端每条连接都有读写 goroutine 和缓冲,实测每条空闲连接约 48 KiB。池不要开得比需要的大。

分步演示

连接池
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。服务端是子进程,goroutine 与内存由子进程自己在 GC 之后报告;每档负载 2~3 秒、一次运行,有噪声(第二节三种策略的 p99 差别在噪声内);选连接策略的实现是我写的最简版本(轮流用原子计数,在途最少用每条连接的在途计数);回环网络
    • 没有验证:真实网络与 TLS 握手下预热的收益;grpc 自带的 round_robin/least_request 在多地址下的差别(本篇池里的几条连接连的是同一个地址);连接池与服务端 MaxConnectionAge 的配合(第 40 篇);连接数很大(上万)时服务端的文件描述符和内存;客户端侧每条连接的开销
    京ICP备2026067502号-1京公网安备11010502063487号