Ba phần trước đều là streaming replication: chép WAL byte-cho-byte sang một bản sao y hệt, cùng phiên bản, chỉ đọc. Nó không làm được những việc hay bị hỏi tới: chép một bảng sang cụm báo cáo đang chạy schema khác, nâng cấp phiên bản lớn với downtime vài giây, hay gom dữ liệu từ nhiều cụm về một chỗ. Logical replication làm được — bằng cách giải mã WAL thành INSERT/UPDATE/DELETE theo bảng và áp lên một cụm khác. Giá của sự linh hoạt đó là một danh sách những thứ không đi qua đường ống, và mỗi thứ trong danh sách là một cách làm subscription đứng im. Phần này dựng, đo lag so với vật lý, rồi cố tình gây ra từng kiểu đứng để xem lỗi gì và gỡ thế nào.
Chép dòng, không chép byte

Hình 1: Publisher giải mã WAL thành thay đổi theo bảng, subscriber áp vào schema của chính nó. DDL, sequence và xung đột không đi qua đường ống này.
Streaming replication chép byte; logical replication chép dòng. Publisher dùng plugin pgoutput đọc WAL (phải có wal_level = logical) và dịch từng thay đổi thành "bảng X: chèn dòng này, sửa dòng kia", gửi qua một replication slot kiểu logical. Subscriber nhận và chạy lại như câu lệnh bình thường trên bảng của nó — bảng đó phải tồn tại trước, cột phải khớp tên và kiểu, và subscriber có thể là phiên bản khác, có index khác, có thêm bảng khác, và ghi được.
Dựng gồm ba lệnh: CREATE PUBLICATION ở publisher, mang schema sang bằng pg_dump -s, và CREATE SUBSCRIPTION ở subscriber. Mặc định copy_data = true: subscriber chép toàn bộ dữ liệu hiện có trước, rồi mới bắt đầu áp thay đổi. Trạng thái theo dõi ở pg_subscription_rel.srsubstate (i khởi tạo → d đang chép → s đồng bộ → r sẵn sàng), lỗi áp đếm ở pg_stat_subscription_stats.apply_error_count.
Danh sách không chép: DDL (không có ALTER TABLE nào đi qua), sequence (giá trị serial không được đồng bộ), và không có cơ chế giải quyết xung đột — subscriber tự ghi một dòng mà publisher cũng ghi là lỗi, và lỗi làm worker chết. Môi trường đo: pg-prim làm publisher (vẫn đang stream vật lý sang pg-rep để so sánh), pg-sub là cụm PostgreSQL 16 mới tinh, bảng docs một triệu dòng, 158 MB.
Đo thật: dựng, lag, giao dịch lớn, và ba cách đứng

