不需要额外磁盘空间的服务器迁移:用 FastFileLink 串流 Borg 备份
在 Fri 24 April 2026 发布于 Blog 分类
搬一个 production app,表面上像是把资料复制到新机,实际上往往是在跟时间、空间和环境条件搏斗。
最常见的情况是这样:旧机快满了,大家想尽快搬走;新机刚准备好,还没正式接手流量;DNS 不能立刻切;资料量又大。这时如果 migration 流程还要求旧机先多生出一份大型 tarball,通常只会让事情更难推进。
这篇文章整理的是我们最近改造 migration 流程的实战经验。我们本来用的是很典型的做法:先用 Borg 备份,再把备份汇出成 tar 或 tar.gz,接着透过 scp 传到目标机器,最后在新机还原。
那套方式在磁盘空间充裕时很好用,也很好调试。问题出在它对来源机器的空间要求太高。偏偏真正急着搬的主机,通常就是空间最吃紧的那一台。
所以我们把传输层改成以 FastFileLink CLI 为核心,让资料从 Borg 直接串流到目标端。在这条流程里,来源端不会先落一份 tarball,目标端也不会先下载一份 tarball 再解压。资料会一路从旧机流到新机,直接还原到定位。
这个做法不只适合我们自己的部署系统。只要你有大量备份要搬、要把 production 资料还原到 development VM,或想在没有事先配置 SSH trust 的情况下做一次性大档传输,这个模式都很值得采用。

