作为 Apecloud 团队,我们致力于通过开源的 KubeBlocks 项目,在 Kubernetes (K8s) 上为用户提供企业级的数据库高可用方案[1]。其中,SQL Server on K8s with Always On 是我们支持的关键能力之一,相比Microsoft 为在容器中运行 SQL Server 提供的基础StatefulSet方案, KubeBlocks 的 MSSQL Addon 提供了一整套生产级的生命周期管理能力,包括:多节点高可用配置、动态扩缩容、数据库/账户管理、参数管理、监控告警、全量/增量/PITR备份恢复,以及 TDE/TLS 数据加密等,是现有 SQL Server operator 中最成熟、最完善的方案之一[2][3]。
近期,某客户计划在 Oracle Kubernetes Engine (OKE) 环境中部署我们的 SQL Server 高可用集群。为了确保方案的可靠性,我们在 OKE 环境中对 MSSQL Addon 进行了一轮全方位的回归测试。
在测试 Failover、资源动态扩缩容等运维操作(Ops)时,我们发现了一个在 OKE 上独有的异常现象,这与我们自建的 K8s 或其他云厂商环境中的表现截然不同:在 Pod 滚动重启或主备切换后,旧的主节点 Pod 在重建后需要耗时约 15 分钟才能重新加入集群。这个意料之外的延迟,促使我们深入排查,并最终揭示了一个潜藏在云网络深处的“TCP 流量黑洞”问题。本文将完整记录这一问题的排查与解决全过程。
一、 问题现象:切换后的长连接“假死”
在一次例行的主备切换测试中,我们搭建了如下的测试环境:
- 原主节点:Pod IP 为
10.0.10.129。在 09:33 左右被手动kill模拟故障,重建后的新 Pod IP 为10.0.10.98。 - 副本一:Pod IP 为
10.0.10.227,主备切换后的新主节点,同时也是本次抓包分析的机器。 - 副本二:Pod IP 为
10.0.10.56。
图1: 切换前pod状态
图2: 切换后pod状态
切换过程中的关键时间线日志如下:
- 09:32:49:触发了新主的切换操作。
2026-03-18T15:48:05Z INFO SQLServer Setting replica to SECONDARY role...
2026-03-19T01:32:49Z INFO HA Cluster has no leader, attempt to take the leader
2026-03-19T01:32:49Z INFO SQLServer Replica is now PRIMARY
2026-03-19T01:32:49Z INFO HA Take the leader success!
2026-03-19T01:32:57Z INFO HA This member is Cluster's leader
2026-03-19T01:32:57Z DEBUG HA Refresh leader ttl
2026-03-19T01:33:57Z INFO HA This member is Cluster's leader
2026-03-19T01:33:57Z DEBUG HA Refresh leader ttl
2026-03-19T01:34:57Z INFO HA This member is Cluster's leader
2026-03-19T01:34:57Z DEBUG HA Refresh leader ttl
2026-03-19T01:35:57Z INFO HA This member is Cluster's leader
2026-03-19T01:35:57Z DEBUG HA Refresh leader ttl
-
09:33:02:老节点(IP
10.0.10.129)被正式关闭。 -
09:48:05:原主节点重建后POD(IP
10.0.10.98)以备节点身份重新加入集群。
异常现象:从 09:33:02 开始,原有连接的 TCP 流量仿佛掉入了黑洞,主备之间的同步彻底中断。SQL Server 的日志中只看得到同步中断,但没有任何连接异常的报错。系统长时间无法自动恢复,最终耗时约 15 分钟,旧主节点(Pod 10.0.10.98)才以备库的身份重新加入集群。
[HADR TRANSPORT] AR[16FD79D5-4819-43D0-B534-C5132DDFF886]->[20D27FE9-032A-43E7-969F-78A9976AEA33] Setting Reconnect Delay to 0 s
[HADR TRANSPORT] LOCAL AR:[16FD79D5-4819-43D0-B534-C5132DDFF886]->[20D27FE9-032A-43E7-969F-78A9976AEA33] in
CHadrTransportReplica::Reset called from function [CHadrTransportReplica::ReconnectTask], primary = 0,
primaryConnector = 1[HADR TRANSPORT] LOCAL AR:[16FD79D5-4819-43D0-B534-C5132DDFF886]->[20D27FE9-032A-43E7-969F-78A9976AEA33] in
CHadrConfigState::ChangeState with session ID A23BA718-9B73-4588-893B-0F18C0275526 change from
HadrSessionConfig_ConfigRequest to HadrSessionConfig_ConfigRequest - function [CHadrSession::Reset][HADR TRANSPORT]
AR[16FD79D5-4819-43D0-B534-C5132DDFF886]->[20D27FE9-032A-43E7-969F-78A9976AEA33]
Session:[1DF067D5-EB9C-4A15-9216-A025B534D66F] CHadrTransportReplica State change from HadrSession_Timeout to
HadrSession_Configuring - function [CHadrTransportReplica::Reset_Deregistered][HADR TRANSPORT] AR[16FD79D5-4819-43D0-B534-C5132DDFF886]->[20D27FE9-032A-43E7-969F-78A9976AEA33],
Seesion:[1DF067D5-EB9C-4A15-9216-A025B534D66F] Queue Timeout (10) from [CHadrTransportReplica::Reset_Deregistered][HADR TRANSPORT]
LOCAL AR:[16FD79D5-4819-43D0-B534-C5132DDFF886]->[20D27FE9-032A-43E7-969F-78A9976AEA33] in CHadrConfigState::ChangeState
with session ID 1DF067D5-EB9C-4A15-9216-A025B534D66F change from HadrSessionConfig_ConfigRequest to
HadrSessionConfig_WaitingSynAck - function [CHadrSession::GenerateConfigMessage][HADR TRANSPORT] LOCAL AR:
[16FD79D5-4819-43D0-B534-C5132DDFF886]->[20D27FE9-032A-43E7-969F-78A9976AEA33] in CHadrSession::GenerateConfigMessage
with session ID 1DF067D5-EB9C-4A15-9216-A025B534D66F Generate configure message(1) with viersion(1)[HADR TRANSPORT] AR
[16FD79D5-4819-43D0-B534-C5132DDFF886]->[20D27FE9-032A-43E7-969F-78A9976AEA33] Transport is not in a connected state,
unable to send packet2026-03-18 10:22:48.60 spid27s Using 'dbghelp.dll' version '4.0.5'
二、 抓包排查:经典的 TCP 指数退避重传
为了弄清流量去向,我们对通信链路(10.0.10.227:38101 -> 10.0.10.129:5022,5022 为 SQL Server Always On 的端点端口)进行了抓包分析。
关键报文序列如下:
09:32:54.234 129:5022 → 227:38101 ACK=40464 [正常确认]
09:33:02.246 227:38101 → 129:5022 seq=40464:52704 len=12240 [发送数据]
09:33:02.247 129:5022 → 227:38101 ACK=52704 [确认收到,这是129最后的遗言]
# 之后 227 继续发送新数据,但再也等不到 129 的 ACK
09:33:13.258 227:38101 → 129:5022 seq=52704:64944 len=12240 [发送新数据]
09:33:13.468 227:38101 → 129:5022 seq=52704:61652 len=8948 [重传1,间隔0.4s]
09:33:13.876 227:38101 → 129:5022 seq=52704:61652 len=8948 [重传2,间隔0.9s]
09:33:14.740 227:38101 → 129:5022 seq=52704:61652 len=8948 [重传3,间隔1.7s]
...
09:36:46.964 227:38101 → 129:5022 seq=52704:61652 len=8948 [持续重传,间隔已达106.5s]
分析结论:这是一个非常典型的 TCP 重传机制 现象。当发送端未收到 ACK 时,会触发 RTO(超时重传),并且重传间隔呈指数级退避(0.4s → 0.9s → 1.7s → 3.3s ... 106.5s)。在 Linux 默认配置下,会重传 15 次,总耗时约 15 分钟才会彻底放弃并断开连接。
三、 剖析:为何应用层(SQL Server)未触发超时?
很多同学可能会问:SQL Server HADR(高可用灾备)机制默认有 10 秒的 SESSION_TIMEOUT,为什么没有生效?
陷阱在于:SESSION_TIMEOUT 主要用于检测心跳 ping 的丢失。
在当前场景下:
- 操作系统传输层的 TCP 连接仍然处于
ESTABLISHED状态,内核正在努力重传,并没有向应用层抛出连接断开的 Error。 - SQL Server 认为“底层 TCP 连接还活着,只是数据传输慢”,因此一直在挂起等待。
- 最终结果:应用层无限期等待,直到 15 分钟后 TCP 栈最终放弃。这就造成了所谓的“幽灵连接”。
TCP 层:持续重传,连接未断开(ESTABLISHED)
↓
SQL Server 层:认为连接还活着(TCP 未报告错误)
↓
HADR 层:等待数据同步完成,未触发 SESSION_TIMEOUT
↓
结果:无限等待,直到 TCP 最终放弃(15 次重传 ≈ 15-20 分钟)
四、 根因定位与云网络背景
结合业内经验与本次故障特征,我们梳理出了完整的根因链条:
- 云网络环境的特殊性:在部分云厂商的基础网络架构中,为了抵抗网络抖动,底层网络设备会尽量避免向对端发送
RST报文。但在容器化 IP 频繁变更(Pod 重建)的场景下,这会导致对端无法感知连接断裂,从而引发 15 分钟黑洞问题。
图1: OKE环境宿主机tcp_retries2参数默认值
- 应用关闭动作粗暴:最根本的原因是,在执行 Delete Pod 时,MSSQL 进程没有进行优雅关闭(Graceful Shutdown)。进程被暴力 Kill,操作系统和应用都来不及向对端发送
FIN或RST报文主动断开连接,对端(案例中的227 节点)毫不知情,傻傻地等待并不断重传。
五、 解决之道:双管齐下
针对上述根因,我们采取了“应用层主动阻断 + 系统层被动兜底”的综合解决方案。
1. 应用层修复:实现优雅关闭 (主动解法)
最彻底的解法是让连接主动断开。我们为 MSSQL Addon 引入了 Graceful Shutdown 的 Patch。在接收到停止信号时,保证应用能主动执行 close() 释放 TCP 连接,向对端发送 FIN/RST。
2. 系统层兜底:优化内核参数 tcp_retries2 (被动防御)
[!IMPORTANT] 在 Kubernetes 这样的容器化环境中,Pod 的生命周期是动态的,其 IP 地址随时可能因为调度、升级或故障而改变。虽然实现优雅关闭(Graceful Shutdown)是应用开发的最佳实践,但在很多突发场景下,例如 OOMKilled、节点故障(Node Failure)或进程直接崩溃(Crash),应用根本没有机会执行优雅关闭。这就导致了连接的另一端无法感知对端已经消失,从而陷入长时间的等待,形成网络黑洞。因此,仅仅依赖应用层的优雅关闭是不够的,必须在系统层面建立一道“被动防御”的兜底机制。
对于使用长连接访问的应用来说(默认使用都是 tcp 长连接,无论 HTTP/2 还是 HTTP/1),在没有设置合适请求 timeout 参数的情况下可能会出现 15mins 的超时问题,为了应对网络脑裂或机器突然断电等无法执行优雅关闭的极端物理故障,我们需要缩短操作系统的 TCP 放弃时间。[4]
Linux 内核的 net.ipv4.tcp_retries2 参数[5]控制着处于 ESTABLISHED(已建立连接)状态下的数据传输失败重传行为。需要澄清的一个常见误区是:tcp_retries2 并不是简单的绝对重传“次数”,它其实决定了内核计算总超时时间的边界。
TCP 超时计算逻辑原理:
内核采用指数退避算法计算重传超时时间(RTO,初始为 1s,受限于 TCP_RTO_MIN 200ms 和 TCP_RTO_MAX 120s),计算公式大致如下:
- 当
retries2 <= 9时,总超时时间呈指数增长:timeout = ((2 << retries2) - 1) * 200ms - 当
retries2 > 9时,总超时时间转为线性增长:timeout = (2^9 - 1) * 200ms + (retries2 - 9) * 120s
基于上述算法:
- 默认值 15:计算得出的总超时时间约为 924.6 秒(15.4 分钟)。在这 15 分钟内,应用层将毫无察觉地死等,这也是引发 15 分钟黑洞的直接原因。
- 优化值 8:计算得出的总超时时间大幅降至 约 25.5 秒。
我们将 tcp_retries2=8 的配置项纳入了基础设施的交付流程中(可通过初始化节点 sysctl 或 Pod initContainer 注入)。这样即使遇到连接黑洞,TCP 层最多等待约 25 秒就会强制断开连接,并向上层应用抛出 ETIMEDOUT 异常,从而让应用层的 Failover 机制迅速介入。
六、 验证与后续改进
在 OCI 集群应用上述修复后,我们进行了场景验证和测试:
- 测试场景:集群下发变配Ops,测试滚动重启过程中主备切换是否正常。
- 测试结果:我们通过对比
tcp_retries2参数在默认值(15)和优化值(8)下的表现,来验证修复效果。
| 对比项 | 场景一:默认值 (tcp_retries2=15) | 场景二:优化后 (tcp_retries2=8) |
|---|---|---|
| 恢复时长 | 约 16 分钟 | 2 分钟内 |
| 现象 | 主备切换后,连接长时间中断,出现明显的“流量黑洞”,集群长时间无法恢复同步。 | 主备切换后,连接迅速恢复,集群快速完成同步,有效解决了流量黑洞问题。 |
| GIF 演示 | 见表格下方 | 见表格下方 |
后续改进与展望: 此次故障排查不仅解决了眼前的问题,也为我们提升系统整体的健壮性提供了宝贵的经验。后续,KubeBlocks团队将从以下几个方面深化改进:
-
全面推广优雅关闭(Graceful Shutdown)实践: 将优雅关闭作为所有有状态应用(包括但不限于 Redis、PostgreSQL 等数据库)开发和上线的强制性标准。确保应用在退出时能主动清理和释放资源,从根源上杜绝“幽灵连接”。
-
优化基础设施的交付流程: 将
tcp_retries2=8等关键内核参数的优化,作为节点初始化的标准配置项,纳入了基础设施的交付流程中,确保集群环境的一致性和可靠性。 -
常态化混沌工程演练: 将此类网络分区、Pod 强制删除等故障场景纳入常态化的混沌工程演练平台。通过主动注入故障,持续性地检验和提升系统的弹性和自愈能力,变被动响应为主动防御。
Ref: