goroutine泄漏导致RPC吞吐卡在100 QPS,主因是未处理响应、未设超时、未检查错误;HTTP模式因连接池小致吞吐下降;sync.Pool误用含指针结构体引发数据污染;rpc.Client非并发安全需合理池化。

goroutine 泄漏导致 RPC 吞吐量卡在 100 QPS 上不去
不是并发数设少了,而是没等响应就丢掉了 goroutine。常见于用 go rpcClient.Call(...) 后不检查返回、不处理 done 通道、也不加超时控制——这些 goroutine 会一直挂起,直到连接断开或服务端超时,期间持续占着内存和调度资源。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 永远用
context.WithTimeout()包裹 RPC 调用,比如ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - 调用后必须读取
Call.Reply或检查Call.Error,哪怕只是if call.Error != nil { ... } - 避免裸写
go client.Call(...);改用带错误传播的封装,例如把结果发到带缓冲的chan *rpc.Call,主 goroutine 统一收拢处理 - 用
runtime.NumGoroutine()在压测中观察是否线性增长——涨得停不下来基本就是泄漏了
net/rpc 默认 HTTP 传输在高并发下吞吐反而下降
net/rpc 的 HTTP 模式本质是每请求启一个 http.ServeHTTP,底层走标准 net/http,而后者默认 MaxIdleConnsPerHost 是 2,客户端复用连接能力极弱,大量 TIME_WAIT + 新建连接开销直接拖垮吞吐。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 服务端别用
http.Serve暴露 RPC,改用自定义 TCP listener:listener, _ := net.Listen("tcp", ":8080"); rpc.ServeConn(listener.Accept()) - 客户端必须复用连接:用
rpc.NewClientWithCodec配合自定义gob.ClientCodec,绕过 HTTP 栈 - 如果非 HTTP 不可(如需穿透网关),至少调大客户端连接池:
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 100 - 注意
net/rpc的 gob 编解码无字段名校验,结构体字段增删会导致静默解析失败——压测时偶发 panic 往往是这个原因
sync.Pool 误用于 RPC 请求/响应结构体引发数据污染
有人为减少 GC 把 Request 和 Response 放进 sync.Pool 复用,但忘了 net/rpc 内部会直接对指针字段赋值,上一次请求残留的 slice 数据、map 引用可能被下一次调用直接读到,出现「返回了别人的数据」这种诡异问题。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 绝不要把含指针字段(
[]byte、map[string]interface{}、嵌套 struct)的结构体丢进sync.Pool - 若真要复用,只允许纯值类型(
int、string、不含指针的 flat struct),且每次从 Pool 取出后必须显式清零:req.Reset() - 更稳妥的做法是用
proto.Message或json.RawMessage这类明确生命周期的载体,靠序列化层隔离状态 - 压测时开启
GODEBUG=gctrace=1观察 GC 频次——如果复用后 GC 次数没降,说明对象根本没逃逸,Pool 反而增加调度开销
客户端未设置合理的连接池大小,导致连接争抢严重
单个 *rpc.Client 实例本身不是线程安全的,官方文档写的是「not safe for concurrent use」,但很多人把它当连接池用,所有 goroutine 共用一个 client,结果 client.Go 内部锁竞争激烈,QPS 上不去还报 rpc: client protocol error。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每个 goroutine 自己 new 一个
rpc.Client开销不大,TCP 连接由底层复用(前提是用了长连接);或者用连接池管理*rpc.Client实例,而非复用单个实例 - 连接池 size 不要硬写 10 或 100,按后端服务的 CPU 核心数 × 2 估算起步,再根据
netstat -an | grep :8080 | wc -l观察 ESTABLISHED 数是否稳定 - 务必在
defer client.Close()前确认调用已结束——client.Close()会中断所有 pending call,造成批量失败 - 如果后端是 gRPC,别硬套
net/rpc;Go 生态里golang.org/x/net/rpc已事实废弃,新项目优先考虑google.golang.org/grpc+ 连接池中间件
真正卡吞吐的往往不是协程数量,而是连接生命周期管理、错误路径遗漏、以及那些你以为“省事”实则埋雷的复用逻辑。










