事故时间:2026年3月30日 22:40 左右
触发条件:三方数据库升级(锁库 + 杀连接)
影响:系统完全不可用,Tomcat 无法 accept 新连接
一、问题现象
系统运行过程中出现异常:
Socket accept failed
java.io.IOException: Too many open files
at java.base/sun.nio.ch.Net.accept(Native Method)
at java.base/sun.nio.ch.ServerSocketChannelImpl.implAccept(ServerSocketChannelImpl.java:433)
at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:518)
at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:128)
表现为:
- Tomcat Acceptor 线程无法 accept 新连接
- 服务对外完全不可用
二、系统背景
2.1 技术栈
- Spring Boot + JDK 21 + Tomcat(内嵌)
- ORM:自研框架(基于 JDBC + HikariCP 连接池)
- 部署方式:systemd
2.2 systemd 配置
[Service]
User=root
ExecStart=/data/tools/jdk21/bin/java \
-Duser.timezone=Asia/Shanghai \
-Xms4g -Xmx14g \
-jar /data/code/web3/web3-1.0.0.jar \
--spring.profiles.active=prod \
--server.port=8888
LimitNOFILE=4096
Restart=always
RestartSec=5
2.3 架构特点
- 多数据源:启动时从
data_source 表全表加载,为每行创建独立的 HikariCP 连接池
- 流程引擎:flow 任务在Tomcat线程池中执行
- 外部依赖:JDBC、RabbitMQ、Kafka、RocketMQ、Redis、Elasticsearch、MinIO、HTTP 客户端等
2.4 正常状态 FD 使用情况
文件描述符/File Descriptors
lsof -p <pid> | wc -l
≈ 867
FD 分类:
| 类型 |
数量 |
| RegularFile |
559 |
| Socket |
98 |
| EventPoll |
86 |
| EventFD |
86 |
| TimerFD |
8 |
正常状态下 FD 总量约 867,远低于 4096 限制。
三、 Tomcat NIO 模型
看懂这个,就知道为啥4096被占满。

通过“餐厅服务流程”来理解 Tomcat 的三个核心组件:Acceptor、Poller 和 Worker。
3.1 Acceptor 线程:门口的“迎宾员”
- 职责:负责接收连接。
- 动作:
- 站在餐厅大门口,负责把门口小板凳(TCP 队列
accept-count)上排队的人叫进餐厅。
- 为他们安排一个座位(分配给
max-connections 里的一个名额)。
- 特点:
- 术业有专攻:不负责点菜,也不负责端菜。只负责建立连接(SocketChannel)。
- 高效流转:把客人带到座位后,立即回到门口接待下一位。
- 当餐厅满员时:
- 通过对讲机(
LimitLatch)确认 8192 个座位已满。
- 迎宾员会原地“打盹”(阻塞),停止带人。此时后续客人只能在门外排队(
accept-count 队列)。
3.2 Poller 线程:大堂的“巡视经理”
- 职责:负责轮询 IO 事件。通常只有 1-2 个。
- 动作:
- 客人坐下后可能还在看菜单,并没立刻点菜(连接已建立,但 HTTP 请求报文还没发过来)。
- 巡视经理手持“全场雷达仪”(NIO Selector 多路复用器),不断在大堂巡视这 8192 个座位。
- 特点:
- 只看不干:不提供具体服务。
- 事件触发:一旦发现有客人举手(触发
OP_READ 事件,缓冲区有数据可读),立即通过对讲机呼叫后勤服务员。
3.3 Worker 线程池:忙碌的“服务员”
- 职责:负责处理具体的业务逻辑。通常最大 200 个(
max-threads)。
- 动作(全流程处理):
- 读请求:跑到举手的桌前,记录客人需求(读取并解析 HTTP 请求报文)。
- 转交后厨:把订单送到后厨(将请求交给 Spring Boot Controller 处理业务)。
- 送菜响应:厨师做完菜后,服务员负责端菜并核对(封装 HTTP 响应报文,写回客户端)。
- 特点:
- 非阻塞高效:菜端上去后,服务员无需在桌边死等客人吃完。
- 快速复用:任务完成后立即回到后台待命,或听从经理(Poller)安排去处理下一桌。
3.4 核心参数对照表
| 餐厅角色/设施 |
Tomcat 参数 |
物理意义 |
| 门口小板凳 |
accept-count |
默认100 操作系统层面的 TCP 全连接队列长度 |
| 餐厅总座位数 |
max-connections |
默认8192 Tomcat 能够同时维护的最大连接数 |
| 迎宾员 |
Acceptor |
监听端口,负责 serverSocket.accept() |
| 巡视经理 |
Poller |
轮询 Selector,检测已建立连接的读写事件 |
| 服务员线程池 |
max-threads |
默认200 真正执行业务代码的线程池 |
| 客人点菜 |
OP_READ |
网络数据已到达内核缓冲区,可供读取 |
问:在默认配置且满载的情况下,Tomcat 占用的 FD 数量大约为?
Linux中一切皆文件,也就是上述的Tomcat NIO模型中每个连接都会占用一个FD。
FD = max_connections (8192) + 库文件及系统占用 (约 200) + 业务 FD (动态)
在一个标准的 Spring Boot / Tomcat 默认环境下,瞬时峰值通常会达到 8500 左右。这要求我们对 操作系统级限制 有清晰的认知。
四、根因分析
贵州数据源的 最大连接数为254,没有设置socketTimeout。
完整的故障链条
外部系统的正常请求:
外部系统请求进入 → Tomcat 线程处理 → 查三方数据库 → hang 住 → Tomcat 线程被占住不释放。
假设外部系统每秒有 5 个请求命中三方数据源,Tomcat 默认最大 200 线程:
- 200 个线程全部 hang 住
- 每个线程对应 1~2 个 Socket(
isValid 探测 + 新建连接)
此时,Tomcat处理线程全部被占住,新的请求,得不到处理,占用餐厅座位(8192),消耗一个FD,持续堆积最终达到4096上限。
FD 持续堆积
| FD 来源 |
数量估算 |
| 正常基础 FD(JAR、日志、epoll 等) |
~600 |
| HikariCP 多个池重建连接(TCP 握手 hang 中) |
15~45 |
| Tomcat 线程 hang 住(200 线程 × 1~2 Socket) |
200~400 |
| 外部系统重试 + 定时任务持续触发(新旧叠加) |
持续增长 |
| 合计 |
持续增长直到突破 4096 |
FD 耗尽 → 服务崩溃
FD 达到 4096 上限 → Tomcat Acceptor 线程 accept() 失败 → 抛出 Too many open files → 服务对外完全不可用。
五、修复方案
5.1 紧急修复
提高 FD 限制
# /etc/systemd/system/web3-8888.service
LimitNOFILE=65536
systemctl daemon-reload && systemctl restart web3-8888
同步调整系统级限制:
# /etc/security/limits.conf
root soft nofile 65536
root hard nofile 65536
5.2 代码修复
优先级 1:降低数据源连接池大小改为20~30
优先级 2:JDBC URL 添加 connectTimeout 和 socketTimeout
优先级 3:为 HikariCP 添加容错配置
dataSource.setValidationTimeout(3000); // 验证超时 3 秒
dataSource.setLeakDetectionThreshold(60000); // 60 秒连接泄漏检测
dataSource.setInitializationFailTimeout(-1); // 启动时连接失败不阻塞
六、复现方案
环境准备
- 使用开发环境,修改
LimitNOFILE=2000
- 确保
data_source 表有 2~3 个三方数据源
- Jmeter或ApiFox压测接口,我选择ApiFox
复现步骤
1. 修改启动脚本

