Phần 3 đo WAL như một chi phí. Phần này dùng nó như một cỗ máy thời gian. Tình huống là thứ ai vận hành cơ sở dữ liệu cũng gặp ít nhất một lần: một câu DELETE thiếu WHERE chạy lúc 08:22:48, ứng dụng vẫn ghi tiếp, và mười phút sau có người hỏi "lấy lại được không". Bản dump đêm qua trả lời "được, nhưng mất cả ngày hôm nay". Basebackup cộng WAL lưu trữ trả lời "được, về đúng 08:22:47". Phần này dựng đúng tình huống đó, khôi phục năm lần với năm đích khác nhau, và đếm dòng.

Cỗ máy thời gian: basebackup, kho WAL, và một đích dừng

Ảnh chụp đoạn mã nền tối PITR quay về một giây trước câu DELETE nhầm với PostgreSQL 16 basebackup 700 MB qua mạng 1,05 giây cộng WAL lưu trữ archive_command cp khôi phục vào container mới, kịch bản 15 giây chèn 100 dòng mỗi giây điểm mốc DELETE nhầm chèn tiếp 10 giây pgbench chạy nền suốt gồm select pg_create_restore_point truoc_delete tại T_rp đặt mốc có tên trước thao tác nguy hiểm, delete from orders_live tại T_del nhầm 1500 dòng bay, ứng dụng vẫn chạy 1000 dòng mới chèn sau đó WAL tiếp tục được lưu trữ, khôi phục bản basebackup cộng recovery.signal cộng đích dừng gồm cp -a basebackup sang thư mục dữ liệu mới touch recovery.signal, postgres -c restore_command cp /wal/%f %p lấy WAL từ kho lưu trữ, -c recovery_target_time 2026-11-08 08:21:00 dừng trước giao dịch commit sau mốc này, -c recovery_target_inclusive false, -c recovery_target_action promote xong thì mở ghi timeline mới, hoặc -c recovery_target_name truoc_delete mốc có tên không cần đoán giờ, hoặc -c recovery_target_lsn, ba điều WAL replay quyết định gồm dừng ở đâu recovery_target giao dịch commit sau đích không được áp lệch 1 giây là khác hẳn kết quả, dừng xong làm gì promote ra timeline 2 WAL mới ghi dưới tên 00000002 hoặc pause xem trước rồi quyết, đích không tới được đích trước lúc basebackup kết thúc thì FATAL máy chủ không lên, đo gì gồm R1 đích T_del trừ 1 giây còn bao nhiêu dòng timeline, R2 đích T_del cộng 1 giây còn gì, R3 đích mốc truoc_delete giống R1 không đoán giờ, R4 đích trước basebackup lỗi gì, mỗi lần thời gian khởi động cộng replay số segment WAL kéo về tốc độ replay MB mỗi giây

Hình 1: Basebackup là điểm xuất phát, kho WAL là đường đi, recovery_target là chỗ dừng. Dừng xong thì promote sang timeline mới.

PITR (point-in-time recovery) cần ba thứ. Một basebackup làm điểm xuất phát — phần 1 đã đo: 700 MB chép qua mạng trong 1,05 giây, tự thân chưa nhất quán. Kho WAL liên tục từ lúc basebackup tới hiện tại — archive_command của phần 3 đã đẩy từng segment 16 MB vào /wal. Và một đích dừng: recovery_target_time, recovery_target_name (mốc đặt trước bằng pg_create_restore_point), hoặc recovery_target_lsn.

Quy trình khôi phục: chép basebackup vào thư mục dữ liệu mới, tạo tệp recovery.signal, khai restore_command để máy chủ tự kéo WAL từ kho, và khai đích. Máy chủ replay WAL từ đầu basebackup; khi gặp bản ghi commit có thời điểm sau đích (với recovery_target_inclusive=false), nó dừng, không áp giao dịch đó, và làm theo recovery_target_action: promote mở ghi trên một timeline mới, pause giữ ở chế độ chỉ đọc để bạn kiểm tra trước. Timeline mới là cách PostgreSQL tách lịch sử "đã quay lại" khỏi lịch sử cũ — WAL sinh ra sau promote mang tên 00000002…, không đè lên segment cũ trong kho.

Kịch bản dựng thật: bảng orders_live nhận 100 dòng mỗi giây trong 15 giây, pgbench 4 client chạy nền để WAL có khối lượng thật; lúc 08:22:47,40 đặt mốc truoc_delete; lúc 08:22:48,63 chạy DELETE FROM orders_live — 1.500 dòng bay; rồi chèn tiếp 10 giây nữa, 1.000 dòng mới. Production kết thúc với 1.000 dòng và 50 segment WAL (785 MB) trong kho.

Năm lần khôi phục, năm kết quả

