Phần 4 dùng WAL để quay về quá khứ. Streaming replication dùng cùng dòng WAL đó để một máy chủ thứ hai bám theo hiện tại: primary gửi từng bản ghi ngay khi ghi, replica nhận, ghi, fsync và replay. Nghe như một đường ống đơn giản, và nó đúng là đơn giản — cho tới khi phải trả lời "replica đang trễ bao nhiêu", "nếu nó tắt mười phút thì sao", và "vì sao bảng cảnh báo báo trễ 20 giây lúc nửa đêm không ai ghi gì". Phần này dựng replica thật, đo lag từng giây dưới tải, rồi tắt nó giữa chừng hai lần: một lần có replication slot, một lần không.

Replica bám primary bằng chính WAL

Ảnh chụp đoạn mã nền tối streaming replication replica bám primary bằng chính WAL với PostgreSQL 16 pg-prim primary sang pg-rep replica cùng mạng Docker replication slot rep1 pgbench tạo tải ghi, dựng replica bằng basebackup cộng hai tệp gồm pg_basebackup h pg-prim U lab D data Fp Xstream c fast R C S rep1, R viết standby.signal và primary_conninfo vào postgresql.auto.conf, C S rep1 tạo replication slot trên primary để primary giữ WAL cho tới khi replica nhận xong, postgres c hot_standby on để replica trả lời truy vấn đọc trong lúc replay, bốn mốc LSN của một bản ghi WAL độ trễ đo ở mỗi mốc gồm primary ghi WAL sang sent_lsn walsender gửi sang write_lsn replica ghi tệp sang flush_lsn fsync sang replay_lsn áp, trên primary pg_stat_replication cho write_lag flush_lag replay_lag giây và pg_wal_lsn_diff sent replay byte, trên replica pg_last_wal_receive_lsn pg_last_wal_replay_lsn now trừ pg_last_xact_replay_timestamp, bẫy hiệu now trừ last_xact_replay tăng mãi khi primary không ghi gì không phải trễ là im lặng, replica tắt một lúc slot là lưới an toàn và là bẫy đĩa gồm có slot primary giữ WAL pg_replication_slots wal_status retained bytes replica về là bắt kịp nhưng replica chết hẳn thì WAL giữ mãi đầy đĩa primary max_slot_wal_keep_size chặn, không slot wal_keep_size hết là WAL bị dọn replica báo requested WAL segment has already been removed chỉ còn cách dựng lại từ basebackup, đo gì gồm A thời gian dựng replica tới lúc streaming, B lag byte giây từng giây dưới pgbench c 8 T 30, C tắt replica 20 giây giữa tải WAL giữ lại bao nhiêu bật lại bắt kịp mất bao lâu có slot, D bỏ slot cộng wal_keep_size 0 lặp lại lỗi gì replica còn cứu được không

Hình 1: Một lệnh pg_basebackup với -R và -C dựng xong replica. Lag đo ở bốn mốc LSN; slot quyết định WAL được giữ hay bị dọn khi replica vắng mặt.

Dựng replica là một lệnh: pg_basebackup … -R -C -S rep1. -R viết standby.signal và primary_conninfo vào postgresql.auto.conf; -C -S rep1 tạo một replication slot trên primary. Slot là lời hứa của primary: "tôi sẽ giữ WAL từ vị trí replica này đã nhận cho tới khi nó nhận tiếp", bất kể checkpoint hay wal_keep_size. Không có slot, primary dọn WAL theo nhịp checkpoint của nó và chỉ giữ thêm wal_keep_size; replica vắng mặt lâu hơn thế là mất dấu.

Một bản ghi WAL đi qua bốn mốc: sent (walsender đẩy đi), write (replica ghi vào tệp), flush (fsync), replay (áp vào trang dữ liệu). pg_stat_replication trên primary cho cả bốn LSN lẫn ba độ trễ theo giây; trên replica, pg_last_wal_replay_lsn() cho biết đã áp tới đâu. Có một cách đo phổ biến và sai: now() − pg_last_xact_replay_timestamp() trên replica — nó là khoảng cách tới giao dịch cuối cùng đã replay, nên khi primary không ghi gì, con số này tăng đều mà không có byte nào trễ.

Môi trường: pg-prim (613 MB dữ liệu, archive_mode=on từ các phần trước) và pg-rep cùng mạng Docker; pgbench 8 client tạo khoảng 25 MB WAL mỗi giây.

Đo thật: lag, im lặng, và hai lần tắt máy

