# Replication Monitoring — HA Dashboard

> Source: <https://dev.to/nguoidungai/replication-monitoring-ha-dashboard-32mc>
> Published: 2026-07-07 15:49:50+00:00

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

# Cho phép replication từ standby (append pg_hba)
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
  # -R tạo standby.signal + primary_conninfo tự động
  # Thêm application_name để dashboard nhận diện được
  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

# Standby phải là read-only
docker exec pg-standby psql -U postgres -c "SELECT pg_is_in_recovery();"
# t
```

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();"
# Hai LSN gần nhau; lặp lại để thấy chúng cùng tiến
```

Bước 5 — Ép failure mode 1 (standby chết) và xem metric nào chuyển:

```
docker stop pg-standby

# Chờ walsender phát hiện — thường theo wal_sender_timeout (default 60s)
sleep 15
docker exec pg-primary psql -U postgres -c \
  "SELECT application_name, state FROM pg_stat_replication;"
# Row của standby-1 biến mất — đây là tín hiệu alert "replica down"

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;"
# active=false nhưng slot vẫn giữ WAL — đây là mầm của failure mode 2
```

Bước 6 — Ép failure mode 2 (slot pin WAL), quan sát `pg_wal/`

phình:

```
# Giữ standby down, sinh nhiều WAL trên primary
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;"
# retained tăng dần; wal_status có thể vẫn 'reserved' nếu chưa set max_slot_wal_keep_size
```

Á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;"
# wal_status chuyển 'extended' rồi 'lost' — đây chính là metric HA dashboard
# phải alert TRƯỚC khi disk đầy
```

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
# Nếu wal_status='lost', standby báo lỗi không thể catch up
# Fix: pg_basebackup lại từ primary (chấp nhận mất consumer thay vì mất cluster)
```

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.
