找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4519

积分

0

好友

623

主题
发表于 3 天前 | 查看: 19| 回复: 0

事故时间: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 NIO 三阶段模型餐厅比喻示意图

通过“餐厅服务流程”来理解 Tomcat 的三个核心组件:AcceptorPollerWorker

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)。
  • 动作(全流程处理)
    1. 读请求:跑到举手的桌前,记录客人需求(读取并解析 HTTP 请求报文)。
    2. 转交后厨:把订单送到后厨(将请求交给 Spring Boot Controller 处理业务)。
    3. 送菜响应:厨师做完菜后,服务员负责端菜并核对(封装 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 添加 connectTimeoutsocketTimeout

优先级 3:为 HikariCP 添加容错配置

dataSource.setValidationTimeout(3000);         // 验证超时 3 秒
dataSource.setLeakDetectionThreshold(60000);    // 60 秒连接泄漏检测
dataSource.setInitializationFailTimeout(-1);    // 启动时连接失败不阻塞

六、复现方案

环境准备

  • 使用开发环境,修改 LimitNOFILE=2000
  • 确保 data_source 表有 2~3 个三方数据源
  • Jmeter或ApiFox压测接口,我选择ApiFox

复现步骤

1. 修改启动脚本

Tomcat 线程池与连接数配置修改截图

将Tomcat最大线程数改为30,最大连接数改为3000

2. 开启表锁

BEGIN;
LOCK TABLE bak_t_yxt_yh_kcxx_out_0910 IN ACCESS EXCLUSIVE MODE;

3. 开始压测

这里使用apifox

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,开始报错。

进程 FD 占用分类统计结果

Tomcat 报错 Too many open files 日志截图

7. 恢复

释放表锁

COMMIT;

七、总结

根因

因素 说明
直接原因 LimitNOFILE=4096 在故障场景下不够用
根本原因 JDBC URL 缺少 socketTimeout,TCP 握手无限 hang,Tomcat线程池打满假死,请求持续堆积占用FD
放大因素 Tomcat线程池被占满 + 外部请求持续涌入 + 无熔断机制

故障排查是一个系统性工程,涉及应用配置、系统参数和代码逻辑的联动。在 云栈社区后端 & 架构 板块,你可以找到更多关于高并发系统设计和故障预防的深度讨论。




上一篇:张雪WSBK夺冠:拆解成功路径与幸存者偏差,哪些经验普通人可借鉴
下一篇:C++项目日志记录:为何应弃用cout转向spdlog?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 19:46 , Processed in 0.862791 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表