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