主题
文件传输方案
一句话:文件传输是在不可靠网络上做可靠搬运,核心不是「怎么发」,而是断了能否续、传完能否证明没坏、多种协议能否用一套引擎管住。
适用:curl 8.x、rsync 3.2+、OpenSSH 9.x;对象存储分片上传以各厂商文档为准。
为什么需要
1GB 文件传到 99% 断网,从头重传的代价不只是流量,还有窗口期和上游等待。批量传输几百个文件时,单机串行会把日终窗口拖长;跨网络区域还要过代理和防火墙。
文件传输的故障大多不是「完全连不上」,而是「连上了但传了一半」「传完了但内容不对」「重试了但重复入库」。方案要围绕这三类问题设计。
核心机制
任务状态机
一个可观测的传输任务至少要有这些状态:
text
PENDING -> CONNECTING -> TRANSFERRING -> VERIFYING -> SUCCESS
| | |
v v v
RETRYING <--------+-------------+
|
v
FAILED(超过最大重试次数或总超时)每个状态都要记录:文件标识、源和目的、当前偏移量、重试次数、最后错误、校验值。没有偏移量就无法续传,没有校验值就无法判断「传完」是否等于「传对」。
断点续传
通用流程:
- 取本地已有文件大小
localSize。 - 取远端文件大小
remoteSize,并确认远端文件没有被替换(大小、mtime,最好有 ETag/校验值)。 localSize < remoteSize时,从localSize偏移处继续;否则重新传。- 写入临时文件,校验通过后再原子重命名到目标路径。
不同协议的偏移方式:
- HTTP:
Range: bytes=<offset>-,服务端返回 206。 - FTP:
REST <offset>后接着RETR。 - SFTP:以追加模式打开,从本地偏移继续写。
续传的前提是「远端文件还是同一个文件」。如果远端在中断期间被覆盖,按旧偏移追加会把两个版本拼在一起。生产上要用临时文件、文件版本号或校验值来防止这种拼接。
校验:MD5 分段不能合并
MD5 是迭代哈希:每一段的摘要依赖前一段的内部状态。把文件切成 N 段,分别算 MD5,再把 N 个摘要拼起来,不等于整个文件的 MD5。分段 MD5 只能证明「每段各自没坏」,不能直接合并成整体校验值。
可行的做法:
- 每片哈希 + 清单 + 整体哈希:传输前生成
part-0001.sha256等分片摘要和一份 manifest,传完逐片校验;再用流式方式算一遍整体 SHA-256。 - Merkle 树:分片哈希两两合并成根哈希,可以定位坏片,也支持并行校验。
- 对象存储分片上传:每个 part 有 ETag,完成后再校验对象整体 checksum。注意 multipart 的 ETag 通常不是整文件 MD5。
MD5 还有碰撞问题,完整性校验优先用 SHA-256;追求速度可以用 BLAKE3,但两端要实现一致。
整体哈希可以流式计算,内存是常数,只是 CPU 要扫一遍文件。校验不要阻塞传输主线程:传输完成后先落临时文件,异步校验,校验通过再切正式路径。
重试策略
重试要同时有四个边界:最大次数、单次超时、总截止时间、退避上限。只写「重试 10 次」不够,因为每次可能卡很久。
固定间隔适合单次操作时长可控的场景;文件传输的单次操作可能从秒级到分钟级,指数退避加随机抖动通常更稳,避免大量任务在同一时刻重试。弱网可以把缓冲区调小,减少单次丢包的重传量,代价是系统调用变多、吞吐下降。
协议抽象与网络穿越
把变化的部分(FTP/SFTP/HTTP/对象存储)和不变的部分(重试、续传、校验、调度)分开:端点接口只负责连接、读大小、读偏移、写数据;传输引擎负责状态机和策略。
跨网络区域时,代理配置要按区域选择,而不是全局一个 HTTP 代理。代理对上层透明,但会增加延迟,也会让超时和重试参数需要重新估算。
批量传输用 transferId 做幂等:传输前查状态表,已成功的不重复传;节点挂了,分片任务重新分配给其他节点,靠租约防止两个节点同时写同一目标。
最小上手
HTTP 断点续传:
bash
curl -C - -O https://example.com/big.bin
# 中断后再次执行同一条命令,会从本地文件末尾继续分片传输并生成校验清单:
bash
split -b 8M big.bin part_
sha256sum part_* > manifest.sha256
sha256sum -c manifest.sha256
sha256sum big.bin # 整体校验值,另行保存rsync 续传(适合 SSH 可达的场景):
bash
rsync -avP --partial --append-verify big.bin user@host:/data/--append-verify 只在远端文件更短时追加,并在结束后校验;远端文件已被替换时不要用它。
生产动作
- FTP 被动模式:服务器配置固定数据端口范围(如 30000–30100),防火墙放行该范围;只开 21 端口会出现「连上但传不动」。
- SFTP 资源泄漏:
ChannelSftp必须在finally里disconnect();监控 SSH 会话数和文件描述符。 - 临时文件:先写
.part,校验通过后rename;避免下游读到半个文件。 - 校验异步化:大文件校验放独立线程池,设置并发上限,避免校验把传输线程池占满。
- 幂等:
transferId+ 状态表 + 租约;重试前先查是否已成功。 - 监控:传输成功率、平均耗时、重试次数、校验失败率、积压任务数;失败任务保留现场信息(偏移量、最后错误、远端大小)。
- 参数示例:重试 5–10 次、间隔 500ms–1s、分片 8MB,都是经验值,要按网络质量和文件大小压测后调整。
边界
- 服务端不支持 Range/REST 时无法续传;只能重传或改用支持续传的协议。
- 追加模式不是幂等操作,远端文件变化时会拼坏;必须有文件身份校验。
- 分片 MD5 不能合并成整体 MD5;需要整体校验就流式算一遍,或用 Merkle 树。
- 自研传输网关适合多协议、多网络区域、强审计的场景;单一协议、内网可达时,rsync/对象存储 SDK 更省事。
- 校验会消耗 CPU 和 IO,超大文件要接受「传输完成」和「校验完成」之间的时间差。
讲清楚
文件传输要解决三件事:断了从哪继续、传完怎么证明没坏、多种协议怎么统一管。续传靠记录偏移量,并且要确认远端还是同一个文件,否则追加会把两个版本拼坏。校验不能把分段 MD5 拼起来当整体 MD5,MD5 是迭代哈希,段摘要不能合并;正确做法是每片哈希加清单,再流式计算整体 SHA-256,或者用 Merkle 树定位坏片。重试要有最大次数、单次超时、总截止时间和退避上限。工程上再补临时文件、原子重命名、transferId 幂等和代理穿越,才算一套可运维的传输方案。