Bản sao lưu hàng đêm giúp bạn quay về nửa đêm hôm qua. Khôi phục theo thời điểm giúp bạn quay về một giây trước khi ai đó gõ nhầm. Phần này chạy đủ quy trình đó từ đầu tới cuối.
Ba thành phần bắt buộc
Thiếu một cái là không làm được, và điều đó phải chuẩn bị trước khi tai nạn xảy ra.
1. Lưu trữ WAL. Mọi tệp WAL phải được sao ra khỏi máy chủ trước khi PostgreSQL tái sử dụng chúng:
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /wal/%f && cp %p /wal/%f'
Mệnh đề test ! -f là quan trọng: nó từ chối ghi đè tệp đã có. Nếu archive_command trả về mã 0 mà không thật sự lưu tệp, PostgreSQL coi như xong và xoá bản gốc — mất vĩnh viễn.
2. Ảnh chụp gốc định kỳ. Trên cơ sở dữ liệu thử của tôi, pg_basebackup mất 2,4 giây cho 551 MB.
3. Mọi tệp WAL từ lúc chụp trở đi. Kho của tôi có 160 tệp, mỗi tệp 16 MB.
Kiểm việc lưu trữ có chạy không:
select archived_count, failed_count, last_archived_time
from pg_stat_archiver;
failed_count khác 0 nghĩa là chuỗi WAL bị đứt, và bạn chỉ khôi phục được tới chỗ đứt đó.
Kịch bản tai nạn
14:53:53 tạo bảng, chèn 1.000 dòng
14:53:56 MỐC AN TOÀN
14:53:58 delete from quan_trong where id > 100 -> còn 100 dòng
14:53:59 hệ thống ghi tiếp 101 dòng mới -> tổng 201 dòng
Đây là hình dạng thật của một tai nạn: không phải "xoá rồi phát hiện ngay", mà là "xoá, rồi hệ thống chạy tiếp và ghi thêm dữ liệu hợp lệ".
Không thể chỉ hoàn tác câu DELETE — giao dịch đã commit. Và không thể chỉ khôi phục bản sao lưu hàng đêm — bạn sẽ mất luôn 101 dòng ghi sau đó.
Quy trình khôi phục
Không đụng vào máy chủ đang chạy. Dựng một bản riêng, ở cổng khác:
cp -a /base /pitr
cat > /pitr/postgresql.auto.conf <<EOF
restore_command = 'cp /wal/%f %p'
recovery_target_time = '2026-08-29 14:53:56.093594+00'
recovery_target_action = 'promote'
EOF
touch /pitr/recovery.signal
pg_ctl -D /pitr -o '-p 5433' start
Ba dòng cấu hình:
restore_command — ngược với archive_command: nó nói cho PostgreSQL cách lấy một tệp WAL từ kho về.
recovery_target_time — mốc dừng. PostgreSQL phát lại WAL cho tới đúng đó rồi ngừng.
recovery_target_action = 'promote' — sau khi tới mốc thì mở cho ghi. Mặc định là pause, nghĩa là cơ sở dữ liệu dừng ở chế độ chỉ đọc và đợi bạn gọi pg_wal_replay_resume(). Mặc định đó an toàn hơn: bạn kiểm tra dữ liệu trước rồi mới quyết định.
Tệp recovery.signal là thứ báo cho PostgreSQL biết đây là một lần khôi phục chứ không phải khởi động bình thường. Từ PostgreSQL 12 nó thay cho tệp recovery.conf cũ.
Sẵn sàng sau 2,3 giây.
Bằng chứng
Log của bản khôi phục:
LOG: consistent recovery state reached at 0/9D000100
LOG: recovery stopping before commit of transaction 797, time 2026-08-29 14:53:58.179402+00
LOG: redo done at 0/9E0557A8
LOG: archive recovery complete
Dòng thứ hai là dòng quan trọng: nó dừng trước khi commit giao dịch 797 — chính là giao dịch chứa câu DELETE.
Kết quả:
| Số dòng | |
|---|---|
| Bản gốc (cổng 5432) | 201 |
| Bản khôi phục (cổng 5433) | 1.000 |
| Dòng ghi sau tai nạn lọt vào bản khôi phục | 0 |
Đúng trạng thái trước tai nạn.
Tìm chính xác giao dịch gây tai nạn
Đặt mốc theo thời gian đòi bạn biết tai nạn xảy ra lúc mấy giờ mấy giây. Thường thì không ai biết chính xác.
pg_waldump đọc được nội dung WAL:
$ pg_waldump /wal/00000001000000000000009E | grep -iE 'DELETE|COMMIT'
tx: 795, desc: COMMIT 2026-08-29 14:53:53.886998 UTC
tx: 796, desc: COMMIT 2026-08-29 14:53:53.946207 UTC
tx: 797, desc: DELETE xmax: 797, off: 101, blkref #0: rel 1663/5/24652 blk 0
tx: 797, desc: DELETE xmax: 797, off: 102, blkref #0: rel 1663/5/24652 blk 0
Thủ phạm hiện rõ: giao dịch 797, và giao dịch tốt cuối cùng là 796.
Nên có thể đặt mốc theo số giao dịch thay vì theo thời gian:
recovery_target_xid = '796'
Đo được: recovery stopping after commit of transaction 796 và bảng có đúng 1.000 dòng.
Cách này chính xác hơn hẳn mốc thời gian, vì nó không phụ thuộc vào việc đồng hồ có chuẩn hay không, và không có rủi ro chọn nhầm vào giữa một giao dịch dài.
Bốn kiểu mốc
| Tham số | Dừng ở |
|---|---|
recovery_target_time |
Một thời điểm |
recovery_target_xid |
Sau một giao dịch cụ thể |
recovery_target_lsn |
Một vị trí trong WAL |
recovery_target_name |
Một mốc do bạn đặt trước bằng pg_create_restore_point() |
Mốc thứ tư đáng biết: trước khi chạy một lần di trú rủi ro, gọi
select pg_create_restore_point('truoc_di_tru_v42');
Sau đó khôi phục về đúng chỗ đó bằng tên, không cần nhớ giờ.
Còn recovery_target_inclusive (mặc định on) quyết định mốc là "tới và bao gồm" hay "tới nhưng không bao gồm".
Đặt mốc sai thì sao
Tôi thử đặt mốc về năm 2020 — trước cả ảnh chụp gốc:
LOG: recovery stopping before commit of transaction 795, time 2026-08-29 14:53:53.886998+00
LOG: pausing at the end of recovery
HINT: Execute pg_wal_replay_resume() to promote.
Nó không báo lỗi. Nó dừng ở điểm nhất quán sớm nhất có được, rồi tạm dừng và đợi.
Đây là hành vi hợp lý nhưng dễ gây hiểu nhầm: bạn nghĩ mình khôi phục về 2020, thực ra bạn nhận trạng thái tại thời điểm ảnh chụp gốc. Luôn kiểm dữ liệu trước khi promote — và đó chính là lý do recovery_target_action mặc định là pause.
Vài điều đáng nhớ
Khôi phục vào chỗ khác, đừng ghi đè bản gốc. Trong bài này bản khôi phục chạy ở cổng 5433 song song với bản gốc ở 5432. Nếu chọn sai mốc, bạn thử lại; nếu đã ghi đè, bạn không còn gì để thử lại.
Chuỗi WAL không được đứt. Thiếu một tệp ở giữa là dừng ở đó. Đó là lý do failed_count trong pg_stat_archiver phải luôn bằng 0, và tại sao mệnh đề test ! -f trong archive_command quan trọng.
Thời gian khôi phục tỉ lệ với lượng WAL phải phát lại. Ảnh chụp càng cũ thì càng nhiều WAL. Chụp hàng ngày thì tệ nhất là phát lại một ngày; chụp hàng tuần thì tệ nhất là một tuần.
Kho WAL lớn hơn bạn nghĩ. Phần 46 đã đo: một câu UPDATE trên bảng 40 MB sinh 126 MB WAL. Kho lưu trữ cần dung lượng cho toàn bộ WAL giữa hai lần chụp gốc, và con số đó thường lớn hơn chính cơ sở dữ liệu.
Công cụ chuyên dụng làm việc này tốt hơn. pgBackRest và WAL-G lo giúp việc nén, dọn WAL cũ, kiểm tra tính toàn vẹn, và sao lưu tăng dần. Bài này dùng công cụ có sẵn để cho thấy cơ chế; trên máy chủ thật thì nên dùng chúng.
Thử ba mươi giây
select archived_count, failed_count,
last_archived_file, last_archived_time,
last_failed_file, last_failed_time
from pg_stat_archiver;
Ba câu hỏi:
archive_modecó bật không? Nếu không, bạn không có khôi phục theo thời điểm — chỉ có bản sao lưu hàng đêm.failed_countcó bằng 0 không? Khác 0 nghĩa là chuỗi WAL có lỗ.last_archived_timecó gần đây không? Nếu nó từ ba ngày trước mà hệ thống vẫn ghi, việc lưu trữ đã chết mà không ai biết.
Và câu hỏi cuối, quan trọng nhất: bạn đã thử khôi phục lần nào chưa? Toàn bộ quy trình trong bài này mất 2,3 giây trên dữ liệu 551 MB. Nếu chưa từng chạy thử, hôm nay là ngày tốt hơn hai giờ sáng thứ Bảy.
Phần sau đo sao chép luồng: dựng máy dự phòng, đo độ trễ thật, và chuyện gì xảy ra khi nó tụt lại quá xa.