CKH 網誌

PostgreSQL 16 備份實作:每日排程、保留策略與還原驗證

本次實測環境

虛擬化平台Proxmox VE 9.2.18,新建測試 VM
CPU2 vCPU(x86-64-v2-AES)
記憶體4 GiB RAM
虛擬磁碟32 GiB,HDD-backed、VirtIO SCSI、ext4
作業系統Ubuntu 24.04.5 LTS,amd64
PostgreSQL16.15,Ubuntu 原生套件
驗證日期2026-10-02

本文使用新建的 Ubuntu 測試主機實際執行。這是本次實測配置,並非最低硬體需求;以少量範例資料確認功能,不作為效能或大規模負載測試結果。

實測結果:備份腳本、SHA-256、互斥鎖、保留檔案預覽、隔離還原與重開機驗證均通過;損壞的備份會被拒絕。timer 已實際觸發備份,測試時加速觸發後恢復每日排程,沒有等待隔夜排程。本文尚未實測異地傳送、PITR 或大量資料負載。

上一篇完成了 Ubuntu 24.04 安裝 PostgreSQL 16,這一篇把備份做成每天能執行、出錯能追查、需要時能還原的流程。沿用範例資料庫 appdb 與應用帳號 app_user,採原生工具與 systemd,不使用 Docker。

本文範例適用於本機 PostgreSQL 16、Unix socket 目錄 /var/run/postgresql、連接埠5432,且 Linux 的 postgres 帳號可使用 peer 驗證。若你的版本、叢集或連接埠不同,請先對照 pg_lsclusters;以下路徑都是教學範例。

終端畫面說明:下方截圖來自測試主機實際執行的 PTY 紀錄,使用瀏覽器終端樣式呈現;提示符統一為 lab$,不顯示內網主機識別。echo $? 的0表示上一個指令成功。畫面沒有改寫執行結果,文字指令仍保留供複製。

一、先決定要能救回什麼

先寫下兩個目標:最多能接受遺失多久的資料,以及多久內必須恢復服務。每天一次的備份,在每次都成功的前提下,可能遺失接近一天的變更;若失敗沒人發現,缺口會更大。還原需要的時間,則要靠實際演練量測。

這篇使用 pg_dump 的 custom 格式,保存一個資料庫的結構與資料。它能在資料庫仍有讀寫時取得一致的備份,但大型資料庫仍會增加磁碟與 CPU 負擔,應避開尖峰與結構異動。這種備份沒有涵蓋整台主機、應用程式上傳檔案或所有伺服器設定,工具範圍可查 pg_dump 官方文件。

二、準備只供備份使用的目錄

使用有 sudo 權限的一般帳號執行。若這個目錄已存在,先確認用途與現有檔案,不要直接套用到共用目錄。此範例讓 postgres 帳號寫入自己的備份,其他一般帳號無法讀取。

pg_lsclusters
sudo install -d -o postgres -g postgres -m 0700 /var/backups/postgresql/appdb
sudo -u postgres /usr/lib/postgresql/16/bin/psql \
  -h /var/run/postgresql -p 5432 -d appdb \
  -c "SELECT current_database(), current_user;"
df -h /var/backups/postgresql/appdb

請先確認連線成功、資料庫名稱正確、磁碟有足夠空間。本篇透過本機 peer 驗證,不把密碼放進腳本。若改成跨主機備份,還需要另行規劃 TLS、備份角色與憑證保管。

三、建立每天可重複執行的備份腳本

用 sudoedit /usr/local/sbin/backup-appdb 建立下列檔案;若已有同名腳本,先保存原檔並確認用途。腳本先寫入暫存檔,成功檢查後才改成正式名稱,避免把失敗的半成品當作完成的備份。

#!/bin/bash
set -euo pipefail
umask 077

backup_dir=/var/backups/postgresql/appdb
pg_bin=/usr/lib/postgresql/16/bin
cd "$backup_dir"

exec 9>.backup.lock
flock -n 9 || { echo "Another backup is running" >&2; exit 1; }

stamp=$(date -u +%Y%m%dT%H%M%SZ)
name="appdb-${stamp}.dump"
temp_file=$(mktemp .appdb-XXXXXX.partial)
trap 'rm -f -- "$temp_file"' EXIT

"$pg_bin/pg_dump" -h /var/run/postgresql -p 5432 \
  -U postgres -d appdb -Fc -f "$temp_file"
test -s "$temp_file"
"$pg_bin/pg_restore" --list "$temp_file" > /dev/null