将Tomcat最大线程数改为30,最大连接数改为3000
2. 开启表锁
BEGIN;
LOCK TABLE bak_t_yxt_yh_kcxx_out_0910 IN ACCESS EXCLUSIVE MODE;
3. 开始压测
这里使用apifox

4. 持续监控 FD 数量
PID=$(pgrep -f web3-1.0.0.jar)
watch -n 2 "ls /proc/$PID/fd | wc -l"
5. 观察 Socket 状态
PID=$(pgrep -f web3-1.0.0.jar)
watch -n 2 "ss -tnp | grep $PID | awk '{print \$1}' | sort | uniq -c"
# 按类别查看FD占用情况
PID=$(pgrep -f web3-1.0.0.jar)
sudo ls -l /proc/$PID/fd | awk '{
if ($11 ~ /^socket:/) type="Socket";
else if ($11 ~ /^pipe:/) type="Pipe";
else if ($11 ~ /^anon_inode:\[eventpoll\]/) type="EventPoll";
else if ($11 ~ /^anon_inode:\[eventfd\]/) type="EventFD";
else if ($11 ~ /^anon_inode:\[timerfd\]/) type="TimerFD";
else if ($11 ~ /^\/dev/) type="Device";
else if ($11 ~ /^\/.*/) type="RegularFile";
else type="Other";
count[type]++
}
END {
for (t in count) print count[t], t
}' | sort -nr
6. 预期现象
- 压测开始:FD 持续增长,外部请求不断叠加新 Socket
- ~N 分钟后:FD 达到 2000,出现
Too many open files
最终结果:FD占用达到2000,开始报错。


7. 恢复
释放表锁
COMMIT;
七、总结
根因
| 因素 |
说明 |
| 直接原因 |
LimitNOFILE=4096 在故障场景下不够用 |
| 根本原因 |
JDBC URL 缺少 socketTimeout,TCP 握手无限 hang,Tomcat线程池打满假死,请求持续堆积占用FD |
| 放大因素 |
Tomcat线程池被占满 + 外部请求持续涌入 + 无熔断机制 |
故障排查是一个系统性工程,涉及应用配置、系统参数和代码逻辑的联动。在 云栈社区 的 后端 & 架构 板块,你可以找到更多关于高并发系统设计和故障预防的深度讨论。