EMBRA

Engineering / Hướng dẫn thực hành

Backup PostgreSQL: chưa restore thử thì chưa phải backup

Một bản backup chưa từng restore thử mới chỉ là một file. Bài này đi qua bốn bước để file đó dùng được khi mất dữ liệu: dump đúng định dạng, restore thử và đối chiếu, giữ nhiều bản, chép ra ngoài máy.

Mọi lệnh đã chạy trên PostgreSQL 18.6 ngày 02/11/2026, với database thử 200.000 khách và 3 triệu đơn hàng. Hướng dẫn dùng được với VPS, PaaS khác hay tự host; không cần tài khoản Embra.

Trong bài này

Chọn kiểu backup trước

Có hai cách chính. Chúng khác nhau ở chỗ bạn có thể mất bao nhiêu dữ liệu và restore được về lúc nào.

pg_dump định kỳBase backup + WAL (PITR)
Có thể mấtMọi thay đổi sau lần dump gần nhất; dump mỗi ngày thì tới 24 giờPhần WAL chưa kịp đẩy đi, thường vài phút trở xuống
Restore vềĐúng lúc dumpMột thời điểm bất kỳ trong khoảng còn giữ
Phạm viMột databaseCả cluster
Công cụpg_dump, pg_restore có sẵnarchive_command, thường dùng pgBackRest hoặc WAL-G

App nhỏ, mất tối đa một ngày dữ liệu vẫn chấp nhận được: bắt đầu với pg_dump hằng ngày như bài này. Không chấp nhận được: xem phần PITR. Cả hai cách đều cần bước restore thử bên dưới.

Bước 1: Dump đúng định dạng

pg_dump -Fc -d shop -f shop.dump
pg_dumpall --globals-only -f globals.sql

-Fc (custom format) nén sẵn và cho pg_restore chạy song song hoặc chọn từng bảng để restore. Trong lab, database 665 MB cho file 110 MB sau 15 giây. Cùng dữ liệu xuất dạng SQL thường (pg_dump > shop.sql) là 371 MB văn bản.

pg_dump lấy một ảnh nhất quán và không chặn đọc hay ghi của app. Nó giữ lock ACCESS SHARE trên các bảng tới khi dump xong, nên lệnh cần lock độc quyền như ALTER TABLE sẽ phải chờ, và truy vấn tới bảng đó xếp hàng sau lệnh ấy. Tránh chạy migration trong giờ backup.

Bản dump của một database không chứa role và quyền ở cấp cluster; globals.sql giữ phần đó. Dùng pg_dump cùng hoặc mới hơn major version của server.

Mật khẩu kết nối nên nằm trong ~/.pgpass (quyền 0600), không ghi vào lệnh. Mật khẩu trong lệnh sẽ nằm lại trong lịch sử shell và danh sách process.

Bước 2: Restore thử vào một server riêng

Restore vào một server khác, không phải database production. Tạo role trước, rồi database, rồi mới restore:

psql -d postgres -f globals.sql
createdb -O app_owner shop_verify
pg_restore --exit-on-error -j 4 -d shop_verify shop.dump
echo $?   # phải là 0

globals.sql báo role postgres đã tồn tại là bình thường. Trong lab, restore với 4 tiến trình mất 17 giây và trả exit 0.

Vì sao phải có --exit-on-error

Lần thử đầu, chúng tôi restore vào một server chưa có role app_owner và không dùng cờ này. pg_restore vẫn chạy tới cuối và nạp đủ 3 triệu đơn. Nó in 4 lỗi role "app_owner" does not exist, rồi trả exit 1.

Dữ liệu đủ nhưng chủ sở hữu bảng sai. Một script chỉ xem "đã chạy xong" sẽ coi đó là thành công. Với --exit-on-error, lỗi đầu tiên dừng restore ngay và exit code khác 0.

Đối chiếu dữ liệu, không chỉ đếm dòng

Exit 0 chỉ nói restore không lỗi. Để biết dữ liệu đúng, chạy cùng một truy vấn trên nguồn và trên bản restore, rồi so kết quả:

SELECT (SELECT count(*) FROM customers),
       (SELECT count(*) FROM orders),
       (SELECT sum(total) FROM orders),
       (SELECT max(code) FROM orders),
       (SELECT md5(string_agg(code || total, ',' ORDER BY id))
          FROM orders WHERE id % 1000 = 0);

Trong lab, hai bên khớp: 200.000 khách, 3.000.000 đơn, cùng tổng tiền, cùng mã đơn lớn nhất, cùng md5 của mẫu 1/1000 đơn. Thêm một mã đơn bạn biết trước, như DH000123456, và so từng trường.

Lab không có ghi đồng thời lúc dump. Database đang chạy thì số trên nguồn sẽ tăng sau đó; khi ấy so trên các đơn tạo trước giờ dump.

Ghi lại thời gian restore. Đó gần với thời gian app phải dừng nếu phải restore thật.

Bước 3: Mỗi ngày một file mới, giữ nhiều ngày

Lỗi hay gặp nhất là cron ghi đè cùng một file mỗi đêm. Nếu bản đó hỏng, hoặc dữ liệu đã sai từ hôm qua, bạn không còn bản nào cũ hơn.

Script dưới tạo file mới theo giờ UTC, ghi checksum, rồi xoá bản quá hạn:

#!/bin/sh
set -eu
DB=${1:?database name}
DIR=${BACKUP_DIR:?BACKUP_DIR}
KEEP=${KEEP_DAYS:-14}
TS=$(date -u +%Y%m%dT%H%M%SZ)
FILE="$DIR/$DB-$TS.dump"

mkdir -p "$DIR"
pg_dump -Fc -d "$DB" -f "$FILE.part"
mv "$FILE.part" "$FILE"
(cd "$DIR" && sha256sum "$(basename "$FILE")" > "$(basename "$FILE").sha256")