Ảnh chụp output thật nền tối lag 9 KB tắt 20 giây giữ 476 MB không slot thì chết với pg-prim sang pg-rep postgres 16 cùng mạng Docker pgbench c 8 tạo khoảng 25 MB WAL mỗi giây, A dựng replica gồm pg_basebackup R C S rep1 700 MB qua mạng 961 ms replica trả lời truy vấn sau 1728 ms, pg_stat_replication walreceiver streaming async lag 0 B slot rep1 active t wal_status reserved, B lag từng giây dưới pgbench c 8 T 30 30 mẫu gồm byte sent trừ replay min 0 p50 9032 p90 43800 max 50376 B khoảng 2 ms WAL ở 25 MB mỗi giây, replay_lag p50 0,001 giây max 0,082 giây 1 trên 30 mẫu từ 0,05 giây flush_lag max 0,043 giây, KB mỗi giây 42 49 11 43 26 3 0 9 10 2 3 12 4 2 2 8 2 10 1 1 12 2 3 1 16 40 17 16 0 0, B2 primary im lặng lag thật 0 lag giả tăng đều gồm sau 0 giây byte lag 0 now trừ pg_last_xact_replay_timestamp 5,1 giây, sau 5 giây 10,2 giây, sau 15 giây 20,4 giây không có giao dịch nào để replay cảnh báo theo số này là báo oan, C tắt replica 20 giây giữa tải có slot gồm slot rep1 active false wal_status reserved retained 476 MB pg_wal primary 1025 sang 1041 MB tái dùng segment, bật lại bắt kịp lag dưới 1 MB sau 5207 ms lag còn 0 B retained về 0 nên 476 MB replay trong khoảng 5 giây khoảng 90 MB mỗi giây qua mạng, D lặp lại không slot wal_keep_size 0 gồm sau checkpoint segment cũ nhất trên primary 0005000000ED replica cần từ 0005000000DD 16 segment đã bị dọn, replica log 3 lần FATAL requested WAL segment 0000000100000005000000DD has already been removed, pg_stat_replication 0 dòng replica kẹt ở 5 DD2DC6B0 primary đã tới 5 FD0000D8 không tự cứu được, dựng lại từ basebackup có slot streaming lại sau 1608 ms, nói thẳng hai container cùng máy nên lag 9 KB là sàn không phải số bạn sẽ thấy qua mạng thật thứ mang sang được là hình dạng CSDL 700 MB nên dựng lại chỉ 1,6 giây với 700 GB đó là hàng giờ và slot là thứ đứng giữa hai con số đó

Hình 2: Lag dưới tải tính bằng KB và mili giây; "lag" khi im lặng tăng tới 20 giây mà không có byte nào trễ; slot giữ 476 MB cho replica vắng 20 giây; không slot thì 16 segment bị dọn và replica phải dựng lại.

phép đo kết quả
dựng replica 700 MB tới lúc trả lời truy vấn 1.728 ms (basebackup 961 ms)
lag byte dưới pgbench, 30 mẫu min 0, p50 9.032, p90 43.800, max 50.376 B
replay_lag p50 0,001 s, max 0,082 s
"lag" trên replica khi primary im lặng 0 / 5 / 15 s 5,1 / 10,2 / 20,4 s, byte lag = 0
tắt 20 s giữa tải, có slot giữ 476 MB, bắt kịp sau 5.207 ms
tắt 20 s, không slot, wal_keep_size=0 16 segment bị dọn, FATAL … already been removed, phải dựng lại (1.608 ms)

Dựng replica: 1,7 giây, và từ đó nó tự bám. pg_basebackup -R -C -S rep1 chép 700 MB trong 961 ms, container khởi động, và 1.728 ms sau lệnh đầu tiên replica đã trả lời select count(*) from docs với đủ một triệu dòng, pg_stat_replication báo streaming, lag 0 byte. Không có bước cấu hình tay nào — hai tệp mà -R viết ra là toàn bộ "cấu hình replica".

Dưới 25 MB WAL mỗi giây, lag trung vị 9 KB và 1 ms. Ba mươi mẫu mỗi giây trong khi pgbench 8 client chạy: khoảng cách sent − replay trung vị 9 KB, phân vị 90 là 44 KB, tối đa 50 KB — ở tốc độ 25 MB/s đó là khoảng 2 ms WAL. replay_lag trung vị 1 ms, một mẫu duy nhất chạm 82 ms (flush_lag 43 ms cùng lúc — fsync trên replica). Hai container cùng máy nên đây là sàn; qua mạng thật mỗi mốc cộng thêm RTT và độ trễ đĩa, nhưng hình dạng không đổi: lag byte dao động theo nhịp ghi, còn replay_lag chỉ nhảy khi replica bận fsync hoặc replay một bản ghi lớn.

