一、背景

前几天下午飞书告警群里报起了java.lang.OutOfMemoryError: unable to create new native thread告警,看见后艾特了对应的项目负责人但是负责人说没时间,无奈自己亲自上阵。

二、事情经过

2.1 问题排查

从报错信息就可以看出是服务申请不到足够的内存去创建新的线程导致的,熟悉JVM的都知道线程用的是虚拟机栈内存,这里是虚拟机栈内存不够跟堆没有关系。

然后立马登录阿里云进入容器使用如下命令dump出线程堆栈

jstack -l 1 > dump.log

让运维帮我把文件下载下来后,就把dump.log上传到网站https://fastthread.io/进行分析,分析结果如下:

可以看到服务的线程数竟然高达2000多个!!!而且也给出来警告这么高的线程数可能导致OOM,那么问题原因就很清楚了就是线程数过多导致的,那么是哪些线程过多呢?

第二张图显示数量做多的是名字为com.alibaba.nacos.client.Worker的线程,并且排行前三名都是nacos的线程,我一度怀疑是nacos出bug了

再点进去看看对应的线程堆栈如下图:

可以看出线程阻塞在从队列获取任务那里,说明这些线程都是没活干,空闲挂起的,具体看不出问题原因,只能看代码了。

打开项目,使用idea全局搜索com.alibaba.nacos.client.Worker,发现是ClientWorker类在构造方法里创建了一个定时线程池,线程名称就叫com.alibaba.nacos.client.Worker,其中核心线程数是4。

继续跟踪源码,发现只有在NacosConfigService的构造方法里调用了ClientWorke的构造方法。

但是点击NacosConfigService的构造方法却发现没有调用,这是什么情况呢?

点击其父类com.alibaba.nacos.api.config.ConfigService查找引用可以看到有个NacosFactory的工厂类,原来ConfigService的是实例是通过工厂方法创建的,而工厂方法内部是通过反射创建的,如下图。

最终找到了业务类NacosConfigServiceImpl如下图:

@Slf4j
@Component
public class NacosConfigServiceImpl implements ConfigService { /**
* nacos地址key
*/
public static final String SERVER_ADDR = "spring.cloud.nacos.config.server-addr"; /**
* nacos配置namespace
*/
public static final String CONFIG_NAMESPACE = "spring.cloud.nacos.config.namespace"; @Getter
@Value("${spring.cloud.nacos.config.group}")
private String groupId; @Value("${" + SERVER_ADDR + "}")
private String nacosServerAddr; @Value("${" + CONFIG_NAMESPACE + "}")
private String nacosConfigNamespace; private ConfigService configService; private static final long TIMEOUT_MILLIS = 3000L; @PostConstruct
public void init() throws NacosException {
this.configService = getConfigService();
} @Override
public void registerListener(String dataId, ConfigListener configListener) {
AssertUtil.that(StringUtils.isNotBlank(dataId), BasicErrorCode.PARAM_ERROR, "dataId empty");
AssertUtil.that(configListener != null, BasicErrorCode.PARAM_ERROR, "configListener null"); try {
//注册监听器
// 第一次创建
ConfigService configService = getConfigService();
configService.addListener(dataId, getGroupId(), new DefaultListener(dataId, configListener)); //首次更新配置
// 第二次创建
String configInfo = getConfigInfo(dataId);
configListener.receiveConfigInfo(configInfo); log.info(LogUtil.message("registerListener success",
dataId, InvokeLineUtil.simplifiedClassName(configListener)));
} catch (Exception e) {
log.error(LogUtil.exceptionMessage("registerListener exception", e, dataId, configListener));
throw new ApiException(BasicErrorCode.CONFIG_ERROR, e.getMessage());
}
} @Override
public String getConfigInfo(String dataId) {
try {
ConfigService configService = getConfigService();
return configService.getConfig(dataId, getGroupId(), TIMEOUT_MILLIS);
} catch (Exception e) {
log.error(LogUtil.exceptionMessage("getConfigInfo exception", e, dataId));
throw new ApiException(BasicErrorCode.CONFIG_ERROR, e.getMessage());
}
} private ConfigService getConfigService() throws NacosException {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, nacosServerAddr);
properties.put(PropertyKeyConst.NAMESPACE, nacosConfigNamespace);
// 调用工厂方法创建configService
ConfigService configService = NacosFactory.createConfigService(properties);
return configService;
} }

