Muốn dời một căn nhà, bạn có hai cách. Đóng thùng nguyên cả căn rồi chở đi — mọi thứ y nguyên vị trí, nhưng nặng nề và chỉ đặt vừa đúng loại nền móng cũ. Hoặc gửi bản vẽ rồi dựng lại tại chỗ mới — đi được tới bất cứ đâu, xây bằng vật liệu địa phương, nhưng đồ đạc, màu sơn, đường dây điện bạn quên ghi trong bản vẽ thì không theo sang. Hai kiểu sao chép của PostgreSQL đúng là hai cách đó. Sao chép luồng ở phần trước sao chép byte; sao chép logic sao chép dòng. Khác biệt ấy mở ra những thứ cách kia không làm được, và mang theo ba chỗ hỏng cách kia không có.

Dựng

Cần wal_level = logical ở cả hai bên. Rồi ba câu lệnh:

-- máy nguồn
create publication p_all for table d2, khong_khoa;

-- máy đích
create subscription s_all
  connection 'host=m1 dbname=postgres user=postgres password=x'
  publication p_all;

Đồng bộ ban đầu 500.000 dòng trong 1,2 giây, tự động, không cần pg_basebackup.

Chú ý câu create publication liệt kê từng bảng. Đó là khác biệt đầu tiên và là lý do chính người ta chọn cách này.

So sánh với sao chép luồng

Sao chép luồng Sao chép logic
Độ trễ dưới tải 1,4 – 25 kB 55 – 303 kB
Độ trễ theo giây dưới 0,003 s 0,00 s
Chọn được từng bảng không có
Máy đích ghi được không (chỉ đọc) có
Khác phiên bản PostgreSQL không có
DDL, chuỗi, quyền có (sao byte) không

Độ trễ của sao chép logic cao hơn khoảng mười lần tính theo byte, nhưng cả hai đều dưới một phần trăm giây — với phần lớn ứng dụng thì không phân biệt được.

Bốn dòng giữa là lý do sao chép logic tồn tại. Nâng cấp PostgreSQL 13 lên 16 mà không ngừng dịch vụ, gửi vài bảng sang hệ thống phân tích, gộp dữ liệu từ nhiều máy về một chỗ — sao chép luồng không làm được cái nào.

Dòng cuối là toàn bộ phần còn lại của bài này.

Chỗ hỏng 1: bảng không có khoá chính

update khong_khoa set v = 'da sua' where id = 1;

ERROR:  cannot update table "khong_khoa" because it does not have a replica identity
        and publishes updates
HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.

Lỗi nổ ra trên máy nguồn và chặn hẳn câu lệnh.

Sao chép logic gửi đi "dòng nào đã đổi", và nó cần một cách để chỉ ra dòng đó ở máy đích. Mặc định nó dùng khoá chính. Không có khoá chính thì không có cách nào chỉ.

Điểm tốt: nó từ chối ngay thay vì để hai bên phân kỳ âm thầm. Điểm không tốt: bảng của bạn đột nhiên không UPDATE được nữa, và lỗi xuất hiện ở tầng ứng dụng.

Cách chữa:

alter table khong_khoa replica identity full;

Sau đó DELETE chạy được và hai bên đều còn 99 dòng.

REPLICA IDENTITY FULL nghĩa là gửi toàn bộ dòng cũ để máy đích tự tìm. Nó tốn WAL hơn nhiều và khiến máy đích phải quét tuần tự cho mỗi thay đổi — trên bảng lớn thì rất chậm. Đặt khoá chính vẫn là cách đúng.

Chỗ hỏng 2: DDL không được sao chép, và nó làm đứt hẳn

Đây là chỗ đáng nhớ nhất.

-- chỉ chạy trên máy nguồn
alter table d2 add column moi text default 'x';

Nguồn có 4 cột, đích còn 3. Rồi chèn một dòng dùng cột mới:

ERROR:  logical replication target relation "public.d2" is missing replicated column: "moi"
LOG:    background worker "logical replication worker" (PID 226) exited with exit code 1

Máy đích nhận 0 dòng mới. apply_error_count lên 7. Tiến trình áp dụng chết đi sống lại liên tục, và sao chép dừng hoàn toàn cho tới khi ai đó sửa lược đồ.

Chữa bằng cách thêm cột trên máy đích:

alter table d2 add column moi text;

Đo được: ngay sau đó máy đích nhận đủ dòng và độ trễ về 0 byte. Nó không mất dữ liệu — nó chỉ đợi.

Điều này định ra một quy tắc vận hành cứng: mọi thay đổi lược đồ phải áp lên máy đích trước, máy nguồn sau. Thêm cột thì đích trước; xoá cột thì nguồn trước. Làm ngược là sao chép đứng.

Có công cụ tự động hoá chuyện này, nhưng PostgreSQL bản thân nó không làm.

Chỗ hỏng 3: chuỗi không được sao chép

Đây là chỗ nguy hiểm nhất vì nó hoàn toàn im lặng.

Chèn 1.000 dòng vào bảng có cột serial:

nguồn   1.000 dòng    chuỗi ở giá trị 1.000
đích    1.000 dòng    chuỗi ở giá trị     1

Dữ liệu khớp hoàn toàn. Mọi kiểm tra bạn nghĩ ra đều xanh. Nhưng chuyển đổi sang máy đích rồi chèn dòng đầu tiên là trùng khoá chính ngay lập tức — và tiếp tục trùng 999 lần nữa.

Sao chép logic gửi giá trị id như một giá trị bình thường; nó không đụng tới trạng thái của chuỗi. Đó là hành vi hợp lý (chuỗi không phải dữ liệu bảng) và là cái bẫy hoàn hảo.

