grpc-go服务内存攀升

grpc-go服务在个别response比较大的情况下会导致服务占用内存也随之增大,这块拍脑袋想可能是自然的情况,response大那么需要的网络io时间就长,那么这些response的[]byte就一直留在内存中。

rpc框架是否会发部分清除这部分数据,看go的slice的原理,感觉是能做到的,相当于不断把slice中的ptr向后移动就行了,这块后续可以考虑进行验证。

继续上边说的,这个问题如果我们不拍脑袋就纸上谈兵,那么怎么做到数据层面的计算:

在cpu仍有余力的情况下,内存增大是我们要探讨的问题,response大是当前状况,当前网络io假设我们独占。

基于以上条件,在这里说下grpc-go的sendResponse设计方式,结合起来看是否grpc-go设计上有问题还是grpc-go已经尽力了,其实是网络带宽不够,client和server之间保持一个长连接,基于h2协议,单连接启动单个goroutine处理所有可以异步的任务(这里只说server的逻辑):窗口调整,headerFrame和dataFrame发送,单response是分frame(最大16k)发送出去,在当前server其他的api都是常规流量的情况下,该response发送出去的时间应该与大小成近似线性的增长,单response不能并发发送,调用系统调用中的tcp write必须有序,这样client端tcp上层收到的data才是有序的,但同一stream的不同frame是否可以更快的发送出去?是否可以在loopyWriter加入并发能力,我觉得grpc-go在这块的设计上采用了最简方案,能保证stream顺序和逻辑简单,但不引入并发能力,可能导致利用单cpu的情况,不能充分的利用网络io,我们是千兆网,当时看到网络比平时多了300m左右吧,最大350m。这里面还有个点是最大frame是有限制的,最大frame的限制我觉得可以参考tcp为什么要分packet传输(这块后续补充),先假设这块的设计是合理的。

tcp的分segment是和cpu分时间片执行的设计思想应该类似,方式一个请求持续占用过多资源,将请求分segment后,带宽切片允许并行的请求数增多,这时候在带宽非瓶颈的情况下请求是一定能得到公平处理的

通过上面对grpc-go机制的分析,我们可以断定,网络带宽 & cpu都有余力,那么就是我们在loopyWriter这里的dataFrame处理上存在排队,导致response发送时间延长,当积累很多这种response时,内存自然上升,假设response足够大,那么这种情况在一段时间内(类似几个小时)表现就是内存的不断攀升。

其实,我还考虑过其他的情况,上游的在这时其实已经超时了,下游是否能感知到,答案是能感知到,但需要看取消发生在什么环节,出现上文情况的时候,通过log判断业务逻辑的处理时间几十ms,都直接走到sendResponse环节,这块是没有打断机制的,我觉得这块实际也可以想办法引入打断机制,每个frame都探测一次,没什么损失。

还有个先入为主的思路,是我们有很多grpc服务,单纯这个在grpc层面出问题是不太现实的,之所以定位到grpc占用内存是通过pprof发现的proto.Marshal下面的grow方法,申请的内存总和巨大,这种断定得基于开发者对于grpc-go框架的掌控程度,rpc服务之间在外界条件相同的情况下,是可以断定grpc-go没问题的,相同条件比较复杂,同机房/同样请求量/机器配置一样/负载相似。

当时还又一个错误的判断,grpc-go在sendResponse时需要Marshal业务逻辑产生的resp对象为[]byte,grpc-go利用sync.Pool存储上次使用的buffer大小,如果某次response发送的时候,从Pool得到上次buffer大小是多少,就直接申请这么大空间,用完之后再调整buffer的建议申请大小,这依赖于rpc服务的拆分/治理/统计/监控,让rpc服务的输入输出波动不大,才能更稳定,但是实际情况肯定不是这样,实际环境依赖于工程师的水平/公司技术积累,中小甚至大公司都存在大量不合理,不稳定的rpc服务,还有可能有些rpc服务以功能划分,就存在response相差大的情况,grpc-go框架设计者好心,但是与实际相悖的可能性很大。

上面标记为黄色的两个思路,后续我会实验下,看是否可以让grpc在异常情况下更稳定,更能发挥机器的资源利用率。

grpc-go服务内存攀升
Share this