现象

我们生产最近有个服务偶尔会挂掉,接口报错"connection reset by peer",上服务器curl也是同样报错,意思连接被server拒绝了。

通过dump以及日志分析,我们已经知道了问题代码所在,就是使用easyexcel上传、解析文件,开发同学没有做分页,导致内存溢出。这点在easyexcel文档也有提到:参见

内存溢出后,触发频繁的full gc,由于gc很难有效回收内存,所以程序抛出了OutOfMemoryError,原因是:Java heap space。

关于OOM的异常原因,我们也需要知道,有如下几种:

Java heap space

内存无法分配新的对象。典型的场景是:内存不足,内存泄漏,分配的对象过大。

GC Overhead limit exceeded

gc回收异常,多次发生了98%的时间用于gc,但只回收2%的内存。典型的场景是:内存不足,内存泄漏。

Metaspace

元空间不足。典型的场景是:元空间设置太小,程序异常创建过多的Class。

Direct buffer memory

直接内存不足。典型的场景是:直接内存设置太小,直接内存泄漏。

Unable to create new native thread

无法创建新的线程。典型的场景是:系统ulimit -u设置太小。

Requested array size exceeds VM limi

数组大小太大。典型的场景是:new ClassA[Long.MAX_VALUE]

CodeCache

jit编译缓存溢出。典型的场景是jit缓存设置过小。

注意,我们上面提到问题的是内存溢出,而不是内存泄漏,两者有本质的区别。

内存溢出,通常是分配大对象,应用内存不足,通常分配多点空间就可以解决问题,而且所占的内存用完还是可以被回收的。

内存泄漏,则是程序有问题,内存是无法被回收的,分配再多的空间也都会被慢慢消耗完。

打个比方,内存溢出只是人长得不好看,不是坏人,内存泄漏则长得不好看,也是坏人。当然两者我们都需要对其进行优化。

通过我们的观察也发现确实如此,经过一段时间后,文件解析数据处理完,内存就被回收了,也没有full gc了。

但问题来了,此时应用http请求还是继续报错的,依然是"connection reset by peer"。这点就不好理解了,应用恢复了,为什么tomcat没有恢复,tomcat线程此时在做什么?从日志也看不到tomcat相关错误,tomcat假死了

从监控上看,挂掉之前tomcat的请求线程数和连接数没有什么波动。

我们的两个核心问题是:

  • 什么是"connection reset by peer"?
  • tomcat线程此时处于什么状态?

连接报错

我们搜索源码,并没有抛出"connection reset by peer"的代码,也就是可能是jvm层面的抛出,或者系统层面的抛出。

既然是跟connection相关,那我们就用netstat命令看下当前进程的连接情况:

netstat -tlnp | grep 8100
netstat -anp | grep 8100

从输出可以发现,有一个特殊的101,我们用正常的进程看一般都是0。

这个参数叫:backlog,表示连接等待队列的长度,对应tomcat的acceptCount参数,默认是100。

当连接超过这个值时,就会报"connection reset by peer",我们当前是101,所以新来的请求就被拒绝了。

tomcat线程模型

对于第二个问题,tomcat线程正在干什么。一般我们可以通过jstack pid导出线程堆栈分析,不过当我们的服务运行一段时间,例如好几天后,执行jstack,jmap都会报错,似乎是某些信息被系统清除掉,这点我还找到根本原因,如果你知道答案请告知我一下。

幸好有arthas,我们可以通过thread命令,查看线程和其堆栈信息。

thread -n 10  #top 10 cpu
thread 1 #展示线程1的堆栈
thread --all #展示所有线程
thread --all | grep http #展示http线程

我们可以看到,tomcat的10个核心线程还是在的,且处于waitting状态。

处于waitting状态是因为它在等任务执行,从堆栈可以看出是阻塞在TaskQueue.take方法,org.apache.tomcat.util.threads.TaskQueue是tomcat中的LinkedBlockingQueue,是生产者-消费者模型,take方法阻塞表示当前队列是空的,没有任务需要执行,一旦有任务放入TaskQueue,take方法就会唤醒,进入Runnable状态。

这点就很奇怪了,前面说连接队列都满了,但tomcat任务队列确是空的,执行线程都处于等待任务状态,一边满载一边空闲。

要搞清楚这个问题,需要我们对tomcat线程模型有所了解。tomcat支持几种IO模型,BIO、NIO、AIO(NIO2)、APR,我们可以通过server.tomcat.protocol参数进行设置,默认用的是NIO,NIO是一种同步非阻塞IO。

NIO的核心目的是可以用少量线程处理大量连接,在linux用select/poll/epoll实现。

NIO在很多中间件都有应用,kafka,redis,rocketmq,gateway等等,可以说涉及到网络处理的都离不开NIO。

