cd /news/developer-tools/replication-monitoring-ha-dashboard · home topics developer-tools article
[ARTICLE · art-49619] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Replication Monitoring — HA Dashboard

A developer built a high-availability (HA) dashboard for PostgreSQL streaming replication monitoring. The dashboard aggregates metrics from pg_stat_replication, pg_stat_wal_receiver, and pg_replication_slots to detect failure modes such as replica disconnection, replay lag, and slot WAL pinning before they become critical. It exposes four LSN-based lag metrics (sent, write, flush, replay) and uses time-based lag columns to provide real-time visibility into replication health.

read13 min views1 publishedJul 7, 2026

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()

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

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

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.

── more in #developer-tools 4 stories · sorted by recency
── more on @postgresql 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/replication-monitori…] indexed:0 read:13min 2026-07-07 ·