不需要額外硬碟空間的伺服器搬遷:用 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 的情況下做一次性大檔傳輸,這個模式都很值得採用。

? FastFileLink CLI ?? Borg ????????

我們的部署模型

我們內部有一套輕量的部署系統,概念上接近小型 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 工具,例如 backupinstallrestoremigrate 等。全域的部署系統會呼叫這些工具,但 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 還支援 pubkeypubkey+pickupemail 等模式,也支援用 --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 offdownload --stdout 很適合這條路徑。對正在處理 server migration、備份傳輸、低磁碟空間主機,或臨時還原環境的團隊來說,這是一種很務實、很好自動化,也很容易說清楚的解法。

當舊機空間告急、搬遷又勢在必行時,這樣的行為模型會讓整個流程輕鬆很多。