Streaming replication trong Postgres là một tunnel WAL từ primary sang standby: primary chạy một walsender
cho mỗi standby, standby chạy một walreceiver
nhận WAL và một startup
process replay. Đau nhất trong vận hành không phải setup — mà là replica đã ngắt nhiều giờ mà không ai biết, tới lúc primary chết mới phát hiện HA thực ra là single-node từ tuần trước. HA dashboard là tập metric + alert được rút ra từ pg_stat_replication
, pg_stat_wal_receiver
, và pg_replication_slots
để ép mọi trạng thái xấu — replica disconnect, replay đứng, slot pin WAL, sync standby biến mất — thành tín hiệu nhìn thấy được trước khi biến thành sự cố.
Trên primary, mỗi standby đang kết nối tạo ra một backend loại walsender
— Postgres đọc WAL từ pg_wal/
(hoặc từ WAL buffers khi còn nóng) và stream qua replication connection. Trên standby, walreceiver
nhận từng WAL record, ghi vào pg_wal/
local, fsync (tùy synchronous_commit
), rồi startup
process apply record lên shared buffers — đây chính là replay. Bốn LSN xuất hiện trong luồng này và tương ứng với bốn cột trong pg_stat_replication
:
sent_lsn
— byte cuối cùng primary đã gửi qua socket.write_lsn
— byte cuối cùng standby đã write()
vào OS page cache.flush_lsn
— byte cuối cùng standby đã fsync
xuống đĩa.replay_lsn
— byte cuối cùng standby đã replay vào shared buffers (dữ liệu đã "thấy được" trên standby).Postgres docs quy định replay_lsn <= flush_lsn <= write_lsn <= sent_lsn <= pg_current_wal_lsn()
— bốn "vạch" này chính là bốn nhịp của lag. Ba cột write_lag
, flush_lag
, replay_lag
(kiểu interval
) là thời gian mà standby chậm hơn primary tương ứng với ba mốc write/flush/replay — được đo qua feedback message định kỳ từ standby.
-- Trên primary: bức tranh đầy đủ cho một HA dashboard
SELECT
application_name,
client_addr,
state, -- streaming | catchup | startup | backup | stopping
sync_state, -- async | potential | sync | quorum
pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS sent_lag_bytes,
pg_wal_lsn_diff(pg_current_wal_lsn(), write_lsn) AS write_lag_bytes,
pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn) AS flush_lag_bytes,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes,
write_lag, flush_lag, replay_lag,
EXTRACT(EPOCH FROM (now() - reply_time)) AS seconds_since_reply
FROM pg_stat_replication;
Trên standby, pg_stat_wal_receiver
cho biết đang kết nối tới primary nào (conninfo
, sender_host
), status
(streaming
/waiting
/stopping
), last_msg_send_time
, last_msg_receipt_time
, và latest_end_lsn
. Đây là view duy nhất chứng minh standby thực sự đang nghe primary — không có row nào trong view này = standby đã ngắt hoặc chưa kết nối lại. pg_last_wal_replay_lsn()
và pg_last_xact_replay_timestamp()
là hai hàm dùng để tính lag theo thời gian: now() - pg_last_xact_replay_timestamp()
cho ra "đọc trên standby chậm bao nhiêu giây so với transaction cuối cùng đã commit trên primary".
Một cái nữa nằm cạnh: pg_replication_slots
. Slot pin restart_lsn
lại để primary không recycle WAL segment mà standby chưa consume. Cột wal_status
(Postgres 13+) có bốn giá trị reserved
/ extended
/ unreserved
/ lost
— trạng thái lost
là khi slot đã bị vượt max_slot_wal_keep_size
và WAL bị invalidate; consumer phải resync từ base backup. safe_wal_size
cho biết còn bao nhiêu byte WAL nữa slot có thể tụt trước khi bị lost.
Failure mode 1: replica ngắt kết nối và không ai biết. Không có dashboard, cách duy nhất phát hiện là "một ngày failover và thấy standby đứng ở LSN cũ". Nguyên nhân thường gặp: TCP giữa primary–standby bị firewall/idle-timeout cắt, walreceiver
chết vì crash, wal_receiver_timeout
trên standby hết hạn khi mạng blip, standby được restart nhưng primary_conninfo
sai. Row của standby biến mất khỏi pg_stat_replication
trên primary — không có exception ở đâu, log Postgres chỉ có một dòng terminating walsender process due to replication timeout
rồi im.
-- Sai: alert kiểu "SELECT count(*) FROM pg_stat_replication > 0"
-- Không phân biệt được standby A hay B đã rớt trong setup nhiều replica
SELECT count(*) AS replica_count FROM pg_stat_replication;
-- Đúng: alert theo tên (application_name của từng standby)
-- Standby set application_name qua primary_conninfo hoặc recovery.conf
SELECT expected.name,
CASE WHEN r.application_name IS NULL THEN 'DOWN'
WHEN r.state <> 'streaming' THEN r.state
ELSE 'ok' END AS health
FROM (VALUES ('standby-1'), ('standby-2')) AS expected(name)
LEFT JOIN pg_stat_replication r ON r.application_name = expected.name;
Ép mỗi standby set application_name
(qua primary_conninfo='... application_name=standby-1 ...'
) là điều kiện tiên quyết cho alert theo tên — không có nó, không có cách nhận diện replica nào rớt.
Failure mode 2: standby ngắt nhưng slot còn — pg_wal/ phình. Nếu standby dùng physical replication slot (
primary_slot_name
set), khi standby down slot vẫn active=false
nhưng restart_lsn
không tiến. Primary giữ mọi WAL segment từ restart_lsn
trở đi → pg_wal/
lớn dần không giới hạn cho tới khi hết đĩa và PANIC: could not write to file ... No space left on device
. Đây là kịch bản đã có trong nhiều postmortem công khai — GitLab đã có sự cố database năm 2017 mà một trong các layer bảo vệ chính là monitoring bị thiếu tín hiệu. Cứu cứng bằng max_slot_wal_keep_size
(Postgres 13+), nhưng phải kèm alert wal_status IN ('extended','lost')
để biết trước khi mất consumer.
-- Cột phải đưa lên dashboard cho MỌI slot
SELECT slot_name, slot_type, active, wal_status,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained,
pg_size_pretty(safe_wal_size) AS safe_left
FROM pg_replication_slots;
Failure mode 3: sync replication treo write khi sync standby biến mất. Với synchronous_commit = on
và synchronous_standby_names = 'FIRST 1 (standby-1)'
, khi standby-1
rớt, mọi COMMIT
trên primary sẽ treo chờ ACK — client thấy connection hang, không phải error. Backend đứng ở state waiting for sync replication
trong pg_stat_activity
(wait_event_type = 'IPC'
, wait_event = 'SyncRep'
). Nếu dashboard chỉ theo dõi sync_state
mà không alert khi không còn standby nào ở sync
, sẽ không phát hiện được cho tới khi có báo cáo "app đơ khi commit". Cách xử lý là dùng quorum thay vì FIRST 1 (ANY 2 (s1, s2, s3)
cho phép mất bất kỳ 1 trong 3), và alert cứng khi số standby sync_state IN ('sync','quorum')
tụt dưới ngưỡng cần thiết.
Failure mode 4: replay_lag lớn dù flush_lag gần 0 — replay bị conflict. Standby đã nhận và fsync WAL, nhưng replay đứng vì recovery conflict: một query dài trên standby giữ snapshot chặn quá trình apply vacuum WAL, hoặc lock trên relation bị hold. Symptom:
write_lsn
bám sát sent_lsn
nhưng replay_lsn
tụt lại. Đọc pg_stat_database_conflicts
(confl_snapshot
, confl_lock
, confl_bufferpin
) trên standby để biết loại conflict. Nếu app đọc replica cho query nhạy freshness, người dùng sẽ thấy "vừa commit xong đọc lại không thấy" — không phải vì network mà vì replay bị block. Bật hot_standby_feedback = on
giảm confl_snapshot
nhưng đổi lại có bloat trên primary; tăng max_standby_streaming_delay
cho phép replay đợi thay vì hủy query, nhưng tăng lag; hoặc route query dài sang một replica riêng.Failure mode 5: dashboard "chỉ xem khi có sự cố". Metric có, panel có, nhưng không ai nhìn cho tới khi PagerDuty kêu. Không có alert threshold + không có runbook = MTTR bằng thời gian phát hiện thủ công. Điểm sửa: mọi metric trên dashboard phải có ít nhất một alert rule tương ứng — nếu không có alert, không nên đưa lên dashboard làm gì.
Triệu chứng cần soi khi ai đó nói "app đọc dữ liệu cũ" hoặc "commit đôi khi treo":
pg_stat_replication
→ replica disconnect.state <> 'streaming'
kéo dài — catchup
bình thường sau restart nhưng không được kéo dài.pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)
tăng đơn điệu → replay đứng.write_lag
gần 0 nhưng replay_lag
lớn → conflict trên standby, không phải network.wal_status IN ('extended','lost')
trên slot → gần mất consumer.reply_time
cũ hơn vài giây → walsender không nhận feedback, connection có thể đã chết.View và hàm chính (đều có trong Postgres docs mục "System Views" và "System Information Functions"):
-- Primary side: pg_stat_replication, pg_replication_slots
-- Standby side:
SELECT status, sender_host, sender_port,
last_msg_send_time, last_msg_receipt_time,
latest_end_lsn, latest_end_time,
EXTRACT(EPOCH FROM (now() - last_msg_receipt_time)) AS silence_seconds
FROM pg_stat_wal_receiver;
SELECT pg_is_in_recovery(),
pg_last_wal_receive_lsn(),
pg_last_wal_replay_lsn(),
pg_last_xact_replay_timestamp(),
EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS replay_delay_sec;
-- Recovery conflict counter
SELECT datname, confl_tablespace, confl_lock, confl_snapshot,
confl_bufferpin, confl_deadlock
FROM pg_stat_database_conflicts;
Tool stack thực tế:
postgres_exporter
pg_stat_replication
, pg_replication_slots
, pg_stat_wal_receiver
dưới dạng metric Prometheus. Có custom queries file cho phép thêm bất cứ SELECT nào không có sẵn./cluster
endpoint có role, state, xlog location của từng node — nguồn thứ hai để cross-check với pg_stat_replication
.replay_lag_bytes
ngưỡng bucket, (3)
replay_lag
seconds > SLO đọc replica, (4) sync standby count < required, (5) slot wal_status
extended/lost, (6) walreceiver
silence > 30 giây.Rule phòng ngừa cứng:
application_name
riêng qua primary_conninfo
, không dùng tên mặc định walreceiver
.log_replication_commands = on
khi debug — nhưng tắt lại lúc bình thường vì log volume cao.hot_standby_feedback
bật cho replica phục vụ query dài; kèm alert bloat trên primary.Dựng dashboard đầy đủ tốn công một lần: exporter, panel, alert rule, runbook. Cái được là MTTR giảm mạnh — replica down phát hiện trong vài chục giây thay vì "phát hiện lúc failover"; slot pin WAL phát hiện khi wal_status = 'extended'
(trước khi lost
) thay vì lúc PANIC
disk full; replay conflict phát hiện lúc lag > SLO thay vì lúc user báo bug. Cái mất: mỗi metric là một alert rule cần bảo trì, exporter là một service thêm phải HA, và mỗi standby thêm một hàng metric — với setup cascade nhiều tầng dashboard sẽ đông. Quy tắc thực tế: dashboard tối thiểu phải trả lời được ba câu — "mỗi standby có streaming không?", "lag byte và second của mỗi standby là bao nhiêu?", "có slot nào sắp mất WAL không?" — nếu ba câu này không đọc được trong 10 giây, dashboard chưa xong; và mọi câu phải có alert đi kèm, không thì không đưa lên.
Replication trong Postgres cần monitor những gì, và vì sao chỉ đếm số replica trong
pg_stat_replication
không đủ?
Đủ interview-grade: cần chia theo bốn nhóm metric — (1) connectivity: row trong pg_stat_replication
tồn tại cho từng standby theo tên (application_name
), state = 'streaming'
, reply_time
gần đây; và đối xứng bên standby là pg_stat_wal_receiver.status = 'streaming'
, last_msg_receipt_time
không im lặng quá vài chục giây. (2) lag hai chiều: byte lag qua pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)
cho mỗi standby, và time lag qua write_lag/flush_lag/replay_lag
— phải tách write_lag
với replay_lag
vì replay_lag
cao mà write_lag
thấp là chỉ dấu recovery conflict trên standby chứ không phải network. (3) sync guarantee: sync_state
của từng standby (async
/potential
/sync
/quorum
), tổng số standby ở sync
/quorum
so với yêu cầu của synchronous_standby_names
— thiếu là commit sẽ treo. (4) slot health: pg_replication_slots.active
, wal_status
, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
, safe_wal_size
— slot inactive kéo pg_wal/
phình và có thể bít cluster. Chỉ đếm count(*)
không đủ vì (a) không biết replica nào rớt trong nhiều-replica, (b) state
có thể catchup
không phải streaming
, (c) không phát hiện replay_lag
cao trong khi vẫn có connection, (d) không thấy sync standby biến mất khi vẫn còn async standby. Hậu quả production khi thiếu: replica chết âm thầm nhiều ngày phát hiện lúc failover; slot bỏ quên làm disk đầy → cluster PANIC dừng nhận write; sync standby rớt → commit trên primary treo và client thấy connection hang chứ không phải error rõ ràng. Điểm ăn điểm là gắn từng nhóm metric với view/hàm cụ thể (pg_stat_replication
, pg_stat_wal_receiver
, pg_replication_slots
, pg_last_xact_replay_timestamp()
) và nêu một alert rule cho mỗi — không chỉ liệt kê "lag, state, sync".
Mục tiêu: dựng primary + standby bằng Docker, dựng physical replication slot, viết query dashboard core, rồi ép hai kịch bản vỡ (standby chết, slot pin WAL) và quan sát đúng metric nào chuyển màu.
Bước 1 — Dựng primary với streaming replication bật sẵn:
docker network create pgnet 2>/dev/null || true
docker run -d --name pg-primary --network pgnet \
-e POSTGRES_PASSWORD=pw -p 5440:5432 \
postgres:16 \
-c wal_level=replica \
-c max_wal_senders=10 \
-c max_replication_slots=10 \
-c hot_standby=on \
-c log_replication_commands=on
sleep 5
docker exec -i pg-primary psql -U postgres <<'SQL'
CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD 'replpw';
SELECT pg_create_physical_replication_slot('standby_1_slot');
SQL
docker exec pg-primary bash -c \
"echo 'host replication repl 0.0.0.0/0 md5' >> /var/lib/postgresql/data/pg_hba.conf"
docker exec pg-primary psql -U postgres -c "SELECT pg_reload_conf();"
Bước 2 — Dựng standby bằng pg_basebackup
rồi start:
docker run -d --name pg-standby --network pgnet \
-e POSTGRES_PASSWORD=pw -p 5441:5432 \
--entrypoint sleep postgres:16 infinity
docker exec pg-standby bash -c '
rm -rf /var/lib/postgresql/data/*
PGPASSWORD=replpw pg_basebackup \
-h pg-primary -U repl -D /var/lib/postgresql/data \
-R -X stream -S standby_1_slot -P
echo "primary_conninfo = '"'"'host=pg-primary user=repl password=replpw application_name=standby-1'"'"'" \
>> /var/lib/postgresql/data/postgresql.auto.conf
'
docker restart pg-standby
sleep 5
docker exec pg-standby psql -U postgres -c "SELECT pg_is_in_recovery();"
Bước 3 — Query dashboard core trên primary:
docker exec -it pg-primary psql -U postgres <<'SQL'
SELECT application_name, state, sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes,
replay_lag,
EXTRACT(EPOCH FROM (now() - reply_time)) AS since_reply_sec
FROM pg_stat_replication;
SELECT slot_name, active, wal_status,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained,
pg_size_pretty(safe_wal_size) AS safe_left
FROM pg_replication_slots;
SQL
Trên standby:
docker exec -it pg-standby psql -U postgres <<'SQL'
SELECT status, sender_host, latest_end_lsn,
EXTRACT(EPOCH FROM (now() - last_msg_receipt_time)) AS silence_sec
FROM pg_stat_wal_receiver;
SELECT pg_last_wal_replay_lsn(),
EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS replay_delay_sec;
SQL
Bước 4 — Sinh tải trên primary, quan sát LSN tiến trên cả hai:
docker exec -i pg-primary psql -U postgres <<'SQL'
CREATE TABLE IF NOT EXISTS t(id bigserial PRIMARY KEY, data text);
INSERT INTO t(data) SELECT repeat('x', 200) FROM generate_series(1, 200000);
SQL
docker exec pg-primary psql -U postgres -c "SELECT pg_current_wal_lsn();"
docker exec pg-standby psql -U postgres -c "SELECT pg_last_wal_replay_lsn();"
Bước 5 — Ép failure mode 1 (standby chết) và xem metric nào chuyển:
docker stop pg-standby
sleep 15
docker exec pg-primary psql -U postgres -c \
"SELECT application_name, state FROM pg_stat_replication;"
docker exec pg-primary psql -U postgres -c \
"SELECT slot_name, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;"
Bước 6 — Ép failure mode 2 (slot pin WAL), quan sát pg_wal/
phình:
docker exec -i pg-primary psql -U postgres <<'SQL'
INSERT INTO t(data) SELECT repeat('y', 500) FROM generate_series(1, 500000);
CHECKPOINT;
INSERT INTO t(data) SELECT repeat('z', 500) FROM generate_series(1, 500000);
CHECKPOINT;
SQL
docker exec pg-primary du -sh /var/lib/postgresql/data/pg_wal
docker exec pg-primary psql -U postgres -c \
"SELECT slot_name, wal_status,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;"
Áp lưới cứu và quan sát wal_status
chuyển:
docker exec pg-primary psql -U postgres <<'SQL'
ALTER SYSTEM SET max_slot_wal_keep_size = '32MB';
SELECT pg_reload_conf();
SQL
docker exec -i pg-primary psql -U postgres <<'SQL'
INSERT INTO t(data) SELECT repeat('w', 500) FROM generate_series(1, 500000);
CHECKPOINT;
SQL
docker exec pg-primary psql -U postgres -c \
"SELECT slot_name, wal_status, pg_size_pretty(safe_wal_size) AS safe_left
FROM pg_replication_slots;"
Bước 7 — Khôi phục standby, xác nhận slot lost thì phải resync:
docker start pg-standby
sleep 10
docker logs --tail 30 pg-standby
Bước 8 — Dọn:
docker rm -f pg-primary pg-standby
docker network rm pgnet
Sau bài này, dashboard không còn là "vài panel Grafana đẹp" — nó là ánh xạ 1-1 giữa từng dòng trong pg_stat_replication
/pg_replication_slots
/pg_stat_wal_receiver
và một alert rule, để mỗi khi bức tranh HA lệch khỏi trạng thái tốt, có đúng một tín hiệu ping ra ngoài, kèm đúng một runbook.