mv -n -- "$temp_file" "$name"
test ! -e "$temp_file"
sha256sum "$name" > "$name.sha256"
sha256sum --check "$name.sha256"
printf 'Backup completed: %s\n' "$name"

set -euo pipefail 讓錯誤停止流程;flock 防止本腳本同時執行;umask 077 限制新檔權限。正式檔名使用 UTC 時間。最後的 SHA-256 用於確認檔案複製前後一致,不能證明資料庫一定能還原;pg_restore --list 也只是檢查備份目錄可讀。

sudo chown root:postgres /usr/local/sbin/backup-appdb
sudo chmod 0750 /usr/local/sbin/backup-appdb
sudo bash -n /usr/local/sbin/backup-appdb
sudo -u postgres /usr/local/sbin/backup-appdb
sudo -u postgres ls -lh /var/backups/postgresql/appdb

先手動跑通再排程。備份檔雖有權限限制,仍是未加密的資料;postgres 帳號也能修改這個目錄,因此它只適合作為本機備份起點,還需要獨立保存的副本。

實際執行 backup-appdb 並完成 SHA-256 校驗
圖1:手動執行備份腳本與 SHA-256 校驗;systemd Result 為前一次服務執行的結果。(點圖可開啟原尺寸)

四、交給 systemd 每天執行

用 sudoedit /etc/systemd/system/appdb-backup.service 建立服務。服務以 postgres 執行,腳本本身由 root 管理;不要把 postgres 設成腳本擁有者。

[Unit]
Description=Logical backup of appdb
After=postgresql@16-main.service
Requires=postgresql@16-main.service

[Service]
Type=oneshot
User=postgres
Group=postgres
UMask=0077
ExecStart=/usr/local/sbin/backup-appdb
TimeoutStartSec=2h

再用 sudoedit /etc/systemd/system/appdb-backup.timer 建立排程。以下指定台北時間每天03:15,最多再延遲5分鐘,避免多個工作同時搶磁碟。服務的兩小時上限需要依實際資料量調整。

[Unit]
Description=Daily appdb backup

[Timer]
OnCalendar=*-*-* 03:15:00 Asia/Taipei
RandomizedDelaySec=5m
Persistent=true
Unit=appdb-backup.service

[Install]
WantedBy=timers.target

Persistent=true 會在排程恢復啟用時補一次錯過的日曆觸發;不會把停機期間每一天的資料補成歷史備份。它也沒有提供外部通知,排程語意可查 systemd 255 的 timer 文件。

sudo systemd-analyze verify /etc/systemd/system/appdb-backup.service \
  /etc/systemd/system/appdb-backup.timer
systemd-analyze calendar '*-*-* 03:15:00 Asia/Taipei'
sudo systemctl daemon-reload
sudo systemctl start appdb-backup.service
sudo systemctl enable --now appdb-backup.timer
systemctl list-timers appdb-backup.timer --all

五、確認完成,而不只看排程已啟用

systemctl show appdb-backup.service -p Result -p ExecMainStatus
sudo journalctl -u appdb-backup.service -n 50 --no-pager
sudo -u postgres ls -lh /var/backups/postgresql/appdb
df -h /var/backups/postgresql/appdb

成功的一次應有 Result=success、ExecMainStatus=0,日誌包含完成訊息,且有相配的 .dump 與 .sha256。oneshot 服務做完後顯示 inactive 是正常的;若還在執行,狀態欄位可能尚未代表本次完成結果。

正式使用時,應監控「最後成功時間是否超過預期」、「磁碟剩餘容量」與「服務是否失敗」,並配置你會收到的通知管道。本文沒有替讀者設定通知,也沒有自動清除舊備份。

六、還原到另一個資料庫,驗證可用

從清單選定一份備份,把下方的範例檔名換成實際名稱。確認 appdb_restore_check 尚未存在;還原目標使用新資料庫,保留正式的 appdb。此練習只適用於前文由 app_user 擁有物件的簡單 appdb,沿用已存在的角色與擴充套件環境。

backup_dir=/var/backups/postgresql/appdb
name=appdb-20261002T031500Z.dump
backup_file="$backup_dir/$name"

sudo -u postgres bash -c 'cd "$1" && sha256sum --check "$2.sha256"' \
  bash "$backup_dir" "$name"

sudo -u postgres /usr/lib/postgresql/16/bin/createdb \
  -h /var/run/postgresql -p 5432 --owner=app_user appdb_restore_check

