Bài trước ta thấy replica async có thể trễ, khiến đọc phải dữ liệu cũ. Nhưng lag còn một hệ quả nghiêm trọng hơn: nếu primary chết đột ngột, những giao dịch cuối chưa kịp sang standby sẽ mất trắng. Nhân bản đồng bộ (synchronous) giải bài toán đó bằng cách bắt primary chờ standby xác nhận trước khi báo commit thành công — đổi lại mỗi lần ghi chậm hơn. Bài này đo thật cái giá đó bằng pgbench, và trình diễn cái bẫy chết người của sync: commit treo khi standby chết.
Async và sync: primary có chờ hay không
Khác biệt nằm ở một câu hỏi: khi bạn COMMIT, primary báo thành công ngay hay chờ standby?
- Async (mặc định): primary ghi WAL cục bộ xong là commit ngay, không quan tâm standby đã nhận chưa. Nhanh, nhưng nếu primary chết trước khi WAL sang standby, giao dịch đó mất.
- Sync: primary chờ standby xác nhận đã nhận (và ghi) WAL rồi mới báo commit. Không mất dữ liệu ngay cả khi primary chết ngay sau đó — nhưng mỗi commit gánh thêm một vòng đi-về mạng.
-- Bật sync: chỉ định standby nào phải xác nhận
ALTER SYSTEM SET synchronous_standby_names = 'walreceiver';
SELECT pg_reload_conf();

Hình 1: Async commit ngay (nhanh, có thể mất giao dịch cuối); sync chờ standby xác nhận (bền, chậm hơn). Dải synchronous_commit từ off tới remote_apply. Và hiểm hoạ: standby duy nhất chết thì commit treo.
Đo thật: dải thông lượng theo mức chờ
PostgreSQL cho một dải mức synchronous_commit, mỗi mức chờ tới một điểm khác nhau. Tôi chạy pgbench ghi (TPC-B, 8 client) trên primary với standby đang kết nối, đổi từng mức:

Hình 2: Thông lượng giảm dần khi chờ xa hơn — off 5.021, local 3.495, on (sync) 2.732, remote_apply 2.475 tps. Latency async 2,018 ms so với sync 2,709 ms. Và demo commit treo với wait_event SyncRep khi standby chết.
Con số thật cho bốn mức:
off— không chờ cả việc ghi WAL xuống đĩa local: 5.021 tps (nhanh nhất, nhưng mất dữ liệu ngay cả khi chỉ primary crash).local— chờ WAL ghi xuống đĩa local, không chờ standby (đây chính là "async" về mặt nhân bản): 3.495 tps.on— chờ standby flush WAL xuống đĩa (mức sync mặc định): 2.732 tps.remote_apply— chờ standby phát lại xong (đọc từ standby thấy ngay): 2.475 tps.
Dải này thể hiện đúng bản chất đánh đổi: càng chờ xa, càng bền, càng chậm. Bật sync (on) đổi khoảng 22% thông lượng so với async (local) để lấy đảm bảo không mất dữ liệu khi primary chết. So latency trực tiếp: async 2,018 ms so với sync 2,709 ms — thêm ~0,69 ms mỗi commit, chính là một vòng đi-về mạng tới standby. Trên mạng nội bộ con số này nhỏ; trên WAN (standby ở data center khác) nó sẽ lớn hơn nhiều.
Hiểm hoạ: standby chết thì commit treo
Đây là cái bẫy phải biết trước khi bật sync. Nếu bạn cấu hình một standby sync và nó chết, primary sẽ chờ mãi một xác nhận không bao giờ tới — mọi giao dịch ghi treo cứng.
Tôi trình diễn thật: bật sync, dừng standby, rồi chạy một INSERT:
23:52:51 dừng standby; chạy INSERT (sync on) ở nền
23:52:55 sau 4 giây: INSERT VẪN TREO — wait_event = SyncRep
23:52:56 khởi động lại standby -> INSERT 0 1 hoàn tất ngay
Backend đang chờ ở đúng trạng thái wait_event = SyncRep (chờ nhân bản đồng bộ) — bằng chứng nó bị kẹt vì không standby nào xác nhận. Ngay khi standby quay lại, commit được giải phóng và hoàn tất. Nghĩa là sync với một standby biến standby thành một điểm chết mới cho việc ghi: standby sập là cả primary ngừng nhận ghi.
Cách giảm rủi ro: cấu hình nhiều standby và dùng ANY N (s1, s2, s3) — chỉ cần N standby bất kỳ xác nhận là đủ, nên một standby chết không làm treo. Ví dụ ANY 1 (s1, s2) chỉ cần một trong hai xác nhận.
Đánh đổi cần cân nhắc
Sync không miễn phí, và không phải lúc nào cũng cần. Với đa số ứng dụng, async là đủ: mất vài giao dịch cuối khi primary crash đột ngột là chấp nhận được (và crash như vậy hiếm). Chỉ bật sync khi mất một giao dịch là không thể chấp nhận — giao dịch tài chính, đơn hàng đã thu tiền. Trả giá bằng thông lượng và độ trễ cho đúng chỗ cần.
remote_apply mạnh nhất nhưng chậm nhất. Nó đảm bảo standby đã phát lại xong, nên đọc từ standby ngay sau commit sẽ thấy dữ liệu mới (giải bài toán read-your-writes ở bài trước). Nhưng nó chờ lâu nhất (2.475 tps ở đây). Chỉ dùng khi bạn thật sự cần đọc-nhất-quán-từ-replica ngay sau ghi.
Sync cần ít nhất hai standby để an toàn. Như demo cho thấy, sync với một standby là một điểm chết. Muốn vừa có độ bền của sync vừa không sợ treo, cần tối thiểu hai standby với ANY 1. Đây là chi phí hạ tầng thật, phải cân với giá trị của việc không mất dữ liệu.
Ba ý mang về
- Async commit ngay (nhanh, có thể mất giao dịch cuối khi primary chết); sync chờ standby xác nhận (bền, chậm hơn): đo thật dải
synchronous_committừ off 5.021 tps xuống remote_apply 2.475 tps — bật synconđổi ~22% thông lượng và thêm ~0,69 ms mỗi commit để đảm bảo không mất dữ liệu. - Hiểm hoạ sync với một standby: standby chết là commit treo — demo thật một INSERT kẹt ở
wait_event = SyncRepcho tới khi standby quay lại; giảm rủi ro bằng nhiều standby vàANY N (...)để chỉ cần N bất kỳ xác nhận. - Chọn theo giá trị dữ liệu: async đủ cho đa số (mất giao dịch cuối khi crash hiếm là chấp nhận được); bật sync chỉ khi mất một giao dịch là không thể chấp nhận, và khi đó cần tối thiểu hai standby để sync không tạo điểm chết mới.
Phần sau ta xét một cơ chế nhân bản linh hoạt hơn về chọn lọc: Phần sau tìm hiểu logical replication — nhân bản theo bảng thay vì cả cụm, cho phép chọn lọc dữ liệu và nhân bản giữa các phiên bản PostgreSQL khác nhau.