Linux 文件句柄 (File Descriptor) 耗尽故障排查实战

一、故障现象

1.1 典型症状

1
2
3
4
5
6
7
8
9
10
11
# 应用日志报错
ERROR: Too many open files
ERROR: Cannot create new socket: Too many open files
ERROR: Failed to open log file: EMFILE

# 系统命令报错
$ ls
ls: cannot open directory '.': Too many open files

$ cat /var/log/messages
cat: /var/log/messages: Too many open files

1.2 影响范围

  • 应用无法创建新连接(HTTP、数据库、Redis 等)
  • 日志无法写入,导致日志丢失
  • 无法打开新文件进行读写
  • 严重时可导致服务完全不可用

二、故障诊断

2.1 检查系统级文件句柄限制

1
2
3
4
5
6
7
8
9
10
11
# 查看系统整体限制
$ cat /proc/sys/fs/file-max
655360

$ cat /proc/sys/fs/file-nr
24576 0 655360
# 格式:已分配句柄数 未使用句柄数 最大限制数

# 查看当前打开的文件句柄总数
$ lsof | wc -l
24576

2.2 检查用户级限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 查看当前用户限制
$ ulimit -n
1024

$ ulimit -a
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 63695
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) 63695
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

2.3 定位高句柄占用进程

1
2
3
4
5
6
7
8
9
10
11
12
# 方法1:使用 lsof 统计每个进程的句柄数
$ lsof -n | awk '{print $2}' | sort | uniq -c | sort -rn | head -20
15234 12345
8765 23456
5432 34567

# 方法2:直接查看进程的 fd 目录
$ ls -l /proc/12345/fd | wc -l
15234

# 方法3:使用 ls 统计
$ for i in /proc/[0-9]*/fd; do echo $(ls -l $i 2>/dev/null | wc -l) $(dirname $i); done | sort -rn | head -20

2.4 分析进程打开的文件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看进程打开的具体文件
$ lsof -p 12345 | head -50

# 按文件类型分类统计
$ lsof -p 12345 | awk '{print $5}' | sort | uniq -c | sort -rn
8234 IPv4
4521 REG
1890 DIR
456 sock
123 FIFO

# 查看具体是哪些文件/连接
$ lsof -p 12345 | grep -E "(IPv4|sock|REG)" | head -100

2.5 检查文件句柄泄漏

1
2
3
4
5
6
7
8
9
# 持续监控进程句柄数变化
$ watch -n 1 'ls /proc/12345/fd | wc -l'

# 使用 strace 追踪 open 系统调用
$ strace -p 12345 -e trace=open,openat,close -c

# 检查是否有大量未关闭的文件
$ lsof -p 12345 | grep deleted
# 如果看到大量 deleted 文件,说明文件被删除但句柄未释放

三、故障处理

3.1 临时应急处理

1
2
3
4
5
6
7
8
9
# 临时提高用户限制(当前会话有效)
$ ulimit -n 65536

# 重启占用句柄最多的服务
$ systemctl restart nginx
$ systemctl restart java-app

# 关闭不必要的连接
$ kill -9 12345 # 谨慎使用,先确认进程

3.2 永久修改系统限制

修改 /etc/security/limits.conf

1
2
3
4
5
6
7
8
9
10
11
12
# 编辑配置文件
$ vim /etc/security/limits.conf

# 添加以下内容
* soft nofile 65536
* hard nofile 65536
root soft nofile 65536
root hard nofile 65536

# 针对特定用户
appuser soft nofile 65536
appuser hard nofile 65536

修改 systemd 服务限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 编辑服务配置
$ systemctl edit nginx

# 添加以下内容
[Service]
LimitNOFILE=65536
LimitNOFILESoft=65536

# 或者修改服务文件
$ vim /etc/systemd/system/nginx.service
[Service]
LimitNOFILE=65536

# 重新加载并重启
$ systemctl daemon-reexec
$ systemctl daemon-reload
$ systemctl restart nginx

修改系统级限制

1
2
3
4
5
6
7
8
9
# 编辑 /etc/sysctl.conf
$ vim /etc/sysctl.conf

# 添加或修改
fs.file-max = 2097152
fs.nr_open = 2097152

# 生效
$ sysctl -p

3.3 应用层优化

Nginx 优化

1
2
3
4
5
6
7
8
# nginx.conf
worker_rlimit_nofile 65536;

events {
worker_connections 65536;
multi_accept on;
use epoll;
}

Java 应用优化

1
2
3
4
5
6
7
8
9
10
11
12
13
# JVM 启动参数
java -Xms2g -Xmx2g \
-Djava.util.logging.config.file=logging.properties \
-XX:+HeapDumpOnOutOfMemoryError \
-jar app.jar