Ảnh chụp output thật nền tối lệch 1 giây 1500 dòng hay 100 dòng với basebackup 700 MB lúc 08:20:11 DELETE nhầm lúc 08:22:48,63 50 segment WAL 785 MB lưu trữ container mới mỗi lần, kịch bản ghi lại từ máy chủ gồm 08:22:32 bắt đầu chèn 100 dòng mỗi giây vào orders_live pgbench c 4 chạy nền, 08:22:47,40 pg_create_restore_point truoc_delete 1500 dòng, 08:22:48,63 DELETE FROM orders_live 0 dòng, 08:23:00 dừng sau DELETE đã chèn thêm 1000 dòng mới production giờ có 1000 dòng mất 1500, năm lần khôi phục từ cùng một basebackup cộng kho WAL gồm R1 time T_del trừ 1 giây 08:22:47,63 tổng 1878 ms 29 segment redo 1,06 giây 1500 dòng t_max 08:22:46 timeline 2, R2 time T_del cộng 1 giây 08:22:49,63 1886 ms 30 segment redo 1,26 giây 100 dòng chỉ còn dòng chèn sau khi xoá, R3 name truoc_delete 2955 ms 29 segment redo 1,82 giây 1500 dòng dừng đúng mốc không đoán giờ, R4 time 08:19 trước basebackup 538 ms 2 segment redo 0,01 giây không lỗi dừng ở commit đầu tiên sau khi nhất quán orders_live chưa tồn tại tạo lúc 08:22:32, R5 không đích replay hết WAL 3763 ms 48 segment redo 3,03 giây 1000 dòng y hệt production DELETE vẫn còn, log của R1 những dòng đáng đọc gồm starting point-in-time recovery to 2026-11-08 08:22:47.63, database system is ready to accept read-only connections đã nhất quán còn đang replay hot standby, recovery stopping before commit of transaction 5054799 time 08:22:47.630039 giao dịch đầu tiên sau đích, redo done at 5 73CECE58 elapsed 1.06 s, selected new timeline ID 2 archive recovery complete ready to accept connections, tốc độ replay R1 29 nhân 16 MB bằng 464 MB trong 1,06 giây khoảng 440 MB mỗi giây R5 48 nhân 16 MB bằng 768 MB trong 3,03 giây khoảng 250 MB mỗi giây nên RTO của PITR xấp xỉ chép basebackup cộng WAL từ basebackup tới đích chia tốc độ replay backup càng cũ WAL càng dài, nói thẳng ready to accept read-only connections xuất hiện khi mới nhất quán script đo lần đầu đọc số lúc đó và thấy 500 dòng phải chờ pg_is_in_recovery false mới là xong cùng một sai lầm sẽ làm runbook thật báo đã khôi phục sớm

Hình 2: Cùng basebackup, cùng kho WAL; đích lệch một giây cho 1.500 dòng hay 100 dòng. Mốc có tên dừng đúng chỗ. Replay 250–440 MB/s.

lần đích thời gian segment kết quả
R1 time = T_del − 1 s 1.878 ms 29 1.500 dòng, timeline 2
R2 time = T_del + 1 s 1.886 ms 30 100 dòng (chỉ dòng chèn sau DELETE)
R3 name = truoc_delete 2.955 ms 29 1.500 dòng, dừng đúng mốc
R4 time trước basebackup 538 ms 2 không lỗi; dừng ở commit đầu sau khi nhất quán, bảng chưa tồn tại
R5 không đích 3.763 ms 48 1.000 dòng = production, DELETE vẫn còn

Một giây trước: đủ 1.500 dòng; một giây sau: 100 dòng. R1 dừng tại recovery stopping before commit of transaction 5054799, time 08:22:47.630039 — giao dịch đầu tiên có commit sau đích — và orders_live có đúng 1.500 dòng, dòng mới nhất lúc 08:22:46. R2 với đích chỉ lệch hai giây sang bên kia câu DELETE: 100 dòng, toàn bộ là dòng chèn sau khi xoá. Cùng một basebackup, cùng kho WAL, cùng lệnh; thứ khác nhau là một con số trên dòng lệnh. Đây là lý do PITR cần biết chính xác lúc nào tai nạn xảy ra — từ log ứng dụng, từ log_statement, hay từ pg_stat_statements — trước khi chạy bất cứ thứ gì.

Mốc có tên bỏ việc đoán giờ. R3 dùng recovery_target_name = 'truoc_delete': dừng tại recovery stopping at restore point "truoc_delete", 1.500 dòng, không cần biết DELETE xảy ra lúc mấy giờ mấy phút mấy giây. pg_create_restore_point tốn một bản ghi WAL và không tốn gì khác; đặt nó trước mỗi migration, mỗi lần chạy script xoá, mỗi lần "chỉ sửa nhanh trên production" — và đích khôi phục có sẵn tên, không phải hẹn giờ.

