摘要:内网往海外服务器推一个 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 不再下降,典型的”对端收不到、本端发送窗口填满”;- 进程列表干净、对端目录确实可写——排除权限、磁盘、路径三类常见嫌疑;
- 日志停在某一块之后不再前进:不是报错退出,而是永久阻塞。
三、判据:是链路丢包,还是脚本缺陷
两条路都会表现为”卡住”,分辨起来只看三件事:
- 换通道传小文件。
echo ok | ssh 目标 'cat > /tmp/t'传 20 字节:能过,说明认证、权限、磁盘、路径全部正常,大概率是链路质量;过不去,才是配置问题。 - 看丢包率。持续丢包超过 5% 的链路上,单条 TCP 流会进入”发一段、退一段”的循环,吞吐趋近于零,最终表现为永久阻塞。
- 看阻塞形态。脚本缺陷一般有明确报错或非零退出码;链路问题往往是”静默不动 + 队列堆积”,没有任何错误信息可抓。
这次的判定是链路丢包。但请注意:判定成链路问题,不代表脚本可以不动——下面两节,一节修链路,一节修脚本,缺一个都还会再卡。
四、对策一:断点续传 + 校验放行

在丢包链路上,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 全等才放行后续动作。传输工具说”完成”不算完成,校验和相等才算——这一条把”传坏了却继续发布”的可能性彻底封死。
五、对策二:传输脚本自身必修的四项
链路修好了,脚本里的坑还得填。这四项都是这次真踩到的:
timeout N ssh杀不掉卡住的 ssh。进程阻塞在 write 上时会忽略 SIGTERM,超时形同虚设 → 一定用timeout -k 5 N,先 TERM、超 5 秒再 KILL。- ssh 漏写目标主机会报一句很像”配置错”的假错误:
hostname contains invalid characters。原因是命令行里的cat > /tmp/x被当成了主机名。看到这句,先检查 ssh 行有没有漏"$HOST"。 - 脚本必须 LF 换行。CRLF 会把
\r混进变量尾部,$SG变成root@x\r,报的还是上面那句 invalid characters,但根源是换行符 → 上送前tr -d '\r'。 - 别用”长内联命令 + 管道”上送脚本。中间的管道层会把内容截断或吞掉(我们遇到过写进去只剩两百多字节,人却以为写好了)→ 本地落盘 → base64 编码 → 远端
base64 -d > 文件→bash -n语法检查 → 回读 md5 确认完整。
按这四条改完脚本,重试一次就通了。
六、验收:怎么确认”真的传到位了”
传输类操作最容易自欺欺人:”命令没报错”不等于”文件到位了”。验收顺序固定四步:
- 对端
stat -c%s与本地stat -c%s相等; - 对端
md5sum与本地md5sum相等,全等才放行; - 解包后文件数量正确(
ls | wc -l)、关键文件可读; - 发布脚本自己再跑一遍前端探测: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 续传加一个循环重试,成本是一行循环,收益是不再需要人守着。