当时看到上面代码我都惊呆了,既然已经在init()方法里初始化了属性configService,为什么还要在registerListener()方法里两次调用getConfigService()去创建两次configService实例呢?

然后找了下registerListener()方法的调用,发现多达50处!

问题原因大概就清楚了,nacos的configService设计上是作为单例去使用的,但是被滥用导致创建了很多线程池和线程把虚拟机栈内存耗尽,再申请创建新线程时就报了OOM

2.2 相关疑问

此时同事小A提出疑问,registerListener()方法执行完了,configService对象就会被gc回收,对应的线程池不也是被回收了吗?

这个问题看似很有道理其实不然,对象能不能回收是看这个对象到GC Root还有没有可达路径,线程池的gc root其实是线程,对应的gc路径是thread->workers->线程池,因为那些nacos线程池也没有设置属性allowCoreThreadTimeOut(true),所以就算线程空闲了核心线程也不会回收。

如果我们要在方法里创建并使用线程池,用完一定要记得调用shutdown方法,这样线程池才会被回收,当然更推荐线程池作为单例bean注入Spring容器使用。

爱专研的同事A又说你这线程数也对不上啊,50x4x2=400也没到500个啊。

大家有兴趣的可以去看看源码,NacosConfigService的构造方法里的ServerListManager类内部也有一个线程池,而且前面线程数量排行图中第三名的com.alibaba.nacos.client.remote.worker线程也跟这个有关。

2.3 解决方案

知道了原因解决起来就很快了,NacosConfigServiceImpl#registerListener()方法里创建configSerivce的代码改成用单例属性就行了。

三、总结

在用第三方开源组件的时候还是要多看看文档和其源码,使用正确的姿势不要一上来就撸代码,否则很容易产生生产事故。

  不过在排查问题的过程中也顺便看了下nacos服务端和客户端配置同步的实现,算还有点收获吧。

一次生产环境OOM排查的更多相关文章

  1. 生产环境OOM\死锁问题排查修复

    OOM: 1.快速恢复业务:如果是集群中的一台机器故障,先隔离故障服务器:如果是多台,则根据Nginx转发策略,对该功能转发到单独的集群,与其他流量隔离,确保其他业务不受影响 2.收集内存溢出Dump ...

  2. 生产事故-记一次特殊的OOM排查

    入职多年,面对生产环境,尽管都是小心翼翼,慎之又慎,还是难免捅出篓子.轻则满头大汗,面红耳赤.重则系统停摆,损失资金.每一个生产事故的背后,都是宝贵的经验和教训,都是项目成员的血泪史.为了更好地防范和 ...

  3. 生产环境下JAVA进程高CPU占用故障排查

    问题描述:生产环境下的某台tomcat7服务器,在刚发布时的时候一切都很正常,在运行一段时间后就出现CPU占用很高的问题,基本上是负载一天比一天高. 问题分析:1,程序属于CPU密集型,和开发沟通过, ...

  4. 生产环境JAVA进程高CPU占用故障排查

    问题描述:生产环境下的某台tomcat7服务器,在刚发布时的时候一切都很正常,在运行一段时间后就出现CPU占用很高的问题,基本上是负载一天比一天高. 问题分析:1,程序属于CPU密集型,和开发沟通过, ...

  5. 生产环境下JAVA进程高CPU占用故障排查---temp

    问题描述:生产环境下的某台tomcat7服务器,在刚发布时的时候一切都很正常,在运行一段时间后就出现CPU占用很高的问题,基本上是负载一天比一天高. 问题分析:1,程序属于CPU密集型,和开发沟通过, ...

  6. 总结:利用asp.net core日志进行生产环境下的错误排查(asp.net core version 2.2,用IIS做服务器)

    概述 调试asp.net core程序时,在输出窗口中,在输出来源选择“调试”或“xxx-ASP.NET Core Web服务器”时,可以看到类似“info:Microsoft.AspNetCore. ...

  7. 生产环境部署springcloud微服务启动慢的问题排查

    今天带来一个真实案例,虽然不是什么故障,但是希望对大家有所帮助. 一.问题现象: 生产环境部署springcloud应用,服务部署之后,有时候需要10几分钟才能启动成功,在开发测试环境则没有这个问题. ...

  8. 生产出现oom问题,怎么排查?

    生产出现oom问题,怎么排查?   1.使用dmesg命令查看系统日志 dmesg |grep -E 'kill|oom|out of memory',可以查看操作系统启动后的系统日志,这里就是查看跟 ...

  9. 一次生产 CPU 100% 排查优化实践

    前言 到了年底果然都不太平,最近又收到了运维报警:表示有些服务器负载非常高,让我们定位问题. 还真是想什么来什么,前些天还故意把某些服务器的负载提高(没错,老板让我写个 BUG!),不过还好是不同的环 ...

  10. 理解Docker(6):若干企业生产环境中的容器网络方案

    本系列文章将介绍 Docker的相关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 ...