Bước bắt buộc trong mọi quy trình chuyển đổi:

-- trên máy đích, sau khi ngừng ghi ở nguồn
select setval('sq_id_seq', (select max(id) from sq));

Với nhiều bảng thì phải sinh câu lệnh cho từng chuỗi một. Đây là bước duy nhất trong bài mà quên nó nghĩa là hệ thống hỏng ngay khi bạn chuyển sang.

Ngoài chuỗi, những thứ khác cũng không được sao chép: quyền GRANT, CREATE INDEX, CREATE VIEW, và bảng mới thêm vào sau (phải alter publication ... add table rồi alter subscription ... refresh publication).

Khi nào dùng cách nào

Tình huống Nên
Máy dự phòng để chuyển đổi khi sự cố Sao chép luồng
Bản sao chỉ đọc cho báo cáo Sao chép luồng
Nâng cấp phiên bản lớn không ngừng dịch vụ Sao chép logic
Gửi vài bảng sang hệ thống phân tích Sao chép logic
Gộp dữ liệu từ nhiều máy về một chỗ Sao chép logic
Máy đích cần ghi được Sao chép logic

Hai dòng đầu là lý do sao chép luồng vẫn là mặc định: nó sao chép mọi thứ, nên máy dự phòng thật sự là bản sao đầy đủ. Sao chép logic cho bạn một cơ sở dữ liệu giống về dữ liệu nhưng không giống về lược đồ, chuỗi, quyền, và chỉ mục — trừ khi bạn tự lo hết.

Theo dõi

Trên máy nguồn:

select slot_name, active, wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) as do_tre
from pg_replication_slots where slot_type = 'logical';

Trên máy đích:

select subname, apply_error_count, sync_error_count
from pg_stat_subscription_stats;

select subname, received_lsn, latest_end_lsn,
       round(extract(epoch from now() - last_msg_receipt_time), 2) as giay_tu_tin_cuoi
from pg_stat_subscription;

apply_error_count khác 0 là dấu hiệu duy nhất cho biết sao chép đang đứng vì lược đồ lệch. Nó không tự sửa và không tự cảnh báo.

Và cảnh báo từ phần 51 vẫn đúng nguyên: khe sao chép logic cũng giữ WAL lại. Một subscription bị đứt vì thiếu cột sẽ khiến pg_wal trên máy nguồn phình lên cho tới khi hết đĩa — trừ khi bạn đã đặt max_slot_wal_keep_size.

Mẫu số chung

Chuỗi lệch âm thầm không phải một lỗi lặt vặt — nó là hệ quả tất yếu của việc chọn sao chép logic. Có hai cách sao một hệ thống, và chúng đánh đổi ngược nhau: sao byte cho một bản sao y hệt (trung thành, nhưng cứng — cùng phiên bản, chỉ đọc), còn sao logic chép nội dung rồi dựng lại phần bao quanh (linh hoạt, nhưng hao hụt — thứ gì bạn không chép tường minh thì im lặng không theo sang). Không cách nào "đúng hơn"; bạn chọn kiểu hỏng nào chịu được, và với bản sao logic thì bạn sở hữu luôn cái danh sách những gì nó bỏ lại.

Đúng lằn ranh vật-lý-và-logic này hiện ra ở mọi chỗ có chữ "sao chép" hay "tuần tự hoá":

  • Trong Java, serialization gốc chép byte trạng thái đối tượng — trung thành nhưng giòn tới mức chỉ đổi một chút là serialVersionUID không khớp và vỡ; còn JSON thì mang được qua phiên bản và qua ngôn ngữ khác, nhưng mất định danh kiểu, trường transient, và tham chiếu vòng — bên nhận phải dựng lại đối tượng. Byte thì đầy đủ mà cứng; logic thì mềm mà hao.
  • Trong Go, gob mang theo thông tin kiểu và hợp cho Go-nói-với-Go, còn JSON đi được ra ngoài hệ Go nhưng đánh rơi các kiểu riêng của Go — đúng cùng một đánh đổi.
  • Ngay trong cơ sở dữ liệu, pg_dump định dạng nhị phân so với bản SQL thuần, hay clone khối đĩa so với chép từng tệp (mất hardlink, thuộc tính mở rộng, lỗ thưa) — mọi cặp đôi đều là "chép nguyên vẹn nhưng kén" đối lại "chép ý nghĩa nhưng phải tự dựng phần còn thiếu".

Nên câu hỏi đúng khi sao chép bất cứ thứ gì không phải "cách nào đầy đủ hơn" mà là "cách này không mang theo cái gì, và cái đó có im lặng không". Bản sao byte hỏng ồn ào (không khớp là vỡ ngay); bản sao logic hỏng lặng lẽ (mọi thứ xanh cho tới lúc dùng) — nên với nó, bạn phải tự cầm danh sách kiểm. Bắt đầu bằng đúng danh sách đó nếu đang chạy sao chép logic:

-- trên máy đích
select subname, apply_error_count from pg_stat_subscription_stats;

-- so lược đồ hai bên
select table_name, count(*) as so_cot
from information_schema.columns
where table_schema = 'public' group by table_name order by table_name;

Chạy câu thứ hai ở cả hai máy và so kết quả. Bất kỳ bảng nào lệch số cột là một quả bom hẹn giờ — nó chưa nổ chỉ vì chưa có ai ghi vào cột mới. Và kiểm chuỗi:

select schemaname, sequencename, last_value from pg_sequences;

Nếu máy đích toàn số 1 trong khi nguồn có số lớn, bạn biết chuyện gì sẽ xảy ra vào ngày chuyển đổi.

Phần sau đo giám sát: những khung nhìn thống kê nào đáng theo dõi, và con số nào báo động sớm nhất.