结论:ghz 更贴近真实服务压测场景,go test -bench 只适合测单个函数或客户端 stub 调用开销;前者走真实 HTTP/2 连接、模拟网络栈与流控,后者不建连接、复用 conn、忽略 TLS/序列化等开销,结果无法反映线上 QPS。

gRPC 基准测试该用哪个工具:ghz 还是 go test -bench
直接说结论:ghz 更贴近真实服务压测场景,go test -bench 只适合测单个函数或客户端 stub 调用开销。别拿 go test -bench 的结果去推断线上 QPS——它不走网络栈、不建真实连接、默认复用 grpc.ClientConn,测出来的是“理想裸调用”,不是“并发请求吞吐”。
常见错误现象:go test -bench=. 显示 50k req/s,但用 ghz 一压,20 并发就卡在 800 QPS,CPU 没打满,延迟飙升。原因就是前者没模拟连接建立、TLS 握手、流控、序列化反序列化等真实路径。
-
ghz是命令行工具,走真实 HTTP/2 连接,支持并发控制、持续时长、QPS 限制、TLS 配置,输出含 p90/p99 延迟、错误率、吞吐趋势 -
go test -bench适合定位瓶颈:比如对比proto.Marshalvsjson.Marshal开销,或测试自定义Codec实现的序列化耗时 - 如果你要对比“不同并发数下的吞吐变化”,必须用
ghz或自己写带连接池和并发控制的压测 client,否则数据无参考价值
并发数设置不对,ghz 测出来的吞吐就是假的
很多人跑 ghz --concurrency 100 就以为是 100 并发请求,其实不是:默认 ghz 会为每个 goroutine 复用一个 grpc.ClientConn,而 gRPC 默认对每个 ClientConn 最多只建 100 个 HTTP/2 stream(受 MaxConcurrentStreams 控制)。所以 100 并发下,如果服务器端也设了 MaxConcurrentStreams: 100,实际可能只有不到 100 个请求真正并行。
使用场景:你想看“从 10 并发到 500 并发,吞吐如何变化”,就得确保客户端和服务端都不成为瓶颈。
立即学习“go语言免费学习笔记(深入)”;
- 客户端侧:加
--connections 10让 ghz 创建 10 个独立连接,再分摊 100 并发,避免单连接 stream 耗尽 - 服务端侧:检查
grpc.ServerOption是否显式设置了grpc.MaxConcurrentStreams(1000),没设的话默认是 100 - 还要确认服务端 OS 层的文件描述符限制:
ulimit -n至少大于connections × streams_per_conn + 其他服务开销,否则会报accept: too many open files
Go 客户端里手动写 benchmark,怎么避免常见陷阱
真要写 go test -bench 测 gRPC 调用,最容易踩的坑是“没重用 conn”和“没清空 context”。每次 grpc.Dial 和 context.WithTimeout 都是开销,测出来的是创建成本,不是调用成本。
参数差异:用 grpc.WithTransportCredentials(insecure.NewCredentials()) 替代 TLS,避免加密耗时干扰;用 grpc.WithBlock() 确保 Dial 同步完成,否则 bench 会因连接未就绪而失败。
- 在
BenchmarkXXX函数外初始化*grpc.ClientConn和 client stub,bench内只做client.Method(ctx, req) - 每次请求必须用新
context.WithTimeout,但别在循环里反复time.Now()—— 直接用ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond),结束后cancel() - 如果测流式接口,注意
Recv()调用次数和是否真的触发了多次网络往返,建议用io.Copy(ioutil.Discard, stream)模拟消费,而不是只调一次Recv()
吞吐下降拐点背后,往往是 HTTP/2 流控或 Go runtime 调度问题
当你看到并发从 200 升到 300,QPS 不升反降、p99 延迟跳变,别急着怪业务逻辑——先查两个地方:http2.Transport.MaxConcurrentStreams 和 Go 的 GOMAXPROCS 设置。
性能影响:gRPC 默认用 http2.Transport,其 MaxConcurrentStreams 默认是 100;而 Go 1.21+ 默认 GOMAXPROCS 是 CPU 核心数,如果压测机只有 4 核,但启了 500 goroutine,调度器争抢严重,大量时间花在 parked goroutine 唤醒上,不是花在处理请求上。
- 客户端改
http2.Transport:通过grpc.WithTransportCredentials(credentials.TransportCredentials(...))包一层自定义 transport,并设置MaxConcurrentStreams到 1000+ - 压测机上临时设
GOMAXPROCS=16(根据 CPU 逻辑核数调整),再跑ghz,对比吞吐变化 - 用
go tool trace抓一次压测过程,看Proc状态是否长期Idle或Running时间碎片化,这是调度瓶颈的明确信号
真实压测中,连接复用策略、流控窗口、Go 调度器、甚至 DNS 解析缓存都会成为隐性瓶颈。数据波动大时,先盯住 ghz 输出里的 error rate 和 latency 分布,比盯着平均 QPS 有用得多。











