VPS snapshot、backup 與 clone 有何不同?
了解 VPS snapshot、backup 與 clone 各自能還原什麼,以及 clone 會複製哪些識別資訊。另說明為何 snapshot 不算備份,以及啟動 clone 前必須修正的設定。
VPS snapshot、backup 與 clone 的實際定義
VPS snapshot 是由供應商保存的伺服器磁碟映像,位於供應商的基礎架構中,並隸屬於你的帳戶。backup 是資料的獨立副本。你可以在其他位置還原它,不需要保存原始資料的供應商協助。clone 是從 snapshot 部署的新 instance,因此啟動時會是原始 instance 的完整副本,連同其識別資訊也一併複製。
這些機制解決的問題不同。snapshot 可在幾分鐘內將失敗的升級回復到先前狀態,但帳戶關閉後便無法使用。backup 即使供應商停止營運仍可保留,但還原時間較長,因為必須先重新建置伺服器。clone 可透過一個步驟建立第二台執行中的伺服器,但也會產生兩台都認為自己是同一台伺服器的機器。
為什麼 VPS snapshot 不是備份
問題在於故障範圍,不在於映像品質。snapshot 位於供應商的儲存平台上,通常與來源伺服器位於同一個 region,而且始終隸屬於同一個帳戶。單一事件可能同時影響伺服器及其 snapshot。
- 帳戶遭到停權、付款失敗,或有人竊取登入憑證。
- 具備 API 存取權的人員或 script 刪除 instance。許多供應商會在刪除 instance 時一併刪除其 snapshot。除非已確認相反情況,否則請先閱讀供應商的文件說明。
- region 發生重大故障,其中的所有資源同時無法連線。
- 以 root 身分在伺服器上執行的程式找到你留在
/root中的供應商 API token,並在處理磁碟前刪除 snapshot。
備份是能在上述四種情況下存續的副本。判斷方式只有一個問題:如果供應商帳戶今天下午停止存在,你還能還原什麼,又要還原到哪裡?無法通過這個問題的任何項目,都只是 rollback 工具。請持續建立 snapshot,因為沒有其他方式能更快完成還原。接著,將第二份副本存放在供應商無法控制的儲存空間。
傳統原則仍然適用:資料保留三份副本,使用兩種儲存類型,其中一份位於平台之外。供應商 snapshot 加上位於獨立基礎架構上的 restic 備份儲存庫,即可用兩個元件涵蓋這項需求。
執行中的資料庫快照為何可能無法正常還原
供應商的快照會在某一個時間點複製區塊裝置的狀態。它不會先要求應用程式停止,也看不到仍位於 page cache 中的資料。因此,該映像檔頂多只能達到 crash-consistent。其狀態就像有人直接拔掉電源線後的磁碟。
大多數軟體堆疊都能處理這種情況。ext4 和 XFS 會在掛載時重播 journal,因此檔案系統仍可正常啟動。PostgreSQL 會在啟動時重播 write-ahead log,日誌也會顯示這一點:
LOG: database system was not properly shut down; automatic recovery in progressInnoDB 也會執行相同的處理,並在啟動期間輸出自己的 crash recovery 訊息。這種復原是資料庫的預期行為,因此,對安靜運作中的 PostgreSQL 或 MySQL 建立單一磁碟區快照,通常可以正常還原。
但在某些情況下,crash-consistent 並不足以確保資料正確。若資料分散在兩個磁碟區,root 磁碟與獨立的資料磁碟會在不同時間點建立快照,因此資料檔案與 log 目錄可能互不一致,復原時也沒有正確內容可供重播。應用程式寫入檔案時若未呼叫 fsync,例如尚未完整接收的上傳檔案或佇列檔案,還原後可能會遭到截斷。應用程式保留在記憶體中、再依計時器 flush 的資料,則完全不會出現在映像檔中。
因此,建立快照前,請先將 dump 寫入磁碟。如此一來,無論即時資料檔案處於何種狀態,映像檔中都會包含一個可確認內部一致的檔案。
sudo -u postgres pg_dumpall --clean --file=/var/backups/pg-$(date +%F).sql
sudo mysqldump --single-transaction --routines --all-databases > /var/backups/mysql-$(date +%F).sql--single-transaction 可在不阻塞寫入的情況下,建立一致的 InnoDB 資料表 dump,因為 dump 會在單一 repeatable-read transaction 中執行。它不涵蓋 MyISAM 資料表;這類資料表需要鎖定,或必須先停止伺服器。確認 dump 不為空且未遭截斷後,再信任其內容:完整的 mysqldump 會以 tail -n 1 /var/backups/mysql-$(date +%F).sql 開頭、以 Dump completed 註解結尾。
如果使用獨立的資料磁碟區,可以在快照所需的幾秒鐘內將其 freeze:
sudo fsfreeze -f /srv
# take the snapshot from your provider's panel or API
sudo fsfreeze -u /srv只 freeze 資料磁碟區。絕對不要 freeze /。root 檔案系統一旦 frozen,主機上的所有寫入都會遭到阻塞,包括用來輸入 unfreeze 指令的 shell。如此會把自己鎖在系統外,只能等待 hard reset。
異地備份部分:restic 或 Borg
快照是速度較快的部分。異地副本則是在服務供應商發生問題時仍能保留資料的部分。restic 是很好的預設選擇,因為它會進行重複資料刪除、在用戶端加密,並可寫入相容 S3 的物件儲存、SFTP 或一般目錄。使用 儲存用 VPS 作為異地目標 很適合,因為備份儲存庫需要的是容量,而不是 IOPS。
sudo apt update && sudo apt install -y restic
sudo sh -c 'umask 077; head -c 24 /dev/urandom | base64 > /root/.restic-pass'
sudo chmod 600 /root/.restic-pass
sudo cat /root/.restic-pass現在就將該密語複製到密碼管理器,並儲存在不是這台伺服器的裝置上。沒有密語就無法開啟 restic 儲存庫,也沒有復原途徑。如果密碼的唯一副本位於你剛遺失的伺服器上,備份就只是加密後的無法讀取資料。
export RESTIC_REPOSITORY="s3:https://s3.example.com/vps-backups"
export RESTIC_PASSWORD_FILE=/root/.restic-pass
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
sudo -E restic init
sudo -E restic backup /etc /srv /var/backups
sudo -E restic snapshotssudo -E 會保留這些變數,因為沒有它時,root 會取得乾淨的環境,restic 便會回報未指定儲存庫位置。restic snapshots 應列出你剛才執行的備份,並顯示其主機與路徑。請定期驗證儲存庫本身,並實際讀回部分資料,而不只是檢查結構:
sudo -E restic check --read-data-subset=5%
sudo -E restic restore latest --target /tmp/restore-check
sudo -E restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune未經測試的備份只是推測。至少在另一台 VPS 還原一次並計時,再記下所需時間,因為這個數字才是實際的復原目標。Borg 是另一個可靠的選擇,它會透過 SSH 儲存儲存庫,而不是使用物件儲存;相關取捨請參閱 restic 與 BorgBackup 比較。
複製的 VPS 接近正式環境前要修正的項目
複製品是完全相同的副本。這既是它的賣點,也是問題所在。原始伺服器的所有唯一資訊都會一併複製,導致副本彼此衝突。
重新產生 SSH host keys。 複製品會帶有原始伺服器的 /etc/ssh/ssh_host_* 檔案,因此兩台伺服器會使用相同的主機身分。任何控制其中一台的人,都能向已接受該金鑰的所有用戶端冒充另一台伺服器;SSH 不會發出警告,因為用戶端收到的正是預期中的金鑰。
sudo rm -f /etc/ssh/ssh_host_*
sudo ssh-keygen -A
sudo systemctl restart ssh
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pubssh-keygen -A 會為 daemon 預期的每種類型建立新的金鑰。上一個命令顯示的指紋必須與原始伺服器上的不同。重新啟動 sshd 不會關閉既有連線,因此目前的工作階段仍會保留。請在任何人連線到複製品前完成此操作。若延後處理,所有已信任繼承金鑰的用戶端都會收到 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!,而且必須先執行 ssh-keygen -R <host>。
重設 machine ID。 /etc/machine-id 是 systemd 在第一次開機時產生的一組唯一識別碼,複製品會繼承這組識別碼。
sudo truncate -s 0 /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo ln -s /etc/machine-id /var/lib/dbus/machine-id
sudo reboot空白的 /etc/machine-id 會告訴 systemd 在下一次開機時產生新值,因此要截斷檔案,而不是刪除檔案。識別碼重複時會造成兩項問題。對於透過 DHCP 取得位址的映像檔,systemd-networkd 預設會根據 machine ID 產生 DHCP 用戶端識別碼,因此兩個複製品會以相同用戶端身分要求租約,伺服器也會將相同位址發給兩者。journald 則會在每筆日誌項目上標記 machine ID,因此集中式日誌收集器會將兩台伺服器歸入同一台機器。重新開機後執行 cat /etc/machine-id,確認該值已變更。
變更 hostname。
sudo hostnamectl set-hostname web-02
grep 127.0.1.1 /etc/hostshostnamectl 會寫入 /etc/hostname,並立即套用該名稱。它不會修改 /etc/hosts,因此請編輯 127.0.1.1 行,使其內容一致。若略過此步驟,新名稱將無法解析,因此每次 sudo 呼叫都會等待失敗的查詢,並輸出 sudo: unable to resolve host web-02: Name or service not known。
輪替映像檔中預先寫入的所有憑證。 複製品會保留原始伺服器的 secret,因此兩台機器都能以原始伺服器的身分運作。請逐一處理 SSH authorized_keys 檔案、供應商與 DNS API token、應用程式 .env 檔案、資料庫密碼、TLS 私密金鑰、監控註冊 token,以及 restic repository 密碼。以下命令可找出其中大部分項目:
sudo grep -rIlE 'PASSWORD|SECRET|TOKEN|API_KEY' /etc /srv /opt /home 2>/dev/null
sudo find / -name '.env' -not -path '/proc/*' -not -path '/sys/*' 2>/dev/null如果複製品是永遠不會提供網路流量的測試副本,請予以撤銷,而不是輪替。持有正式環境 API token 的 staging 伺服器,本質上就是修補狀態更差的正式環境伺服器。
停用現在會執行兩次的工作。 兩台伺服器在同一分鐘執行相同的 crontab,會同時存取相同的外部系統。
systemctl list-timers --all
sudo crontab -l
sudo ls -l /etc/cron.d /etc/cron.dailyrestic 的情況值得特別說明,因為它會破壞保留政策,而不只是明確失敗。restic 會以 hostname 標記每個 snapshot,而 restic forget --keep-daily 7 會依主機套用其政策。兩台機器回報相同的 hostname 時,會被視為同一台主機,因此 7 個「daily」snapshot 可能全部來自複製品,而原始伺服器的 snapshot 反而被清理。請在第一次執行備份前修正 hostname,或在複製品上停止該 timer。certbot 的情況較簡單:兩台伺服器為相同名稱續期時,會觸發憑證授權單位的重複憑證速率限制,失敗的執行會輸出該組名稱已簽發過多憑證的錯誤。複製品的網域若仍指向原始伺服器,也無法通過 HTTP challenge,因此請在複製品上停用續期。
處理 monitoring agent。 多數 agent 會以 hostname 或安裝時寫入的 ID 檔案識別,因此兩個 agent 以同一主機回報時,指標會交錯寫入同一個序列。CPU 圖表顯示的數值可能不是任何一台機器實際產生的,警示也會反覆觸發與解除。請在複製品上停止並移除 agent,或依供應商的文件程序,使用新的 hostname 重新註冊。
檢查網路設定是否仍使用原始伺服器的位址。 如果映像檔在 netplan 中包含靜態位址,複製品會宣告屬於另一台機器的 IP。
ip -br addr
sudo grep -r addresses /etc/netplan/如果複製品要成為 template,請清除 cloud-init 狀態。
sudo cloud-init clean --logs這會移除 /var/lib/cloud 下的 cloud-init 狀態,因此下一次開機會再次執行 first-boot modules,其中包括在不存在 SSH host keys 時產生金鑰。部分版本也提供重設 machine ID 的 flag。請在自己的映像檔上執行 cloud-init clean --help,確認該版本支援哪些選項,不要直接相信其他來源列出的 flag。
何時使用哪一種
要回復有風險的升級:建立 snapshot。 在變更前幾分鐘建立 snapshot,執行升級;如果發生問題,再還原 image。還原會捨棄 snapshot 之後的所有寫入內容。因此,對於承載即時網路流量的伺服器,請先傾印資料庫,並確實了解可能遺失的資料時間範圍。對於可以離線十分鐘的主機,do-release-upgrade 使用 snapshot 就能完成整個回復方案。
要遷移到更大的方案:部署 clone。 從 snapshot 在較大的方案上建立 clone,依照上述 identity 清單逐項處理,然後先使用獨立 IP 進行測試,再切換任何網路流量。提前一天降低 DNS TTL,讓切換能快速完成;在新主機承載實際網路流量一段時間前,持續保留原主機運作。先使用 相同的 benchmark 方法測試兩台伺服器,確認較大的方案確實更符合工作負載,因為在較繁忙的硬體上增加 vCPU 不一定代表升級。
要建立 template:為清理完成的機器建立 snapshot。 安裝並強化一台伺服器,然後在建立 image 前移除所有專屬設定。不要保留 host keys,清空 machine ID,不要保留個人 authorized_keys 或 credentials,並清理 cloud-init。接著建立 snapshot。從該 snapshot 部署的每個 instance 都會在第一次開機時產生自己的 identity,因此上述清單不再只是清單。搭配 新 VPS 的標準前十分鐘,讓 template 預先包含原本每次都必須重複執行的工作。
FAQ
VPS snapshot 是備份嗎?
不是,因為它與來源伺服器共用相同的故障網域。Snapshot 位於供應商的儲存空間中,隸屬於你的帳戶,通常也位於相同區域。帳戶遭到停權、API key 被竊,或意外刪除 instance,都可能在一次操作中同時刪除伺服器及其 snapshots;許多供應商也會依設計在刪除 instance 時一併刪除 snapshots。Snapshot 是你能使用的最快回復方式,因此仍應持續建立 snapshots,並在供應商無法控制的基礎架構上保留第二份加密副本。
建立 snapshot 前需要停止資料庫嗎?
不一定,但你必須接受取得的資料狀態。供應商的 snapshot 具備 crash consistency,表示映像檔的狀態等同於磁碟在突然斷電後的狀態。PostgreSQL 和 InnoDB 會在啟動時從這種狀態復原,PostgreSQL 也會在過程中記錄 database system was not properly shut down; automatic recovery in progress。如果資料分散在於不同時間建立 snapshot 的兩個 volume,或應用程式寫入時未使用 fsync,就無法保證復原成功。先將 pg_dumpall 或 mysqldump --single-transaction 寫入磁碟,讓映像檔包含一個你能確認狀態一致的檔案。
為什麼兩台複製的伺服器會爭用相同的 IP address?
因為它們共用 /etc/machine-id。在使用 DHCP 的映像檔中,systemd-networkd 預設會根據 machine ID 建立 DHCP client identifier,因此兩台複製伺服器會以相同的 client 身分要求租約,DHCP server 也會提供相同的 address。將 /etc/machine-id 截斷為 0 bytes,移除 /var/lib/dbus/machine-id,再將它以 symbolic link 連回 /etc/machine-id,最後重新開機,讓 systemd 產生新值。另一個常見原因是 /etc/netplan/ 中寫入了 static address,而複製伺服器會逐字複製該設定;使用 ip -br addr 檢查。
確認複製伺服器可安全投入 production 的最快方式是什麼?
將以下 4 項與原始伺服器比較。在兩台伺服器上執行 ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub,確認 fingerprints 不同。在兩台伺服器上執行 cat /etc/machine-id,確認值不同。執行 hostnamectl status,確認名稱是新的且可解析,這樣 sudo 就不會發出警告。接著執行 systemctl list-timers --all,並停止所有會連線至共用系統的 timer,例如 backup、憑證續期或 monitoring agent,直到你決定哪台機器負責該工作。