sudo -u postgres /usr/lib/postgresql/16/bin/pg_restore \
  -h /var/run/postgresql -p 5432 \
  --exit-on-error --single-transaction \
  --no-owner --role=app_user \
  --dbname=appdb_restore_check "$backup_file"

sudo -u postgres /usr/lib/postgresql/16/bin/psql \
  -h /var/run/postgresql -p 5432 -d appdb_restore_check \
  -c "SELECT id, note FROM public.install_check;"

--single-transaction 將這次還原包在一個交易中,錯誤時不留下部分還原的物件;資料庫本身仍會存在。--no-owner --role=app_user 讓本例還原物件由 app_user 建立,並非複製原有多角色擁有者配置,選項細節見 pg_restore 官方文件。

若沿用上一篇測試資料,應能讀到 install_check 的文字。接著讓測試程式連到這個新資料庫,確認重要查詢、資料量、索引及應用功能。持續變動的正式資料不能直接與昨晚備份逐筆比較;應依備份時間與業務規則確認。記錄本次用的檔名、還原耗時、檢查結果,才有可追查的演練紀錄。

建立 appdb_terminal_restore,還原備份並查詢 PostgreSQL 安裝完成資料
圖2:此次補拍使用全新的 appdb_terminal_restore,避免覆蓋先前演練資料庫;還原指令及資料查詢均成功。(點圖可開啟原尺寸)

七、保留多個版本,另存獨立副本

小型服務可先採「每日保留14份、每週另存一份保留8週」,再依資料量與可接受的回溯範圍調整。週備份應另外分類,不能讓每日清理規則一併移除。資料被誤改數天後才發現時,只有最新一份往往不夠。

下方只列出已滿14個24小時的每日 dump 候選檔案,不刪除。實際清理前,先確認較新備份成功、獨立副本可讀,並把對應的 checksum 檔案當作一組管理。空間不足或連續失敗時,應先排查原因,避免清掉唯一可還原的一份。

sudo -u postgres sh -c '
  cd /var/backups/postgresql/appdb &&
  find . -maxdepth 1 -type f -name "appdb-*.dump" -mtime +13 -print
'

這裡先切換到 postgres 可進入的備份目錄,再執行 find。若直接從只有一般帳號可讀的家目錄啟動 find,部分環境會在結束時出現「Failed to restore initial working directory」;上面的寫法已在私有家目錄環境實測。

同一台主機、同一顆磁碟上的副本,遇到磁碟故障仍可能一起消失。可依需求另存另一台設備、離線磁碟或異地儲存;備份含資料內容,傳輸要加密,遠端權限盡量只允許新增,並使用版本保留或不可變保存。這份教學尚未包含自動傳送與加密程式,採用前需要把副本保存流程一併完成。

八、別漏掉角色、設定與網站檔案

單一資料庫 dump 不保存全伺服器角色與 tablespace 定義。若要搬到全新主機,可另外保存共用物件;以下在一般帳號自己的私有目錄執行,刻意不輸出角色密碼雜湊。

umask 077
globals_file="$PWD/postgresql-globals-$(date -u +%Y%m%dT%H%M%SZ).sql"
sudo -u postgres /usr/lib/postgresql/16/bin/pg_dumpall \
  -h /var/run/postgresql -p 5432 \
  --globals-only --no-role-passwords > "$globals_file"

新環境套用前要先檢視角色與 tablespace 定義,對照既有名稱、路徑及權限,不要直接把整份 SQL 套回正式叢集。因為沒有保存密碼,還原角色後必須重新設定需要密碼登入的角色,例如以管理身分執行 \password app_user;相關限制見 pg_dumpall 官方文件。

此外還要保存並驗證 PostgreSQL 設定、擴充套件需求、應用程式設定與上傳檔案。資料庫與檔案若互相引用,需要設計相配的備份時間,例如短暫停止應用寫入;只對其中一邊備份,可能留下不存在的檔案連結。

需要精確回到某個時間,再規劃 PITR

每天一次的 dump 適合這份入門流程;若要救回備份之後的變更,或還原到誤刪前的某一刻,需要另行設計實體基礎備份、連續 WAL 歸檔與復原演練。只有 dump 或只有 WAL 檔案都不構成完整 PITR 流程,可從 PostgreSQL 的連續歸檔與 PITR 文件 開始。

先讓第一份備份成功、還原成功,再把排程、通知、保留與獨立副本補齊。每次調整資料結構、擴充套件或版本後,再做一次還原演練,才能確認備份仍符合現在的服務。