Backup PostgreSQL đúng cách: Chiến lược, tự động hóa và test restore thực chiến

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Backup “xong” nhưng restore lại fail — câu chuyện mình đã gặp

Làm việc với PostgreSQL được mấy năm, mình nhận ra phần lớn mọi người setup backup xong là… quên luôn. Chạy pg_dump mỗi đêm, log báo thành công, vậy là yên tâm. Cho đến khi cần restore thật sự mới té ngửa.

Mình đã làm việc với cả MySQL, PostgreSQL và MongoDB. Mỗi cái có điểm mạnh riêng. Nhưng PostgreSQL là cái có hệ sinh thái backup bài bản nhất — nếu biết dùng đúng cách. Bài này mình chia sẻ workflow đang dùng thực tế, bao gồm cả phần test restore mà nhiều người hay bỏ qua.

Bối cảnh: Khi nào cần loại backup nào

PostgreSQL có 3 phương pháp backup chính, và chọn sai có thể khiến bạn mất hàng giờ khi incident xảy ra:

  • pg_dump / pg_dumpall: Backup logic, export ra SQL hoặc custom format. Phù hợp khi cần di chuyển data, backup từng database riêng lẻ, hoặc restore có chọn lọc chỉ một vài table.
  • pg_basebackup: Backup toàn bộ cluster ở cấp filesystem. Nhanh hơn nhiều, phù hợp database lớn, cần thiết khi setup streaming replication.
  • WAL Archiving + PITR: Point-in-time recovery — restore về bất kỳ thời điểm nào trong quá khứ. Phức tạp hơn, nhưng đây là lớp bảo vệ cuối cùng cho production quan trọng.

Rule of thumb đơn giản: database nhỏ hơn 10GB dùng pg_dump là đủ. Trên 10GB hoặc cần PITR thì đầu tư vào pg_basebackup kết hợp WAL archiving.

Cài đặt: Chuẩn bị thư mục và quyền truy cập

Trước khi viết script backup, cần chuẩn bị môi trường đúng cách — bước này nhiều người làm qua loa rồi sau mới bị lỗi permission lúc cron chạy:

# Tạo cấu trúc thư mục backup rõ ràng
sudo mkdir -p /var/backups/postgresql/{daily,weekly,monthly}
sudo chown postgres:postgres /var/backups/postgresql -R
sudo chmod 750 /var/backups/postgresql -R

# Kiểm tra postgres user có thể ghi vào không
sudo -u postgres touch /var/backups/postgresql/daily/.test && echo "OK" || echo "Permission denied"

Cấu trúc daily/weekly/monthly giúp rotation sau này dễ hơn nhiều. Dump tất cả vào một thư mục thì chỉ vài tuần sau là không tìm được gì nữa.

Để cron chạy không cần nhập password, dùng file .pgpass:

# ~/.pgpass — format: hostname:port:database:username:password
echo "localhost:5432:*:postgres:your_password" > /var/lib/postgresql/.pgpass
chown postgres:postgres /var/lib/postgresql/.pgpass
chmod 600 /var/lib/postgresql/.pgpass

Cấu hình chi tiết: Script backup tự động

Backup từng database với pg_dump

Custom format (-Fc) nén tốt hơn SQL thuần khoảng 60–70% — database 10GB thường ra file dump chỉ còn 3–4GB. Ngoài ra nó còn hỗ trợ restore song song với nhiều workers, thứ mà plain SQL không làm được:

#!/bin/bash
# /usr/local/bin/pg_backup_daily.sh

BACKUP_DIR="/var/backups/postgresql/daily"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7
LOG_FILE="/var/log/pg_backup.log"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] === Daily backup started ===" >> "$LOG_FILE"

# Backup từng database, bỏ qua template databases
for DB in $(sudo -u postgres psql -Atc "SELECT datname FROM pg_database WHERE datistemplate = false;"); do
    BACKUP_FILE="$BACKUP_DIR/${DB}_${DATE}.dump"

    sudo -u postgres pg_dump \
        --format=custom \
        --compress=9 \
        --file="$BACKUP_FILE" \
        "$DB" 2>> "$LOG_FILE"

    if [ $? -eq 0 ]; then
        SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
        echo "[OK] $DB → $BACKUP_FILE ($SIZE)" >> "$LOG_FILE"
    else
        echo "[FAIL] Backup failed for database: $DB" >> "$LOG_FILE"
        # Thêm alert ở đây: curl webhook Slack, gửi email, v.v.
    fi
done

# Xóa backup cũ hơn RETENTION_DAYS ngày
find "$BACKUP_DIR" -name "*.dump" -mtime +$RETENTION_DAYS -delete
echo "[INFO] Cleaned up backups older than $RETENTION_DAYS days" >> "$LOG_FILE"

Phần find ... -delete ở cuối quan trọng không kém phần backup. Ổ cứng đầy → cron fail âm thầm → không có backup mới → incident xảy ra. Mình từng thấy case này rồi.

Đặt lịch chạy với cron

# Mở crontab cho user postgres
sudo -u postgres crontab -e

# Daily backup lúc 2:00 AM
0 2 * * * /usr/local/bin/pg_backup_daily.sh

# Weekly backup mỗi Chủ nhật lúc 3:00 AM
0 3 * * 0 /usr/local/bin/pg_backup_weekly.sh

Backup cluster với pg_basebackup (database lớn)

Khi database vượt 10GB, pg_dump bắt đầu chậm đáng kể. Database 20GB chạy pg_dump mất khoảng 15–25 phút tùy I/O — trong khi pg_basebackup copy trực tiếp data files, cùng kích thước đó thường chỉ mất 5–8 phút. Chênh lệch càng lớn khi database tăng:

