摘要:内网往海外服务器推一个 1.7MB 的包,脚本一声不响卡了两小时:没有报错、没有超时、字节数就是不动。这是一次完整的排障复盘——先取证判明是链路丢包而不是脚本缺陷,再用 rsync 断点续传 + MD5 全等放行把包送过去;附二十行可复用的传输脚本与六个必修项。

丢包链路概念图:数据流中段被撕裂,粒子四散

这不是一句”网络好慢”的抱怨,而是一份可以直接照抄的排障流程:8 个只读探针、20 行脚本,从”没有报错但就是卡住”定位到链路丢包,再用断点续传把一个 1.7MB 的部署包送到对岸。

一、症状:脚本不报错,字节数就是不动

事情很小:把一个 1.7MB 的压缩包从内网跳板机推到海外目标机,到对岸解包、跑发布脚本。第一跳(本机 → 内网跳板机)秒过;第二跳(跳板机 → 海外机)挂住了。

它的”挂”很有欺骗性:

  • 没有报错,没有 connection reset,没有 permission denied;
  • ssh 进程一直活着,占着终端,看起来像”正在工作”;
  • 目标机上文件已经存在,但 stat -c%s 的字节数停在 1.6MB 的某个位置,再也不变;
  • 客户端 600 秒超时被强杀(exit 124),原样重试一次,结果一模一样。

最容易踩的坑在这里:这种形态看起来像”对端磁盘满 / 权限不对 / 脚本写错路径”,于是你去查磁盘、查权限、查脚本——半小时过去,全是干净的。真正的原因不在这三处。

二、先取证,不重试:二十分钟拿全四件套

取证四件套:四个仪表并排读数

有一条原则必须先立住:同一个故障连续两次失败之后,第三次不许重试,必须先拿到新信息。重复”重试 + 等超时”只是把四十分钟再烧一遍。

四件套,一次问全,全是只读探针,不改变现场:

# 探针 看什么
1 ping -c 10 -i 0.2 <目标> 丢包率与 RTT 抖动(-i 0.2 把十个包压进 2 秒,采样快)
2 ss -tn / ss -tni TCP 发送队列是否堆积(Send-Q 长期高位 = 数据发不出去)
3 ps -ef 过滤 ssh / rsync / cat 进程是否阻塞、有没有上一轮的僵尸残留
4 tail 传输日志 + 对端 stat -c%s 日志停在第几块、对端字节数是否与本地一致

这次的结论非常干净:

  • ping -c 10 -i 0.2:10 个包丢了 3 个——30% 丢包。这不是”慢”,这是”漏”;
  • ss -tn:Send-Q 顶到 61KB 不再下降,典型的”对端收不到、本端发送窗口填满”;
  • 进程列表干净、对端目录确实可写——排除权限、磁盘、路径三类常见嫌疑;
  • 日志停在某一块之后不再前进:不是报错退出,而是永久阻塞。

三、判据:是链路丢包,还是脚本缺陷

两条路都会表现为”卡住”,分辨起来只看三件事:

  1. 换通道传小文件。echo ok | ssh 目标 'cat > /tmp/t' 传 20 字节:能过,说明认证、权限、磁盘、路径全部正常,大概率是链路质量;过不去,才是配置问题。
  2. 看丢包率。持续丢包超过 5% 的链路上,单条 TCP 流会进入”发一段、退一段”的循环,吞吐趋近于零,最终表现为永久阻塞。
  3. 看阻塞形态。脚本缺陷一般有明确报错或非零退出码;链路问题往往是”静默不动 + 队列堆积”,没有任何错误信息可抓。

这次的判定是链路丢包。但请注意:判定成链路问题,不代表脚本可以不动——下面两节,一节修链路,一节修脚本,缺一个都还会再卡。

四、对策一:断点续传 + 校验放行

断点续传循环:进度环在检查点续接

在丢包链路上,cat > file 与 scp -r 这类”单流一把梭”是必死的:它们从第 0 字节重新开始,中途一断,进度全部作废。正确姿势是把大文件交给能续传的工具,并把校验写进流程。

核心就是这一段(最多循环 8 次,每次断点续传,进度累积):

