AWS 连接超时根因分析
AWS 连接超时根因分析
问题现象
业务从本地机房迁移到 AWS 后,Java 服务访问跨机房 MySQL 数据库(AWS EC2 → 专线 → 本地机房 DB),每隔 20~30 分钟出现批量连接超时:
1 | Failed to validate connection (No operations allowed after connection closed.) |
关键特征:
- 网络延迟稳定在 ~8ms,0% 丢包
- MySQL 健康(wait_timeout=86400s, Threads_connected 正常)
- 调整 HikariCP maxLifetime 从 30 分钟改为 20 分钟后,报错间隔也跟着变成 20 分钟
- 旧服务(本地机房内部)无此问题,只有跨机房访问才出现
排查过程
第一轮:HikariCP 连接池分析
初始怀疑是 HikariCP 3.x 的 maxLifetime 机制导致连接集中过期。
验证方法:搭建 HikariCP 3.4.5 + MySQL 5.7 的独立 Demo,配置 poolSize=10, minimumIdle=maximumPoolSize, maxLifetime=90s,观察连接过期行为。
发现:HikariCP 在 maxLifetime 到期时主动异步替换连接,每个替换仅需 3-8ms:
1 | 10:32:23.996 Closing connection @6d21071: (connection has passed maxLifetime) |
结论:在 MySQL 可达且网络正常(RTT < 20ms)的环境下,纯 maxLifetime 批量过期无法导致连接超时。HikariCP 的异步替换机制工作正常。问题的根因不在 HikariCP 本身。
第二轮:TCP keepalive 与中间设备分析
AWS → 本地机房的网络路径中存在防火墙(空闲会话 30 分钟清除),可能还有 NAT Gateway 等中间设备。
关键验证:反编译 MySQL Connector/J 5.1.49 字节码,确认 tcpKeepAlive 默认值:
1 | // ConnectionPropertiesImpl.class 字节码 |
同时通过 ss -tnpei 验证运行中的 JDBC 连接确实开启了 SO_KEEPALIVE:
1 | ESTAB 127.0.0.1:62718 → 127.0.0.1:3316 timer:(keepalive,5.519ms,0) |
AWS EC2 的 tcp_keepalive_time = 1200s(20 分钟)。如果中间设备的空闲超时 < 1200s,keepalive 探测会来不及续命,连接被静默丢弃。
第三轮:抓包分析定位精确超时
在 AWS EC2 上抓取了约 80 分钟的 MySQL 连接网络包(84595 个包,145 个连接),进行系统性分析。
核心发现
1. 零 RST —— 中间设备静默丢弃连接
84595 个包中没有一个 RST 包。中间设备删除会话后不通知任何一方,TCP 连接变成”僵尸”——客户端和服务端都以为对方还在。
2. 精确定位中间设备超时:340~350 秒
按连接空闲时间统计 Server 是否响应了客户端的 FIN:
| 空闲时间 | Server 响应 | 结论 |
|---|---|---|
| 333.9s (5.6min) | ✅ 正常响应 | 连接存活 |
| 334s ~ 358s | 临界区 | |
| 357.7s (6.0min) | ❌ 无响应 | 连接已死 |
- Server 正常响应 FIN 的 27 个连接:最大空闲时间均 ≤ 334s
- Server 未响应 FIN 的 98 个连接:最大空闲时间均 ≥ 358s
中间设备空闲超时 ≈ 340~350 秒,后确认为 AWS EC2 Nitrov6(第 8 代实例)ENI 连接跟踪默认超时 350 秒。
3. 僵尸连接的死亡模式
正常连接关闭(空闲 < 350s):
1 | Client → COM_QUIT → Server 响应 FIN+ACK → 四次挥手完成 ✅ |
僵尸连接关闭(空闲 > 350s):
1 | Client → COM_QUIT → 被中间设备静默丢弃 → 无响应 |
根因
2026-04-15 更新:经查 AWS 官方文档 确认,350 秒超时的真正来源是 EC2 第 8 代实例(Nitrov6 架构)的 ENI 安全组连接跟踪(Connection Tracking)默认超时,而非此前推测的 NAT Gateway。
实例代次 Nitro 版本 TCP Established 默认超时 第 7 代及以下 Nitrov5 及以下 432000s(5 天) 第 8 代(M8/C8/R8 等) Nitrov6 350s 可通过
aws ec2 describe-network-interfaces --network-interface-ids <eni-id> --query 'NetworkInterfaces[0].ConnectionTrackingConfiguration'查询,返回null表示使用默认值。控制台路径:EC2 → Network Interfaces → 选择 ENI → 空闲连接跟踪超时。
1 | 340~350s 1200s |
三个因素叠加导致问题:
- EC2 Nitrov6 ENI 连接跟踪超时 350 秒:第 8 代 EC2 实例(M8/C8/R8 等,Nitrov6 架构)的安全组连接跟踪将 TCP Established 默认超时从 432000s(5天)降至 350s。连接空闲超过 350 秒后,ENI 连接跟踪条目被清除,后续数据包被安全组静默丢弃(不发 RST)
- OS tcp_keepalive_time = 1200 秒:远大于连接跟踪超时,keepalive 探测在连接死后 850 秒才发出,来不及续命
- HikariCP 3.x 无应用层 keepalive:3.x 版本没有 keepaliveTime 功能,不会主动检测空闲连接的存活状态
maxLifetime 的角色:不是根因,而是暴露问题的时间点。改 maxLifetime 只改变了”何时发现尸体”,而不是”何时死亡”。
旧服务不受影响的原因:旧服务在本地机房内部访问数据库,不经过 AWS EC2,不受 Nitrov6 ENI 连接跟踪超时影响。
验证实验
在 AWS EC2 上对比测试(原始 keepalive vs 调优后的 keepalive):
原始配置(tcp_keepalive_time=1200s):
| maxLifetime | 结果 |
|---|---|
| 5min (300s) | ✅ 成功(< NAT 超时 350s) |
| 10min | ❌ 失败 |
| 15min | ❌ 失败 |
| 20min | ❌ 失败 |
| 30min | ❌ 失败 |
调优后(tcp_keepalive_time=20s, intvl=10s, probes=3):
| maxLifetime | 结果 |
|---|---|
| 5min | ✅ 成功 |
| 10min | ✅ 成功 |
| 15min | ✅ 成功 |
| 20min | ✅ 成功 |
| 30min | ✅ 成功 |
低 keepalive_time 让 OS 每 20 秒发送一次探测包,持续重置 NAT Gateway 的空闲计时器,连接永远不会被丢弃。
修复方案
方案 0:修改 ENI 连接跟踪超时(根因级修复,推荐)
直接调大 EC2 ENI 的 TCP Established 超时,从根源解决问题:
1 | aws ec2 modify-network-interface-attribute \ |
或在 EC2 控制台:Network Interfaces → 选择 ENI → Actions → Change connection tracking → 设置 TCP established timeout。
优点:根因级修复,恢复到与旧代实例一致的行为(432000s / 5天),无需改 OS 参数或应用配置。
注意:仅对新建连接生效,已有连接不受影响。
方案 1:降低 OS tcp_keepalive_time(最快生效)
1 | sysctl -w net.ipv4.tcp_keepalive_time=120 # 2 分钟,远小于 NAT 350s |
优点:全局生效,所有 TCP 连接(MySQL、Redis、MQ 等)都受益,无需改代码。
注意:120s 远小于 350s,留足安全余量。
方案 2:升级 HikariCP 4.x+ 启用 keepaliveTime(应用层修复)
1 | spring.datasource.hikari: |
优点:不依赖 OS 配置,应用层独立控制。
限制:需要升级 HikariCP(4.0+ 才有 keepaliveTime),可能需要升级 JDK。
方案 3:不升级 HikariCP 的临时方案
1 | spring.datasource.hikari: |
原理:maxLifetime=5min < NAT 超时 350s,确保连接在被 NAT 丢弃前主动替换。
缺点:5 分钟的 maxLifetime 较短,连接轮换频繁,增加数据库连接创建负担。
推荐组合:方案 0(根因修复)+ 方案 1 + 方案 2(ENI 层 + OS 层 + 应用层三重保险)。
经验教训
1. 跨网络访问必须关注中间设备的空闲超时
本地机房内部的 TCP 连接可以长期空闲而不被打断。但跨机房、跨云的网络路径上往往存在 NAT Gateway、防火墙、LVS 等有状态设备,它们都有空闲会话超时(通常 5~30 分钟)。
迁移到云上时,即使 ping 延迟正常、丢包率为 0,仍然需要检查中间设备的空闲超时配置。
| 设备 | 典型空闲超时 |
|---|---|
| AWS EC2 Nitrov6 ENI 连接跟踪 | 350s(第 8 代实例默认) |
| AWS EC2 旧代 ENI 连接跟踪 | 432000s(5 天) |
| AWS NAT Gateway | 350s(~6 分钟) |
| 防火墙 | 1800s(30 分钟) |
| LVS (IPVS) | 900s(15 分钟) |
| 云厂商 SLB/NLB | 300~900s |
2. tcp_keepalive_time 必须小于路径上最短的空闲超时
操作系统默认的 tcp_keepalive_time 通常是 7200s(2 小时),AWS 默认是 1200s(20 分钟)。这些默认值在存在中间设备的场景下往往过大。
建议值:60~120 秒,覆盖绝大多数中间设备的超时配置。
3. SO_KEEPALIVE 必须开启才有效
tcp_keepalive_time 是系统级默认值,但只对设置了 SO_KEEPALIVE 的 socket 生效。
| 组件 | SO_KEEPALIVE 默认值 |
|---|---|
| MySQL Connector/J 5.1.x | true(从 5.0.7 开始) |
| MySQL Connector/J 8.x | true |
| 应用直接创建 Socket | false |
| Druid 连接池 | 不控制(依赖驱动) |
验证方法:
1 | ss -tnpei dst <db_ip>:<db_port> | head -3 |
4. 静默丢弃是最难排查的连接故障
本案中 NAT Gateway 不发 RST,直接丢弃不匹配的包。这导致:
- 客户端发送数据后只能等待重传超时(数十秒到数分钟)
- 没有任何”连接已断开”的信号
- 从应用层看就是”突然卡住然后超时”
排查手段:在客户端抓包,关注两个信号:
- 发送数据后长时间无 ACK(→ 中间设备丢包)
- FIN 发出后无响应(→ 连接在中间设备已不存在)
5. “修改 maxLifetime 后报错时间跟着变”不能排除中间设备问题
这个现象容易误导排查方向。直觉上会认为”报错时间跟 maxLifetime 走,说明是 HikariCP 的问题”。实际上:
- maxLifetime < 中间设备超时 → 连接在死之前被主动替换 → 不报错
- maxLifetime > 中间设备超时 → 连接已死 → maxLifetime 到期时操作已死连接 → 报错
报错时间跟 maxLifetime 走,恰恰说明存在一个固定的中间设备超时阈值。
6. 连接池不是万能的
连接池(HikariCP、Druid 等)管理的是 JDBC Connection 对象的生命周期,但底层 TCP 连接的存活状态取决于 OS 和网络。连接池无法感知”TCP 连接已被中间设备静默丢弃”——除非主动发送数据去探测。
这就是 HikariCP 4.x 引入 keepaliveTime 的原因:定期向空闲连接发送 SELECT 1,主动打破沉默,让死连接尽早暴露。
7. ENI为什么只丢掉进来的包,而不丢掉出去的包?
这个问题直接触及 AWS 安全组的工作原理。
核心原因:出站靠的是显式规则,入站靠的是连接跟踪。
安全组通常这样配置:
1 | 出站规则 (Outbound): 0.0.0.0/0 All traffic → ALLOW ← 默认就是放行全部 |
连接跟踪活着的时候(空闲 < 350s):
1 | Client 出站 → 匹配出站规则 (allow all) → 放行 ✅ → 同时创建跟踪条目 |
连接跟踪过期后(空闲 > 350s):
1 | Client 出站 → 匹配出站规则 (allow all) → 放行 ✅ ← 规则还在,照样放行 |
所以不对称的根源是:出站有兜底的 allow-all 规则,入站没有。Server 的回包以前全靠连接跟踪
“搭便车”进来,跟踪条目一过期,便车没了,入站规则又没有显式放行这个流量,就被丢了。
如果你在入站规则里加一条 allow from 172.20.64.240/32 port 3306,理论上即使连接跟踪过期,
回包也能通过显式规则放行——但这就变成无状态过滤了,一般不建议这么做。
附录
抓包统计数据
| 指标 | 数据 |
|---|---|
| 抓包时长 | 4796 秒(~80 分钟) |
| 总包数 | 84595 |
| 总连接数 | 145 |
| RST 包数 | 0 |
| Server 正常响应 FIN | 27 个(最大空闲 ≤ 334s) |
| Server 未响应 FIN | 98 个(最大空闲 ≥ 358s) |
| 中间设备空闲超时 | 340~350s(与 EC2 Nitrov6 ENI 连接跟踪默认 350s 吻合) |
测试代码
测试 Demo 代码位于同目录,修改 db.properties 后 bash run.sh 即可运行,无需编译。