Loading... # TCP 长连接黑洞重现和分析 # 一、事件概述 ## 1. 事件背景 这是一个在集团内部反复出现多年的问题,遍及各个不同业务。当数据库 crash 重启恢复后,业务长时间无法恢复正常;依赖的业务做了高可用切换,但业务仍长时间报错;依赖的服务下掉一个节点,导致业务长时间报错;客户做变配升级云服务节点规格,也会导致客户业务长时间报错。 ## 2. 影响范围 ### A. 影响场景 - 数据库高可用切换场景 - LVS 负载均衡切换场景 - Kubernetes Pod 驱逐场景 - 服务节点摘除场景 ### B. 影响时长 默认配置下业务需要 900 秒(约 15 分钟)才能自动恢复 ### C. 影响功能 所有使用 TCP 长连接的业务场景 ## 3. 严重程度 P0 级隐藏问题(高可用切换后业务仍长时间不可用) # 二、事件时间线 ## 1. 问题发现(时间:00:31) ### A. 现象描述 在 MySQL 高可用切换测试中,管控在 31 秒检测到 Master(3307 端口)异常,执行切主操作,将 Slave(3306 端口)提升为新 Master,同时在 LVS 上摘掉 3307,挂上 3306。整个切换过程在 3 秒内完成。 ### B. 预期结果 高可用切换完成后,业务应立即恢复正常访问 ### C. 实际结果 Sysbench 压力测试在第 32 秒 QPS 开始下降,第 33 秒跌至 0,之后持续 900 多秒完全无法访问 ## 2. 问题定位(时间:00:35 - 15:57) ### A. 排查过程 检查 LVS 状态发现 3306 已成功挂上,3307 已摘除,但没有新连接建向 3306。业务仍然使劲薅着 3307 这个已失效的节点。 通过 netstat 命令发现客户端与 LVS 的连接状态为 ESTABLISHED,LVS 与后端 3307 的连接也显示 ESTABLISHED,但实际上 3307 已经不可用。 ### B. 根因定位 问题出在 TCP 协议层。当服务端突然消失(宕机、断网,来不及发 RST)时,客户端如果正在发送数据给服务端,会遵循 TCP 重传逻辑不断重传。如果一直收不到服务端的 ACK,大约会重传 15 次,累计约 900 秒。 ## 3. 问题解决(时间:15:57) ### A. 临时方案 重启应用,5 秒后业务恢复 ### B. 根本方案 将 net.ipv4.tcp_retries2 参数从默认值 15 改为 5,恢复时间从 900 多秒缩短到 20 秒 ```mermaid sequenceDiagram participant C as 客户端 participant L as LVS participant M1 as Master(3307) participant M2 as Slave(3306) Note over C,M2: 正常访问阶段 C->>L: 发起请求 L->>M1: 转发到 Master Note over M1: Master 故障 M1--xL: 连接中断 Note over L,M2: 高可用切换(3秒内完成) L->>M2: 挂载新 Master L--xM1: 摘除旧 Master Note over C,L: 流量黑洞阶段(约900秒) C->>L: 持续发送请求 L->>M1: 仍转发到旧 Master(无效) M1--xL: 无响应 C->>L: TCP 重传(15次) Note over C,L: 恢复阶段 C->>C: 超时后重建连接 C->>L: 新连接请求 L->>M2: 转发到新 Master M2-->>C: 业务恢复 ```   # 三、问题分析 ## 1. 直接原因 TCP 长连接在发送数据包时,如果没收到 ACK,默认会进行 15 次重传(net.ipv4.tcp_retries2 = 15),累计约 924 秒。 ## 2. 根本原因(5 Whys 分析) ### A. 为什么出现这个问题? 服务端突然消失(宕机、断网、Pod 驱逐),来不及发送 RST 包通知客户端断开连接 ### B. 为什么需要 900 多秒? 这是 TCP 协议的默认行为。Linux 默认 tcp_retries2 = 15,根据 RTO(Retransmission Timeout)指数退避算法,15 次重试大约需要 924.6 秒 ### C. 为什么这几年才明显暴露? - 微服务架构普及,服务间依赖增多 - 云上 LVS、Kubernetes Service 大规模使用 - 服务不可靠成为常态,Pod 随时被驱逐 - 之前通过重启业务临时解决,掩盖了问题 ### D. 为什么高可用切换没有用? 高可用切换只解决了服务端问题,但客户端仍然持有旧连接,不断重传到已失效的节点 ## 3. 深层反思 - 业务层面缺乏超时控制和兜底机制 - 依赖系统级高可用,忽略应用层连接管理 - TCP 参数还是几十年前的古董值,不适合现代网络环境 # 四、解决方案 ## 1. 业务层方案(推荐) ### A. SocketTimeout 配置 任何使用 TCP 长连接的业务必须配置恰当的 SocketTimeout。 JDBC 配置示例: ```properties jdbc:mysql://host:3306/db?socketTimeout=30000&connectTimeout=5000 ``` Python 配置对照: | 功能 | JDBC (Java) | mysql-connector-python | PyMySQL | |------|-------------|-------------------------|---------| | 连接建立超时 | connectTimeout | connect_timeout | connect_timeout | | 读写操作超时 | socketTimeout | connection_timeout | read_timeout/write_timeout | | 连接池等待超时 | poolTimeout | pool_timeout | 需手动实现 | ### B. TCP_USER_TIMEOUT(最佳方案) RFC 5482 定义的 TCP_USER_TIMEOUT 参数可以更精确地控制超时,不影响慢查询。 Linux 设置示例: ```c int timeout = 30000; // 30 秒 setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT, (char *)&timeout, sizeof(timeout)); ``` 注意事项: - JDK 不支持直接设置 TCP_USER_TIMEOUT - Netty 框架通过 Native 调用支持此参数 - Redis 的 Java 客户端 Lettuce 依赖 Netty,可设置此参数 ### C. 连接池配置 参考数据库连接池配置推荐: - 配置合理的连接超时 - 配置健康检查机制 - 定期验证连接有效性 ## 2. 系统层方案 ### A. 调整 tcp_retries2 参数 将 OS 层面的重试次数改小,作为兜底方案。 ```bash # 编辑 /etc/sysctl.conf net.ipv4.tcp_retries2 = 8 net.ipv4.tcp_syn_retries = 4 # 应用配置 sysctl -p ``` 建议值: - Azure 建议:5-10 - Oracle RAC 建议:3 ### B. 调整 keepalive 参数 将 keepalive 从默认 7200 秒改为 20 秒。 ```bash net.ipv4.tcp_keepalive_time = 20 net.ipv4.tcp_keepalive_intvl = 5 net.ipv4.tcp_keepalive_probes = 3 ``` ## 3. 负载均衡层方案 ### A. LVS/SLB 配置 阿里云 SLB 支持 connection_drain_timeout 参数,摘除节点时向客户端发送 Reset,强制断开旧连接。 ```bash # 阿里云 SLB 配置示例 connection_drain_timeout = 30 # 秒 ``` 建议:云上所有产品都应支持此参数,管控在购买时设置默认值 ### B. 优雅摘除节点 摘除节点前: 1. 先向客户端发送 Reset 包 2. 等待连接迁移完成 3. 再摘除节点 # 五、经验总结 ## 1. 做得好的地方 - 通过实际重现问题,深入分析根因 - 从多个层面(业务、OS、负载均衡)提供解决方案 - 提供了详细的配置参数和代码示例 ## 2. 需要改进的地方 - 这个问题存在多年才被系统分析 - 之前通过重启业务临时解决,掩盖了真正的问题 - TCP 参数还是几十年前的古董值 ## 3. 最佳实践总结 ### A. 业务层必须做的事 1. 配置合理的 SocketTimeout,对超时进行兜底 2. 优先使用 TCP_USER_TIMEOUT(如果框架支持) 3. 连接池配置健康检查机制 4. 对超时时间做到可控、可预期 ### B. 系统层可以做的事 1. OS 镜像层面将 tcp_retries2 设置为 5-10 作为兜底 2. 将 keepalive 设置为 20 秒左右 3. 固化到 OS 镜像,业务可以按需 patch ### C. 负载均衡层可以做的事 1. 配置 connection_drain_timeout 参数 2. 摘除节点时主动发送 Reset 3. 提供优雅摘除机制 ## 4. 常见误区 ### A. 7 层负载均衡就没问题? 错。只要是 TCP 长连接就有这个问题,4 层挂了 7 层也无法正常工作。 ### B. 直连就没问题? 错。即使去掉 LVS/K8s Service/软负载,让两个服务直连然后拔网线,也会同样出现这个问题。 ### C. 新连接不受影响? 对。新连接会路由到健康的节点,但旧长连接仍然卡在黑洞中。 # 六、预防措施 ## 1. 代码 Review 检查点 - 所有 TCP 长连接是否配置了超时参数 - 连接池是否配置了健康检查 - 超时时间是否合理、可预期 ## 2. 监控告警 - 监控连接数异常波动 - 监控请求超时率 - 监控节点摘除后的业务恢复时间 ## 3. 故障演练 - 定期进行高可用切换演练 - 验证超时参数配置是否生效 - 验证业务恢复时间是否符合预期 # 七、相关资料 ## 1. ALB 黑洞问题详述 https://mp.weixin.qq.com/s/BJWD2V_RM2rnU1y7LPB9aw ## 2. 数据库故障引发的"血案" https://www.cnblogs.com/nullllun/p/15073022.html ## 3. RFC 5482 TCP_USER_TIMEOUT https://datatracker.ietf.org/doc/html/rfc5482 ## 4. Azure Redis 连接最佳实践 https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection ## 5. Red Hat RAC 连接建议 https://access.redhat.com/solutions/726753 ## 6. Netty TCP_USER_TIMEOUT 实现 https://github.com/tomasol/netty/commit/3010366d957d7b8106e353f99e15ccdb7d391d8f ## 7. Lettuce Redis 客户端 PR https://github.com/redis/lettuce/pull/2499 *** ## 参考资料 1. [长连接黑洞重现和分析 | plantegg](https://plantegg.github.io/2024/05/05/%E9%95%BF%E8%BF%9E%E6%8E%A5%E9%BB%91%E6%B4%9E%E9%87%8D%E7%8E%B0%E5%92%8C%E5%88%86%E6%9E%90-public/) 最后修改:2026 年 01 月 27 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