AWS 连接超时根因分析

AWS 连接超时根因分析

问题现象

业务从本地机房迁移到 AWS 后,Java 服务访问跨机房 MySQL 数据库(AWS EC2 → 专线 → 本地机房 DB),每隔 20~30 分钟出现批量连接超时:

1
2
Failed to validate connection (No operations allowed after connection closed.)
Connection is not available; request timed out after 5006ms

关键特征:

  • 网络延迟稳定在 ~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
2
10:32:23.996 Closing connection @6d21071: (connection has passed maxLifetime)
10:32:24.004 Added connection @60401ca4 ← 8ms 后替换完成

结论:在 MySQL 可达且网络正常(RTT < 20ms)的环境下,纯 maxLifetime 批量过期无法导致连接超时。HikariCP 的异步替换机制工作正常。问题的根因不在 HikariCP 本身。

第二轮:TCP keepalive 与中间设备分析

AWS → 本地机房的网络路径中存在防火墙(空闲会话 30 分钟清除),可能还有 NAT Gateway 等中间设备。

关键验证:反编译 MySQL Connector/J 5.1.49 字节码,确认 tcpKeepAlive 默认值:

1
2
3
// ConnectionPropertiesImpl.class 字节码
3970: ldc_w #519 // String tcpKeepAlive
3973: ldc_w #514 // String true ← 默认值 = true(从 5.0.7 开始)

同时通过 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
2
3
4
5
Client → COM_QUIT    → 被中间设备静默丢弃 → 无响应
Client → 重传 (0.2s) → 丢弃 → 无响应
Client → 重传 (0.4s) → 丢弃 → 无响应
Client → 重传 (0.8s) → 丢弃 → 无响应
Client → FIN → 丢弃 → 永远无响应

根因

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
2
3
4
5
6
7
                340~350s                    1200s
ENI 连接跟踪(Nitrov6) keepalive
空闲超时 首次探测
─────────────────┼──────────────────────────────┼──────────
连接空闲 │连接跟踪条目清除 │探测发出
│连接变僵尸 │但已经晚了
│双端不知情 │

三个因素叠加导致问题:

  1. EC2 Nitrov6 ENI 连接跟踪超时 350 秒:第 8 代 EC2 实例(M8/C8/R8 等,Nitrov6 架构)的安全组连接跟踪将 TCP Established 默认超时从 432000s(5天)降至 350s。连接空闲超过 350 秒后,ENI 连接跟踪条目被清除,后续数据包被安全组静默丢弃(不发 RST)
  2. OS tcp_keepalive_time = 1200 秒:远大于连接跟踪超时,keepalive 探测在连接死后 850 秒才发出,来不及续命
  3. 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
2
3
4
aws ec2 modify-network-interface-attribute \
--network-interface-id <eni-id> \
--connection-tracking-specification TcpEstablishedTimeout=432000 \
--region <region>

或在 EC2 控制台:Network Interfaces → 选择 ENI → Actions → Change connection tracking → 设置 TCP established timeout。

优点:根因级修复,恢复到与旧代实例一致的行为(432000s / 5天),无需改 OS 参数或应用配置。
注意:仅对新建连接生效,已有连接不受影响。

方案 1:降低 OS tcp_keepalive_time(最快生效)

1
2
3
4
5
6
7
8
9
10
sysctl -w net.ipv4.tcp_keepalive_time=120    # 2 分钟,远小于 NAT 350s
sysctl -w net.ipv4.tcp_keepalive_intvl=30
sysctl -w net.ipv4.tcp_keepalive_probes=5

# 持久化
cat >> /etc/sysctl.conf << 'EOF'
net.ipv4.tcp_keepalive_time = 120
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
EOF

优点:全局生效,所有 TCP 连接(MySQL、Redis、MQ 等)都受益,无需改代码。
注意:120s 远小于 350s,留足安全余量。

方案 2:升级 HikariCP 4.x+ 启用 keepaliveTime(应用层修复)

1
2
3
4
spring.datasource.hikari:
keepalive-time: 120000 # 2 分钟,应用层主动 ping 空闲连接
max-lifetime: 1800000 # 30 分钟
connection-test-query: SELECT 1

优点:不依赖 OS 配置,应用层独立控制。
限制:需要升级 HikariCP(4.0+ 才有 keepaliveTime),可能需要升级 JDK。

方案 3:不升级 HikariCP 的临时方案

1
2
3
4
5
6
7
spring.datasource.hikari:
maximum-pool-size: 32
minimum-idle: 10 # 必须 < maximum-pool-size
idle-timeout: 120000 # 2 分钟淘汰多余空闲连接
max-lifetime: 300000 # 5 分钟(< NAT 350s)
connection-test-query: SELECT 1 # 借出时验证
connection-timeout: 10000 # 给更多时间创建替换连接

原理: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
2
ss -tnpei dst <db_ip>:<db_port> | head -3
# 看 timer:(keepalive,...) → 有则开启,无则关闭

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
2
3
出站规则 (Outbound):  0.0.0.0/0  All traffic  → ALLOW   ← 默认就是放行全部
入站规则 (Inbound): 10.0.0.0/8 TCP 22 → ALLOW ← 只开放特定端口
(没有 "允许来自 172.20.64.240:3306 的回包" 这条规则)

连接跟踪活着的时候(空闲 < 350s):

1
2
Client 出站 → 匹配出站规则 (allow all) → 放行 ✅ → 同时创建跟踪条目
Server 回包 → 匹配跟踪条目 (return traffic) → 放行 ✅ ← 不看入站规则,靠跟踪放行

连接跟踪过期后(空闲 > 350s):

1
2
Client 出站 → 匹配出站规则 (allow all) → 放行 ✅   ← 规则还在,照样放行
Server 回包 → 无跟踪条目 → 回退检查入站规则 → 无匹配规则 → 丢弃 ❌

所以不对称的根源是:出站有兜底的 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.propertiesbash run.sh 即可运行,无需编译。