gRPC双向流服务(go语言实现)内存优化小记

Posted 拖地先生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gRPC双向流服务(go语言实现)内存优化小记相关的知识,希望对你有一定的参考价值。


gRPC (https://grpc.io) 是一个由Google开发的高性能、开源、跨多种编程语言和通用的远程过程调用协议(RPC) 框架,用于客户端和服务器端之间的通信,使用HTTP/2协议并将 ProtoBuf (https://developers.google.com/protocol-buffers)作为序列化工具。


双向数据流模式(Bidirectional streaming RPC),是客户端和服务端都可以向对方发送数据流,这个时候双方的数据可以同时互相发送,实现了实时交互。我们想利用这个模式,简单替代长连接管理,让App具备在线用户感知并且可以随时触达任意在线客户端。


该服务通过gRPC进行了连接和用户ID的管理,以下简称双向流。在第一个版本上线后,如下图发现内存占用较大,UID和连接总数并不是一一对应。在关闭链接开关后,连接也未能如期望立刻释放。若再次开启,内存占用仍会持续增长,并不随活跃用户数而波动。

gRPC双向流服务(go语言实现)内存优化小记


由此开始了前前后后多次的优化,终于实现了内存占用的大幅下降和内存的稳定,平均1W链接大约占用500M内存,算是完成了预定的优化目标。这里简单记录下这几次的优化过程。




1、第一次优化:

主要是排查双向流内存和IP索引和UID索引差距太多问题。内存优化主要是通过在项目中添加pprof调试,在程序运行过程中实时查看pprof输出的信息。 


添加pprof包调试后,在浏览器中打开:http://localhost:port/debug/pprof 就可以看到程序运行过程中goroutine和堆分配以及GC相关的信息。 

gRPC双向流服务(go语言实现)内存优化小记

通过查看发现:客户端断开链接后竟然还有1000个goroutine没有释放,而客户端链接正好是1000次。于是通过查看代码发现每次建立链接会多开一个goroutine,为了兼容服务端终止从而自动断开客户端的链接。但是这个goroutine只有服务端中断才能执行完成、释放,如果服务端一直在运行这个goroutine就永远不能退出、不会释放,这样子就造成了goroutine泄漏。


去掉了这块逻辑后,测试goroutine释放正常。本以为是就解决了内存泄漏的问题,结果上线运行一段时间后发现内存还是会随着运行时间增长会慢慢增长,而且UID索引也会慢慢增长,所以还是没能彻底解决内存泄漏的问题。


2、第二次优化:

通过上次优化完成上线运行的情况看,UID索引会随着运行时间慢慢增长。初步估计是UID删除有问题导致UID慢慢增长从而影响双向流内存占用,修改了这部分的逻辑代码重新上线观察发现:UID索引数量是正常了,但是还存在随着运行内存缓慢上涨。这就说明还是有其他的地方存在内存泄漏的问题,没办法只好重新通过pprof观察。


上一次优化只是简单的观察解决了goroutine泄漏的问题,这次要从程序运行过程中分配的堆内存查起。查看运行时间内堆上累计分配的内存,具体命令是:go tool pprof -alloc_space -cum http://localhost:port/debug/pprof/heap,使用-cum是累计函数调用栈的堆分配大小,图形会将调用栈很大的路线加粗标识出来。

gRPC双向流服务(go语言实现)内存优化小记

        

观察发现有多个地方堆内存分配的非常多:其中一个是双向流扩展包的,一个是IP、UID存储的地方。本着打破砂锅查到底的心态,直接在github上找到双向流的扩展包,看看是不是别人也遇到了双向流存在内存泄漏的问题,结果发现还真有:

        

我们用的扩展包版本很旧,最新的版本是修复了双向流一个内存泄漏的bug,于是更换成最新的双向流扩展包。这样双向流本身会造成内存泄漏的问题解决了,接下来就是存储会导致内存泄漏的问题。


双向流存储全部是用map来做的,网上查找资料发现使用map做存储确实会有可能造成内存泄漏,参考文章:https://blog.cyeam.com/json/2017/11/02/go-map-delete 。解决方案是定时拷贝map,然后就可以保证map中删除的数据所占用的空间能够正常释放。

    

3、第三次优化: 

第二次优化完成后,甚至代码中加入了强制释放内存的逻辑,通过观察GC发现还是有部分内存泄漏的问题,通过pprof重新观察发现还是存储的问题。上次优化只是修改了IP索引和UID索引的存储问题没有修改其他的。


通过追查代码发现用map存储的还有其他四个地方:2个统计连接的、1个记录接收包的、1个微服务框架底层用来做grpc统计的。这4个地方都是用map做存储的,存储的东西有的没有太大价值,有的是和统计IP索引同样的数据。 


这些就是无用的内存,索性就把这4个地方全部去掉了。上线运行观察了几天发现一切正常:内存占用比之前的足足少了一个G,内存也趋于稳定了。后面再通过优化goroutine数量也没有太大效果了,至此双向流的优化就告一段落了。



 

总结:      

一、排查内存泄漏

       1、通过pprof查看goroutine是否存在泄漏:本地压测观察goroutine变化,如果压测结束后goroutine数量没有下降则证明程序存在goroutine泄漏。可查看goroutine具体信息进而定位到具体的代码逻辑,修改就可以解决goroutine泄漏问题。

      2、通过pprof查看压测这段时间内堆上累计分配的内存,可以查看堆内存分配多的地方,追溯代码逻辑进行优化处理。

      3、通过命令GODEBUG=gctrace=1 ./serivce_name 观察GC。

       

二、内存优化

      1、去除无用的、重复的存储使用。

      2、尽量少用map或者map中value尽量少为指针,因为GC扫描会扫描map中的指针类型,如果map中指针太多会增加GC扫描时间。

      3、如果用map做缓存,而每次更新只是部分更新,更新的 key 如果偏差比较大,有可能会有内存逐渐增长而不释放的问题,这时候就可以通过拷贝map来解决。数十万int64的内存拷贝在100ms以下。

      

三、GC机制

      1、Go语言有内存池,每次GC后只是把没有使用的对象放入内存池中,不会立即归还给操作系统。这样子如果程序还需要内存的话会优先从内存池中分配,不会向操作系统请求分配内存。内存池中对象如果5分钟还没有再被使用就会把这些内存归还给操作系统。这两部分别为:gc和scvg。

      2、强制GC:runtime.GC()

      3、强制释放内存给操作系统:debug.FreeOSMemory()




拖地先生,从事互联网技术工作,在这里一起聊聊日常的技术点滴和管理心得。


如果对你有帮助,让大家也看看呗~

以上是关于gRPC双向流服务(go语言实现)内存优化小记的主要内容,如果未能解决你的问题,请参考以下文章

gRPC如何在Golang和PHP中进行实战?7步教你上手!

gRPC 的 4 种基础通信模式

《Go-micro微服务框架入门教程》学习笔记 | gRPC

Go语言实战 gRPC 实现一个简单微服务

Go语言实战 gRPC 实现一个简单微服务

gRPC-go服务端实现简析