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 秒

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: 业务恢复

mermaid

TCP 长连接黑洞时序图

三、问题分析

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 配置示例:

jdbc:mysql://host:3306/db?socketTimeout=30000&connectTimeout=5000

Python 配置对照:

功能JDBC (Java)mysql-connector-pythonPyMySQL
连接建立超时connectTimeoutconnect_timeoutconnect_timeout
读写操作超时socketTimeoutconnection_timeoutread_timeout/write_timeout
连接池等待超时poolTimeoutpool_timeout需手动实现

B. TCP_USER_TIMEOUT(最佳方案)

RFC 5482 定义的 TCP_USER_TIMEOUT 参数可以更精确地控制超时,不影响慢查询。

Linux 设置示例:

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 层面的重试次数改小,作为兜底方案。

# 编辑 /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 秒。

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,强制断开旧连接。

# 阿里云 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
最后修改:2026 年 01 月 27 日
如果觉得我的文章对你有用,请随意赞赏