记一次线上频繁GC
内存泄露Bug现场
线上某核心链路服务的一个节点疯狂GC,监控图如下:
文章图片
平均1分钟触发CMS GC 36次,已无法正常处理线上请求。
准备工作
发现该节点有问题后,找运维将该节点从服务注册中心上摘掉
,因为我们需要去jmap dump服务的堆栈信息,而dump内存会STW
,必须先摘流。
dump命令如下:
jmap -dump:format=b,file=heap.bin [pid]
dump好以后gzip压缩,便于文件传输到本地,从2G压缩到300+M左右。
分析 【记一次线上频繁GC】将dump的文件导入到MAT中,MAT内存分布图如下:
文章图片
1.2G的内存都被AccountChangeTask中的ConcurrentHashMap对象占用了,那思路就很清晰了,去检查代码中什么地方使用了 这个AccountChangeTask对象。
AccountChangeTask的整体结构如下:
@Service
public class AccountChangeTask {// 缓存SQL和tableName映射关系
private static final Map sqlMap = new ConcurrentHashMap<>();
@Async
public void processTask(String sql) {
// 对sqlMap对象的get put操作,key是SQL,value是表名
// 原因是逻辑中有一些对SQL做正则解析的操作,可能比较耗时和耗CPU,所以想通过缓存优化
... 其他业务逻辑
}
}
我们再去查找使用了AccountChangeTask.processTask()方法的地方,代码如下:
@Intercepts({
@Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }) })
@Component
public class MybatisInterceptor implements Interceptor {
@Autowired
private AccountChangeTask accountChangeTask;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 省略...
String sql = showSql(config,boundSql)// 填充为真实的SQL,将?填充为真实的SQL参数
// 业务逻辑判断,如果true走下面逻辑
accountChangeTask.processTask(sql);
}
}
问题解决 分析代码,原因就是缓存在ConcurrentHashMap中的SQL是被参数填充过的SQL,而线上环境的sql参数千变万化,有不同uid和时间等等,请求量一上来就把ConcurrentHashMap撑爆了。
解决思路其实也很简单:在对性能没有极致要求的情况下,移除代码中对SQL的缓存;而直接走正则逻辑 并且 提前对正则表达式做好编译,可能是更合理的选择。
总结 在没有极致性能要求的情况下,简化我们的设计,服务可能会更具健壮性。
推荐阅读
- EffectiveObjective-C2.0|EffectiveObjective-C2.0 笔记 - 第二部分
- 野营记-第五章|野营记-第五章 讨伐梦魇兽
- 20170612时间和注意力开销记录
- 2018年11月19日|2018年11月19日 星期一 亲子日记第144篇
- 叙述作文
- 2019年12月24日
- 【故障公告】周五下午的一次突发故障
- 人生感悟记#环境仪器宋庆国成长记#072
- 2019.4.18感恩日记
- 我要我们在一起(二)