AIO是真正的异步IO,但Linux对其支持不够完善,且NIO已经足够高效,所以NIO用得最多。

reactor模型

如何更好的实现NIO是个问题,就好像我们实现某个功能要用到设计模式一样,reactor就是NIO一种实现模式。doug lee在scalable io in java总结了3种模型:

单reactor单线程

整个过程由一个线程完成,包括创建连接,读写数据,业务处理。redis 6.0以前的版本就是这种模式,实现起来简单,没有线程切换,加锁的开销。缺点是单线程不能发挥多核cpu的优势,如果有一个业务处理阻塞了,那么整个服务都会阻塞。

单reactor多线程

接收连接(accept)和IO读写还是由一个线程完成,但业务处理会提交给业务线程池,业务处理不会阻塞整个服务。

多reactor多线程

接收连接由一个main reactor处理,建立连接后将其注册到sub reactor上,每个sub reactor都是单reactor多线程模式。

tomcat的实现

tomcat的NIO由3种线程实现,分别是:Acceptor线程、Poller线程、请求处理线程。

对于请求处理线程池我们比较熟悉,常用的两个参数:

server.tomcat.minSpareThreads = 10
server.tomcat.maxThreads = 200

对应到reactor模型,可以看成它是一种多reactor多线程模型,Acceptor线程负责建立连接,然后将建立好的连接注册到Poller,由Poller进行读写。Poller读写后创建请求,将其交给请求处理线程池。

Acceptor和Poller线程对应的run方法都是一个死循环,源源不断的接收连接、读写连接。

从reactor模型上看,在多核cpu下,多reactor多线程模型可以获得更高的效率,但tomcat10以下默认只能有一个Acceptor和一个Poller线程。

源码分析

源码位置:org.apache.tomcat.util.net.NioEndpoint#startInternal,开启Acceptor线程和Poller线程:

源码位置:org.apache.tomcat.util.net.Acceptor#run,while循环,建立连接:

源码位置:org.apache.tomcat.util.net.NioEndpoint.Poller#run,while循环,读取数据:

源码位置:org.apache.tomcat.util.net.AbstractEndpoint#processSocket,将封装好的请求交给请求线程池处理:

关于tomcat线程池有一个点要注意,它和jdk不一样的是,它是先开启核心线程,当任务超过核心线程数,就继续开启至最大线程数,如果还超过才进入等待队列。

水落石出

通过上面的分析,让我们回到问题出现时的这张图

可以看到,Acceptor和Poller线程消失了!

这样我们现象就很好解释了,Acceptor没有拿新的连接来处理了,此时连接在系统层面积压,tomcat请求处理线程空闲。

我们重启后再执行一下thread命令,正常的是:

从Acceptor源码上看,它捕获了异常,但对于OOM,选择重新抛出,Acceptor线程就中断了,可见OOM对于tomcat来说是个致命异常,一旦程序有此类报错,需要优化,否则可能导致整个服务异常。

且Acceptor和Poller线程抛出这个位置在打印日志之前,所以也看不到错误日志,这点似乎不太好,但最新的tomcat版本也是保持如此。

如果要获得这个日志,我们也可以通过Thread的全局异常来捕获:

Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
if (t.getName().equals("http-nio-8100-Acceptor")) {
log.error("tomcat Acceptor error", t);
}
});

总结

OOM异常对于tomcat服务来说是致命的,发现即需要处理。

对于内存泄漏来说,留有时间给我们dump内存分析。但对于内存溢出来说,由于其会回收,可能在某个时间OOM,顺便把tomcat打挂了,然后就回收了,此时我们去dump也未必有用,所以-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath 参数是很有必要需要加上的。