for i in 1 2 3 4 5 6 7 8; do
  timeout -k 5 100 rsync -e "ssh $SSHOPT" -a --partial --append-verify --timeout=20 "$F" "$SG:/tmp/$F"
  RM=$(timeout -k 5 25 ssh -n $SSHOPT "$SG" "md5sum /tmp/$F" | cut -d' ' -f1)
  [ "$RM" = "$LM" ] && break
done
[ "$RM" = "$LM" ] || { echo TRANSFER_INCOMPLETE; exit 1; }

三个关键开关:

  • --partial:断掉时保留已传部分,不做清理;
  • --append-verify:下一次接着传之前,先校验已传部分的校验和再追加,避免”前面被改过却硬接上去”;
  • --timeout=20:链路完全僵死时让 rsync 快速放弃,把重试权交还给外层循环。

实测效果:第 1 次推了约 1.6MB 被链路掐断,第 2 次从断点接上补齐——两轮拿下一个在 30% 丢包下”单流 40 秒都过不去”的包。

最要紧的是最后那行 [ "$RM" = "$LM" ]:MD5 全等才放行后续动作。传输工具说”完成”不算完成,校验和相等才算——这一条把”传坏了却继续发布”的可能性彻底封死。

五、对策二:传输脚本自身必修的四项

链路修好了,脚本里的坑还得填。这四项都是这次真踩到的:

  1. timeout N ssh 杀不掉卡住的 ssh。进程阻塞在 write 上时会忽略 SIGTERM,超时形同虚设 → 一定用 timeout -k 5 N,先 TERM、超 5 秒再 KILL。
  2. ssh 漏写目标主机会报一句很像”配置错”的假错误:hostname contains invalid characters。原因是命令行里的 cat > /tmp/x 被当成了主机名。看到这句,先检查 ssh 行有没有漏 "$HOST"。
  3. 脚本必须 LF 换行。CRLF 会把 \r 混进变量尾部,$SG 变成 root@x\r,报的还是上面那句 invalid characters,但根源是换行符 → 上送前 tr -d '\r'。
  4. 别用”长内联命令 + 管道”上送脚本。中间的管道层会把内容截断或吞掉(我们遇到过写进去只剩两百多字节,人却以为写好了)→ 本地落盘 → base64 编码 → 远端 base64 -d > 文件 → bash -n 语法检查 → 回读 md5 确认完整。

按这四条改完脚本,重试一次就通了。

六、验收:怎么确认”真的传到位了”

传输类操作最容易自欺欺人:”命令没报错”不等于”文件到位了”。验收顺序固定四步:

  1. 对端 stat -c%s 与本地 stat -c%s 相等;
  2. 对端 md5sum 与本地 md5sum 相等,全等才放行;
  3. 解包后文件数量正确(ls | wc -l)、关键文件可读;
  4. 发布脚本自己再跑一遍前端探测:HTTP 状态码、正文字节数、关键串命中数——不达标的产物宁可不上线,也不发一个”看起来像成功”的版本。

七、可复用清单

放行判据:一条流被拦,一条流穿过校验环

症状 探针 判据 处置
静默卡住、无报错 ping -c 10 -i 0.2 丢包 ≥ 5% 判为链路问题,转断点续传
发送队列高位不降 ss -tn Send-Q 顶住不动 同上,别再加并发
报 invalid characters bash -x 看展开后的行 漏写主机名 / \r 污染 补 "$HOST"、tr -d '\r'
超时命令杀不掉 看进程是否仍存活 忽略 TERM 改 timeout -k 5
字节数到一半不动 对端 stat -c%s 与本地不等 --partial --append-verify
报”完成”但产物坏 双侧 md5sum 校验和不等 重传,禁止放行

八、这次的整体账与三条教训

  • 时间账:诊断约 40 分钟,其中一半烧在”重试 + 等超时”上;处置约 10 分钟。如果第一次失败就立刻取证,总时长能压到 20 分钟以内。
  • 教训一:连续两次失败必须换维度。 第二次重试之前先要一个新信息,否则只是在重复同一次失败。
  • 教训二:把校验写进流水线,而不是写进脑子。 人会在深夜忘记比对 MD5,脚本不会。
  • 教训三:不要用单流工具对抗丢包链路。 scp -r、cat > 在丢包链路下是必然卡死;换成 rsync 续传加一个循环重试,成本是一行循环,收益是不再需要人守着。