一.背景
线上频繁出现报警,提示内存被打爆问题,几个服务出现短暂不可用的现象,现就分析过程和解决过程记录如下
二.问题描述:
报警群里开始报警:活动中心集群中某个节点内存超过90%,有接口出现短暂不可用。
立即重启机器,但是重启依然挂掉。然后进行扩容,但是刚起来就内存打爆。
通过可以看到以下接口收到大量请求,同时有三台天津的机器 内存使用率超过90%
初步怀疑是请求量大,导致内存被打爆, 可能是本地内存淘汰的速率远远小于缓存速率导致的,内部容量不够时会扩容。
使用go heap tool分析如下
内存空间图如下:
可以明显的看到bigCache在init初始化时用了大量内存,而且当前业务场景key的值并不是太多,所以排除了缓存速度远远大于淘汰速度的原因。
然后深入到代码,以及查看bigCache发现是参数设置有问题。这里的MaxEntrySize并不是bigcache的内存大小,
只是每个entry(key)的最大大小
这里1000*10*60*500Byte = 300M和上图的基本吻合,但是发现还有个2.83G
原来另一个接口中设置了更大的本地缓存:
1000*10*60*5000Byte 约等于3G。设置内存大小需要使用:WithHardMaxCacheSize,否则会导致OOM.
但是有一个疑问,为啥平时没有报出这个错误那?
回到业务场景,这个服务是为了首页不断拉取横幅和小工具,属于读多写少的情况,为了前端速度,采用本地缓存,这就造成如果用户更新,不能影响全局的数据,所以设置了比较短的过期时间。在高并发情况下这就可能出现大量set cache的情况
,代码发现代码设置缓存开了goroutine 异步执行,本身高并发导致goroutine set cache时,锁等待,大量goroutine hang 住了。短过期时间下,高并发set 导致gorountiue 过多,进而导致gotournie内存泄漏。那个时间段的监控确实goroutine 过多, 高并发导致goroutine set cache时,锁等待,大量goroutine hang 住了. 因为是本地缓存,如果用户更新db,为了拉到最新数据,让cache快速过期。
带锁,而且bigCache本身也是有lock机制,当并发上来时直接产生竞争,导致内存不能及时回收,撑爆内存。
这快采用单例模式进行set,每次只保证一个gorountine去设置缓存。
三.总结
1.本地缓存BigCache设置有问题.
2.Set bigcache key是加锁操作,高并发场景下,当多个goroutine 同时进行set时,导致其他goroutine hang住,短时间内大量gotinue没有被释放撑爆内存。
四.解决方案
1. 初始化较小本地缓存,设置缓存最大值,出发LRU淘汰。
2.使用SingleFlight-- 合并相同请求,是为了加固代码,采用double check机制,set 之前get一次,减少无谓的cpu计算和内存资源浪费
五 效果如何:
经过修改,同时压测了1000qps的数据,测试环境如下:
经过上线后对比数据如下,可以清楚的看到变更后内存使用率显著下降。
分析go内存heap 可以看到init New bigCache显著下降