先给结论:「静默死亡窗口」不是一个固定值,而是取三条探测路径里最慢那条——活跃连接走应用 read timeout(你实测 3.4s),空闲连接走内核 keepalive(默认 7200s,基本等于无),跨机房场景其实还有第四条更快也更可靠的路径:控制面信号(BGP/路由收敛 <1s)。所以别把宝押在 keepalive 上,keepalive 只该兜「空闲连接」这一类;真正的快速发现要么靠控制面事件,要么靠 HTTP/2 PING。下面给量化方法 + 参数推导 + 防误杀判据。
一、先把「静默窗口」定义清楚
窗口 = max(该连接被探测到死的时间) - 0(故障 T+0)。它取决于这条连接在故障时刻处于什么状态:
- 活跃连接(故障时正在收发):下一个包发出即进入内核 RTO 重传。若路径是黑洞(你这种单臂故障、包被丢),重传按 RTO 指数退避,应用侧表现为 read timeout 到期(你配的 3s)。发现时间 ≈ 应用 read timeout。
- 空闲连接(连接池预热、无流量):没有应用超时兜底,只能等内核 keepalive。默认参数下 ≈ 7200s + 3×75s ≈ 2 小时——这才是你说的「理论上无上界」。
- 对端主动 RST / 中间设备回 ICMP unreachable:连接立即 ESTAB→CLOSE_WAIT,秒级发现。
所以窗口不是一个数,是一个分布,上界由空闲连接 + keepalive 决定。
二、量化「2 万条里有多少半开」
故障瞬间所有 ESTAB 连接都变成「半开候选」(对端活、路径断)。真正会拖尾的是空闲那部分。可观测集(内核 5.15,eBPF 或 ss):
# 故障时刻抓半开候选:ESTAB 且最近一次收发包已超过空闲阈值
ss -ti state established sport = :443 | \
awk '/idle:/{print $NF}' | sort -n | head
# 或 eBPF 挂 tcp_recvmsg/tcp_sendmsg,记录每 socket last_ts,
# 故障时刻对 (now - last_ts) > 60s 的 ESTAB socket 计数
估算式(给事故复盘用):
半开空闲候选数 ≈ 连接池 idle 数 × (跨机房链路上占比)
+ Σ 其他空闲 ESTAB
你 2 万存量里,若连接池 idle 占 30%(6000),这 6000 就是 keepalive 要兜的拖尾主体。活跃那 ~1.4 万会在 3.4s 内自己暴露,不用管。
三、keepalive 参数怎么推「及时又不误杀」
内核探测周期 ≈ tcp_keepalive_time + probes × tcp_keepalive_intvl(黑洞场景,无 ACK 才按 intvl 退)。目标:空闲连接的发现时间 < 应用 read timeout(3s)没可能,但至少要 < 业务能接受的「下次访问」上界,且探测包量可承受。
推导:
探测包 pps ≈ N_idle / keepalive_time + 新建速率
要 < 1000 pps(10G 链路 <0.01% 带宽,你已验证)
给一个比 7200s 现实得多、又不至于误杀的组合作为起点:
net.ipv4.tcp_keepalive_time = 120 # 空闲 120s 后开始探
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 5
空闲连接最坏发现时间 ≈ 120 + 5×10 = 170s,把 2 小时的无上界压到 3 分钟内,探测包 ≈ 6000/120 + 800 ≈ 850 pps,可接受。注意:这仍是「兜底」,不是「主发现手段」——170s 对你 3s 的 SLO 还是太慢。
四、更靠前、更可靠的快速发现(这才是正解)
- 控制面信号当主触发:你 BGP 邻居 down 在 T+90ms、路由撤销 T+400ms 就完成了。把「本链路上 BGP session Established→Idle / 该 next-hop 从路由表消失」做成事件,一旦命中就主动 drain 该链路上的存量连接(让应用层主动 RST/关闭并走备用路径),而不是等 keepalive。发现时间直接从 3.4s → <1s。这条路径比任何 keepalive 都靠前。
- HTTP/2 / gRPC 用 PING 帧,别用 TCP keepalive:gRPC 的
keepalive_time(应用层 PING)走的是 RFC 7540 PING,在已建立连接上探测,比内核 keepalive 更准、可控。设keepalive_time=15s, keepalive_timeout=5s,空闲连接 ~20s 内发现,且 PING 对端必须回 PING-ACK,回不回是明确信号。 - eBPF 观测集提前预警:盯
tcp_retrans(重传计数突增)+ss的retrans字段 +tcp_rcv_qlen堆积。链路将断未断时重传会先飙,比 read timeout 早一个量级。
五、防误杀判据(关键)
「及时」和「不误杀」的边界在判定信号上:
- 硬信号(可判死):收到 RST、收到 ICMP unreachable、对端不回 PING-ACK、控制面确认该路径 down → 立即关连切流。
- 软信号(不可单点判死):单次 PING 超时 / 单次 read timeout → 只触发重探 1 次或降级切备用路径,不直接杀。避免把「对端 GC 卡 1 秒」误判成「链路断」。
- 原则:控制面事件 + 硬信号才允许批量关连;应用层超时只做单连接重试/切路径。这样 2 万连接不会因为一次抖动被雪崩式重连打爆对端(800 conn/s 的新建速率,一旦误杀 6000 条,对端瞬时压力 = 6000/重建窗口,这才是真事故)。
可落地 checklist
- 故障时刻用
ss -ti/ eBPF 落盘 ESTAB 空闲分布 → 算出 keepalive 要兜的 N_idle。 - 把 BGP/路由收敛事件接入连接生命周期(事件触发 drain),作为主发现。
- gRPC 开应用层 PING(15s/5s),HTTP/1.1 连接池开应用层 idle 探活(每 30~60s 对空闲连接发一次轻量请求或 RST 探测)。
- 内核 keepalive 降到 120s/10s/5,只作最后兜底。
- 判定策略:硬信号批量关、软信号单连重试,避免误杀雪崩。
一句话:把「发现」从 keepalive 挪到控制面 + 应用层 PING,keepalive 只兜空闲尾;判死只认硬信号,防误杀。