SSH 能连、git clone 却卡死:云服务器 MTU 黑洞排查实录
现象:握手成功,数据卡死
上周把一台 Ubuntu 22.04 的 Flexus 云服务器接到客户机房(走一条 IPsec 隧道),之后出现了一组"看着像三个 bug、其实是一个 bug"的故障:
- SSH 能登录,但
scp传一个 3MB 的 jar 卡在 0%,Ctrl+C之后连接还活着; - 服务器上
git clone https://...一直停在Receiving objects: 0%; docker pull nginx拉到一半报unexpected EOF,重试几次偶尔能成功。
而 ping、curl 小接口、apt update 全都正常。运维最容易在这一步误判成"网络抖动"或"对端服务挂了",于是开始换 DNS、换镜像源、重装 docker —— 都没用。真正的原因是 MTU 黑洞(PMTUD blackhole):小包能过,大包被静默丢弃。
为什么"小包好、大包坏"
TCP 建连只需要几十字节的包,所以三次握手一定成功;一旦开始传数据,发送端按自己网卡的 MTU(1500)把数据切成 1500 字节的包,而中间链路实际只能过 1450 字节。正常流程是中间设备回一个 ICMP Destination Unreachable / Fragmentation Needed (type 3 code 4),告诉发送端"把 MSS 降到 1430",发送端收到后自动调小。
问题在于:云安全组和大多数 NAT 设备默认不放通 ICMP,这个"请调小"的通知被丢掉了。发送端不知道要降,继续发 1500 的大包,收端全部丢弃且不回 ACK —— 于是表现为"连上了但一个字节都传不动",ss -ti 里能看到大量重传。
第一步:先量化,别猜
用 ping 的禁止分片模式二分找真实 MTU(1500 - 20 IP 头 - 8 ICMP 头 = 1472 是理论上限):
# 探测能通过的最大 payload;卡住 = 被丢
for s in 1472 1442 1400 1380 1300; do
if ping -M do -s $s -c 2 -W 1 10.20.0.5 >/dev/null 2>&1; then
echo "payload $s OK -> MTU $((s+28))"
else
echo "payload $s FAIL"
fi
done
这次的结果是 1472 不通、1400 通,说明链路 MTU 落在 1428~1472 之间,再用 tracepath 定位到隧道出口:
tracepath -n 10.20.0.5 # 会直接打印 "pmtu 1450" 和 "async" 标记
ip link show eth0 | grep mtu # 本机仍写着 1500,问题就在这
再看内核层的重传证据:
ss -tin dst 10.20.0.5 # retrans 持续增长、mss 仍是 1460
netstat -s | grep -i retrans
怎么区分 MTU 黑洞和普通丢包
这两者现象很像,但处置完全不同。三个判别点:
# 1. 抓一次真实传输,看 SYN 里的 MSS
tcpdump -ni eth0 -c 20 'host 10.20.0.5 and tcp[tcpflags] & tcp-syn != 0'
# MSS 仍写 1460 = 发送端还以为是 1500 的链路
# 2. 看重传形态:整段被丢 vs 零散丢包
ss -tin dst 10.20.0.5
# rtt:0.8/1.1 retrans:0/42 reordering:0
# 42 次重传、吞吐约 0 = 典型的整段丢弃
# 3. 小文件正常、大文件归零,基本可以定案
curl -s -o /dev/null -w '%{speed_download}\n' https://10.20.0.5/1k.bin # 正常
curl -s -o /dev/null -w '%{speed_download}\n' https://10.20.0.5/2m.bin # 掉到几 KB/s
普通丢包的 retrans 一般是个位数百分比、吞吐仍在;MTU 黑洞则是「小文件秒回、大文件几乎零进度」,retrans 一路涨到连接超时。带宽不足的表现是稳定但慢,黑洞是先快后完全停住。
第二步:四个真实根因
按踩坑顺序:
- 隧道 / VPC 对等的 MTU 不是 1500:IPsec、GRE、VXLAN 封装要额外 40~100 字节,客户侧隧道是 1450,本机还是 1500。
- 容器网络又叠了一层:
docker0默认 1500,宿主机调成 1450 后容器内仍是 1500,于是"宿主机能 clone、容器里不能"。 - ICMP 被安全组吞掉:安全组只放通了 22/80/443,ICMP 全拒,PMTUD 从机制上就不可能工作。
- NAT / SLB 不回 ICMP 也不回 RST:最坑的一种,
curl会一直挂着直到超时,看起来像"对端应用 hang 住"。
第三步:修,从临时到持久
临时验证(重启失效,先确认有效再固化):
sudo ip link set dev eth0 mtu 1450
# 立刻复测
ping -M do -s 1442 -c 2 10.20.0.5 && echo OK
持久化(Ubuntu 22.04 用 netplan):
# /etc/netplan/50-cloud-init.yaml
network:
version: 2
ethernets:
eth0:
mtu: 1450
sudo netplan apply
如果不想全网降 MTU,更优雅的做法是让网关做 MSS clamp,只影响需要的那条连接:
# 在路由器/网关上把经过该隧道 SYN 的 MSS 按 PMTU 改写
sudo iptables -t mangle -A FORWARD -o tun0 -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
# 或者硬编码(1450 - 40 = 1410)
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --set-mss 1410
再打开内核的 PMTU 探测兜底(尽力而为,不是万灵药):
# /etc/sysctl.d/99-mtu.conf
# 0=关闭 1=仅黑盒时探测 2=总是探测
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_base_mss = 1024
sudo sysctl --system
Docker 要单独给,它不继承宿主机的 link mtu:
// /etc/docker/daemon.json
{ "mtu": 1450 }
sudo systemctl restart docker # 会让容器重启,生产上排窗口
云上还要连带改三处
只改主机 MTU 往往不生效,云侧还有三个地方会把大包重新放进来:
- 安全组放通 ICMP 的必要类型。不用全开,只放开分片通知这一条就够了(控制台选「ICMP」协议、类型 3、代码 4;iptables 对应
--icmp-type 3/4)。这是从根上修 PMTUD 的方式。 - 弹性公网 IP / SLB 侧的 MTU。经 SLB 转发的流量如果 SLB 内部按 1500 处理,后端调到 1450 也没用,要按厂商文档把后端链路一起降。
- 对端的 MTU。隧道两端必须一致:单边改成 1450、另一边还是 1500,只会把黑洞推到反方向,表现成「我能拉你的文件、你拉不了我的」。
容器侧复现一次,比改完再猜快得多:
# 宿主机 vs 容器各打一个大包,结果对比就能定位到哪一层
docker run --rm --network host alpine ping -M do -s 1442 -c 2 10.20.0.5
docker run --rm alpine ping -M do -s 1442 -c 2 10.20.0.5
# 第一条通、第二条不通 => CNI 网络 MTU 没跟着改,去改 daemon.json
踩坑清单
ip link set mtu只改本机,对端和中间设备不会跟着变;只改一边常常"好了 5 分钟又坏",因为已协商的 MSS 不会重算,老连接必须断掉重连。tcp_mtu_probing=1只对新连接生效,且要等内核超时重传后才开始探测;已经卡死的scp不会自愈,别拿它当实时修复手段。- 安全组别一刀切关 ICMP:只放通
type 3 code 4就够了,抗暴力破解靠限速而不是封 ICMP。 - 容器里
ip link看到的 MTU 由 CNI 决定,改宿主机无效,要改 CNI 配置或daemon.json。 - IPv6 没有分片协商的退路,最小 MTU 固定 1280,双栈混跑时更容易踩。
ping通只能证明小包通,永远不能证明链路健康 —— 这是本次误判耗时最长的原因。
复盘:为什么绕了三个小时
回头看,时间几乎全花在「按业务模块分组」上,而不是「按包的大小分组」:
| 耗时 | 做的事 | 结果 |
|---|---|---|
| 40 分钟 | 换 pip / npm 镜像源、换 docker registry | 无效,因为问题不在源 |
| 35 分钟 | 查对端服务日志、重启 gunicorn | 无效,请求根本没到应用层 |
| 20 分钟 | 怀疑 DNS 与证书,重签了一张证书 | 无效,curl -v 里 TLS 握手其实成功了 |
| 10 分钟 | ping -M do 二分 + tracepath |
一次定位到 1450 |
| 5 分钟 | ip link set mtu 1450 + netplan 持久化 |
全部恢复 |
如果第一条就用「小包通、大包不通」这个特征去查,后面 95 分钟的弯路都可以省掉。先给现象分组,再给模块分组,这条经验比 MTU 本身更值钱。
收尾:把它变成开局检查项
新机器、新隧道、新专线接入后,先跑一遍:
# 5 秒判断是否存在 MTU 黑洞
target=10.20.0.5
ping -M do -s 1472 -c 2 -W 1 "$target" >/dev/null 2>&1 \
&& echo "MTU 1500 OK" \
|| { echo "疑似 MTU 黑洞,实测:"; tracepath -n "$target" | head -5; }
再加一条重传率告警(netstat -s 里 segments retransmited / segments sent out 持续 > 1% 就该看),比等业务报障快得多。
三个看起来毫不相干的故障,最后被同一个 MTU 数值修好 —— 这也是这次最大的收获:排查时要按"包的大小"给现象分组,而不是按"业务模块"分组。
- 点赞
- 收藏
- 关注作者
评论(0)