早上刚到公司,产品就跑过来说:xxx看看这个工单数据是怎么回事?为什么和预期的不一致呢?顿时感觉头大。早饭也顾不上吃了,就拿到数据ID在Elk中搜索日志…
1.问题描述:
根据数据ID,在ELK海量日志中搜索,发现该请求竟然OOM了!!! what? 真是让我惊讶。 仔细查看下报错的原因竟是,JVM申请不到线程来处理本次请求,而导致的OOM。具体日志如下:
2.原因定位:
nested exception is java.lang.OutOfMemoryError: unable to create new native thread
字面意思:jvm无法创建新的线程来处理此次请求。
分析导致次异常的原因主要由以下两点导致的:
1. 开启线程数过多,导致jvm申请不到线程
2. 服务器用户线程数设置太小(这种情况较少)
3. 堆内存及堆外空间内存不足
3.分析并解决
3.1开启线程数过多,导致jvm申请不到线程
部署系统时由于运维同学不采用我的建议(jvm添加oom异常时导出dump文件),导致无法分析dump文件,很难定位问题… 那也只能从其它层面分析这个问题了。
3.1.1 分析线程池使用
1.首先检查项目所有关于线程池使用的相关代码,发现了有好几处地方定义了线程池的使用,如下:
// 创建的线程数最大值为int类型的最大值。
ExecutorService executorService = Executors.newCachedThreadPool();
当我看到上述代码时,心想写这个代码的同学一定对线程池的不熟悉,只处于会用的状态…
根据这个代码结合我们具体的业务场景,线程池调用的接口是最频繁的,但是由于执行链过长,并发一集中,就导致系统创建了很多的线程,直至jvm无法申请线程…
于是就对线程池的使用做了一些调整,具体如下:
ThreadPoolExecutor executorService = new ThreadPoolExecutor(10, 20,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
new DefaultThreadFactory("工单同步线程"),
new RejectHandleBase());
/**
* 自定义线程拒绝策略
*
*/
@Slf4j
public class RejectHandleBase implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
log.info("进入线程拒绝策略");
BlockingQueue<Runnable> queue = executor.getQueue();
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
log.error("线程拒绝策略异常:{}", e);
}
}
}
3.2 分析SimpleAsyncTaskExecutor.java 237异常
除了上述一个线程的优化,还远远不够,继续分析错误的日志,又发现了下面的错误日志:
根据异常信息,找到对应源码如下:
查看源码分析得出:发现每次请求都会调用doExecute(),会创建一个线程,这肯定是我们系统中哪个地方引起的。
于是查询类的关系图,排查哪些代码会调用doExecute(),具体如下:
由类图得出,SimpleAsyncTaskExecuto.javar实现了AsyncListenableTaskExecutor.java接口,之后查看相应源码猜测是由:@Async注解引起的线程数增加
为了印证上面的猜想,于是本地开启了调试,具体如下:
当看到 async 数据时,答案已经得出了,就是因为添加了 @Async注解引发的线程数增加。
于是查找项目中用到 @Async注解 的地方。竟然发现 全局切面类 中调用了 @Async注解中类的某个方法 ,导致每次请求都会创建一个新线程。
具体代码如下:
具体实现:
线程增加的原因又排查出了一个,接着评估了上述代码的作用,与业务无法,用处不大,就删除了相关代码…
3.3 尝试增加程序线程数
另外:用户线程数如果小于了java程序所申请的线程数,可以尝试调大用户线程数…
3.4堆内存及堆外空间内存不足
分析具体的GC日志文件
服务器是6G内存,部署3个节点,jvm参数大概如下:
-Xmx1024m -Xms1024m -Xmn400m
题外话:我们服务器配置是真的低…
由于部署3个节点,内存占用为3G,剩余3G大小为系统其它线程和线程栈占用。 结合下面进程能够申请到的线程数计算结果得出,我们服务器配置是比较低
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
各个参数的解释如下:
MaxProcessMemory | 进程最大寻址空间 (32位,进程的虚拟空间为4G,但是大约2G可用于一般使用。64位限制实际会消失) |
---|---|
JVMMMEMORY | jvm的内存空间(堆+永久区)-Xmx大小 (应该是实际分配大小) |
ReservedOsMemory | 操作系统预留内存 |
ThreadStackSize | -Xss大小 |
解决意见:加大服务器内存,保证堆外空间充足