# 在代码中确保资源关闭
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
// 使用资源
} catch (IOException e) {
// 处理异常
}

MySQL 优化

1
2
3
4
5
6
7
8
9
-- 查看当前限制
SHOW VARIABLES LIKE 'open_files_limit';
SHOW STATUS LIKE 'Open_files';

-- 修改配置文件 /etc/my.cnf
[mysqld]
open_files_limit = 65536
max_connections = 1000
table_open_cache = 4000

四、预防措施

4.1 监控告警配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用 Prometheus 监控文件句柄
# node_exporter 自动采集以下指标:
# - node_filefd_allocated
# - node_filefd_maximum

# Grafana 告警规则
- alert: HighFileDescriptorUsage
expr: node_filefd_allocated / node_filefd_maximum > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "文件句柄使用率过高"
description: "{{ $labels.instance }} 文件句柄使用率 {{ $value | humanizePercentage }}"

4.2 定期巡检脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
# check_fd_usage.sh

THRESHOLD=80
LOG_FILE="/var/log/fd_check.log"

# 获取系统文件句柄使用情况
FILE_NR=$(cat /proc/sys/fs/file-nr)
ALLOCATED=$(echo $FILE_NR | awk '{print $1}')
MAX=$(echo $FILE_NR | awk '{print $3}')
USAGE=$(awk "BEGIN {printf \"%.2f\", ($ALLOCATED/$MAX)*100}")

echo "$(date) - 文件句柄使用率:${USAGE}%" >> $LOG_FILE

if (( $(echo "$USAGE > $THRESHOLD" | bc -l) )); then
echo "WARNING: 文件句柄使用率超过 ${THRESHOLD}%" >> $LOG_FILE
# 发送告警
echo "文件句柄告警:${USAGE}%" | mail -s "FD Usage Alert" admin@example.com
fi

# 检查 Top 10 进程
echo "Top 10 进程句柄占用:" >> $LOG_FILE
for i in /proc/[0-9]*/fd; do
echo $(ls -l $i 2>/dev/null | wc -l) $(dirname $i)
done | sort -rn | head -10 >> $LOG_FILE

4.3 代码审查要点

  • 所有打开的文件/连接都有对应的关闭操作
  • 使用 try-with-resources(Java)或 context manager(Python)
  • 连接池配置合理,有最大连接数限制
  • 异常处理中正确释放资源
  • 定期检查和关闭空闲连接

五、典型案例

5.1 案例一:日志文件未关闭导致泄漏

现象:Java 应用运行一周后无法创建新连接

排查

1
2
$ lsof -p 12345 | grep deleted | wc -l
8500

原因:日志滚动时旧文件被删除,但应用仍持有句柄

解决

1
2
3
4
5
6
7
// 错误示例
FileInputStream fis = new FileInputStream(logFile);

// 正确示例
try (FileInputStream fis = new FileInputStream(logFile)) {
// 使用资源
}

5.2 案例二:连接池配置过大

现象:高并发时数据库连接失败

排查

1
2
$ lsof -p 12345 | grep MySQL | wc -l
5000

原因:连接池最大连接数设置为 5000,超过系统限制

解决

1
2
3
4
5
6
7
# 调整连接池配置
spring:
datasource:
hikari:
maximum-pool-size: 100 # 降低到合理值
connection-timeout: 30000
idle-timeout: 600000

5.3 案例三:Nginx worker_connections 配置不当

现象:高并发时 Nginx 返回 502 错误

排查

1
2
3
$ ulimit -n
1024
# nginx.conf 中 worker_connections 设置为 10000

解决

1
2
3
4
5
6
7
8
9
# 修改系统限制
echo "* soft nofile 65536" >> /etc/security/limits.conf
echo "* hard nofile 65536" >> /etc/security/limits.conf

# 修改 nginx 配置
worker_rlimit_nofile 65536;
events {
worker_connections 65536;
}

六、总结

检查项 命令 正常值
系统文件句柄上限 cat /proc/sys/fs/file-max > 655360
用户文件句柄限制 ulimit -n ≥ 65536
进程句柄使用率 lsof -p PID | wc -l < 限制的 80%
已分配句柄数 cat /proc/sys/fs/file-nr < file-max 的 80%

关键要点

  1. 早发现:配置监控告警,使用率达到 80% 时告警
  2. 准定位:快速定位高占用进程和文件类型
  3. 快恢复:临时提高限制 + 重启服务
  4. 根治:修改配置 + 代码优化 + 定期巡检

参考文档