tomcat为什么假死了.md的更多相关文章

  1. tomcat 假死现象(转)

    1.1 编写目的 为了方便大家以后发现进程假死的时候能够正常的分析并且第一时间保留现场快照. 1.2编写背景 最近服务器发现tomcat的应用会偶尔出现无法访问的情况.经过一段时间的观察最近又发现有台 ...

  2. TCP 连接的 TIME_WAIT 过多 导致 Tomcat 假死

    最近系统二次开发之后,发现使用的 Tomcat 7 会经常假死.前端点击页面无任何反应,打开firebug,很多链接一直在等待服务器的反应.查看服务器的状态,CPU占用很少,最多不超过10%,一般只有 ...

  3. tomcat 假死

    1.1 编写目的 为了方便大家以后发现进程假死的时候能够正常的分析并且第一时间保留现场快照.1.2编写背景最近服务器发现tomcat的应用会偶尔出现无法访问的情况.经过一段时间的观察最近又发现有台to ...

  4. Tomcat假死的原因及解决方案

    服务器配置:linux+tomcat 现象:Linux服务器没有崩,有浏览器中访问页面,出现无法访问的情况,没有报4xx或5xx错误(假死),并且重启tomcat后,恢复正常. 原因:tomcat默认 ...

  5. TCP连接的TIME_WAIT过多导致 Tomcat 假死

    最近发现使用的Tomcat 7会经常假死.前端点击页面无任何反应,打开firebug,很多链接一直在等待服务器的反应.查看服务器的状态,CPU占用很少,最多不超过10%,一般只有2%,3%左右,内存占 ...

  6. tomcat假死现象 - 二

    1 编写背景 最近服务器发现tomcat的应用会偶尔出现无法访问的情况.经过一段时间的观察最近又发现有台tomcat的应用出现了无法访问情况.简单描述下该台tomcat当时具体的表现:客户端请求没有响 ...

  7. tomcat假死现象(转)

    1.1 编写目的 为了方便大家以后发现进程假死的时候能够正常的分析并且第一时间保留现场快照. 1.2编写背景 最近服务器发现tomcat的应用会偶尔出现无法访问的情况.经过一段时间的观察最近又发现有台 ...

  8. Tomcat9.0.13 Bug引发的java.io.IOException:(打开的文件过多 Too many open files)导致服务假死

    问题背景: 笔者所在的项目组最近把生产环境Tomcat迁移到Linux,算是顺利运行了一段时间,最近一个低概率密度的(too many open files)问题导致服务假死并停止响应客户端客户端请求 ...

  9. 分析java进程假死状况

    摘自: http://www.myexception.cn/internet/2044496.html 分析java进程假死情况 1 引言 1.1 编写目的 为了方便大家以后发现进程假死的时候能够正常 ...

  10. FTPClient下载文件,程序假死问题

    [所属类包] org.apache.commons.net.ftp.FTPClient [现象描述] 这两天java项目中用到了FTP下载,像之前的项目写好代码,但是点击下载后,程序调试到下面这一行, ...

随机推荐

  1. 使用Win32控制台实现boost共享内存通信

    发送端: #define BOOST_DATE_TIME_NO_LIB #include <boost/interprocess/shared_memory_object.hpp> #in ...

  2. Portainer安装配置

    什么是portainer 官网:https://www.portainer.io/ Portainer(基于 Go) 是一个轻量级的Web管理界面,可让您轻松管理 Docker 主机 或 Swarm ...

  3. 【JDBC第7章】DAO及相关实现类

    第7章:DAO及相关实现类 DAO:Data Access Object访问数据信息的类和接口,包括了对数据的CRUD(Create.Retrival.Update.Delete),而不包含任何业务相 ...

  4. EntityFrameworkCore 分页问题

    场景重现 使用 EntityFrameworkCore 连接 SQL Server 2008 执行.Skip().Take()分页查询时出现如下异常: SqlException: 'OFFSET' 附 ...

  5. 张高兴的大模型开发实战:(四)使用 LangGraph 实现多智能体应用

    目录 环境搭建与配置 定义智能体 加载模型 提取关键词 生成回答 连接智能体 定义图的状态 定义节点方法 根据指令路由 生成回答 文件处理 提取关键词 网络搜索 定义图的结构 运行图 运行指南 在控制 ...

  6. WPF静态资源StaticResource和动态资源DynamicResource有什么区别,x:Static又是什么意思?

    什么叫WPF的资源(Resource) 资源是保存在可执行文件中的一种不可执行数据.WPF中资源用ResourceDictionary类表示,这个类就是一个字典,字典的key和value都是objec ...

  7. pytorch 实战教程之 Feature Pyramid Networks (FPN) 特征金字塔网络实现代码

    原文作者:aircraft 原文链接:pytorch 实战教程之 Feature Pyramid Networks (FPN) 特征金字塔网络实现代码 - aircraft - 博客园 学习YOLOv ...

  8. Windows系统设置开机自启动+分块压缩+文件共享

    开机自启动+分块压缩+文件共享 一.设置开机自启动 win+R 打开运行窗口,输入 shell:startup 此时桌面会弹出一个目录文件夹,只需要将需要启动的软件放入该文件夹即可开机自启. C:\U ...

  9. toRefs 与 toRef 的详解

    一.引言在 Vue 3 的响应式系统里,toRefs 和 toRef 是两个实用的工具函数,它们在处理响应式数据时发挥着重要作用.合理运用这两个函数,可以让我们在操作响应式对象和数组时更加灵活,避免一 ...

  10. 关于网传微信聊天记录提取工具"留痕"盗取个人信息的分析

    今天早上看到一篇文章,是关于一个微信聊天记录提取工具泄露个人信息的内容,于是我就好奇,看了一下作者的 github,然后也是自己小小的分析了一下 1.官方地址 Github: https://gith ...