一、背景

前几天下午飞书告警群里报起了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. 【原创】关于xenomai3 RTnet的一点记录

    xenomai3协议栈RTnet支持TCP.UDP,但不支持IGMP: 对ARP支持有限制:地址解析的延迟会影响数据包传输延迟,RTnet为实时性考虑,路由表设计静态的,只在设置期间配置,或者接收到其 ...

  2. Docker容器基础入门认知-Namespce

    在使用 docker 之前我一般都认为容器的技术应该和虚拟机应该差不多,和虚拟机的技术类似,但是事实上容器和虚拟机根本不是一回事. 虚拟机是将虚拟硬件.内核(即操作系统)以及用户空间打包在新虚拟机当中 ...

  3. MySQL查询聚合函数与分组查询

    连接数据库 mysql -hlocalhost -uroot -proot 聚合函数 聚合函数:作用于某一列,对数据进行计算. ps: 所有的null值是不参与聚合函数的运算的. 06 常见的聚合函数 ...

  4. VictoriaMetrics源码阅读:极端吝啬,vm序列化数据到磁盘的细节

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu 公众号:一本正经的瞎扯 源码请看:https://github.com/ahfuzhang/vi ...

  5. docker 安装minio

    1.拉取镜像 docker pull minio/minio 2.运行容器 docker run -d -p 9000:9000 --name=minio --restart=always -e &q ...

  6. ASP.NET MVC 通过ActionFilter获取请求的参数

    using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using Syst ...

  7. 深度学习基础入门篇[二]:机器学习常用评估指标:AUC、mAP、IS、FID、Perplexity、BLEU、ROUGE等详解

    A.深度学习基础入门篇[二]:机器学习常用评估指标:AUC.mAP.IS.FID.Perplexity.BLEU.ROUGE等详解 1.基础指标简介 机器学习的评价指标有精度.精确率.召回率.P-R曲 ...

  8. Linux进程间通信 [补档-2023-07-27]

    Linux进程间通信 10-1 简介 ​ 在Linux下,进程之间相互独立,每个进程都有自己不同的用户地址空间.任何一个进程的全局变量在另 一个进程中都看不到,所以进程和进程之间不能相互访问.如果非要 ...

  9. ubuntu系统单网卡配置多网段IP

    环境 系统版本:Ubuntu 16.04.5 LTS 配置 ubuntu系统网卡文件是interfaces,修改网卡配置文件vim /etc/network/interfaces添加2个IP地址: a ...

  10. Docker从认识到实践再到底层原理(七)|Docker存储卷

    前言 那么这里博主先安利一些干货满满的专栏了! 首先是博主的高质量博客的汇总,这个专栏里面的博客,都是博主最最用心写的一部分,干货满满,希望对大家有帮助. 高质量博客汇总 然后就是博主最近最花时间的一 ...