不需要額外硬碟空間的伺服器搬遷:用 FastFileLink CLI 串流 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、備份傳輸、低磁碟空間主機,或臨時還原環境的團隊來說,這是一種很務實、很好自動化,也很容易說清楚的解法。
當舊機空間告急、搬遷又勢在必行時,這樣的行為模型會讓整個流程輕鬆很多。