一、背景

前几天下午飞书告警群里报起了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. 【JS 逆向百例】浏览器插件 Hook 实战,亚航加密参数分析

    关注微信公众号:K哥爬虫,QQ交流群:808574309,持续分享爬虫进阶.JS/安卓逆向等技术干货! 声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途 ...

  2. docker 发布web项目到linux

    一.准备工作 1.连接到Liunx的工具 MobaXterm 填好Ip 直接点ok就行 输入用户名和密码进入系统 2.已发布的.netcore网站或微服务 在要发布的项目上右键---->添加-- ...

  3. 如何区分Unity国内版和国际版

    从这三个地方都可以判断使用的Unity是国内版本还是国际版,国内版的版本号后面会多出c1,而国际版则不会有c1结尾. 安装目录名 unity hub - 安装 - 查看当前安装的Unity各版本 un ...

  4. svn把文件日期设置为最后提交的时间

    在使用svn进行checkout或update时,我想让文件的日期为提交那时的日期,这要怎样做? 说明:我是在windows下使用TortoiseSVN进行操作的 方法1.修改config [misc ...

  5. Walrus 0.5发布:重构交互流程,打造开箱即用的部署体验

    开源应用管理平台 Walrus 0.5 已于近日正式发布! Walrus 0.4 引入了全新应用模型,极大程度减少了重复的配置工作,并为研发团队屏蔽了云原生及基础设施的复杂度.Walrus 0.5 在 ...

  6. 解决SystemExit: 2,args = parser.parse_args() 的问题,

    报错: File "/home/barry/PycharmProjects/EEPC/detect.py", line 283, in parse_opt opt = parser ...

  7. 2.1 C++ STL 数组向量容器

    Vector容器是C++ STL中的一个动态数组容器,可以在运行时动态地增加或减少其大小,存储相同数据类型的元素,提供了快速的随机访问和在末尾插入或删除元素的功能. 该容器可以方便.灵活地代替数组,容 ...

  8. Gin 中间件

    中间件 在Gin框架中,中间件(Middleware)指的是可以拦截http请求-响应生命周期的特殊函数,在请求-响应生命周期中可以注册多个中间件,每个中间件执行不同的功能,一个中间执行完再轮到下一个 ...

  9. WebAssembly核心编程[1]:wasm模块实例化的N种方式

    当我们在一个Web应用中使用WebAssembly,最终的目的要么是执行wasm模块的入口程序(通过start指令指定的函数),要么是调用其导出的函数,这一切的前提需要创建一个通过WebAssembl ...

  10. WebAssembly核心编程[3]: Module 与 Instance

    WebAssembly程序总是以模块来组织,模块是基本的部署.加载和编译单元.在JavaScript编程接口中,模块通过WebAssembly.Module类型表示.WebAssembly.Modul ...