创建一个 gRPC 客户端连接,会创建的几个协程:
1)transport.loopyWriter.run 往服务端发送数据协程,流控时会阻塞,结果是数据堆积,内存上涨
2)transport.http2Client.reader 接收服务端数据协程,并会调用 t.controlBuf.throttle() 执行流控
现象描述:
客户端到服务端单个连接,压测时内存快速增长,直到 OOM 挂掉。在 OOM 之前停止压测,内存会逐渐下降。客户端到服务端改为两个连接时,压测时未出现内存快速增长。
问题原因:
每一个 gRPC 连接均有一个独立的队列,挂在该连接的所有 streams 共享,请求相当于生产,往服务端发送请求相当于消费,当生产速度大于消费速度时,就会出现内存持续上长。该队列没有长度限制,所以会持续上长。快速上涨的原因是协程 transport.loopyWriter).run 没有被调度运行,队列消费停止,导致队列只增不减。停止压测后,协程 transport.loopyWriter).run 会恢复执行。
当不再消费时,可观察到大量如下协程:
grpc/internal/transport.(*Stream).waitOnHeader (0x90c8d5)
runtime/netpoll.go:220 internal/poll.runtime_pollWait (0x46bdd5)
使用 netstat 命令可观察到发送队列大量堆积。
解决方案:
控制生产速度,即控制单个 gRPC 客户端连接发送的请求数量。此外,还可以启用客户端的 keepalive 关闭连接。
后话
go gRPC 如果提供取 controlBuffer 的队列 list 的大小接口,可使得更为简单和友好。
相关源码:
- http2Client
// 源码所在文件:google.golang.org/grpc/http2_client.go
// http2Client 实现了接口 ClientTransport
// http2Client implements the ClientTransport interface with HTTP2.
type http2Client struct {
conn net.Conn // underlying communication channel
loopy *loopyWriter // 生产和消费关联的队列在这里面,所在文件:controlbuf.go
// controlBuf delivers all the control related tasks (e.g., window
// updates, reset streams, and various settings) to the controller.
controlBuf *controlBuffer // 所在文件:controlbuf.go
maxConcurrentStreams uint32
streamQuota int64
streamsQuotaAvailable chan struct{}
waitingStreams uint32
initialWindowSize int32
}
type controlBuffer struct {
list *itemList // 队列
}
type loopyWriter struct {
// 关联上 controlBuffer,
// 消费 controlBuffer 中的队列 list,
// 生产由 http2Client 通过 controlBuffer 进行。
cbuf *controlBuffer
}
- 一个 gRPC 客户端连接被创建时,即会创建一个 run 协程,run 协程为队列的消费者
// 源码所在文件:internal/transport/http2_client.go
// 所在包名:transport
// 打断点方法:
// (dlv) b transport.newHTTP2Client
// 被调用:协程 grpc.addrConn.resetTransport
func newHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts ConnectOptions, onPrefaceReceipt func(), onGoAway func(GoAwayReason), onClose func()) (_ *http2Client, err error) {
// 建立连接,注意不同于 grpc.Dial,
// grpc.Dial 实际不包含连接,对于 block 调用也只是等待连接状态为 Ready 。
// transport.dial 的实现调用了 net.Dialer.DialContext,
// 而 net.Dialer.DialContext 是更底层 Go 自带包的组成部分,不是 gRPC 的组成部分。
// net.Dialer.DialContext 的实现支持:TCP、UDP、Unix等:。
conn, err := dial(connectCtx, opts.Dialer, addr.Addr)
t.controlBuf = newControlBuffer(t.ctxDone) // 含发送队列的初始化
if t.keepaliveEnabled {
t.kpDormancyCond = sync.NewCond(&t.mu)
go t.keepalive() // 保活协程
}
// Start the reader goroutine for incoming message. Each transport has
// a dedicated goroutine which reads HTTP2 frame from network. Then it
// dispatches the frame to the corresponding stream entity.
go t.reader()
// Send connection preface to server.
n, err := t.conn.Write(clientPreface)
go func() {
t.loopy = newLoopyWriter(clientSide, t.framer, t.controlBuf, t.bdpEst)
err := t.loopy.run()
}
}
0 0x00000000008f305b in google.golang.org/grpc/internal/transport.newHTTP2Client
at /root/go/pkg/mod/google.golang.org/grpc@v1.33.2/internal/transport/http2_client.go:166
1 0x00000000009285a8 in google.golang.org/grpc/internal/transport.NewClientTransport
at /root/go/pkg/mod/google.golang.org/grpc@v1.33.2/internal/transport/transport.go:577
2 0x00000000009285a8 in google.golang.org/grpc.(*addrConn).createTransport
at /root/go/pkg/mod/google.golang.org/grpc@v1.33.2/clientconn.go:1297
3 0x0000000000927e48 in google.golang.org/grpc.(*addrConn).tryAllAddrs
at /root/go/pkg/mod/google.golang.org/grpc@v1.33.2/clientconn.go:1227
// 下列的 grpc.addrConn.resetTransport 是一个协程
4 0x000000000092737f in google.golang.org/grpc.(*addrConn).resetTransport
at /root/go/pkg/mod/google.golang.org/grpc@v1.33.2/clientconn.go:1142
5 0x0000000000471821 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1374
// 源码所在文件:grpc/clientconn.go
// 所在包名:grpc
// 被调用:grpc.addrConn.getReadyTransport
func (ac *addrConn) connect() error {
// Start a goroutine connecting to the server asynchronously.
go ac.resetTransport()
}
// 传统类型的 RPC 调用从 grpc.ClientConn.Invoke 开始:
// XXX.pb.go // 编译 .proto 生成的文件
// ->

本文分析了gRPC客户端连接创建时协程堆积导致内存飙升的问题,重点在于生产者和消费者的不平衡以及队列无限制。提出通过控制请求速率和启用Keepalive来缓解内存增长,同时建议gRPC提供队列大小查看功能以优化管理。
最低0.47元/天 解锁文章
1200

被折叠的 条评论
为什么被折叠?



