Bỏ qua để đến nội dung
Knowledge Hub

Replication và read replica: vì sao vừa ghi xong đọc lại vẫn thấy dữ liệu cũ Trung cấp

Thời gian đọc ước tính: 8 phút

Primary nhận mọi lệnh ghi, một read replica nhận SELECT để giảm tải. User 42 đổi avatar từ avatar-v1.png sang avatar-v2.png, thấy “Đã lưu”, rồi F5. Trang profile đọc replica và vẫn hiện avatar-v1.png; vài giây sau mới đúng.

Replica chỉ chưa áp dụng xong thay đổi. Replica trễ bao lâu, đo bằng gì, và làm sao user luôn thấy dữ liệu mình vừa ghi (read-your-writes)?

Tình huống Chọn Điều kiện
Bản sao chỉ đọc của cả cluster, dự phòng failover Streaming replication (hot standby) Cùng major version
Sao chép vài bảng, nâng major version Logical replication Tự đồng bộ schema, sequence
Màn hình user vừa ghi (profile, giỏ hàng) Đọc primary, hoặc chờ replica replay tới LSN App lưu thời điểm hoặc LSN của lần ghi
Listing, báo cáo chấp nhận trễ Đọc replica Giám sát replay_lag
Commit phải thấy ngay trên standby synchronous_commit = remote_apply Commit chậm thêm thời gian replay

Mọi thay đổi được ghi trước vào WAL (write-ahead log). Vị trí trong WAL là LSN (log sequence number), dạng 0/3033518, so sánh được như số tăng dần.

Streaming (physical) Logical
Gửi gì Bản ghi WAL mức block Thay đổi theo row
Phạm vi Cả cluster Bảng trong publication, nhận qua subscription
Phía nhận Standby chỉ đọc Database thường, ghi được (dễ xung đột)
Version Phải cùng major version Được khác major version
Không sao chép DDL, sequence, large object

Read replica ở đây là hot standby: server liên tục replay WAL và vẫn nhận query chỉ đọc (hot_standby = on là default); lệnh ghi, SELECT ... FOR UPDATE, nextval() bị từ chối.

Dữ liệu trên standby là eventually consistent: thay đổi chỉ hiện với snapshot mới sau khi commit record được replay.

Từ commit đến lúc replica thấy dữ liệu, WAL qua những mốc nào?

Đang tải diagram…

Bốn mốc là bốn cột LSN trong pg_stat_replication trên primary. Đi kèm:

  • write_lag, flush_lag, replay_lag: thời gian từ khi primary flush WAL đến khi nhận báo cáo replica đã write / flush / replay.
  • Với replica async, replay_lag xấp xỉ độ trễ trước khi transaction mới hiện ra với query.
  • Hệ rảnh, đã bắt kịp: cột lag về NULL sau một lúc.
Replica đã nhận WAL nhưng chưa replay: F5 đọc replica vẫn thấy avatar cũ
Replica đã nhận WAL nhưng chưa replay: F5 đọc replica vẫn thấy avatar cũBước 1/5: App ghi avatar-v2.png lên primary; commit xong, primary trả OK cho user.thời gianUPDATE avataravatar-v2.pngCOMMITLSN 0/3033518Appuser 42 đổi avatarPrimarynhận ghiReplicahot standby, async
Bước 1/5

App ghi avatar-v2.png lên primary; commit xong, primary trả OK cho user.

Bước Replica User thấy
Sau commit (LSN 0/3033518) flush_lsn đã tới, replay_lsn chưa “Đã lưu”
F5 lần 1 Snapshot chưa có commit record avatar-v1.png (sai)
Sau replay replay_lsn ≥ 0/3033518 avatar-v2.png

Streaming replication mặc định async. Khi synchronous_standby_names khác rỗng, synchronous_commit quyết định commit chờ standby đồng bộ tới đâu:

Giá trị Commit chờ Thấy ngay trên standby đồng bộ?
off Không chờ cả flush cục bộ Không
local Flush WAL cục bộ Không
remote_write Standby write vào OS Không
on (default) Standby flush xuống đĩa Không: bền nhưng chưa replay
remote_apply Standby replay xong Có

Khi synchronous_standby_names rỗng (default), mọi giá trị khác off chỉ chờ flush cục bộ. remote_apply cũng chỉ bảo đảm cho standby đồng bộ; replica async khác vẫn có thể trễ.

Đo lag. Trên primary:

SELECT application_name, sync_state, replay_lsn, replay_lag,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_gap_bytes
FROM pg_stat_replication;

Trên replica: pg_last_wal_replay_lsn() và now() - pg_last_xact_replay_timestamp(); con số sau tăng mãi khi primary không có commit mới.

Conflict trên hot standby. VACUUM trên primary có thể xoá row version mà query dài trên replica còn cần (MVCC). Replay chờ query đó tối đa max_standby_streaming_delay (default 30s), rồi huỷ query với SQLSTATE 40001.

  • Trong lúc chờ, replay dừng: lag tăng dù mạng bình thường.
  • hot_standby_feedback = on (default off) bảo primary đừng VACUUM các row replica còn cần; đổi lại primary giữ dead tuple lâu hơn, gây bloat.
  • Lock ACCESS EXCLUSIVE (DDL) cũng gây conflict; feedback không chữa loại này. Đếm query bị huỷ: pg_stat_database_conflicts trên replica.

Failover nằm ngoài bài: promote replica async có thể mất các commit cuối chưa kịp gửi.

PostgreSQL không biết request nào “của user vừa ghi”: đây là trách nhiệm của ứng dụng.