Hình 2: Một triệu dòng sẵn sàng sau 1,2 s; lag logic bằng vật lý dưới tải nhẹ; giao dịch 300k dòng áp mất 1,2 s; DDL, sequence và xung đột mỗi thứ một kiểu đứng.
| phép đo | kết quả |
|---|---|
chép 1.000.000 dòng ban đầu tới srsubstate = r |
1.185 ms |
| lag dưới 200 × UPDATE 500 dòng: vật lý / logic | 0 B, 1–2 ms / 0 B, 1–2 ms; md5 khớp |
| UPDATE 300.000 dòng một giao dịch: publisher / subscriber áp xong | 639 ms / 1.836 ms |
| ADD COLUMN ở publisher | worker chết, missing replicated column; thêm cột ở subscriber → nối lại sau 1.314 ms |
| sequence sau 1.000 insert: publisher / subscriber | last_value 1.000 / 1; insert đầu ở subscriber: duplicate key |
| xung đột PK | worker chết, thử lại mỗi 5 s, mọi giao dịch sau kẹt; SKIP (lsn) → thông sau 1.862 ms |
| TRUNCATE | có sang |
Một triệu dòng sẵn sàng sau 1,2 giây — và từ đó là stream. CREATE SUBSCRIPTION tạo slot sub_docs trên publisher, chép 158 MB bằng COPY nội bộ, và srsubstate chuyển sang r sau 1.185 ms. So với dựng replica vật lý ở phần 5 (961 ms cho 700 MB cả cụm), đây là chép đúng một bảng sang một cụm đang có việc riêng — thứ streaming replication không có khái niệm.
Dưới tải cập nhật nhẹ, logic bám sát y như vật lý. 200 giao dịch, mỗi giao dịch UPDATE 500 dòng, trong 20 giây: cả walreceiver (vật lý) lẫn sub_docs (logic) đều báo sent − replay = 0 B, replay_lag 1–2 ms ở mọi mốc; md5 của 100.500 dòng đầu khớp hai bên. Ở tốc độ 5.000 dòng/giây, một apply worker đuổi kịp dễ dàng; giải mã và áp lại từng dòng không phải nút thắt ở đây.
Giao dịch lớn mới lộ ra khác biệt: 300.000 dòng áp mất 1,2 giây sau commit. Publisher chạy UPDATE 300.000 dòng trong 639 ms; subscriber có đủ dữ liệu ở 1.836 ms — nghĩa là ~1,2 giây lag trong lúc một worker áp 300.000 thay đổi tuần tự, bằng câu lệnh từng dòng. Vật lý trong cùng lúc đó chỉ chép vài chục MB trang. Đây là hình dạng chi phí của logical replication: tỉ lệ với số dòng đổi, không với số byte WAL, và với giao dịch lớn thì toàn bộ lag dồn vào sau commit (PostgreSQL 14+ có streaming = on để gửi giao dịch đang chạy theo từng đoạn, không bật ở đây).
DDL không sang: thêm một cột là worker chết. ALTER TABLE docs ADD COLUMN tag ở publisher, rồi một UPDATE chạm cột đó. Subscriber: logical replication target relation "public.docs" is missing replicated column: "tag", apply_error_count 2, pg_stat_subscription không còn worker — subscription đứng, và sẽ đứng cho tới khi có người làm gì đó. Thêm cột ở subscriber: worker tự khởi động lại, dòng id=1 có tag='y' sau 1.314 ms. Quy tắc vận hành: DDL ở subscriber trước, publisher sau với cột thêm; ngược lại với cột xoá. Không có công cụ nào làm thay — pg_dump -s chỉ là ảnh chụp lúc dựng.
Sequence không sang: đổi vai là duplicate key ở dòng đầu tiên. 1.000 dòng tk chép sang đủ, nhưng tk_id_seq ở subscriber vẫn last_value = 1, is_called = f trong khi publisher ở 1.000. Ngày subscriber lên làm primary (đó là kịch bản nâng cấp phiên bản lớn), INSERT đầu tiên là duplicate key value violates unique constraint "tk_pkey". Sửa bằng setval tới max(id) cho từng sequence — một bước trong runbook cutover mà không có cảnh báo nào nhắc.
Xung đột khoá chính: một dòng kẹt là mọi dòng sau kẹt. Subscriber tự có id = 5000; publisher chèn 5000 rồi 5001. Worker lỗi duplicate key, chết, và vì áp tuần tự theo thứ tự commit, dòng 5001 không bao giờ tới — sau 6 giây vẫn 0 dòng, worker chờ restart theo wal_retrieve_retry_interval = 5s, apply_error_count tăng mỗi vòng. Log CONTEXT cho đúng LSN của giao dịch lỗi (finished at 6/E313D538); ALTER SUBSCRIPTION … SKIP (lsn = …) bỏ qua đúng giao dịch đó và 5001 sang sau 1.862 ms; dòng 5000 giữ bản của subscriber. PostgreSQL 16 không có "ai thắng" tự động: xung đột là việc của người vận hành, và mỗi phút chưa gỡ là một phút lag cộng dồn.
TRUNCATE thì sang. Từ PostgreSQL 11, TRUNCATE được chép như một thay đổi: publisher truncate tk → subscriber 0 dòng. Tiện, và cũng là một lệnh không bị chặn bởi "subscriber ghi được" — bảng báo cáo ai đó đang dùng ở subscriber trống sau một TRUNCATE ở publisher.
Đánh đổi cần cân nhắc
Linh hoạt đổi lấy việc tự lo tính toàn vẹn. Streaming replication không thể sai lệch — nó chép byte. Logical replication có ba chỗ lệch được: schema (DDL), sequence, và dữ liệu ghi hai bên. Mỗi chỗ cần một quy ước vận hành: DDL theo thứ tự, setval khi cutover, và subscriber chỉ-ghi-vào-bảng-không-được-chép. Vi phạm không gây lỗi ồn ào; nó gây lag tăng dần cho tới khi ai đó mở pg_stat_subscription_stats.
Slot logic giữ WAL như slot vật lý. Subscription đứng (D1, D3) là slot trên publisher ngừng tiến — và pg_wal của publisher phình đúng như phần 5 đo, với cùng thuốc max_slot_wal_keep_size. Subscription chết lặng lẽ ở cụm báo cáo có thể làm đầy đĩa cụm sản xuất.
Phạm vi đo. Hai container cùng máy; 5.000 dòng/giây là tải nhẹ, nên lag 0 B là cận dưới; giao dịch lớn mới đại diện cho giới hạn một worker. Chưa chạm: streaming = on và parallel apply (PG16), publication theo cột hoặc WHERE (PG15), và dùng logical replication để nâng cấp 16 → 17.
Ba ý mang về
- Logical replication chép dòng của một bảng sang cụm khác: 1.000.000 dòng sẵn sàng sau 1,2 s, lag bằng vật lý dưới tải nhẹ, nhưng chi phí tỉ lệ với số dòng đổi — một giao dịch 300.000 dòng là 1,2 s lag dồn sau commit.
- Ba thứ không sang, ba kiểu đứng im: ADD COLUMN giết worker cho tới khi subscriber có cột; sequence đứng ở 1 cho tới
setval; xung đột PK kẹt mọi giao dịch sau cho tớiSKIP (lsn). Không cái nào tự gỡ, và đứng im nghĩa là slot giữ WAL trên publisher. - Quy ước thay cho cơ chế: DDL ở subscriber trước,
setvaltrong runbook cutover, subscriber không ghi vào bảng được chép — và vẽapply_error_countlên bảng theo dõi, vì đó là chỗ duy nhất lỗi hiện ra.
Nguồn
- PostgreSQL 16 — Logical Replication: https://www.postgresql.org/docs/16/logical-replication.html
- PostgreSQL 16 — Logical Replication: Restrictions (DDL, sequence, large objects): https://www.postgresql.org/docs/16/logical-replication-restrictions.html
- PostgreSQL 16 — Conflicts và
ALTER SUBSCRIPTION … SKIP: https://www.postgresql.org/docs/16/logical-replication-conflicts.html - PostgreSQL 16 —
pg_stat_subscription_stats: https://www.postgresql.org/docs/16/monitoring-stats.html#MONITORING-PG-STAT-SUBSCRIPTION-STATS - PostgreSQL 16 — CREATE SUBSCRIPTION (
copy_data,streaming): https://www.postgresql.org/docs/16/sql-createsubscription.html
Phần sau quay về bên trong một cụm: UPDATE nhiều lần làm bảng phình bao nhiêu, autovacuum có theo kịp không, đo bloat bằng pgstattuple, và wraparound trông như thế nào trước khi nó dừng máy chủ.