Đích trước basebackup không báo lỗi — và đó là cái bẫy. R4 nhắm 08:19:00, trước cả lúc basebackup bắt đầu (08:20:11). Không có FATAL. Máy chủ replay tới điểm nhất quán (bắt buộc), gặp commit đầu tiên sau đó lúc 08:22:32, thấy nó muộn hơn đích, dừng và promote. Kết quả là trạng thái lúc kết thúc basebackup: orders_live chưa tồn tại vì bảng được tạo sau đó. Nếu runbook chỉ kiểm "máy chủ lên chưa" thì nó đã lên, trên timeline 2, với dữ liệu của một thời điểm không ai yêu cầu. Muốn về trước basebackup này thì phải dùng basebackup cũ hơn — đó là lý do giữ nhiều hơn một bản.

Không đích thì về đúng hiện tại, kể cả câu DELETE. R5 bỏ recovery_target_*: replay hết 48 segment, promote, 1.000 dòng — y hệt production. Đây không phải PITR, đây là khôi phục sau crash dùng kho WAL; nó trả lời câu "máy chủ cháy" chứ không trả lời câu "xoá nhầm".

Replay 250–440 MB/s, và RTO là phép cộng. R1 kéo 29 segment (464 MB) và redo mất 1,06 giây; R5 kéo 48 segment (768 MB) trong 3,03 giây. Thời gian khôi phục bằng chép basebackup cộng WAL từ basebackup tới đích chia tốc độ replay cộng khởi động. Basebackup càng cũ, đoạn WAL càng dài; với 25 MB/s WAL của tải này, basebackup một ngày tuổi là 2 TB WAL phải replay — hàng giờ. Lịch basebackup vì thế không phải "hằng tuần cho đỡ tốn", mà là hàm của tốc độ sinh WAL và RTO chấp nhận được.

Bẫy khi đo — và khi vận hành. Lần chạy đầu của script đọc số liệu ngay khi pg_isready trả lời, và thấy 500 dòng, in_recovery = true. database system is ready to accept read-only connections xuất hiện lúc máy chủ vừa nhất quán và còn đang replay (hot standby). "Xong" là khi pg_is_in_recovery() trả về false sau promote. Một runbook chờ cổng 5432 mở rồi báo "đã khôi phục" sẽ sai đúng theo cách này.

Đánh đổi cần cân nhắc

PITR đổi dung lượng lấy độ chính xác. Kho WAL của hai mươi phút thí nghiệm là 785 MB; phần 3 đã thấy một tải pgbench sinh 25 MB WAL mỗi giây. Lưu trữ WAL là chi phí liên tục và nó phải liên tục — thiếu một segment là chuỗi đứt, mọi đích sau chỗ đứt đều không tới được. pg_stat_archiver.failed_count khác 0 là chuông báo cấp một.

Promote là một chiều. Sau promote, timeline 2 tách khỏi lịch sử cũ; muốn thử đích khác phải khôi phục lại từ basebackup — như năm lần ở đây, mỗi lần một bản chép. recovery_target_action = pause cho phép nhìn dữ liệu ở chế độ chỉ đọc rồi mới quyết định pg_wal_replay_resume() hay thử lại; với CSDL lớn, pause rẻ hơn rất nhiều so với chép lại 700 GB.

Phạm vi đo. 700 MB và 785 MB WAL trên SSD cùng máy, không có mạng giữa kho WAL và máy khôi phục — restore_command kéo từ S3 qua mạng sẽ chậm hơn redo nhiều lần và trở thành nút cổ chai. Chưa chạm: pg_receivewal thay cho archive_command để không mất segment đang viết dở, và kiểm tra tính toàn vẹn bằng pg_verifybackup.

Ba ý mang về

  1. Basebackup + WAL liên tục + một đích = về bất kỳ giây nào: lệch một giây là khác biệt giữa 1.500 dòng và 100 dòng, nên đặt pg_create_restore_point trước thao tác nguy hiểm thay vì đoán giờ sau tai nạn.
  2. Hai bẫy im lặng: đích trước basebackup không lỗi mà dừng ở điểm nhất quán với dữ liệu sai thời điểm; và "ready to accept read-only connections" không phải xong — chờ pg_is_in_recovery() = false.
  3. RTO là phép cộng của chép basebackup và WAL phải replay (250–440 MB/s ở đây): lịch basebackup phải tính từ tốc độ sinh WAL, và failed_count của archiver phải bằng 0 mọi lúc.

Nguồn

Phần sau dùng cùng cơ chế WAL cho mục đích khác: không quay về quá khứ mà theo kịp hiện tại. Dựng một replica từ basebackup, cho nó bám primary qua streaming replication, đo độ trễ bằng byte và bằng giây dưới tải ghi, và xem điều gì xảy ra khi replica tắt một lúc rồi quay lại.