Primary im lặng: lag thật bằng 0, số đo trên replica tăng tới 20 giây. Sau khi pgbench dừng, pg_wal_lsn_diff(sent_lsn, replay_lsn) là 0 suốt, nhưng now() − pg_last_xact_replay_timestamp() trên replica đọc 5,1 → 10,2 → 20,4 giây — nó đo khoảng cách tới giao dịch cuối cùng đã có để replay, và khi không ai ghi thì giao dịch cuối cùng cứ lùi xa dần. Bảng cảnh báo dùng con số này sẽ kêu lúc nửa đêm vì hệ thống rảnh. Đo lag đúng là so LSN (trên primary, hoặc pg_last_wal_receive_lsn() với pg_last_wal_replay_lsn() trên replica); muốn số giây thì đọc replay_lag từ pg_stat_replication, nó là NULL khi không có gì để đo thay vì tăng vô nghĩa.

Tắt 20 giây giữa tải, có slot: primary giữ 476 MB, replica về và bắt kịp sau 5,2 giây. Trong lúc replica tắt, slot rep1 chuyển active=false nhưng wal_status=reserved và retained leo lên 476 MB — primary không dọn một byte nào trong khoảng đó. pg_wal chỉ tăng 1.025 → 1.041 MB vì segment được tái dùng chứ không phải cấp mới; khi khoảng giữ vượt max_wal_size thì thư mục mới thật sự phình. Bật lại: 476 MB replay trong 5,2 giây (khoảng 90 MB/s qua mạng Docker, chậm hơn 250–440 MB/s của PITR cục bộ ở phần 4 vì có thêm khâu nhận), lag về 0, retained về 0. Không có bước nào của người vận hành.

Cùng kịch bản, không slot, wal_keep_size=0: replica chết. Sau checkpoint, segment cũ nhất còn trên primary là …ED; replica cần nối tiếp từ …DD — mười sáu segment đã bị dọn. Replica khởi động, nối vào primary, nhận FATAL: requested WAL segment 0000000100000005000000DD has already been removed, ngắt, thử lại, lặp ba lần trong tám giây; pg_stat_replication trên primary trống; replay_lsn đứng yên ở 5/DD2DC6B0 trong khi primary đã tới 5/FD…. Không có cách tự cứu: dữ liệu giữa hai LSN đó không còn ở đâu để kéo về (trừ kho WAL lưu trữ của phần 4, nếu restore_command được khai — ở đây cố tình không). Cách duy nhất là dựng lại từ basebackup: 1,6 giây với 700 MB, và sẽ là hàng giờ với 700 GB.

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

Slot bảo vệ replica bằng đĩa của primary. Đó là một đổi chác đúng khi replica vắng mặt vài phút; nó là thảm hoạ khi replica chết hẳn mà slot còn đó — primary giữ WAL mãi tới khi đầy đĩa và dừng. max_slot_wal_keep_size đặt trần: vượt là slot chuyển wal_status=lost và replica phải dựng lại, nhưng primary sống. Đặt nó, và giám sát pg_replication_slots cho slot active=false lâu hơn một ngưỡng.

Lag byte hay lag giây: hỏi câu gì thì đo cái đó. Byte cho biết replica còn phải xử lý bao nhiêu — thứ quyết định failover mất bao nhiêu dữ liệu ở chế độ async (phần 7). Giây cho biết người đọc trên replica thấy dữ liệu cũ bao lâu (phần 6). Và now() − pg_last_xact_replay_timestamp() không trả lời câu nào trong hai câu đó khi primary rảnh.

Phạm vi đo. Hai container trên một máy: lag 9 KB và 1 ms là giới hạn dưới của chính cơ chế, không phải của mạng; catch-up 90 MB/s bị giới hạn bởi CPU replay đơn luồng và đĩa chung. Chưa chạm: synchronous_commit và replica đồng bộ (phần 7), cascading replica, và pg_rewind.

Ba ý mang về

  1. Replica là một lệnh và hai tệp: 1,7 giây tới lúc trả lời truy vấn; dưới 25 MB WAL/giây lag trung vị 9 KB và 1 ms, tối đa 82 ms — đo bằng LSN hoặc replay_lag, không bằng now() − last_xact_replay (tăng tới 20 giây khi primary rảnh).
  2. Slot là khác biệt giữa "bắt kịp sau 5 giây" và "dựng lại từ đầu": cùng 20 giây vắng mặt, có slot giữ 476 MB và tự hồi; không slot thì 16 segment bị dọn và replica không có đường về.
  3. Slot giữ WAL bằng đĩa của primary: đặt max_slot_wal_keep_size và giám sát slot active=false, nếu không replica chết sẽ kéo primary chết theo.

Nguồn

Phần sau cho ứng dụng đọc trên chính replica này: truy vấn dài bị huỷ vì xung đột với WAL replay, max_standby_streaming_delay và hot_standby_feedback đổi điều đó ra sao, và đo xem đọc ngay sau khi ghi thất bại bao nhiêu phần trăm.