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
Cần đọc trước
Vấn đề
Phần tiêu đề “Vấn đề”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)?
Chọn gì? (tóm tắt trước)
Phần tiêu đề “Chọn gì? (tóm tắt trước)”| 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 |
Khái niệm cốt lõi
Phần tiêu đề “Khái niệm cốt lõi”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.
Cách hoạt động
Phần tiêu đề “Cách hoạt động”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_lagxấ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ề
NULLsau một lúc.
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 |
synchronous_commit chờ tới mốc nào
Phần tiêu đề “synchronous_commit chờ tới mốc nào”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ễ.
Chi tiết vận hành
Phần tiêu đề “Chi tiết vận hành”Đ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_bytesFROM 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(defaultoff) 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_conflictstrê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.
Xử lý read-your-writes ở ứng dụng
Phần tiêu đề “Xử lý read-your-writes ở ứng dụng”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.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Demo PostgreSQL 17: primary (port 5434), streaming replica (5435) tạo bằng pg_basebackup -X stream -C -S replica1 -R:
docker compose -f examples/compose.replication-read-replica.yml up -d --waitnpx --prefix examples ts-node examples/src/replication-read-replica/demo.tsdocker compose -f examples/compose.replication-read-replica.yml down -vLag 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/3033518Replica đọ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=112Chờ replay_lsn >= 0/3033518: 3007 ms, replica đọc: avatar-v2.pngreplay 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 recoveryDETAIL: 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.
Trade-offs
Phần tiêu đề “Trade-offs”Ư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_applygắn độ trễ commit vào tốc độ replay của standby.
Nhược điểm
Checklist production
Phần tiêu đề “Checklist production”- Liệt kê màn hình cần read-your-writes; định tuyến tới primary hoặc kiểm tra LSN.
- Giám sát
replay_lagvà chênh LSN theo từng replica, có ngưỡng cảnh báo. - Timeout và fallback về primary khi replica chưa bắt kịp; đo tỉ lệ fallback.
- Chọn
max_standby_streaming_delay,hot_standby_feedbacktheo loại query trên replica; theo dõipg_stat_database_conflictsvà bloat primary. - Retry
40001từ replica trong transaction mới. - Dùng
remote_applythì đo độ trễ commit thật, ưu tiênSET LOCALcho từng transaction.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”| 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 |
Tự kiểm tra
Phần tiêu đề “Tự kiểm tra”- Trong demo,
flushđã bằng LSN của commit mà replica vẫn trảavatar-v1.png. Vì sao? - 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?
- 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?
Tóm tắt
Phần tiêu đề “Tóm tắt”Tham khảo
Phần tiêu đề “Tham khảo”- Log-Shipping Standby Servers: async mặc định,
remote_apply. - Hot Standby: lệnh bị cấm, các loại conflict,
hot_standby_feedback. - synchronous_commit: từng mức chờ.
- Replication settings: default của delay, feedback.
- pg_stat_replication: cột LSN và lag.
- Recovery Information Functions: hàm LSN, replay.
- Logical Replication Restrictions: những gì không được sao chép.