如何通过策略优化一次gRPC服务器内存与吞吐量?

摘要:背景 最近,上线的采集器忽然时有OOM。采集器本质上是一个grpc服务,网络设备通过grpc协议将数据上报后,采集器进行格式等整理后,发往下一个系统(比如分析,存储)。 打开运行环境,发现特性如下: 每个采集器实例,会有数千个设备相连。并且
背景 最近,上线的采集器忽然时有OOM。采集器本质上是一个grpc服务,网络设备通过grpc协议将数据上报后,采集器进行格式等整理后,发往下一个系统(比如分析,存储)。 打开运行环境,发现特性如下: 每个采集器实例,会有数千个设备相连。并且会建立一个双向 grpc stream,用以上报数据。 cpu的负载并不高,但内存居高不下。 初步猜想,内存和stream的数量相关,下面来验证一下。 优化内存 这次,很有先见之明的在上线就部署了pprof。这成为了线上debug的关键所在。 import _ "net/http/pprof" go func() { logrus.Errorln(http.ListenAndServe(":6060", nil)) }() 先看协程 一般内存问题会和协程泄露有关,所以先抓一下协程: go tool pprof http://localhost:6060/debug/pprof/goroutine 得到了抓包的文件 /root/pprof/pprof.grpc_proxy.goroutine.001.pb.gz,为了方便看,scp到本机。 在本地执行: go tool pprof -http=0.0.0.0:8080 ./pprof.grpc_proxy.goroutine.001.pb.gz 如果报错没有graphviz,安装之: yum install graphviz 此时进入浏览器输入http://127.0.0.1:8080/ui/,会有一个很好看的页面。 在这里,会发现有13W个协程。有点多,但考虑到连接了10000多个设备。 这些协程,有keepalive, 有收发包等协程。都挺正常,其实问题不大。 几乎所有的协程都gopark了。在等待。这也解释了为什么cpu其实不高,因为设备连上了但是不上报数据。占着资源不XX。 再看内存 协程虽然多,但没看出什么有价值的东西。那么再看看内存的占用。这次换个命令: go tool pprof -inuse_space http://127.0.0.1:6060/debug/pprof/heap -inuse_space 代表观察使用中的内存 继续得到数据文件,然后scp到本机执行: go tool pprof -http=0.0.0.0:8080 ./pprof.grpc_proxy.alloc_objects.alloc_space.inuse_objects.inuse_space.003.pb.gz 发现grpc.Serve.func3 ->...-> newBufWriter占用了大量内存。 问题很明显,是buf的配置不太合适。 这里多提一句,grpc服务端内存暴涨一般有这几个原因: 没有设置keepalive,使得连接泄露 服务端处理能力不足,流程阻塞,这个一般是下一跳IO引起。 buffer使用了默认配置。ReadBufferSize和WriteBufferSize默认为每个stream配置了32KB的大小。如果连接了很多设备,但其实cpu开销并不大,可以考虑减少这个值。 修改后代码添加grpc.ReadBufferSize(1024*8)/grpc.WriteBufferSize(1024*8)配置 var keepAliveArgs = keepalive.ServerParameters{ Time: 10 * time.Second, Timeout: 15 * time.Second, MaxConnectionIdle: 3 * time.Minute, } s := grpc.NewServer( ....... grpc.KeepaliveParams(keepAliveArgs), grpc.MaxSendMsgSize(1024*1024*8), // 最大消息8M grpc.MaxRecvMsgSize(1024*1024*8), grpc.ReadBufferSize(1024*8), // 就是这两个参数 grpc.WriteBufferSize(1024*8), ) if err := s.Serve(lis); err != nil { logger.Errorf("failed to serve: %v", err) return } 重新发布程序,发现内存占用变成了原来的一半。内存占用大的问题基本解决。
阅读全文