情况说明:
- 近期项目经常出现负载压力过大的情况,导致项目可以访问但是无法做数据查询操作。
- 项目部署在两台服务器上,通过nginx 通过ip_hash 机制做分发。而其中一台经常会出现连接数过大导致项目假死的情况。
- 前期出现无法连接数据库的情况,更改过连接池后此问题不再出现。
问题排查:
1、查看log日志,找寻错误是否有报错。排查于此无关。
2、排查是否为内存溢出导致,经查询后与内存无关。
3、服务器内 top -H -p 进程id 查看进程所占用的CPU程度,CPU占用正常,于此无关。
4、netstat -apn |grep 进程id 查看当前进程链接状态 发现有80%链接状态close_wait,正常情况下该状态不会持续太久,而问题发生时此状态久久不能被释放 。
5、通过jstack -l 进程id >> jstack.log 查看进程所有线程状态。
链接状态描述
状态 | 描述 |
LISTENING | 表示正在监听进入的连接 |
CLOSED | 表示socket连接没被使用 |
SYN_SENT | 表示正在试着建立连接 |
SYN_RECEIVED | 进行连接初始同步 |
ESTABLISHED | 表示连接已被建立 |
CLOSE_WAIT | 表示远程计算器关闭连接,正在等待socket连接的关闭 |
FIN_WAIT_1 | 表示socket连接关闭,正在关闭连接 |
CLOSING | 先关闭本地socket连接,然后关闭远程socket连接,最后等待确认信息 |
LAST_ACK | 远程计算器关闭后,等待确认信号 |
FIN_WAIT_2 | socket连接关闭后,等待来自远程计算器的关闭信号 |
TIME_WAIT | 连接关闭后,等待远程计算器关闭重发 |
由此可见出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接,但是我方忙与读或者写,没有关闭连接。
用中文来描述下这个过程(此段转载自腾讯云一文章):
Client: 服务端大哥,我事情都干完了,准备撤了,这里对应的就是客户端发了一个FIN
Server:知道了,但是你等等我,我还要收收尾,这里对应的就是服务端收到 FIN 后回应的 ACK
经过上面两步之后,服务端就会处于 CLOSE_WAIT 状态。过了一段时间 Server 收尾完了
Server:小弟,哥哥我做完了,撤吧,服务端发送了FIN
Client:大哥,再见啊,这里是客户端对服务端的一个 ACK
到此服务端就可以跑路了,但是客户端还不行。为什么呢?客户端还必须等待 2MSL 个时间,这里为什么客户端还不能直接跑路呢?主要是为了防止发送出去的 ACK 服务端没有收到,服务端重发 FIN 再次来询问,如果客户端发完就跑路了,那么服务端重发的时候就没人理他了。这个等待的时间长度也很讲究。
Maximum Segment Lifetime 报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃
这里一定不要被图里的 client/server 和项目里的客户端服务器端混淆,你只要记住:主动关闭的一方发出 FIN 包(Client),被动关闭(Server)的一方响应 ACK 包,此时,被动关闭的一方就进入了 CLOSE_WAIT 状态。如果一切正常,稍后被动关闭的一方也会发出 FIN 包,然后迁移到 LAST_ACK 状态。
处理方法:
小弟才疏学浅,病急乱投医。
- 首先改了项目的连接池,结果没用。
- 更改tomcat内存配置,结果没用。
- 怀疑进程可打开的文件数限制导致项目到一定程度出现问题,更改配置,结果没用。ulimit -a 可查看最大文件打开数量
- 通过jstack -l 进程id >> jstack.log 查看进程所有线程状态。发现大量线程是等待状态,等待一个锁的释放,由此找到该锁的位置,查看对应代码,终于发现问题所在。
发现问题:
通过对项目启动线程的排查,发现了众多线程在等待状态,而原因是因为一个底层通用方法使用了synchronized。
synchronized是有缺陷的,如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
显然这个锁是因为某些原因被阻塞并且没有释放,导致其他线程一直在等待。
经过查看代码,发现是一个查询导致的,查询sql是hibernate自动生成,且该查询的数据量极大,因为hibernate对大数据处理的效率不是很高,而又要用底层通用的方法去处理这批数据,导致数据处理出现问题,锁一直不被释放。如果多个线程都进行读的操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。那么只要使用该底层方法的地方都要等待,所以造成了项目假死的现象。
解决问题:
对该查询方法优化或重写该方法,hibernate的因为sql是自动生成的,所以只有优化数据源,使用分区、索引,或使用mybaits重写此方法。