雅洁网

低带宽下的 S3 存储迁移:分片并发的实践与权衡

雅洁 发布于

一次 5GB 数据的跨服务器迁移,在源端带宽仅 5Mbps 的限制下,通过分片并发将传输效率推到带宽上限。

一、背景与约束

我维护了一个自托管的 RustFS 服务(兼容 S3 API / MinIO),主要用于存储学习教育相关的音频文件(单词/例句 MP3,约 5GB),支撑着一个小型在线教育应用。
由于旧服务器即将下线,需要将全部数据迁移到新服务器。但面临两个硬约束:

约束 说明
源端带宽 旧服务器出带宽仅 5Mbps(某厂商轻量应用服务器入门型)
目标端状态 新服务器为全新实例,未安装任何存储服务软件

如果使用最直接的 scp 单连接传输,理论极限速度约 625KB/s,3.5GB 压缩包需要 1.5~2 小时。在真实的公网环境下,TCP 拥塞控制、丢包重传等因素会进一步拖慢速度,且一旦 SSH 断开,传输即中断,需要从头再来。

这意味着:需要一种能跑满 5Mbps 带宽、且具备中断可恢复能力的传输方案。

二、方案选型

2.1 备选方案

方案 原理 可行性 不选的原因
scp 直传 单连接 SSH 传输 ❌ 无法充分利用带宽,无断点续传
rsync 增量同步 + SSH 通道 ❌ 小文件场景下校验开销大;目标端无服务,无法使用 daemon 模式,ssh 模式下单连接瓶颈依旧存在
rclone 多线程 多连接并发,支持断点续传 ⚠️ 需要目标端先有一个运行中的 S3 服务,形成"鸡生蛋"问题
mscp / para 封装好的多线程 SCP 工具 ❌ 实测官方下载链接已失效(404),无法快速部署
split + 并发 scp 手动分片,多连接并行传输 ✅ 无额外依赖,仅需 SSH;可控性强,可观测性好

2.2 选择理由

split + 并发 scp 的核心思路是:将大文件切分为多个分片,每个分片通过独立的 SSH 连接并行传输。

这样做有两个收益:

  1. 多连接并发:通过建立多个 TCP 连接,减小单连接受 BDP(带宽延迟积)限制的影响,将带宽利用率从约 60% 提升到 95% 以上;
  2. 可观测性:每个分片独立传输,可以清晰看到各分片的进度,方便排查慢连接。

三、关键设计决策

3.1 为什么先打包再分片?

原始数据是 5GB 的目录树,包含数千个 MP3 小文件。如果直接分片目录,会产生两个问题:

  • 每个小文件都需要建立 SSH 连接,握手开销远大于传输本身;
  • 文件分配不均衡,无法控制每个分片的大小。

因此选择 先 tar 打包成单文件,再 split 切分,将小文件的元数据开销摊销到一次打包中。

3.2 分片大小为什么选 500MB?

这是一个需要权衡的参数:

分片大小 并发度 优势 劣势
太小(如 100MB) 35 个分片 并发度高 SCP 握手次数多;后台管理 35 个进程,资源开销大
适中(500MB) 7 个分片 并发度足够填满 5Mbps;进程数可控 单个分片传输失败重传代价尚可接受
太大(1GB) 4 个分片 管理简单 并发度不足,无法充分利用带宽

500MB 是在并发度和管理成本之间的一个平衡点。 7 个并发连接足以将 5Mbps 带宽跑满,同时进程数在 ps 和 jobs 的视野内清晰可查。

3.3 为什么用 SCP 而不是 HTTPS?

  • 新服务器预装了 SSH 服务,无需额外部署;
  • 通过 ssh-copy-id 配置免密登录后,所有 scp 命令自动复用同一个密钥,无需处理 Token 过期或签名计算。

四、实现与踩坑

4.1 打包

tar -czf /tmp/rustfs_data_backup.tar.gz -C /www/wwwroot/rustfs data

打包后大小:3.5GB(MP3 本身已压缩,gzip 压缩率有限)。

4.2 切分与并发传输

# 切分为每个 500MB 的分片
split -b 500M -d --suffix-length=2 rustfs_data_backup.tar.gz rustfs_part_

split 命令各参数含义:

  • -b 500M:每个分片大小为 500MB;
  • -d:使用数字后缀(00、01…),而非字母后缀;
  • --suffix-length=2:后缀长度为 2 位,支持最多 99 个分片;
  • rustfspart:分片文件的前缀。

执行后生成分片文件

# 配置免密登录(避免多次密码输入)
ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa
ssh-copy-id root@目标服务器ip

验证免密登录是否生效:

ssh root@目标服务器ip "echo 'OK'"
# 并发传输
for part in rustfs_part_*; do
    scp "$part" root@目标服务器ip:/tmp/ &
done
wait

这段脚本的执行逻辑:

  • for part in rustfspart*:遍历所有分片文件;
  • scp ... &:每个分片通过 & 放入后台执行,不阻塞下一个分片的启动;
  • wait:等待所有后台 scp 进程完成,再继续后续操作。

实际传输效果:多个分片同时传输,单分片速度约 80~90KB/s,总带宽稳定在 620KB/s 左右,接近 5Mbps 的理论上限。

4.3 目标端合并

# 合并分片
cat rustfs_part_* > rustfs_data_backup.tar.gz

# 验证合并结果
ls -lh rustfs_data_backup.tar.gz
tar -tzf rustfs_data_backup.tar.gz | head -20

踩坑记录:在执行合并操作时遇到 No space left on device 报错。检查磁盘后发现根分区 / 已满,而 /www 数据分区还有充足空间。解决方案是将分片移动到 /www/temp 目录后再合并,避免写入系统分区。

4.4 解压与启动

# 创建数据目录
mkdir -p /www/wwwroot/rustfs

# 解压数据
tar -xzvf rustfs_data_backup.tar.gz -C /www/wwwroot/rustfs

# 启动 RustFS 容器
docker run -d \
  --name rustfs \
  --restart always \
  -p 9000:9000 \
  -p 9001:9001 \
  -e RUSTFS_ROOT_USER='<用户名>' \
  -e RUSTFS_ROOT_PASSWORD='<你的密码>' \
  -e MINIO_SERVER_URL='http://<你的存储域名>' \
  -e MINIO_BROWSER_REDIRECT_URL='http://<你的控制台域名>' \
  -v /<你的宿主机路径>/data:/data \
  -v /<你的宿主机路径>/config:/config \
  rustfs/rustfs

验证容器运行状态:

docker ps | grep rustfs
docker logs rustfs --tail 20

五、延伸思考:方案的局限性

这套方案适用于:

  • 数据量在 10GB 以内
  • 源端带宽在 50Mbps 以下
  • 目标端无需预先部署任何服务

但如果数据量增长到 500GB,这套方案将不再适用。更合适的选择包括:

  • 使用 rclone 的多线程上传到对象存储中转(如 AWS S3 / 腾讯云 COS);
  • 使用专业工具如 bbcp(专为高速网络设计)或 fpart(并行文件传输);
  • 若两台服务器同属一个云厂商,可使用内网高速通道 + 快照迁移。

六、命令速查

# 打包
tar -czf backup.tar.gz -C /source data

# 验证压缩包
tar -tzf backup.tar.gz | head -20

# 分片
split -b 500M -d --suffix-length=2 backup.tar.gz part_

# 免密登录
ssh-keygen -t rsa -b 4096
ssh-copy-id root@target_ip

# 并发传输
for f in part_*; do scp "$f" root@target:/path/ & done; wait

# 合并
cat part_* > backup.tar.gz

# 查看磁盘空间
df -h

📌 本文所有操作在 CentOS 7/8 环境下验证通过,RustFS 版本为 v1.0+。