SSH 能连、git clone 却卡死:云服务器 MTU 黑洞排查实录

举报
yd_238266496 发表于 2026/11/04 13:15:37 2026/11/04
【摘要】 现象:握手成功,数据卡死上周把一台 Ubuntu 22.04 的 Flexus 云服务器接到客户机房(走一条 IPsec 隧道),之后出现了一组"看着像三个 bug、其实是一个 bug"的故障:SSH 能登录,但 scp 传一个 3MB 的 jar 卡在 0%,Ctrl+C 之后连接还活着;服务器上 git clone https://... 一直停在 Receiving objects:...

现象:握手成功,数据卡死

上周把一台 Ubuntu 22.04 的 Flexus 云服务器接到客户机房(走一条 IPsec 隧道),之后出现了一组"看着像三个 bug、其实是一个 bug"的故障:

  1. SSH 能登录,但 scp 传一个 3MB 的 jar 卡在 0%,Ctrl+C 之后连接还活着;
  2. 服务器上 git clone https://... 一直停在 Receiving objects: 0%;
  3. 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 一路涨到连接超时。带宽不足的表现是稳定但慢,黑洞是先快后完全停住。

第二步:四个真实根因

按踩坑顺序:

  1. 隧道 / VPC 对等的 MTU 不是 1500:IPsec、GRE、VXLAN 封装要额外 40~100 字节,客户侧隧道是 1450,本机还是 1500。
  2. 容器网络又叠了一层:docker0 默认 1500,宿主机调成 1450 后容器内仍是 1500,于是"宿主机能 clone、容器里不能"。
  3. ICMP 被安全组吞掉:安全组只放通了 22/80/443,ICMP 全拒,PMTUD 从机制上就不可能工作。
  4. 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 往往不生效,云侧还有三个地方会把大包重新放进来:

  1. 安全组放通 ICMP 的必要类型。不用全开,只放开分片通知这一条就够了(控制台选「ICMP」协议、类型 3、代码 4;iptables 对应 --icmp-type 3/4)。这是从根上修 PMTUD 的方式。
  2. 弹性公网 IP / SLB 侧的 MTU。经 SLB 转发的流量如果 SLB 内部按 1500 处理,后端调到 1450 也没用,要按厂商文档把后端链路一起降。
  3. 对端的 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 数值修好 —— 这也是这次最大的收获:排查时要按"包的大小"给现象分组,而不是按"业务模块"分组。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。