Cách 1: đọc primary trong N giây sau khi ghi (lưu lastWriteAt vào session). Đơn giản, nhưng N phải lớn hơn replay_lag p99, kể cả lúc conflict chặn replay; lag vượt N thì lỗi quay lại.

Cách 2: so LSN. Ngay sau COMMIT, lấy pg_current_wal_lsn(): vị trí write của WAL, không nhỏ hơn cuối commit record vừa flush. Nếu transaction chạy synchronous_commit = off, commit record có thể chưa được ghi: dùng pg_current_wal_insert_lsn(). Khi đọc, chỉ dùng replica nếu pg_last_wal_replay_lsn() >= LSN.

Lần đọc ngay sau khi ghi

Hay gặp

await primary.query('UPDATE users SET avatar_url = $2 WHERE id = $1', [42, url]);
return replica.query('SELECT avatar_url FROM users WHERE id = 42'); // có thể ra bản cũ

Nên viết

await primary.query('UPDATE users SET avatar_url = $2 WHERE id = $1', [42, url]);
session.minLsn = (await primary.query('SELECT pg_current_wal_lsn() AS lsn')).rows[0].lsn;
// Lần đọc sau:
const ok = await replica.query('SELECT pg_last_wal_replay_lsn() >= $1::pg_lsn AS ok', [session.minLsn]);
const db = ok.rows[0].ok ? replica : primary;

Vì sao

Không đoán N. LSN có thể gồm WAL của transaction khác: chỉ chờ lâu hơn chút, không sai.

Production cần thêm: kiểm tra đúng replica sẽ đọc, timeout và fallback về primary.

Demo PostgreSQL 17: primary (port 5434), streaming replica (5435) tạo bằng pg_basebackup -X stream -C -S replica1 -R:

Terminal window
docker compose -f examples/compose.replication-read-replica.yml up -d --wait
npx --prefix examples ts-node examples/src/replication-read-replica/demo.ts
docker compose -f examples/compose.replication-read-replica.yml down -v

Lag tự nhiên trên một máy rất nhỏ; demo tạo lag nhân tạo bằng recovery_min_apply_delay = '3s' trên replica: WAL nhận và flush ngay, commit record replay sau 3 giây. Output một lần chạy (LSN, số ms đổi mỗi lần):

Primary commit avatar-v2.png, pg_current_wal_lsn() = 0/3033518
Replica đọc ngay: avatar-v1.png (received=0/3033518, replayed=0/30334F0)
pg_stat_replication: async, sent=0/3033518 write=0/3033518 flush=0/3033518 replay=0/30334A8 replay_gap_bytes=112
Chờ replay_lsn >= 0/3033518: 3007 ms, replica đọc: avatar-v2.png

replay phía primary thấp hơn replayed phía replica vì replica báo về theo chu kỳ.

Chạy psql trên cùng stack, delay 3s, replica đặt thành sync qua synchronous_standby_names = 'walreceiver':

Primary đặt UPDATE mất Đọc replica ngay sau
synchronous_commit = on 1.034 ms avatar-v1.png
synchronous_commit = remote_apply 3003.615 ms avatar-v3.png

Conflict (replica đặt max_standby_streaming_delay = '2s', query dài trên replica, primary DELETE rồi VACUUM):

ERROR: 40001: canceling statement due to conflict with recovery
DETAIL: User query might have needed to see row versions that must be removed.

Bật hot_standby_feedback rồi lặp lại: query chạy hết, nhưng VACUUM (VERBOSE) trên primary báo 1001 are dead but not yet removable.

Ưu điểm

  • Giảm tải đọc cho primary.

  • Streaming replica là bản sao đầy đủ, dùng được cho failover.

  • Logical replication cho sao chép một phần và nâng major version.

  • < slot="cons">
  • Replica async luôn có cửa sổ dữ liệu cũ; app phải định tuyến đọc.

  • Query dài trên replica: bị huỷ, kéo dài lag, hoặc gây bloat primary.

  • remote_apply gắn độ trễ commit vào tốc độ replay của standby.

Nhược điểm

  1. Liệt kê màn hình cần read-your-writes; định tuyến tới primary hoặc kiểm tra LSN.
  2. Giám sát replay_lag và chênh LSN theo từng replica, có ngưỡng cảnh báo.
  3. Timeout và fallback về primary khi replica chưa bắt kịp; đo tỉ lệ fallback.
  4. Chọn max_standby_streaming_delay, hot_standby_feedback theo loại query trên replica; theo dõi pg_stat_database_conflicts và bloat primary.
  5. Retry 40001 từ replica trong transaction mới.
  6. Dùng remote_apply thì đo độ trễ commit thật, ưu tiên SET LOCAL cho từng transaction.
Cách hiểu sai Hệ quả Cần làm
Lag nhỏ thì bỏ qua read-your-writes Lỗi lúc conflict hoặc tải ghi lớn Định tuyến theo LSN hoặc thời gian ghi
now() - pg_last_xact_replay_timestamp() là lag Báo động giả khi primary rảnh Dùng replay_lag và chênh LSN
Logical replication sao chép cả schema Subscriber lỗi khi đổi cột; sequence lệch khi cutover Chạy DDL ở subscriber trước; đồng bộ sequence
  1. Trong demo, flush đã bằng LSN của commit mà replica vẫn trả avatar-v1.png. Vì sao?
  2. Vì sao query báo cáo dài trên replica có thể làm user thấy dữ liệu cũ lâu hơn?
  3. Một standby đồng bộ, hai replica async, primary dùng remote_apply: đọc replica nào thì read-your-writes được bảo đảm?