find "$DIR" -maxdepth 1 -name "$DB-*.dump" -mtime +"$((KEEP - 1))" -delete
find "$DIR" -maxdepth 1 -name "$DB-*.dump.sha256" -mtime +"$((KEEP - 1))" -delete
echo "ok $FILE"
# /etc/cron.d/pg-backup: 02:15 mỗi đêm, giữ 14 ngày
15 2 * * * postgres BACKUP_DIR=/var/backups/pg KEEP_DAYS=14 /usr/local/bin/backup.sh shop

Dump vào tên tạm .part rồi mới đổi tên, nên một lần dump dở không trông như file hoàn chỉnh. set -e dừng script với exit khác 0 khi pg_dump lỗi, trước khi kịp xoá bản cũ; file .part còn lại là dấu vết của lần lỗi đó.

Trong lab, với KEEP_DAYS=7 và 10 bản cũ giả lập, script giữ lại 7 bản: 6 bản cũ và bản mới. Chạy lần hai ngay sau đó vẫn tạo file mới, không ghi đè.

Đặt thêm một cảnh báo khi file mới nhất cũ hơn 26 giờ. Cảnh báo đó bắt được trường hợp cron đã ngừng chạy mà không ai biết.

Bước 4: Chép một bản ra ngoài máy

Backup nằm cùng máy với database sẽ mất cùng lúc khi máy hỏng, bị xoá nhầm hay bị chiếm quyền. Giữ ít nhất một bản ở nơi khác, ví dụ object storage S3-compatible ở nhà cung cấp hoặc region khác.

export RCLONE_CONFIG_OFFSITE_TYPE=s3
export RCLONE_CONFIG_OFFSITE_PROVIDER=Other
export RCLONE_CONFIG_OFFSITE_ENDPOINT=https://s3.example.com
# RCLONE_CONFIG_OFFSITE_ACCESS_KEY_ID / _SECRET_ACCESS_KEY: nạp từ file quyền 0600

rclone copy /var/backups/pg offsite:my-pg-backups --include '*.dump' --include '*.sha256'
rclone check /var/backups/pg offsite:my-pg-backups --one-way --download --include '*.dump'

rclone check --download tải lại từng file để so nội dung, không chỉ so kích thước. Trong lab, đích là rclone serve s3 chạy cục bộ thay cho dịch vụ thật; kết quả 8 file khớp.

Khoá truy cập chỉ nên có quyền ghi vào đúng một bucket. Bật versioning hoặc object lock trên bucket để ai chiếm được máy cũng không xoá được bản sao. Dữ liệu có thông tin khách thì mã hoá file trước khi gửi đi.

Khi nào cần PITR

pg_dump mất mọi thay đổi sau lần dump. Nếu app nhận đơn hay thanh toán, mất một ngày thường không chấp nhận được.

PITR (point-in-time recovery) giữ một base backup cộng chuỗi WAL, nhật ký mọi thay đổi. Nhờ đó bạn restore được về một thời điểm cụ thể, ví dụ một phút trước lệnh DELETE nhầm.

Tự cấu hình được bằng archive_command theo tài liệu PostgreSQL. Phần giữ chuỗi WAL liền mạch, xoay vòng và restore nên giao cho công cụ như pgBackRest hoặc WAL-G. Restore thử và bản sao ngoài máy vẫn cần như trên.

Database chạy trên Embra: WAL được đẩy vào backup ít nhất mỗi phút, bản đầy đủ chạy mỗi 7 ngày, và mỗi 7 ngày Embra tự restore thử từ backup rồi hiện kết quả trên console. Backup còn được chép khoảng mỗi phút sang object storage tại Hà Nội, ngoài server chạy database. Chi tiết ở tài liệu backup và khôi phục.

Lỗi hay gặp

Giới hạn của hướng dẫn này

Lab chạy trên một máy dev (8 vCPU, 30 GiB RAM, SSD), dữ liệu tổng hợp 665 MB, không có ghi đồng thời lúc dump. Thời gian trên database thật phụ thuộc kích thước, index và đĩa; hãy đo trên dữ liệu của bạn.

Đích offsite trong lab là rclone serve s3 cục bộ, không phải dịch vụ thật. Bài không thay runbook PITR cho hệ thống của bạn.

Ghi kết quả

Bảng dưới là mẫu để tự điền, không phải kết quả của Embra. "Chưa chạy" khác với "đạt".

Phép kiểmBằng chứng cần lưuKết quả của bạn
DumpFile mới có ngày giờ, checksum, exit 0Chưa chạy
Roleglobals.sql có các role app dùngChưa chạy
Restore thửServer riêng, --exit-on-error, exit 0Chưa chạy
Đối chiếuTruy vấn so sánh khớp; một bản ghi mẫu khớp từng trườngChưa chạy
Thời gian restoreSố phút đo được với dữ liệu cỡ thậtChưa chạy
Giữ nhiều bảnSố bản còn giữ, ngày của bản cũ nhấtChưa chạy
Ngoài máyrclone check --download khớp; nơi lưu khác máy databaseChưa chạy
Cảnh báoBáo khi bản mới nhất cũ hơn 26 giờChưa chạy

Nguồn: tài liệu PostgreSQL 18 về SQL dump, pg_dump, pg_restore, pg_dumpall, continuous archiving và PITR; rclone check. Đối chiếu ngày 02/11/2026.

Đội Embra
Đội xây dựng và vận hành Embra: build, deploy, PostgreSQL, backup và restore cho app chạy tại Việt Nam. Người xây dựng · GitHub

Chạy app có Postgres tại Việt Nam, Embra lo diễn tập migration, backup và restore.

Bắt đầu Bảng giá · Tài liệu