我们的部署模型
我们内部有一套轻量的部署系统,概念上接近小型 PaaS,主要入口是 Deploy.py。
它会处理 install、backup、restore、migrate、update、health check 和环境准备等维运工作。共通流程集中在部署层,app 特有的细节则交给各 app 自己的维运脚本与设定。
我们在这个场景没有选 Kubernetes,理由很单纯:对目前这类工作负载来说,它的整体复杂度偏高。很多服务是小到中型的 production app,我们更在意的是单机维运流程清楚、脚本好读、复原步骤可预期,而且能从 app 目录一路追到整个生命周期。
在这种前提下,保留一套结构化、但不过度庞杂的部署界面,比导入完整丛集编排更合适。
每个 App 自带 Working Folder 的好处
这套设计里,一个很关键的选择是:每个 app 都有自己的 working folder。
那个目录里会放 app 的执行配置、container 定义、service scripts、app 自己的维运工具,以及 persistent volumes 的还原方式。数据库则透过 dump、backup hook 或相关工具纳入同一套维运契约。
换句话说,如果能在另一台机器重现以下几个部分,app 基本上就能先在新机跑起来:
| 部分 | 作用 |
|---|---|
| Container 定义 | 重建 service process 与 runtime image |
| Volumes | 保留上传档、使用者资料与需要持久化的状态 |
| Database | 保留结构化资料 |
| Configuration | 重现 domain、port、path、secrets 与环境变数 |
| App 维运工具 | 让新机也能用同一套 install、backup、restore、migrate 界面 |
这也是为什么每个 app 都会有自己的 bin 工具,例如 backup、install、restore、migrate 等。全域的部署系统会呼叫这些工具,但 app 内部的行为仍由 app 自己掌握。
这种设计刻意保持朴素。对 production 维运来说,朴素常常是优势。
旧版 Migration 流程长什么样子
原本的 migration 流程大致如下:
source server
-> 执行 Borg backup
-> 从 Borg 汇出 volumes
-> 在磁盘上产生 tar 或 tar.gz
-> 用 scp 传到 target server
target server
-> 接收 archive
-> 解压缩 archive
-> 还原 database 与 config
-> 启动 app
-> 切流量前先验证
这是很容易理解的一条路。Borg 适合做备份,scp 几乎每个维运人员都熟,tarball 也很方便检查。
真正麻烦的是磁盘使用模式。
痛点就在来源机器快没空间
假设某个 app 的 volumes 有 80 GB。
旧流程很可能让来源机器同时承受这些空间需求:
既有 application data
+ Borg repository 或 backup cache
+ 汇出的 tar archive
+ 额外的压缩档
+ 传输中的 partial file
+ log 与暂存档
这种需求和 migration 的动机其实互相冲突。会急着搬机器,通常就是因为旧机太满、太旧、太难再撑下去。这时再要求它额外生出一份大型副本,等于把最重的负担放回来源机器身上。
更合理的做法,是让备份输出直接进入传输流程,不再经过来源端的落地档案。
rsync 能不能解这个问题
如果来源资料本来就是一棵可以直接同步的档案树,而且 source 与 target 之间的 SSH 通信也早就配置完成,那 rsync 依然很好用。
rsync -aHAX --numeric-ids --partial --partial-dir=.rsync-partial \
/srv/apps/myapp/volumes/ user@target:/srv/apps/myapp/volumes/
它能续传、保留 metadata,也很适合同步现成目录。如果 migration 的意思只是「把 live volumes 复制到新机,然后把 app 起起来」,那 rsync 其实很合理,很多时候也可能比这类带 relay 能力的工具更快。
我们这次的情况不太一样。我们本来就会在 migrate 前先做一次备份。既然备份已经建立完成,用它当资料来源就很自然。新的 Borg archive 等于是一个干净、明确的时间点快照,而 borg export-tar 则刚好提供了现成的 stdout stream,可以直接接到传输流程里。
这里要特别说明一点:Borg 并不是 migration 存在的原因,它只是我们已经在使用、也已经信任的备份机制。做完备份之后,它顺手成了 migration 的资料来源。至于完整的备份历史记录,要不要一起带到新机,是另一个决策。在很多环境里,这其实不是必要条件,因为整台 server 本来就已经有另外的整机备份。
另一个因素是 target 的弹性。有些 target 是干净 VM、临时测试机,这时我们未必想先处理 SSH key、sshpass 或额外的 server-to-server 信任配置。
在这样的条件下,stream-oriented 的传输工具就会更顺手。
为什么 FastFileLink CLI 很适合
FastFileLink CLI 可以传档案、资料夹,也可以传 stdin。这正是整个设计改变的起点。
比起先汇出 tarball,再把 tarball 复制到目标机器,我们可以让资料一路保持在流动状态:
borg export-tar -> stdout -> FastFileLink CLI -> stdout on target -> tar extract
在这条流程里,来源机器不需要先落一份 tarball,目标机器也不需要先收一份 tarball。
来源端的指令形状大致如下:
borg export-tar "$BORG_REPO::$ARCHIVE" - \
| "$FFL" - \
--name "$APP_NAME-volumes.tar" \
--e2ee \
--stdin-cache off \
--max-downloads 1 \
--pickup-code "$PICKUP_CODE"
目标端则直接把收到的资料送进 tar:
"$FFL" download "$LINK" \
--pickup-code "$PICKUP_CODE" \
--e2ee \
--stdout \
| tar xvf - -C "$RESTORE_ROOT"
这代表:
来源机器:不落地汇出 archive
目标机器:不落地下载 archive
传输过程:资料一路串流到目标目录
这次主示例不把 gzip 放进来,也是刻意的取舍。因为这次 migration 的主要痛点是额外磁盘空间,不是压缩率,而 plain tar stream 可以让 source 和 target 两端少一层 CPU 负担,也更容易调试。
对低空间 migration 来说,这正是最想看到的形状。
--stdin-cache、--stdout 和 --pickup-code 在做什么
如果只是把上面的指令贴出来,不多解释,读者其实很难立刻理解这几个选项的意义。
先说 --pickup-code。它是这条配对流程的一部分。sender 会产生一个 link,但 receiver 除了 link 之外,还要知道 pickup code 才能真正取走资料。如果你用过 croc,应该会对这种短码信任模型很熟悉:一组短码会成为取件流程的一部分。放在 migration 这里,它的好处很直接,因为我们可以把 link 和 code 一起交给 target process,不必把下载入口完全裸露出去。有了 --pickup-code 之后,FastFileLink CLI 其实就已经知道这次用的是 pickup 模式,所以不必再另外把 --recipient-auth pickup 写出来。
FastFileLink CLI 也不只有这一种收端验证方式。除了 pickup 之外,CLI 还支援 pubkey、pubkey+pickup、email 等模式,也支援用 --auth-user、--auth-password 走传统的 HTTP Basic Authentication。这次我们选 pickup,是因为它在脚本化流程里很方便,对一次性的 migration 也很顺手。
再来是 --stdin-cache off。FastFileLink CLI 默认的心智模型,偏向一般档案分享:档案可能会被重复下载、也可能会有多个 receiver。因此它会考虑 cache,这样才能支援多次取用。但 migration stream 的情境刚好相反:我们知道这份资料只会被一个 receiver 消费一次。如果还保留 stdin cache,只是在来源端额外花磁盘空间。把 cache 关掉之后,来源端就更接近真正的单次串流 producer。
--e2ee 在这里也很值得开启。WebRTC 本身已经走安全传输,但 migration 过程有时会 fallback 到 relay 或 tunnel。把 end-to-end encryption 打开之后,即使资料走到中继路径,中间节点看到的也只是加密后的内容。对备份资料和 production data 来说,这层保护很有价值。
download --stdout 则是 target 端的关键。接收端拿到位元流之后,可以直接往 stdout 写,后面接 tar 就能原地解开,不需要先下载到一个 tarball,再做第二步解压。
合在一起之后,整个资料路径会变得很清楚:
source app backup snapshot
-> Borg export stream
-> FastFileLink CLI sender
-> FastFileLink CLI receiver
-> tar 直接解到 target app folder
-> volumes 就定位
怎么整合回既有的 Deploy.py
我们没有重做一支全新的 migration 工具。
原本的 Deploy.py migrate 已经很清楚自己要做什么:
- 准备 source / target app layout
- 呼叫 app 自己的 migrate / backup 脚本
- 汇出 volumes
- dump 与 restore database
- 还原 config
- 启动 target app 并验证
因此改造的重点在于把传输层换掉,让其他既有能力继续工作。
整体流程现在可以理解成:
Deploy.py migrate
-> app bin/migrate
-> ExportVolumes from Borg
-> source 端串流到 FastFileLink CLI
-> target 端从 FastFileLink CLI 接出来
-> 直接解到 target app folder
-> restore database 与 privilege
-> 视需要传送 backup history
-> install / start target app
-> cutover 前验证
这样做还有一个好处:原本的 file-based 路径依然留着。当你需要保留 artifact、想把 restore 问题切开来 debug,或在空间充足的情况下想保留旧流程时,仍然有选择。Streaming mode 则负责解决低空间 migration 的核心问题。
FastFileLink CLI 在这个案例中的几个优势
FastFileLink CLI 跟这个场景贴得很近,主要是因为它同时满足了几个需求:
| 能力 | 对 migration 的帮助 |
|---|---|
| 单档 APE binary | 需要时下载,用完删掉,不必预先安装 |
| WebRTC direct transfer | 网络允许时尽量点对点传输 |
| Relay fallback | 打不通直连时仍能透过 relay 完成 |
| End-to-end encryption | relay 看不到明文内容 |
| Pickup code 验证 | 脚本可以安全配对 sender 与 receiver |
--e2ee |
即使走 relay 或 tunnel,中间节点也看不到资料内容 |
| stdin / stdout 支援 | 真的能做到两端都不落地的串流搬迁 |
--hook 事件输出 |
很适合整合到既有 automation 里 |
对老旧 production 主机来说,单档 binary 的意义很实际。我们不需要为了搬走它,先在上面长期安装一个新服务。抓下 ffl.com,跑完流程,清掉档案即可。
--hook 这点也很值得单独讲。FastFileLink CLI 有它自己独特的 embedded mode,可以输出结构化事件拿来做整合。这帮我们省掉很多胶水程序,因为不必再去 parse 人类看的 console output 才知道传输目前发生了什么。这种整合面在同类型工具里并不常见。
和 scp、rsync、croc、Magic Wormhole 的比较
挑工具时,真正要看的还是它的假设是否符合现场条件。
| 工具 | 适合情境 | 这个案例里的限制 |
|---|---|---|
scp |
有现成档案要透过 SSH 复制 | 需要先产生 artifact,也要处理 SSH 认证 |
rsync |
同步 live directory tree | 不擅长直接搬 Borg stdout stream |
croc |
安全的 CLI 传输,也支援 pipe | 是很好的替代方案,但大量资料时常会走 relay,对多百 MB 或多 GB 的 migration 不是特别理想 |
| Magic Wormhole | 人工一次性安全传档 | 比较偏 ad-hoc 传送,不是 unattended migration 的最佳主线 |
| FastFileLink CLI | 档案、资料夹与 stdin/stdout 串流 | 需要把 timeout、cleanup、log 观测一起设计好 |
如果资料已经是可直接同步的目录,而且 SSH trust 很稳定,rsync 仍然会是很强的选项。
但在「资料本来就是备份 stream」加上「目标端可能是新 VM」这种情况下,FastFileLink CLI 的整体手感明显更好,因为它对环境的前置要求更少,同时保留加密、验证、直连与 relay fallback。
实作过程里,真正踩到哪些坑
真正有价值的教训,并不是某条路径 quote 错了,或哪个 cleanup 顺序放反了。那些问题确实存在,也确实修掉了,但大量资料传输真正难的地方,是状态观测。
我们最早的版本走得很直觉:启动 FastFileLink CLI,parse 一般输出,把 link 抓出来,剩下的流程再用 shell wrapper 补。当 proof of concept 没问题,但当它变成真正的 migration command,就会开始暴露观测上的不足。
大档传输把这些问题放得很大:
- sender 到底有没有开始读 stdin?
- receiver 真的接上了吗?
- 目前走的是直连,还是 fallback 路径?
- process 还活着,代表真的在前进,还是其实只是卡住?
- producer 和 transfer tool,究竟是哪一端先失败?
Orchestration 比 pipe 更重要
Streaming migration 绝对不是「把 producer 接到 consumer,中间多一条 pipe」就结束。
file-based transfer 失败时,通常还会留下 partial file 可以看。stream 一旦卡住,系统自己就得知道到底卡在哪里。真正能不能拿来做 production migration,差别常常不在传输工具支不支援 stdin,而在整个 orchestration 有没有把状态观测、清理与重试边界一起设计好。
因此实作上至少要补几件事:
| 关注点 | 做法 |
|---|---|
| temporary binary | ffl.com 下载到 temp directory,用完就删掉 |
| logging | 卡住时能切到 DEBUG,或指定 logging config 帮助排查 |
| receiver readiness | 先确认 target receiver 已启动,再让整体流程往下走 |
| progress | 观察 bytes transferred 或 extracted,不只看 process 还在不在 |
| cleanup | 清掉 orphan sender / receiver process 与临时档 |
| retry | 在 transfer boundary 重跑,不从半个 tar extraction 中间硬接 |
| secrets | auth password、pickup code 这类资讯不应出现在一般 log |
我们实测时也很明显感受到这件事:如果 target receiver 根本没跑起来,source 端表面上可能还在等、还像是在做事,但整条 migration 其实早就停住了。
所以一个够稳的 migration command,至少要能确认:
source process is alive
target process is alive
target output is growing
logs show an accepted transfer path
长时间传输里,「process 还活着」从来不等于有进度。进度一定要能观测。
等我们改成用 FastFileLink CLI 的 --hook 之后,事情顺利很多。share link、progress、receiver 状态、完成与失败事件,都可以透过结构化事件来追。这时候才真正摆脱「把人类看的输出当 API」这种脆弱作法。
这件事的价值非常高。migration automation 需要的不只是资料有没有在传,而是流程状态能不能被可靠地理解。
在这个基础上,我们当然还是修到一些一般 deploy bug,例如 target 路径 quoting、旧 volumes 的清理顺序等。但那已经是能清楚定位、也能直接修正的问题了。
修完之后,整条 migration 已经能顺利完成这些步骤:
- 从 Borg 串流 volumes 到 FastFileLink CLI
- 在 target 还原 database dump
- 还原 database privilege
- 视需要把 Borg backup repository 一起带过去
- pull app image
- 启动 target container
我们最后遇到的剩余问题,已经不在 streaming layer,而是 target 上 Nginx / SSL 收尾的部署细节。这反而是个好讯号,表示最困难的资料搬运路径已经打通,接下来处理的是一般部署尾声该处理的事情。
不切 DNS 也能先测试 migration 结果
好的 migration 流程,应该允许我们在真正切换前,先验证新机是否已经能接手。
这一点和我们的 app folder 设计很契合。因为 app 的 container、volumes、database、config 与维运工具本来就被整理在同一套 working folder 契约里,所以可以先把整个 app 还原到新机,用测试 port 或 temporary hostname 启动,确认结果没问题,再安排 DNS 或 load balancer 的 cutover。
实际上很容易整理成这样一份 checklist:
1. 在 target 还原 app files、volumes、database、config。
2. 启动 target 上的 containers 或 services。
3. 执行 health checks。
4. 验证 static files 与 uploaded files。
5. 验证 database-backed pages 和 login flow。
6. 检查 logs 是否有 path、permission、environment variable 问题。
7. 确认后才安排 DNS 或 load balancer 切换。
这也是 migration 没那么可怕的关键。旧 production server 可以继续服务,新机则先在旁边证明自己真的能接手。
这个模式不只适用于单一 App
虽然这次是从一个具体 migration 案子长出来的,整个模式其实很通用。
只要资料来源能输出到 stdout,就可以沿用同样的传输思路:
borg export-tar repo::archive -
pg_dump mydb
mysqldump mydb
tar -cf - /large/folder
zfs send pool/dataset@snapshot
接着把 stream 接到 FastFileLink CLI:
producer | ffl - --stdin-cache off --max-downloads 1
目标端再接到真正的 consumer:
ffl download "$LINK" --stdout | consumer
这很适合用在:
- 老旧或磁盘快满的 server 搬迁
- 把 production backup 还原到 development VM
- disaster recovery 传输
- 大量使用者上传资料搬移
- 不想在来源机留下 dump file 的 database 传输
- 事前不想配置 SSH trust 的临时环境
结语
这次 migration 改造最重要的收获,在于我们把整个搬迁契约重新整理过。
旧流程要求来源机器先准备出一份大型副本,搬迁才有办法开始。当旧机已经快满时,这个前提很容易成为第一个阻碍。
整理之后,流程会变得很直接:
读出 backup stream
立刻传走
在 target 直接还原
--stdin-cache off 与 download --stdout 很适合这条路径。对正在处理 server migration、备份传输、低磁盘空间主机,或临时还原环境的团队来说,这是一种很务实、很好自动化,也很容易说清楚的解法。
当旧机空间告急、搬迁又势在必行时,这样的行为模型会让整个流程轻松很多。