bigywater.tech
gRPC · 31

Go 的 grpc 服务器:每个请求一个 goroutine 的代价

grpc-go 服务端给每个请求起一个 goroutine,这有多贵?一万个挂着的请求占多少内存?NumStreamWorkers 能限流吗?客户端取消了,服务端的 handler 会不会停?这篇用子进程里的纯服务端和真实的 MemStats 回答。

约 11 KiB
每个在途的 unary 调用在服务端的开销:堆约 3 KiB + goroutine 栈约 8 KiB
峰值 4993
NumStreamWorkers(8) 时 5000 个并发调用同时在处理的峰值——没有限流
500 → 0
取消后 500 ms 仍在运行的 handler 数:不看 ctx 是 500,监听 ctx 是 0

一调用一 goroutine

实测 · 本机执行输出
  unary          基线 2 个 goroutine;1000 个调用在途时 1032 个(每个调用 +1.03);全部结束后 2 个
  server-stream  基线 2 个 goroutine;1000 个调用在途时 1032 个(每个调用 +1.03);全部结束后 2 个
  bidi           基线 2 个 goroutine;1000 个调用在途时 1032 个(每个调用 +1.03);全部结束后 2 个

grpc-go 服务端默认给每个新流起一个 goroutine 跑 handler,结束就退出。goroutine 本身便宜,但一万个挂起的调用也是一万个 goroutine 和它们的栈。

内存

实测 · 本机执行输出
   1000 个在途调用:堆 +3.1 KiB/个;goroutine 栈 +8.6 KiB/个;合计 11.7 KiB/个;进程向系统申请的总量(Sys)+17.8 KiB/个;goroutine 数 2 → 1032
  10000 个在途调用:堆 +2.7 KiB/个;goroutine 栈 +8.1 KiB/个;合计 10.7 KiB/个;进程向系统申请的总量(Sys)+11.7 KiB/个;goroutine 数 2 → 10032

实测每个挂着的调用约 11 KiB(堆约 3 KiB,栈约 8 KiB),不含 handler 自己的分配。

限流

实测 · 本机执行输出
  不限制                    同时在处理的峰值 4539;5000 个调用总耗时 68.9 ms
  信号量 100(拦截器)           同时在处理的峰值  100;5000 个调用总耗时 1099.9 ms
  NumStreamWorkers(8)    同时在处理的峰值 4993;5000 个调用总耗时 57.9 ms

NumStreamWorkers 是一个 goroutine 复用的优化,worker 忙时仍会新开 goroutine,不能限制并发。真要限流用信号量拦截器、每条连接的 MaxConcurrentStreams 或上游限流。

取消

实测 · 本机执行输出
  handler 不看 ctx(time.Sleep 3 秒) 取消前在运行 500;取消后 500 ms:仍在运行 500、goroutine +500;3.5 秒后:仍在运行 0、goroutine +0
  handler 用 select 监听 ctx        取消前在运行 500;取消后 500 ms:仍在运行 0、goroutine +0;3.5 秒后:仍在运行 0、goroutine +0

客户端取消只是让 ctx 变成 Done。handler 要自己在等待和下游调用里监听 ctx;不看 ctx 的 handler 会继续占着 goroutine 直到自然结束。

分步演示

goroutine 与内存
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。第一、二节的服务端是子进程(只做服务端),runtime.NumGoroutine 与 MemStats(HeapAlloc、StackInuse、Sys,GC 之后)由子进程自己报告,客户端的内存不计入;第三、四节在同一个进程里,峰值用 handler 里的原子计数器;“新流默认 go f()”“worker 忙时回退”读自 server.go
    • 没有验证:handler 里分配更多内存时的情形;GOMAXPROCS 或 CPU 核数对大量 goroutine 调度的影响;流式 RPC 里 goroutine 的数量(每个流一个 handler goroutine 之外是否还有别的,只看了总数);客户端侧每个调用的开销;GC 压力与延迟尾部(第 36 篇压测)
    京ICP备2026067502号-1京公网安备11010502063487号