一次现网事故的排查--connection reset by peer
tcpss -ltLISTEN 状态– Recv-Q 表示当前 listen backlog 队列中的连接数目(等待用户调用 accept() 获取的、已完成 3 次握手的 socket 连接数量)– Send-Q 表示了 listen socket 最大能容纳的 backlog ,即 min(backlog, somaxconn) 值。非 LISTEN 状态– Recv-Q 表示了 receiv
背景描述
应用介绍
应用使用spring boot,并使用内嵌的tomcat,即使用java -jar xxx.jar启动应用的,区别于前几年,将应用打成war包并放到tomcat的webpp目录。
问题描述
- 应用本地缓存了许多业务数据,导致日志中出现OOM报错。
- OOM发生后,API网关所有路由到这台机器的请求,都是发生超时。
- 通过单边地址访问(使用curl命令),也是发生超时,请求得不到相应,同时报错connection reset by peer。
排查过程
检查应用层状态
- 检查进程是否挂了
通过命令ps -ef -w w,检查应用的进程是否还存在; --检查未发现异常 - 检查是否发生死锁
通过jstack pid,检查是否出现线程死锁; --检查未发现异常
检查OS层状态
- 检查系统CPU利用率,是否因为应用(不仅本应用,也可能是其他应用)有BUG出现死循环,导致CPU利用率100%,应用的请求无法及时得到cpu资源,响应延时很大,导致超时
通过命令top,查看cpu利用率是否有异常 --检查未发现异常 - 检查网络层是否出现异常,因为是在云上运行的,可能网络层出现状况
通过命令telnet检测端口能否访问通 --检查未发现异常 - 检查系统连接状态,可能有大量的连接请求未释放,超过系统的限制,导致新的请求被丢弃或者阻塞
使用命令netstat -natl --发现异常
LISTEN状态的Recv-Q代表连接已经过三次握手,等待应用的代码调用accept。
多次调用netstat -natl,发现该数值不会变化。 怀疑连接数超出限制,新请求无法接入 - 确认是否进程的连接数超出限制
使用命令netstat -s | grep listen,结果如下
85 times the listen queue of a socket overflowed
使用命令netstat -s | grep LISTEN,结果如下
85 SYNs to LISTEN sockets ignored
使用命令ss -lt,结果如下
通过三条命令发现,确实是连接数超出了限制。tomcat默认的backlog为100(可以到tomcat官网查看,或者搜索tomcat源码“acceptCount”跟踪)。
截图的中的Send-Q代表Accept队列的大小,Resv-Q代表当前Accept队列中已有的连接数。从截图来看Accept队列已经满了,新连接没办法在完成TCP三次握手,进入到这个队列。
在第三次tcp握手时,由于Accept队列满了,客户端发起ack包时,服务端直接回复RST包,所以客户端得到connection reset by peer的报错。 --【Accept队列满了,如何回应ACK包由/proc/sys/net/ipv4/tcp_abort_on_overflow决定,0表示扔掉ACK包,1表示回复RST包】
进一步分析
找出为什么应用层不处理新请求
通过前面步骤发现,cpu利用率很低(通过top可以看到),但是进来的请求仍然得不到处理(问题解决,已经被隔离了,不会有新请求过来,但是通过netstat 发现LISTEN状态的Recv-Q的数量没有发生变化),猜测内嵌的tomcat发生了什么故障,请求无法传递到业务层被处理。
下一步应该是对比正常节点和该故障节点tomcat线程的差异,通过对比发现异常节点,少了截图中线程
从堆栈信息可以看到,该线程是accept客户端的请求的,定位到这里就已经发现请求为啥得不到处理了。
tomcat的线程为什么会挂掉
直觉上,调用accept出现异常时,应该catch掉,因为单个请求accept失败,并不会影响全局。
持着怀疑的态度,翻阅的tomcat的源码(org.apache.tomcat.util.net.Acceptor),代码有删减
前面提到应用本地缓存了很多数据,导致OOM,所以这里抛出java.lang.OutOfMemoryError,该异常是VirtualMachineError的子类。
TOMCAT并未细分VirtualMachineError,而是做了统一处理;既然是虚拟机出现了问题,那应用层无能为力,就不用catch住了。这个逻辑也说的通,避免错误处理过于复杂。
所以从下面代码中可以看到,tomcat遇到VirtualMachineError异常时,直接抛出,导致线程挂掉。
@Override
public void run() {
while (endpoint.isRunning()) {
state = AcceptorState.RUNNING;
try {
U socket = null;
socket = endpoint.serverSocketAccept();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
String msg = sm.getString("endpoint.accept.fail");
}
}
state = AcceptorState.ENDED;
}
public static void handleThrowable(Throwable t) {
if (t instanceof ThreadDeath) {
throw (ThreadDeath) t;
}
if (t instanceof StackOverflowError) {
return;
}
if (t instanceof VirtualMachineError) {
throw (VirtualMachineError) t;
}
}
问题重现
说明
该问题在windows上面无法重现,可能linux和windows在内存管理和网络请求上面的实现有差异,导致JVM虽然内存不足,但是socket = endpoint.serverSocketAccept();这行代码并不会抛异常。这个有精力的人,可以深入分析。
复现步骤
- 去spring官网使用initializatier,生成一个简单的spring boot的项目。
- 增加一个controller
@RestController
@RequestMapping("test")
public class Controller {
private List<String> message = new ArrayList<>();
private boolean flag = true;
@GetMapping("/get/{message}")
public String getMessage(@PathVariable("message")String message) {
return "ok";
}
@GetMapping("/oom")
public String oom() {
try {
while(flag) {
this.message.add(new String("a"));
}
}
return "ok";
}
}
- mvn package打包,并上传到linux服务器上
- linux上启动服务,java -Xms256 -jar xxx.jar
- 打开浏览器访问http://{linux的IP}:8080/test/oom
- 等到发生OOM后
- 浏览器不断的访问http://{linux的IP}:8080/test/get/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
这样就可以复原当时机器的状态
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)