一次因 Too many open files 引发的服务雪崩

2,272 阅读4分钟

事故现场

服务在运行一段时间后,系统监控中load指标飙升到峰值,频繁出现请求失败告警,最后服务因无法建立新的socket连接,导致服务雪崩。必须通过重启服务的方式才能恢复正常。

问题定位

刚开始以为是oom,但是没有发现oom日志,并且堆栈使用和gc情况都在正常的范围内。然后在服务日志发现大量的错误信息:Too many open fils(或 打开的文件过多)。

Too many open files:句柄数超过系统限制,也是Linux系统中常见问题,这里的 files 不仅是系统文件,还包括请求连接 socket,端口监听等。

产生原因

经过排查后确定是因为在加载临时数据文件后删除文件没有关闭stream释放句柄,所以服务在运行一段时间后出现 Too many open files。

句柄原理

在Linux系统中,目录、字符设备、块设备、套接字、打印机等都被抽象成了文件,即通常所说的“一切皆文件”。程序操作这些文件时,系统就需要记录每个当前访问file的name、location、access authority等相关信息,这样一个实体被称为file entry。这些实体被记录在open files table中,Linux系统配置了open files table中能容纳多少file entry。如果超过这个配置值,则Linux就会拒绝其他文件操作的请求,并抛出Too many open files。

解决过程

(1)查看系统句柄限制数

通过 ulimit -a 命令,可以看到 open files 就是允许单个进程打开的最大句柄数,默认是1024 。

[root@kvm-10-12-10-55 ~]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 31211
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 31211
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

(2)找到服务进程PID,查看该进程已打开的文件句柄。

[root@kvm-10-12-10-55 ~]# ps -ef | grep syx-service
root     30118 25539  0 15:51 pts/3    00:00:21 java -jar syx-service-0.0.1-SNAPSHOT.jar
[root@kvm-10-12-10-55 ~]# lsof -p 30118 | wc -l
1024
[root@kvm-10-12-10-55 ~]# lsof -p 30118
java    30118 root *925r      REG              253,0        26     408780 /tmp/a01.txt (deleted)
java    30118 root *926r      REG              253,0        26     408780 /tmp/a02.txt (deleted)
java    30118 root *927r      REG              253,0        26     408780 /tmp/a03.txt (deleted)
java    30118 root *928r      REG              253,0        26     408780 /tmp/a04.txt (deleted)
java    30118 root *929r      REG              253,0        26     408780 /tmp/a05.txt (deleted)
java    30118 root *930r      REG              253,0        26     408780 /tmp/a06.txt (deleted)
java    30118 root *931r      REG              253,0        26     408780 /tmp/a07.txt (deleted)
java    30118 root *932r      REG              253,0        26     408780 /tmp/a08.txt (deleted)
java    30118 root *933r      REG              253,0        26     408780 /tmp/a09.txt (deleted)
java    30118 root *934r      REG              253,0        26     408780 /tmp/a10.txt (deleted)
...

(3)正常情况下,可以通过增大 open files 的方式来解决这个问题,但是我所出现问题采用这个方式,是不能彻底解决的,只能定位到问题代码来解决。

  • 临时修改文件句柄数限制参数
ulimit -n 2048 # 重启后会恢复默认值,非root用户只能设置到4096
  • 通过修改配置文件永久修改
[root@icoolkj ~]# vi /etc/security/limits.conf
 
# 文件末加入
* soft nofile 655360
* hard nofile 655360

(4)定位问题代码。

经排查服务日志发现,定位到代码是 long count = Files.lines(Paths.get(path)).count(); 没有关闭stream导致。

Files.lines()的使用方法在官方文档中是这么说的:

If timely disposal of file system resources is required, the try-with-resources construct should be used to ensure that the stream’s close method is invoked after the stream operations are completed.

原因

  • Files.lines() 这个方法,没有关闭打开的文件
  • 如果需要周期性的读取文件,需要使用try-with-resources语句来保证stream的close方法被调用,从而关闭打开的文件。

解决

修改为下面写法,即可解决啦。

try(Stream<String> stream = Files.lines(Paths.get(path))){
    count = stream.count();
} catch (IOException e){
    e.printStackTrace();
}

等价于

Stream<String> stream = null;
try{
    stream = Files.lines(Paths.get(path));
    count = stream.count();
}catch (IOException e){
    e.printStackTrace();
}finally {
    if(stream != null){
        stream.close();
    }
}

以上,希望对遇到同样问题的小伙伴有帮助。

相关链接:<Files.lines()方法使用相关问题> blog.51cto.com/u_15185289/…


最后,祝大家圣诞快乐,Merry Christmas !!!