sudo -u postgres pg_basebackup \
    --pgdata=/var/backups/postgresql/basebackup/$(date +%Y%m%d) \
    --format=tar \
    --compress=9 \
    --progress \
    --checkpoint=fast \
    --verbose

# Kiểm tra kết quả
du -sh /var/backups/postgresql/basebackup/$(date +%Y%m%d)

Kiểm tra & Monitoring: Phần quan trọng nhất thường bị bỏ qua

Test restore định kỳ — bắt buộc phải làm

Mình học bài này theo cách khó: lần đầu cần restore production gấp, mở file dump ra mới phát hiện script đã fail âm thầm suốt 3 tuần vì ổ cứng đầy. Backup mà không test restore thì coi như không có — bạn chỉ biết nó có hoạt động hay không vào đúng lúc tệ nhất.

#!/bin/bash
# /usr/local/bin/pg_test_restore.sh — chạy hàng tuần

BACKUP_FILE=$(ls -t /var/backups/postgresql/daily/*.dump 2>/dev/null | head -1)
TEST_DB="restore_test_$(date +%Y%m%d)"

if [ -z "$BACKUP_FILE" ]; then
    echo "[FAIL] Không tìm thấy file backup!" >&2
    exit 1
fi

echo "=== Testing restore: $BACKUP_FILE ==="

# Tạo database test tạm thời
sudo -u postgres createdb "$TEST_DB"

# Restore với 4 parallel jobs
sudo -u postgres pg_restore \
    --dbname="$TEST_DB" \
    --jobs=4 \
    --verbose \
    "$BACKUP_FILE"

if [ $? -eq 0 ]; then
    TABLE_COUNT=$(sudo -u postgres psql -Atc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public';" "$TEST_DB")
    echo "[OK] Restore thành công — số bảng trong public schema: $TABLE_COUNT"
else
    echo "[FAIL] Restore thất bại! Cần kiểm tra ngay." >&2
fi

# Dọn dẹp database test
sudo -u postgres dropdb "$TEST_DB"
echo "Test database đã được xóa."

Script này tự động lấy backup mới nhất, restore vào database tạm, kiểm tra số table, rồi xóa đi. Chạy mỗi tuần một lần bằng cron, log kết quả lại. Nếu fail thì alert ngay — đừng chờ đến lúc production bốc khói.

Kiểm tra tính toàn vẹn file backup

# Kiểm tra file backup có bị corrupt không (không cần restore thật)
sudo -u postgres pg_restore --list /var/backups/postgresql/daily/myapp_latest.dump > /dev/null 2>&1
if [ $? -eq 0 ]; then
    echo "Backup integrity: OK"
else
    echo "WARNING: File backup có thể bị corrupt!"
fi

# Xem kích thước các backup gần đây — đột ngột nhỏ bất thường là dấu hiệu cần điều tra
ls -lh /var/backups/postgresql/daily/ | tail -7

Một trick đơn giản nhưng hiệu quả: theo dõi kích thước file backup theo ngày. Ví dụ backup thường ngày ra 2GB, hôm nay chỉ còn 200MB mà không có migration hay xóa data nào — đó là tín hiệu cần kiểm tra ngay. Có thể script chỉ capture được một phần data trước khi timeout.

Restore trong tình huống khẩn cấp

Khi incident thật sự xảy ra, đầu óc dễ rối. Chuẩn bị sẵn runbook để chạy không cần nghĩ nhiều:

# Bước 1: Xác định backup cần restore
ls -lt /var/backups/postgresql/daily/ | head -5

# Bước 2: Dừng ứng dụng trước khi restore (quan trọng!)
systemctl stop myapp

# Bước 3: Restore (--clean xóa objects cũ trước, --jobs song song hóa)
sudo -u postgres pg_restore \
    --dbname=myapp_production \
    --clean \
    --if-exists \
    --jobs=4 \
    /var/backups/postgresql/daily/myapp_20240301_020000.dump

# Bước 4: Sanity check nhanh trước khi start lại app
sudo -u postgres psql myapp_production -c "SELECT COUNT(*) FROM users;"
sudo -u postgres psql myapp_production -c "SELECT MAX(created_at) FROM orders;"

# Bước 5: Start lại ứng dụng
systemctl start myapp

Flag --clean --if-exists sẽ drop và recreate objects trước khi restore — tránh conflict với data cũ còn sót lại. Bước sanity check ở bước 4 mất 5 giây nhưng giúp bạn tự tin hơn trước khi start lại app cho user.

Best practices tóm tắt

  • 3-2-1 rule: 3 bản backup, 2 loại storage khác nhau, 1 bản offsite (S3, Backblaze, v.v.)
  • Test restore mỗi tuần: Tự động hóa, đừng chờ đến lúc cần
  • Monitor kích thước backup: File đột nhiên nhỏ bất thường = cần điều tra ngay
  • Dùng custom format -Fc: Nén tốt hơn SQL, hỗ trợ restore song song, linh hoạt hơn
  • Backup trước migrate: Luôn luôn, không ngoại lệ dù thay đổi nhỏ
  • Log đầy đủ + alert khi fail: Backup chạy silent mà fail là worst case
  • Retention policy rõ ràng: Daily 7 ngày, weekly 4 tuần, monthly 3 tháng — cân bằng giữa storage và khả năng rollback

Sau nhiều năm làm với database, mình rút ra một điều: quy trình backup tốt không phải quy trình phức tạp nhất. Đó là quy trình bạn thực sự test được và tin tưởng vào lúc 3 giờ sáng khi mọi thứ đang cháy.

Share: