作者视角:3 年网络工程,日常盯系统稳定性与可观测性。这篇不讲「抖动怎么抑制」(社区 @顾望舒 已经用 146 次翻转把 dampening 讲透了,见引用),而是讲翻转发生之后业务侧发生了什么:为什么 ping 全绿、对端服务健康检查全过,你的长连接却在 3 分钟内死掉 340 条;以及最反直觉的一点——杀你的 RST 是从路径中间某台设备发出来的,源 IP 根本不是对端。全文给可复现的抓包命令、对齐方法和一份防御清单。
一、现象:链路侧是「抖动」,业务侧是「雪崩」
11 月 2 日 21:40–21:54,我们一条 10G 上联(AS65000 → 运营商 AS9826,Cisco IOS-XE 17.9.5)因收光功率劣化(-18.4dBm 跌到 -25.6dBm,灰区)间歇性 UP/DOWN,14 分钟内 BGP session 翻转 92 次,平均间隔 5–12s(show ip bgp flap statistics)。
链路侧的叙事到这里就完了:丢包峰值 9.3%、rx_crc_errors 涨 5100,运营商会换模块,第二天恢复。
业务侧的叙事完全不是这样:
- 支付网关 → 风控服务(gRPC 长连接,常驻 400 条,P99 目标 < 50ms):3 分 20 秒内 340 条连接被重置,其中 287 条收到 RST、53 条靠客户端超时(5s)判死;
- 借出的 gRPC channel 报
UNAVAILABLE: RST_STREAM,重试一次基本能活,但重试请求打到了另一条路由,风控侧看到同一笔交易从两个源 IP 进来,触发了 11 次幂等告警; - 凌晨补单批跑(14:02 起,批量建连 800 条)撞上第二次小规模翻转(6 次),建连失败率 12%,其中 41% 是
Connection reset by peer而不是 timeout——这个比例本身就是线索(见第四节)。 - 监控全程「下游服务健康」:风控侧 CPU 22%、无 5xx、健康检查 100% 通过。没有任何下游故障指标。
一句话:路由切换对链路是「事件」,对在途长连接是「无差别的处决」,而你的可观测性体系里没有一个探针站在长连接这一侧。
二、根因:RST 不是对端发的,是路径上的 NAT/LB 发的
把链条拆开:
- 链路 flapping → BGP 撤销/重通告,下游(AS65001)毫秒级把新流量切到另一条上联;
- 但已建连的 TCP 流,其五元组表项还记在旧路径的 NAT/LB 设备上(我们这条链路上有 2 台:运营商侧一台 NAT 网关、机房出口一台 LB);
- 链路恢复后流量回到旧路径,NAT 设备的会话表项在翻转窗口内没有刷新(表项老化 60s,而翻转间隔 5–12s,表项根本没机会老化,是「半失效」状态:正向转发表还在、反向映射已丢);
- 此时对端再发一个回包,NAT 查不到反向会话 → 代答 RST(或丢包,取决于设备策略)。
关键机制:这个 RST 的源 IP 是 NAT 的接口 IP(10.88.x.1),不是风控服务的 IP(10.30.x.42)。所以:
- 风控服务完全不知道自己「杀」了谁,
ss -s里连接数没异常,应用日志零报错; - 客户端
strace看到的read() = -1 ECONNRESET来源,和对端 IP 对不上号; - 任何基于「对端健康 = 连接健康」的假设(连接池的
isValid()、负载均衡器的 L4 探活)在这个场景下全部失效——探活包走的是新路由,业务包走的是旧路由,两条路径的生死状态是独立的。
这和 NAT idle 超时剪断假连接是同一类病(中间设备静默改变会话状态)的不同发作窗口:idle 超时是「空闲期被清」,路由翻转是「活跃期被清」。前者社区已有好文章,但翻转型有一个它没有的特征:死亡发生在流量正忙的时候,且 RST 源 IP 会骗人。
三、具体做法:三条命令 + 一次时间线对齐
1) 抓 RST 并统计「源 IP 分布」——判断 RST 是谁发的
# 客户端侧(业务机,Linux 5.15),抓 30 分钟,只留 RST
tcpdump -i eth0 -nn -s0 'tcp[tcpflags] & tcp-rst != 0' -w /tmp/rst.cap
# 统计 RST 的源 IP Top(这是本文最关键的一步)
tcpdump -nn -r /tmp/rst.cap | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
# 我们当时的输出:
# 287 10.88.16.1 ← NAT 网关接口 IP(不是对端!)
# 12 10.30.12.42 ← 风控服务真实 IP(对端主动关,属正常)
判据:如果 RST 源 IP 明显集中且不是对端 IP,基本可以锁定「路径中间设备代答 RST」。这一步 5 分钟内能做完,且能直接回答「是不是网络杀的」。
2) 对齐 BGP 翻转时间戳与 RST 时间戳
# 路由器侧:导出翻转事件时间(IOS-XE 的 syslog)
show logging | include %BGP-5-NTFCHANGE
# 客户端侧:RST 包的时间戳
tcpdump -nn -ttt -r /tmp/rst.cap | head -30
# 输出示例(ttt 显示与上一包的间隔):
# 0.000000 IP 10.88.16.1 > 10.10.3.7: Flags [R]
# 0.004120 IP 10.88.16.1 > 10.10.3.9: Flags [R]
# 11.77231 IP 10.88.16.1 > 10.10.3.21: Flags [R]
我们把 92 次翻转的 syslog 时间戳和 287 个 RST 的时间戳做了对齐:271 个 RST 落在「翻转后 0–8 秒」窗口内,与链路恢复→流量回切→NAT 半失效表项被命中的时序完全吻合。剩下 16 个散落在翻转间隔之外,事后确认是 NAT 表项老化(60s)导致的第二批剪断。
对齐结论的写法(可直接抄进故障报告):
RST 源 IP 10.88.16.1(NAT 网关)占 96.5%;271/287 个 RST 落在 BGP 翻转后 8s 内;NAT 会话老化 60s > 翻转间隔 5–12s,表项处于「正向在、反向丢」的半失效态,代答 RST。
3) 客户端侧的持续观测(把「30 秒后才知道」压到「1 秒内知道」)
# 实时看本机的连接重置速率(每秒增量)
watch -n1 "nstat -az | grep -E 'TCPAbortOnRST|TCPRcvRstrt|ResetDrop'"
# 我们当时的数据:21:40–21:54 窗口内 TCPAbortOnRST 增量 340,
# 平时基线 15 分钟 < 5。这条曲线本身就是「路由翻转」的业务侧影子。
再配一条应用侧的指标:gRPC channel 重建次数(每重建一次 +1)。这两条曲线的相关性,就是「网络事件 → 业务影响」的因果证据链,比任何单侧的监控都硬。
4) 防御清单(按改动量排序)
| 层 | 动作 | 备注 |
|---|---|---|
| 网络 | 上联 BGP dampening + BFD(把 5–12s 的翻转间隔拉长到 60s+) | 治本,减少「半失效窗口」的命中次数;社区已有 dampening 实战(见引用) |
| 网络 | 推动运营商把 NAT 会话老化从 60s 调到 ≥ 120s(或翻转期间不老化) | 需要工单,但改一行配置能消掉 96% 的 RST |
| 应用 | gRPC channel 建连加 retry_policy,RST 类错误退避 500ms 起、指数到 8s,而不是立即重试 | 见第四节的坑:立即重试会打爆对端 |
| 应用 | 连接池借出时做一次 PING(gRPC 的 Health Check 或 TCP SO_KEEPALIVE 探测),失败即剔除重建 | isValid() 在这类场景不可信,必须发探测包 |
| 可观测 | 把 TCPAbortOnRST 增量和「channel 重建次数」做成一对联动告警 | 基线 15min<5 → 阈值 15min>30 触发 P2 |
四、三个踩过的坑 / 反直觉点
坑 1:立即重试是负收益。 我们最初的 gRPC 重试策略是「失败立即重试,最多 3 次」。路由翻转期间,340 条连接 1 分钟内全死,重试风暴让对端(风控)的 SYN 接收队列瞬时 +50%,触发了它侧的 tcp_syncookies 丢弃——重试反而制造了新的丢包。改成退避重试(500ms 起)后,对端 SYN 队列峰值降到原来的 1/8,重试成功率从 61% 升到 94%。
坑 2:isValid() / L4 探活全绿 ≠ 连接健康。 探活包走新路由,业务包走旧路由,两条路径的会话状态是独立的。我们当时看到风控侧健康检查 100% 通过、客户端 ss -ti 里连接状态全部 ESTABLISHED、cwnd 正常——所有本地视角的「连接活着」判断在这个场景下都是错的。唯一可靠的信号是发一个真实数据帧看回包。
坑 3:RST 和 timeout 的比例是诊断线索,不是噪音。 我们 340 条死亡连接里,287 条 RST(84%)、53 条 timeout(16%)。如果反过来(timeout 占大多数),那更可能是「链路单向故障」(包发出去了回不来),而不是「NAT 代答 RST」。RST 占比高 → 中间设备主动杀;timeout 占比高 → 链路单向或 NAT 静默丢。这个比例本身就是一个 30 秒能给出的根因分类器。
五、结论与可复用清单
核心结论:路由翻转对业务的影响,不在「丢包率」这个数字里,而在「在途长连接被中间设备无差别重置」这个事件里。你的可观测性必须站在长连接这一侧,而不是链路侧或下游服务侧——这三侧的指标在翻转窗口内可以全部正常,业务却在雪崩。
可复用清单(按 5 分钟内能做完排序):
tcpdump抓 RST + 统计源 IP 分布 → 判断「谁杀的」(5 min);nstat -az | grep TCPAbortOnRST的增量曲线 → 建立业务侧的影子指标(1 min);- BGP syslog 时间戳 vs RST 时间戳对齐 → 写进故障报告的因果链(10 min);
- RST/timeout 比例 → 30 秒根因分类(RST 高=中间设备杀,timeout 高=单向链路);
- 重试策略从「立即」改成「退避 500ms 起」→ 避免重试风暴制造次生丢包(改一行配置);
- 推动网络侧 dampening + NAT 老化时间 ≥ 120s → 治本(需要工单)。
一句话:链路侧的「抖动」是网络工程师的 KPI,业务侧的「长连接雪崩」才是 SRE 的 KPI——两者之间隔着一个 NAT 会话表,而你的监控里没有任何一个探针站在那个表旁边。