前言:运维老哥说我负责的mq消息中间件(golang)占cpu有些高,于是做了一番优化,先看优化效果。

左图是进程对单core使用的比例,右图是进程占用的核数。

如上右图,按进程所占用的核数,流量高峰从60核降到了6。降比超90%

想无脑解决问题的,直接跳第五结论部分。

一、初步分析。

pprof进行分析:go tool pprof ./bin/mq-pusher http://x.x.x.x:9967/debug/pprof/profile (应用程序的pprof接口怎么开,以及flat cum等指标含义,去搜就好)

top看一下cpu消耗排序。

第一次接口获取到pprof数据后文件会自动保存到本地,第二次分析可以直接指定该本地文件即可

偏底层函数,看不明白,得往上层分析,找上游触发场景。

执行:traces runtime\.\(\*lfstack\)\.pop

看到了runtime.gcDrainN,一搜整个runtime.(*lfstack).pop的traces结果,发现该函数基本都是gcDrainN触发的。

到这已经有一个初步结论了,cpu忙在了频繁gc上。

二、战略方向。

既然忙在了gc上,一拍脑袋,得从两个方面着手:

1、是否频繁在开辟临时内存,导致忙于垃圾回收(gc)。

2、gc机制如何,是否对此服务来说默认的gc参数不合理,导致频繁gc。

三、优化手段。

方向一:看程序开辟内存情况。

pprof还能派上用场:go tool pprof -alloc_objects http://x.x.x.x:9967/debug/pprof/heap

top一把:

对前1、2、3、4、5分别traces,经traces结果分析前4类是一个问题:

1)top 1、2、3、4。

util.ParseMsg这个没法优化,流程中就是要解json消息体的。

consumer.doCompare这个看了下代码,有优化空间,

左图是代码,右图是线上实际配置

可以看到,循环了6次规则,同样的数据也反解了6次。list一把这个反解操作,是比较消耗内存的。

这步操作总内存开辟占比25.53%,如果6次解数据,优化到1次,内存开辟操作理论能降比20%,gc压力也能减轻。

2)top 5(6%)

方法源代码

这个主要是依赖的底层组件实现上的问题了,底层组件为了返回调用方所需的数据格式,上面代码能看到,每次都开新变量对原始数据进行了格式化组装,而调用方调此函数的频率又比较高,所以开辟内存也是个不小的开销。

解决方案两种:

1、调用方对数据做缓存,减少调用频次。

2、组件做改造,直接把数据按调用方所需的格式存起来(单例模式?)。

这里采用的方案2,见下面两张图。

结构体定义变更
新方法的实现

这步操作总内存开辟占比6.09%,按方案2能全部优化掉。

小结:上面两步算下来,理论值总共减少了26%内存开辟。

方向二:gc配置怎么调。

先说下gc这件事:gc配置是个博弈的问题。gc阈值设的比较低,程序可能频繁触碰gc阈值,会导致cpu在gc上消耗了过多的精力;gc阈值大了,单次gc要释放的内存多,耗时太久,而应用逻辑执行与gc是互斥操作,从用户来看就是程序处理耗时周期性的会抖一下。

再分析下我们的程序:我们的程序是一个对接kafka的mq消息中间件,主要做数据的中间流式处理,每条数据都需要开辟临时变量进行数据的解包和打包操作,从现象看gc频频触发,所以这里认为问题更偏向于上面第一种情况。

golang gc调整的方案大家说法很多,我罗列一下:

1)debug.SetGCPercent(1000) (按比例设)//默认值是100

大概逻辑是,当前内存使用达到上次gc后内存使用的2倍左右(假如按默认值100来说),即开始新一轮gc。

2)debug.SetMaxHeap(10G) (按上限设)

大概逻辑是,当前内存使用达到10G后开始新一轮gc。

看issue上说go 1.14版有,但不在主分支上,我当前的1.16也没有看到该函数,所以基本也不予考虑了。

3)Go ballast:建一个大变量,并保活。(按上限设的变种方案。)

没试过,博主说这个并不会占用实际使用的内存,而占的是虚拟内存,但go的gc目测是基于实际使用的内存,那这个操作怎么影响gc阈值的,个人感觉逻辑无法自洽,而我也不喜欢搞骚操作。有试过的欢迎评论留言。

最终我采取的是方案一。debug.SetGCPercent(1000)

四、落地效果

我们看下只改代码 和 改代码+调整gc的效果对比

1)cpu:

CPU top(上半只改代码,下半改代码+ SetGCPercent)
左2部分进程cpu占用情况(21:10前只改代码,21:10后改代码+ SetGCPercent)+右1内存情况(绿线是升级节点)
进程cpu占用情况(21:10前只改代码,21:10后改代码+ SetGCPercent)

局部结论一:gc参数是影响gc频次的主要因素。(内存使用从200M上升到1个G)

2)内存

内存top(什么都不做之前)
内存top(上半只改代码,下半改代码+ SetGCPercent)

局部结论二:代码改完后,内存开辟情况确有好转,原来top的有降低,naming-webfoot组件函数也已经不在top里了。

五、结论

以本程序的情况来说,影响gc的主要还是gc参数(debug.SetGCPercent(1000) )。只改代码,不调gc参数,cpu效果并不理想(毕竟还是有大量的消息流处理)。

想快速解决问题可以只调参,有时间+节操的话可以深度优化一下代码,个人看情况决择。