随机推荐

  1. vue数据更新后在视图上不响应

    一.vue如何追踪变化 当你把一个普通的JS对象传给vue实例的data选项时, vue将遍历此对象的所有属性, 并使用 Object.defineProperty 把这些属性全部转为 getter/ ...

  2. DES加密和base64加密

    DES简介:参考知乎 https://www.zhihu.com/question/36767829 和博客https://www.cnblogs.com/idreamo/p/9333753.html ...

  3. Rider调试时断点打不上(变灰色小叉)

    记录我在使用rider调试Unity的C#代码时遇到断点变灰色小叉叉,断点打不上/(不会进入断点)的几种解决办法 首先要确保你没有禁用所有的断点,然后再尝试使用本文的三种方法. 不要禁用所有断点 在R ...

  4. GPTs prompts灵感库:创意无限,专业级创作指南,打造吸睛之作的秘诀

    GPTs prompts灵感库:创意无限,专业级创作指南,打造吸睛之作的秘诀 优质prompt展示 1.1 极简翻译 中英文转换 你是一个极简翻译工具,请在对话中遵循以下规则: - Prohibit ...

  5. ::v-deep样式穿透

    //如果不加样式穿透,vue永远会在input后面加唯一样式字段data-v-1d9b105c //::v-deep拼在哪个位置,哪个位置就有唯一标识data-v-1d9b105c .divBox : ...

  6. SpringCloud-03-Nacos配置管理

    Nacos配置管理 原理图: 1.统一配置管理 ① 在Nacos中添加配置信息 ② 在弹出表单中填写配置信息 ③ 配置获取的步骤*(原理) ④ 引入Nacos的配置管理客户端依赖 <!--nac ...

  7. 【预定义】C语言预定义代码(宏、条件编译等)内容介绍【最全的保姆级别教程】

    浅谈C语言预定义中的预定义符号,#define,以及符号#,##的相关运用 求个赞求个赞求个赞求个赞 谢谢 先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一 ...

  8. raise的研究发现,弄懂,try except 一定要raise 否则非常不利于调试。

    现在很多人 都反应 下载订单后 提示下载成功,但是 软件中却没有这个订单,经过研究发现  原因是我用了 try except end; 这个结构导致的,当订单下载过程中 遇到错误的 时候,程序 没有 ...

  9. 欢迎加入 DotNet NB 交流学习群

    目录 起因 创建群组 群成员 技术交流 社区推广 社区前辈 欢迎加入 起因 自从2019年参加 .NET Conf China 大会之后,我创办了一个公众号 DotNet NB,内容主要是 关于 .N ...

  10. .NET Core开发实战(第2课:内容综述)--学习笔记

    02 | 内容综述 课程目标 掌握 .NET Core 微服务架构的最佳实践 成长为一个具备良好架构设计能力的架构师 课程内容 第一部分 .NET Core 的必备知识 第二部分 .NET Core ...