2026 年 1 月一个周三的下午,我接到产品同事的微信:"分析平台的报表导出又又又失败了,客户那边都开始问'你们是不是又出 bug 了'。"这是一周内的第七次反馈。我打开 Grafana 看 analytics-service 的指标,RPS 平稳、错误率 0.4%(看着不高)、平均延迟 35ms,所有黄金信号都在正常范围。但下沉到 P99 延迟一看,数字直接刺眼——59800ms。正好卡在客户端 60 秒 timeout 之前的极限值,几乎是个常数。这种"P99 永远等于 timeout 上限"的曲线,网络人一眼就知道——packet 被某处吞了,客户端在等永远不会到的响应,直到自己放弃。
4 天后我们才把这个故障的根因彻底拆出来:跨 VPC VPN 链路的 MTU 是 1400 字节(不是大家以为的 1500),叠加业务自建 stateful firewall 把所有 ICMP "Fragmentation Needed" 包 drop 掉,让 Path MTU Discovery 完全失效——发送方不知道自己的包"过大",IP 路由器 silently 丢弃大于 MTU 的包,TCP 重传又是同样大小,卡住直到 timeout。这是一个所有 IP 网络教科书前 50 页都会讲的经典"MTU 黑洞"现象,但在现代云网络栈把这套机制层层包裹之后,坑藏得比想象的深,排查起来一点都不"经典"。
这篇是完整复盘:走了 4 天,绕过了"VPN 抖动"、"DNS 慢"、"K8s ingress bug"三个错方向,最后在 tcpdump + ping -M do + 防火墙规则审计 三件套下定位到 ICMP fragmentation-needed 被 stateful firewall drop。涵盖 IP MTU 机制、PMTUD 工作原理、ICMP 在云网络的角色、tcpdump + ping -M do 排查方法、MSS clamping 修法、Calico/Cilium MTU 配置,以及最后落地的《跨 VPC / 跨网络网络配置纪律》8 条。如果你的团队有任何形式的跨网络(VPN / 专线 / VPC peering / 多云互联 / overlay)调用,这篇能帮你避开一个隐蔽但破坏力极大的雷。
背景:这套"看起来很普通"的跨 VPC 通信
| 维度 | 数值 |
|---|---|
| 架构 | 主业务 VPC(K8s,Calico CNI)+ 数据分析 VPC(K8s,Cilium CNI),通过 site-to-site VPN 互联 |
| VPN 实现 | 云厂商 IPsec VPN gateway,IKEv2 + AES-256-GCM |
| 中间防火墙 | 云厂商安全组(出 + 入)+ 业务自建 stateful firewall(VM 实现,某商业产品) |
| RPS | main → analytics 约 850 RPS |
| 事故现象 | 大请求(POST body > 2KB)偶发 timeout,小请求正常 |
| 复现率 | 约 20%-40%(随机) |
| 影响 | 分析报表导出偶发 500,客户每周收到约 30 个"导出失败"工单 |
| 故障代价 | 4 天 3 个工程师全力投入 + 30+ 工单 + 1 个客户切换备用方案 |
这套架构是去年底新搭的,主业务一直在 main VPC,数据分析独立放到 analytics VPC 是为了把"高 IO 重负载"的 Spark / ClickHouse 隔离开,避免影响主业务。跨 VPC 流量量级不大,设计阶段我们觉得"挂个 VPN 就行了",VPN gateway 用云厂商默认配置一键启动,中间防火墙沿用了主业务的"严格安全"规则。这种"挂个 VPN 就行"的轻视态度,后来证明是事故的根源——跨网络通信从来都不是"挂个 VPN 就行"这么简单,它涉及到完整的 IP 协议栈协作、ICMP 反馈机制、防火墙策略的隐性影响,任何一环出问题都会以"莫名其妙的偶发故障"的形式暴露。
事故时间线:4 天里走过的路
| 时刻 | 事件 | 关键判断 |
|---|---|---|
| Day 1 全天 | 收到客户反馈"导出失败",初步以为是"VPN 偶发抖动" | 方向偏 |
| Day 2 上午 | 复现成功:用 curl 发 1KB/5KB/10KB POST,小的 OK,大的 hang 直到 60s timeout | 方向锁定 |
| Day 2 下午 | "小 packet 能通,大 packet 卡死"——经典 MTU 问题 | MTU 嫌疑 |
| Day 3 上午 | 两端 tcpdump 抓包,看到 SYN 正常,大 PSH 包发出后再也收不到 ACK | 确认丢包 |
| Day 3 下午 | 用 ping -M do -s 1472 测试,发现 VPN 链路 MTU 是 1400 不是 1500 | MTU 数值确定 |
| Day 4 上午 | 查防火墙规则:业务自建 stateful firewall drop 所有非 echo 的 ICMP | 根因 1 锁定 |
| Day 4 中午 | 验证 PMTUD 失效:发大包 60 秒,发送方完全没收到任何 ICMP type 3 code 4 | 根因 2 锁定 |
| Day 4 下午 | 修法 1:防火墙放行 ICMP type 3 code 4。修法 2:K8s 出口加 MSS clamping | 修复完成 |
| Day 4 晚 | 压测验证,5KB-50KB 请求成功率从 43%-78% 全部回到 100% | 结案 |
第一反应:"VPN 不稳定吧"
跨网络调用偶发失败,任何 SRE 第一反应都是"网络抖动"。我们也走了这条弯路:挂了 mtr 跑 30 分钟,中间路径稳定,丢包率 0.1%(完全可以忽略),pingtime 也稳定。"VPN 抖动"这个假设直接被打死了。然后看应用层日志,看到几个有意思的特征:
- SUCCESS 的请求平均 30ms 返回,P99 也才 80ms
- FAIL 的请求全部都是 60s timeout,没有任何 connection refused / reset / 500
- FAIL 的请求 payload size 平均 4.2KB,SUCCESS 的请求平均 1.8KB
- FAIL 时 TCP 半连接(已经 SYN-ACK 成功),只是后续大包没传过去
"timeout 而不是 RST / refused"——这意味着连接建立成功,数据传输阶段才丢的,排除"DNS 慢"、"连接拒绝"等可能。"大请求失败,小请求成功"——这两个特征叠加,直接指向网络层最经典的 MTU 问题。Day 2 中午我把这个判断发到群里时,网络组的老同学立刻回了一句:"99% MTU,你去测路径 MTU 就行。"事后看,如果 Day 1 就拉网络组一起看,可能 1 天就能定位——但我们当时还在"应用层排查 → 中间件排查 → 网络层排查"的传统逐层下沉路径上,没有第一时间把"网络专家"拉进来。跨团队协同的成本本身就是事故的一部分,这件事在事后复盘里被反复强调。
真凶 1:VPN 链路 MTU 比 VPC 内小,且是个"隐藏知识"
Ethernet 标准 MTU 是 1500 字节,IP packet 最大 1500。这是所有人在大学网络课学的"标准答案",所以 99% 的应用工程师在配 K8s / 网络组件时默认 MTU 是 1500。但 VPN 不是普通 Ethernet——它给每个 packet 加一层"封装"(IPsec ESP header / trailer / authentication tag),让实际可用 payload 变小。具体开销:
| 层 | 开销 | 说明 |
|---|---|---|
| 原始 Ethernet MTU | 1500 字节 | 物理网卡标准 |
| IP header | 20 字节 | 固定开销 |
| IPsec ESP header | 8 字节 | SPI + sequence |
| IPsec ESP IV | 8 - 16 字节 | AES 等加密算法的初始化向量 |
| IPsec ESP padding | 0 - 15 字节 | 对齐到 block size |
| IPsec ESP trailer + auth | 16 - 24 字节 | HMAC 之类的认证 tag |
| 额外 IP header(tunnel mode) | 20 字节 | 外层 IP 头 |
| 实际 IPsec tunnel MTU | ~ 1400-1438 字节 | 取决于加密算法 |
我们的云厂商 VPN 默认 MTU 是 1400(留了余量)。这意味着任何 > 1400 字节的 IP packet 经过 VPN 时必须被分片(IP fragmentation),或者被中间路由器返回 ICMP "Fragmentation Needed and DF bit set"(让发送方调小)。
验证 MTU 的命令是 ping -M do -s <size>,这是网络排查的"灵魂工具":
# 从 main VPC Pod, ping analytics VPC Pod, 设置不允许分片
# -M do 表示 don't fragment (DF=1)
# -s 是 ICMP payload 大小,加上 8 字节 ICMP header + 20 字节 IP header = 实际 packet size
# 第一次试 1472,加上 28 字节 header = 1500
$ ping -M do -s 1472 -c 3 10.20.30.45
PING 10.20.30.45 (10.20.30.45): 1472 data bytes
ping: sendto: Message too long # 本地内核就拒绝了,因为它知道出口 MTU
ping: sendto: Message too long
ping: sendto: Message too long
# 试 1372,加上 28 = 1400 = VPN MTU
$ ping -M do -s 1372 -c 3 10.20.30.45
PING 10.20.30.45 (10.20.30.45): 1372 data bytes
1380 bytes from 10.20.30.45: icmp_seq=0 ttl=63 time=2.43 ms
1380 bytes from 10.20.30.45: icmp_seq=1 ttl=63 time=2.41 ms
1380 bytes from 10.20.30.45: icmp_seq=2 ttl=63 time=2.45 ms
# 二分逼近最大 MTU
$ ping -M do -s 1400 -c 3 10.20.30.45
ping: sendto: Message too long
$ ping -M do -s 1373 -c 3 10.20.30.45
ping: sendto: Message too long
# 最终确认:1372 是最大 ICMP payload,1400 是路径 MTU
这种"二分逼近"的方式是排查路径 MTU 的标准动作。注意 -M do 是 Linux 特有的选项,设置 IP 头的 DF bit,告诉沿途路由器"不要分片我"。macOS 上等价的是 ping -D。这条命令应该写进每个 SRE 的肌肉记忆,跨网络服务上线前先跑一遍,30 秒就能避开"几天后才暴露"的事故。
真凶 2:Path MTU Discovery 被 ICMP 拦截 → 黑洞
正常情况下,TCP 有Path MTU Discovery (PMTUD) 机制来自动适应路径 MTU,完全不需要应用层关心:
- 发送方在 TCP 头里设 DF(Don't Fragment)bit,告诉沿途路由器"我不允许你分片,如果太大就告诉我"
- 沿途遇到 MTU 比 packet 小的路由器,路由器丢弃 packet 并返回 ICMP type 3 code 4("Fragmentation Needed and DF bit set")
- ICMP 包里告诉发送方:你的 packet 太大,我这段 MTU 是 N,你下次发 < N 的
- 发送方收到 ICMP,记录到本地 PMTU 缓存(
ip route get能看到),以后给这个目的 IP 发包都用更小的 MTU - 这个 PMTU 缓存有 10 分钟 TTL,过期重新探测
这套机制设计得非常优雅,但它整个流程依赖 ICMP type 3 code 4 能从中间路由器返回到发送方。问题是现代云网络很多 stateful firewall 默认 drop 大部分 ICMP——这往往是"security best practice"的产物:某个 5 年前的安全合规要求说"减少攻击面",于是有人画了一道"drop all ICMP"的防火墙规则,从此再也没人敢动。结果:
- 大 packet 发出(DF=1),中间路由器(VPN gateway 出口)丢弃 + 发 ICMP fragmentation needed 回发送方
- ICMP 包到达防火墙,被无差别 drop
- 发送方完全收不到任何反馈,只看到自己的 packet 没回 ACK,TCP 协议栈以为是丢包,触发重传(还是同样大小的大 packet)
- 重传 → 再次被默默丢弃 → 再重传 → ... 无限循环直到 TCP timeout 60 秒
- 应用层看到的就是"请求 hang 住 60 秒后报 timeout"
这就是"MTU 黑洞"(MTU Black Hole)——packet silently 丢失,发送方无法学习路径 MTU,完全是个看不见的坑。验证 ICMP 是否被拦截的方法,在发送方端跑 tcpdump:
# 在发送方主机(main Pod 所在 node)抓所有 ICMP unreachable
tcpdump -i any -nn 'icmp[icmptype] == icmp-unreach' -vvv
# 期望:发大包时应该捕获 icmp type 3 code 4 (fragmentation-needed)
# 实际:30 分钟,完全没有任何 ICMP type 3 包到达
# 同时在 VPN gateway 出口(中间节点)抓:
tcpdump -i vpn-tunnel -nn 'icmp' -vvv
# 这里能看到 gateway 发出去的 ICMP fragmentation-needed
# 说明 gateway 知道要发,但发出去的 ICMP 被某段防火墙 drop 了
这种"中间节点发出 ICMP,但接收方收不到"的对比,直接验证了 ICMP 被某段中间设备 drop。这是 MTU 黑洞排查的关键证据,任何 PMTUD 问题都该跑这一组 tcpdump。
真凶 3:具体是哪一段 firewall 拦了 ICMP
云网络复杂,从 main Pod 到 analytics Pod 的物理路径经过至少 6 段,每段都可能有 firewall 规则:
- main Pod → main node → Calico iptables → main VPC subnet
- main VPC subnet → VPC route table → main VPC 安全组(出方向)
- main VPC → VPN gateway(IPsec encap)
- VPN tunnel → analytics VPN gateway(IPsec decap)
- analytics VPC subnet → 业务自建 stateful firewall(VM)
- analytics 安全组 → analytics node → Cilium → analytics Pod
我们一段一段排查,用的方法是"在每段两侧抓 ICMP type 3,看哪段 ICMP 进得来出不去"。最终结果:
| 段 | ICMP type 3 是否通过 |
|---|---|
| main VPC 安全组(出方向) | ✅ allow all egress |
| main VPN gateway 入口 | ✅ allow ICMP |
| VPN tunnel 内 | ✅ ICMP 可以被 encap 走 IPsec |
| analytics VPN gateway 出口 | ✅ allow ICMP |
| analytics 业务自建 stateful firewall | ❌ drop all ICMP except echo-request/reply |
| analytics 安全组 | ✅ allow all |
| Cilium eBPF 程序 | ✅ allow all |
找到了!业务自建的 stateful firewall(用了某商业产品,名字我就不点了)只放 ICMP echo(ping),drop 其他所有 ICMP type。这是个保守的"安全"配置——5 年前的某个合规审计要求"减少攻击面",于是配置成"除了 ping 都禁",从此没人动过。代价就是PMTUD 完全失效,但因为同一 VPC 内通信用的全是 1500 MTU 无需 PMTUD,这个问题在跨 VPC 通信启用之前一直没暴露。这是个"潜伏 5 年的炸弹",直到我们启用 VPN 才被引爆。
这件事让我重新思考"防火墙规则的复利效应":一个 5 年前的"保守"规则,在当时的网络拓扑下是合理的(没有跨 VPC 流量),但拓扑变了之后它就成了定时炸弹。没有人会在加新拓扑时回头审视防火墙规则,因为防火墙规则太多、太杂、太"安全"。这种"配置漂移"和"拓扑漂移"之间的张力,是大型组织网络故障的隐性来源。
因果链:把整个故障串起来
这张图把 6 个关键环节串起来,因果链就明晰了。每个环节看起来都"合理":VPN MTU 比 VPC MTU 小是 IPsec 的物理限制;路由器丢包 + 发 ICMP 是 PMTUD 的标准行为;防火墙严格 ICMP 策略是"安全 best practice"。但它们组合在一起就是MTU 黑洞——单个环节都不"错",合在一起就是事故。这种"合成错误"在复杂系统里是常态,排查不能只看单点。
修法 1:防火墙放行 ICMP fragmentation-needed
最干净的修法是让 ICMP type 3 code 4 (fragmentation-needed)能通过。这个 ICMP 不能用来攻击(它只能"告诉发送方包太大"),放行完全安全。
# iptables 规则:精确放行 fragmentation-needed,其他 ICMP type 3 也建议放行
iptables -A FORWARD -p icmp --icmp-type fragmentation-needed -j ACCEPT
iptables -A INPUT -p icmp --icmp-type fragmentation-needed -j ACCEPT
# 更完整版:同时放行 type 11 (time-exceeded, traceroute 必须)
iptables -A FORWARD -p icmp --icmp-type 3 -j ACCEPT # all unreachable codes
iptables -A FORWARD -p icmp --icmp-type 11 -j ACCEPT # time-exceeded
iptables -A FORWARD -p icmp --icmp-type 12 -j ACCEPT # parameter-problem
# 持久化(Debian/Ubuntu)
netfilter-persistent save
对云厂商防火墙,在 console 里加规则:Protocol = ICMP, Type = 3, Code = 4, Source = any, Action = Allow。商业 stateful firewall 在它的 admin UI 里类似配置。和厂商沟通明确"ICMP type 3 不是攻击向量,必须放行"——这一步在我们公司花了 2 小时,因为需要走变更审批,但实际防火墙规则改动只是 1 分钟。
放行 ICMP 之后立刻验证:tcpdump 在发送方端再次抓 ICMP unreachable,这次能看到 fragmentation-needed 包了:
$ tcpdump -i any -nn 'icmp[icmptype] == icmp-unreach' -vvv
14:32:01 IP 10.40.50.1 > 10.10.20.30: ICMP 10.20.30.45 unreachable -
need to frag (mtu 1400), length 556
IP (tos 0x0, ttl 62, id 31415, offset 0, flags [DF], proto TCP (6),
length 1500)
10.10.20.30.45123 > 10.20.30.45.443: Flags [P.], cksum 0xabcd,
seq 12345:13845, ack 67890, win 65535, length 1500
看到 ICMP 包了,而且 packet 头里明确写着 "need to frag (mtu 1400)"——发送方现在知道路径 MTU 是 1400,会自动调整后续 segment size。
修法 2:TCP MSS Clamping(防御性兜底)
修法 1 依赖防火墙团队配合,但实际很多企业里跨团队改防火墙规则非常困难,可能要走数周审批。备选方案:在 K8s Pod 出口加 MSS clamping——让所有 TCP 连接的 MSS 自动调到安全值,完全不需要依赖 PMTUD 这个机制。即使 PMTUD 失效,大 packet 也不会被发出来。
原理:TCP 三次握手的 SYN 包里有 MSS(Maximum Segment Size)字段,接收方告诉发送方"我最大能收的 TCP 段是多大"。MSS clamping 是中间路由器在 SYN 包穿过时,把 SYN 里的 MSS 值"夹"到一个安全值,这样发送方从一开始就用小 segment,完全避免后续 PMTUD。
# 在 K8s node 上的 iptables 规则(通过 DaemonSet 部署到所有 node)
# --clamp-mss-to-pmtu 让 iptables 自动根据出口 MTU 计算 MSS
iptables -t mangle -A FORWARD \
-p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
# 或者显式 set MSS(更保险,不依赖路径 MTU 探测)
iptables -t mangle -A FORWARD \
-p tcp --tcp-flags SYN,RST SYN \
-o vpn-tunnel0 \
-j TCPMSS --set-mss 1360
# 1360 = 1400 (VPN MTU) - 20 (IP header) - 20 (TCP header)
# 注意 -o 限制出口网卡,避免影响其他 VPC 内通信
设置之后,所有出 VPN tunnel 的 TCP 连接都会被 clamp 到 MSS=1360,意味着发送方每个 TCP segment ≤ 1360 字节 payload,加上 40 字节 header 也才 1400,绝不超过 VPN MTU,不会触发分片或丢包。
K8s 用 Calico / Cilium 这类 CNI 的话,它们也内建 MTU 配置:
# Calico (Operator install)
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
name: default
spec:
calicoNetwork:
mtu: 1400 # 跟 VPN 一致
bgp: Enabled
ipPools:
- cidr: 10.10.0.0/16
encapsulation: IPIPCrossSubnet
---
# Cilium (helm values.yaml)
mtu: 1400
tunnel: vxlan
bpf:
masquerade: true
hostLegacyRouting: false
# 关键:Calico/Cilium 还可以做 MSS clamping
# 当 Pod 跨 node 通信经过 overlay,自动 clamp
bpf:
tcpMaxSegSize: 1360
这种 CNI 层的 MTU 配置是最干净的方案——它在 Pod 网卡层面就限制 MTU,Linux kernel 自然限制 packet size,根本不会发出超过 1400 的 packet。我们最终采用的是"修法 1 + 修法 2 + CNI MTU 配置"三层并存,任何一层失效都有兜底,这才是稳健的网络设计原则——defense in depth。
修法 3:用 Python 脚本自动化路径 MTU 探测
每条新跨网络链路上线前都跑 ping -M do 二分手测太麻烦,我们写了个 Python 脚本自动化:
#!/usr/bin/env python3
"""Path MTU 自动探测工具,二分逼近最大 packet size"""
import subprocess
import sys
from typing import Optional
def probe_mtu(target: str, low: int = 500, high: int = 1500,
timeout: float = 2.0) -> Optional[int]:
"""二分逼近能通过的最大 ICMP payload 大小,返回路径 MTU"""
def try_size(payload_size: int) -> bool:
# -M do 设置 DF=1,-s 是 payload,-c 1 发一个,-W 超时
cmd = ["ping", "-M", "do", "-s", str(payload_size),
"-c", "1", "-W", str(int(timeout)), target]
result = subprocess.run(cmd, capture_output=True, text=True)
# 返回码 0 = 通,1 = 不通(message too long 或 timeout)
return result.returncode == 0
# 先确认 low 一定通,high 一定不通
if not try_size(low):
print(f"[ERROR] even {low} byte payload fails - target unreachable?")
return None
if try_size(high):
# 1500 都通,说明本机直连 LAN
return high + 28 # +28 = ICMP header + IP header
# 二分
while low < high - 1:
mid = (low + high) // 2
if try_size(mid):
low = mid
else:
high = mid
print(f" probing... low={low} high={high}")
path_mtu = low + 28
print(f"[OK] path MTU to {target} = {path_mtu} byte")
return path_mtu
def main():
if len(sys.argv) < 2:
print("usage: mtu_probe.py <target_ip> [target2 ...]")
sys.exit(1)
results = {}
for tgt in sys.argv[1:]:
print(f"\n=== probing {tgt} ===")
mtu = probe_mtu(tgt)
results[tgt] = mtu
print("\n=== summary ===")
for tgt, mtu in results.items():
flag = "WARN" if (mtu and mtu < 1500) else "OK"
print(f"[{flag}] {tgt}: MTU={mtu}")
if mtu and mtu < 1500:
print(f" → recommended MSS clamping: {mtu - 40}")
if __name__ == "__main__":
main()
这个脚本作为我们"新网络链路上线 checklist"的一部分,任何新 VPC peering / VPN tunnel / 跨网络连接,都要先跑一遍 mtu_probe,把路径 MTU 录到 wiki,后续 CNI 配置依据这个值来。30 秒一条链路,能避开几天的事故。
性能 + 可靠性对比
| request size | 修复前成功率 | 修复前 P99 延迟 | 修复后成功率 | 修复后 P99 延迟 |
|---|---|---|---|---|
| 1 KB | 100% | 32ms | 100% | 30ms |
| 2 KB | 95% | 1180ms(慢路径) | 100% | 31ms |
| 5 KB | 78% | 59800ms | 100% | 32ms |
| 10 KB | 62% | 59800ms | 100% | 35ms |
| 50 KB | 43% | 59800ms | 100% | 52ms |
| 500 KB(分页) | 21% | 59800ms | 100% | 180ms |
从 43%-78% 全部回到 100%。同时我们做了横向 audit,跑 mtu_probe 把所有跨 VPC / 跨网络链路扫了一遍,发现还有 2 个跨网络链路有类似 MTU 不匹配(还没暴露但定时炸弹),一并 MSS clamp 兜底,避免下次踩。事故的真正价值不是"修这一个 bug",是借机把同类问题在整个组织范围内扫一遍,这种"批量扫雷"才是事故 ROI 最高的部分。
顺手做的几件长期收益的事
1. MTU 测试加进新环境上线 checklist
任何新 VPC / 新 VPN / 新 peering 连接,上线前必须用 mtu_probe 测路径 MTU,记录到 wiki。这个测试 30 秒,但能避开"几天后才暴露"的事故。Checklist 里强制填写"路径 MTU = ?"字段,空着不让 merge。
2. 防火墙规则审计制度
所有防火墙规则必须明确列出"哪些 ICMP type 允许"。我们的标准模板:
- echo-request / echo-reply(ping,type 8/0):允许(便于排查)
- fragmentation-needed(type 3 code 4):允许(PMTUD 必须)
- destination-unreachable other codes(type 3):允许(快速错误反馈)
- time-exceeded(type 11):允许(traceroute 必须)
- parameter-problem(type 12):允许
- echo-request flood rate limit:加 rate 限制,防 ping flood,但不要完全 drop
- 其他(redirect 等):默认禁止
这个模板已经成为我们组的"网络合规基线",所有新部署的防火墙都要按它配置。"减少攻击面"和"保留必要的协议反馈"不矛盾,关键是细化 ICMP type,而不是一刀切。
3. 监控大 packet 失败
加监控:HTTP 请求按 payload size 分桶(1KB/2KB/5KB/10KB/50KB+),统计每个桶的成功率和延迟。如果某个 size 桶成功率显著低于小 size 桶,可能是 MTU 问题。这个监控帮我们后续提前发现了 1 次类似问题(VPN 厂商升级,默认 MTU 从 1400 变成 1380,我们的 1360 MSS clamping 兜住了但 PMTUD 还是失效——监控数据让我们及时调整)。
立的《跨 VPC / 跨网络网络配置纪律》
- 任何跨网络链路上线必须做 MTU 测试,记录路径 MTU 值到 wiki。30 秒的成本,避免几天的事故。
- 所有防火墙必须放行 ICMP type 3(unreachable) + type 11(time-exceeded),这两类不是攻击向量,且 PMTUD / traceroute 必须依赖。
- K8s 集群网络 MTU 必须显式配置(Calico / Cilium / Flannel 都支持),不要用默认 1500(可能与底层 VPC / VPN 不匹配)。
- 对接外部 VPN / 专线 / Direct Connect / VPC peering 必须 MSS clamping 兜底,不依赖 PMTUD。defense in depth。
- 大 payload 请求(> 2KB)纳入测试套件,定期跑,防止 MTU 问题潜伏。这是早期发现的最便宜机制。
- 跨网络 RPC 应该用 stream / 分片机制(gRPC streaming、HTTP chunked transfer),不要靠"一个大 message"传大数据。
- 监控 TCP 重传率:大量重传 + 大 payload + 跨网络 = MTU 嫌疑。
- 新人 onboarding 必须过"网络分层 + ICMP + MTU"基础课,所有 SRE / backend 同学必须能解释 PMTUD 工作原理和 MTU 黑洞成因。
给读者的几条自查清单
- 跑
ping -M do -s 1472 <cross-network-target>,看能不能通。失败的话,你的网络路径 MTU < 1500。 - 抓 30 分钟 tcpdump 看有没有 ICMP type 3 code 4 包到达发送方。如果你的服务跨 VPC / VPN 通信但完全没这种 ICMP,可能有黑洞。
- 测大 payload 请求成功率,> 2KB request 失败率 > 5% 是 MTU 嫌疑。
- 看你 K8s CNI 配置,MTU 是不是默认 1500?如果底层 VPC 是 1500 没问题,如果有 VPN / overlay 网络,必须小于底层 MTU。
- 跨 VPC peering / VPN 链路的实际 MTU 是多少?云厂商默认 IPsec VPN MTU 通常 1400-1436,具体看加密算法。
- 跨网络服务调用是不是用了 gRPC streaming / 分页?如果是大 message 一次性发,改 streaming 更稳。
- 防火墙规则审计:ICMP 全部 drop 是错误配置,要放行 type 3 + type 11。
- 你的应用层 timeout 是不是设得很长(60s+)?如果有 MTU 黑洞,用户体验就是"卡 60 秒后报错",体验极差。
更深一层:为什么这种"经典问题"会潜伏到 2026 年
MTU / PMTUD / MTU 黑洞这套知识在 IP 网络教科书里讲了 30 年,RFC 1191(PMTUD 标准)是 1990 年发布的。理论上任何"工作 5 年以上的网络工程师"都该熟悉。但现实是:
- 云时代抽象太厚,90% 的工程师不再接触 IP 层。Pod 调 Pod,看起来跟"两个进程通信"一样简单,根本意识不到中间走过的网络层级。
- 开发人员的知识结构跟网络人员的知识结构脱节。开发知道 HTTP 但不知道 IP,网络人员知道 IP 但不写代码。跨团队故障定位时,各自只能看自己那一层。
- 云厂商的默认配置遮盖了细节。VPN MTU=1400 这件事在云厂商文档里有,但藏在 FAQ 第 23 页,没人会主动看。
- K8s / CNI 的封装让 MTU 配置变成"魔法数字"。Calico/Cilium 配 1500 还是 1400?大多数人抄抄文档,没有理解背后的物理含义。
这次复盘让团队明白:云时代不是让网络知识变得"不重要",而是让它变得"更隐蔽"——出问题时,定位的难度比物理网络时代更高,因为多了好几层抽象。我们后来给团队组织了一次"网络分层 + ICMP + MTU"内部分享,把 IP fragmentation、PMTUD、IPsec encap overhead、CNI MTU 选型这几个核心概念过了一遍,2 小时分享,后续 6 个月规避了至少 3 次潜在的类似事故。认知深度本身就是稀缺的工程资产。
另一个心得:"为了安全 drop 全部 ICMP"是流行但代价大的反模式
安全团队往往按"减少攻击面"思路 drop 所有 ICMP,这种思路在 1990 年代某些攻击向量盛行时是合理的(Smurf 攻击、ping flood DoS),但在 2026 年的网络环境里,完全 drop ICMP 让网络变成"黑盒":
- 出问题难定位:traceroute、ping、MTR 全部失效,只能盲猜
- 性能问题难发现:PMTUD 失效导致的 MTU 黑洞、TCP 慢启动失败等
- 负面影响远大于安全收益:实际能用 ICMP 攻击的场景少,但 ICMP 反馈机制是 IP 协议栈正常工作的一部分
正确做法是按 ICMP type 细分:
- 明确"信息性 ICMP"(允许):type 3 unreachable、type 11 time-exceeded、type 12 parameter-problem
- 明确"诊断 ICMP"(允许但 rate-limit):type 8/0 echo,加 rate 限制防 flood
- 明确"攻击向量 ICMP"(可以 drop):type 5 redirect、type 9/10 router advertisement(防 router spoofing)
这种"精细化 ICMP 策略"既保留必要的网络反馈,又限制了真实的攻击向量。下次有人在你的网络配置里看到"ICMP all blocked",问他三个问题:
- PMTUD 怎么办?跨网络通信路径 MTU 不同怎么自适应?
- traceroute 怎么办?网络故障定位用什么?
- 具体哪种 ICMP 是攻击向量,你能列出来吗?
大多数人三个问题都答不上来。这就是最该改的地方——"安全"不能成为"懒"的借口,精细化的策略才是真正的安全。
总结
这次故障让我对"网络分层"有了更深的认知:我们在应用层看到的"偶发 timeout",根因可能藏在 IP / TCP / VPN 加密 / 防火墙规则等好几层下面。每一层都有自己的"默认行为"和"failure mode",合在一起的复杂度超出大多数应用工程师的直觉。云时代的"抽象"是一把双刃剑——它让简单的事变更简单,也让复杂的事变更难定位。
事故落幕后我反复思考一件事:这次的根因——MTU 黑洞——在网络教科书前 50 页就讲过,RFC 1191 是 1990 年的标准,所有这些都是 30+ 年的"老知识"。但我们组没人能在 Day 1 就指向它,因为我们这一代开发人员长在 K8s + 云的环境里,IP 层的细节从来都是"运维的事"。当 K8s + Calico + Cilium + IPsec + 安全组层层包裹之后,真正的问题需要"穿透 5 层抽象"才能看到,这件事在 2026 年比 1996 年困难得多。这不是说云时代的工程师能力低,而是说云时代的"广度"是以"深度"为代价的——我们能用更多组件,但每个组件的深度都比前一代浅。
如果你的服务跨 VPC / VPN / 多云 / 跨网络通信,且偶发出现"莫名其妙的 timeout 大请求失败",请把这篇文章发给负责网络的同事,大概率能在 1 小时内定位。"网络分层"是基础设施工程师的核心能力之一,任何打算长期写跨网络服务的工程师,都应该把 IP 层 + TCP 层 + ICMP 反馈机制 + MTU/PMTUD 这套知识硬学一遍,这不是"网络专家"的专属知识,是所有现代后端工程师的基础设施常识。云没有让网络变得无足轻重,只是让它变得更隐蔽。
最后给所有 backend 同学:当你写一个"跨 VPC RPC 调用"的代码时,请脑子里过一遍这个 packet 会经过几层网络、每层 MTU 多少、每层会不会 drop 你的 packet、ICMP 反馈能不能通。30 秒的思考,可能省下未来 4 天的排查。下次有人说"跨 VPC 通信就是配个 VPN",你可以把这篇甩给他读一遍。
—— 别看了 · 2026