Cái chăn quá ngắn là ẩn dụ tôi luôn nghĩ tới khi chỉnh máy dự phòng. Kéo lên đắp vai thì hở chân, kéo xuống đắp chân thì lạnh vai — bạn dời được chỗ lạnh đi đâu tuỳ ý, nhưng không làm nó biến mất được. Sao chép luồng của PostgreSQL đầy những cái chăn ngắn như vậy, và không nhận ra điều đó là lý do người ta cứ tìm một cấu hình "vừa nhanh vừa an toàn vừa bền" không tồn tại. Máy dự phòng là thứ ai cũng dựng và ít ai đo; phần này dựng một cặp máy thật, đo độ trễ dưới tải, rồi cố tình làm nó hỏng theo hai cách hay gặp nhất.

Dựng

Ba tham số trên máy chính:

wal_level = replica
max_wal_senders = 10
max_replication_slots = 10

Rồi tạo một khe sao chép và lấy ảnh chụp:

# trên máy chính
psql -c "select pg_create_physical_replication_slot('s1')"

# trên máy dự phòng
pg_basebackup -h m1 -U postgres -D /var/lib/postgresql/data -Xs -R -S s1

Cờ -R tự sinh tệp cấu hình và standby.signal, cờ -S s1 gắn vào khe vừa tạo. Khởi động lên là xong:

máy chính   : 1.000.000 dòng   pg_is_in_recovery = f
máy dự phòng: 1.000.000 dòng   pg_is_in_recovery = t

Độ trễ dưới tải

Tám kết nối cập nhật liên tục:

Giây Độ trễ byte Độ trễ giây Trạng thái
5 1.408 byte 0,000 streaming
10 25 kB 0,003 streaming
20 2.224 byte 0,000 streaming
25 2.616 byte 0,001 streaming

Dưới 3 mili giây suốt cả bài đo. Trên mạng nội bộ, sao chép luồng gần như tức thì.

Con số này quan trọng vì nó đặt lại kỳ vọng: nếu bản sao của bạn tụt lại hàng giây hoặc hàng phút, đó không phải giới hạn của cơ chế — đó là dấu hiệu có gì đó khác đang sai. Thường là đĩa của máy dự phòng chậm hơn máy chính, hoặc máy dự phòng đang chạy truy vấn nặng.

Một lưu ý khi đọc pg_last_xact_replay_timestamp(): nó trả về thời điểm của giao dịch cuối cùng đã phát lại. Khi máy chính không ghi gì, con số này lớn dần và trông như độ trễ khổng lồ. Ở lần đo đầu tôi thấy 259 giây, và đó chỉ có nghĩa là 259 giây qua không có giao dịch ghi nào.

Cách đo đúng dưới tải là so pg_current_wal_lsn() trên máy chính với replay_lsn trong pg_stat_replication.

Sao chép đồng bộ lấy đi 37%

synchronous_standby_names tps Độ trễ
'' (bất đồng bộ) 22.337 0,358 ms
'*' (đồng bộ) 13.964 0,573 ms

Máy chính đợi máy dự phòng ghi WAL xuống đĩa trước khi báo commit thành công. Cái giá là 37% thông lượng và 0,215 ms mỗi giao dịch.

Con số 0,215 ms đó gần bằng chi phí fsync đã đo ở phần 48 (0,123 ms) — hợp lý, vì nó là một lần fsync nữa cộng một vòng đi–về mạng.

Đây là đánh đổi giữa "không bao giờ mất giao dịch đã xác nhận" và "37% thông lượng". Phần 48 đã liệt kê năm mức của synchronous_commit, và mức local là lựa chọn ở giữa: giữ độ bền trên máy chính, không đợi máy dự phòng.

Kiểu hỏng 1: truy vấn trên bản sao bị huỷ

Bản sao đang chạy một truy vấn dài. Máy chính DELETE rồi VACUUM những dòng mà truy vấn đó cần đọc:

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

Truy vấn bị huỷ, không phải chậm — huỷ hẳn.

Bản sao phải phát lại WAL của máy chính, kể cả phần WAL nói "dọn những dòng này đi". Nhưng truy vấn đang chạy trên bản sao cần đúng những dòng đó. Hai việc không thể cùng đúng, và PostgreSQL chọn ưu tiên việc bám sát máy chính.

max_standby_streaming_delay quyết định nó đợi bao lâu trước khi huỷ. Mặc định 30 giây; tôi hạ xuống 2 giây để dựng lại hiện tượng trong thời gian đo.

Kiểm bằng:

select * from pg_stat_database_conflicts where datname = current_database();

Ở phép đo của tôi: confl_snapshot = 2.

Có hai cách xử lý, và cả hai đều có giá:

Tăng max_standby_streaming_delay. Truy vấn dài chạy được, nhưng bản sao tụt lại đúng bằng khoảng đó — nếu bạn cần nó để chuyển đổi khi sự cố, nó không sẵn sàng.

Bật hot_standby_feedback = on. Bản sao báo ngược cho máy chính rằng nó đang cần những phiên bản dòng nào, và máy chính không dọn chúng. Hết xung đột — nhưng máy chính giờ hành xử như đang có một giao dịch dài mở, và phần 21 đã đo hậu quả: bảng phình lên, VACUUM không dọn được gì.

Không có lựa chọn miễn phí. Cách thực dụng là tách bạch: bản sao dành cho báo cáo thì bật hot_standby_feedback và chấp nhận; bản sao dành cho chuyển đổi khi sự cố thì không cho ai truy vấn.

Kiểu hỏng 2: khe sao chép làm đầy đĩa máy chính

Đây là kiểu hỏng làm sập máy chính, và nó bắt nguồn từ chính cơ chế bảo vệ.

Tôi dừng máy dự phòng rồi chạy ba vòng ghi trên máy chính:

WAL bị giữ lại:   138 MB
thư mục pg_wal:   448 MB

Khe sao chép bảo đảm máy chính không xoá WAL mà bản sao chưa nhận. Đó là điều bạn muốn khi bản sao chỉ mất kết nối vài phút. Nhưng nếu bản sao chết hẳn và không ai để ý, máy chính giữ WAL cho tới khi hết đĩa — và một cơ sở dữ liệu hết đĩa thì dừng nhận ghi.

Tham số chặn chuyện đó:

show max_slot_wal_keep_size;
--  -1

-1 nghĩa là không giới hạn — mặc định. Đặt một con số thật:

alter system set max_slot_wal_keep_size = '10GB';

Khi khe vượt ngưỡng, PostgreSQL vô hiệu hoá nó và xoá WAL. Bản sao đó hỏng và phải dựng lại từ đầu — nhưng máy chính sống. Đó là đánh đổi đúng: mất bản sao còn hơn mất máy chính.

Theo dõi:

select slot_name, active, wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) as wal_giu_lai
from pg_replication_slots;

Cột wal_status là cột cảnh báo sớm: reserved là bình thường, extended là đã vượt max_wal_size, unreserved là sắp bị vô hiệu hoá, lost là đã mất.

Và khe không dùng nữa thì phải xoá:

select pg_drop_replication_slot('ten_khe');

Một khe bỏ quên từ lần thử nghiệm ba tháng trước vẫn đang giữ WAL, và không có gì nhắc bạn.

Theo dõi cái gì

Trên máy chính:

select application_name, state, sync_state,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn))   as chua_gui,
       pg_size_pretty(pg_wal_lsn_diff(sent_lsn, flush_lsn))              as chua_ghi,
       pg_size_pretty(pg_wal_lsn_diff(flush_lsn, replay_lsn))            as chua_ap_dung,
       write_lag, flush_lag, replay_lag
from pg_stat_replication;

Ba cột lag là ba giai đoạn khác nhau, và chúng nói ba chuyện khác nhau:

Cột lớn Nghĩa là
chua_gui Mạng chậm hoặc máy chính quá tải
chua_ghi Đĩa của máy dự phòng chậm
chua_ap_dung Máy dự phòng đang bận phát lại, hoặc đang đợi vì xung đột truy vấn

Cột thứ ba lớn thường đi kèm với max_standby_streaming_delay cao — đó là bản sao đang chờ một truy vấn dài kết thúc.

Mẫu số chung

Câu "không có lựa chọn miễn phí" ở kiểu hỏng 1 không riêng gì máy dự phòng — nó là một quy luật. Khi một thành phần phải phục vụ hai mục tiêu xung khắc nhau, không cấu hình nào xoá được xung đột; mỗi cái núm chỉ dời chi phí sang chỗ khác. Bản sao vừa phải bám sát máy chính để sẵn sàng chuyển đổi, vừa phải giữ ảnh cũ để phục vụ truy vấn — hai việc kéo ngược nhau, và max_standby_streaming_delay dời cái giá sang "bản sao tụt hậu", còn hot_standby_feedback dời nó sang "máy chính phình bảng". Cái chăn vẫn ngắn như cũ.

Đúng chuyện dời-chứ-không-xoá này lặp ở mọi nơi:

  • Trong Java, không bộ thu gom rác nào rẻ trên cả ba mặt thông lượng – độ trễ – bộ nhớ cùng lúc: ZGC/G1 mua độ trễ thấp bằng CPU và bộ nhớ, Parallel mua thông lượng bằng những cú dừng dài. Đổi GC là dời cái giá, không phải bỏ nó.
  • Trong Go, một núm GOGC duy nhất kéo qua kéo lại giữa RAM và CPU: hạ xuống thì tốn ít bộ nhớ hơn nhưng GC chạy nhiều hơn, nâng lên thì ngược lại. Không có giá trị nào rẻ trên cả hai.
  • Ở tầng hệ thống, một cache TTL cũng vậy: đặt ngắn thì dữ liệu tươi nhưng tải dội về nguồn nhiều, đặt dài thì rẻ nhưng dễ cũ — mỗi TTL chỉ đặt cái giá "cũ hay tải nặng" vào một chỗ khác nhau.

Nên câu hỏi đúng khi chỉnh một hệ có ràng buộc kéo ngược không phải "làm sao cho nó miễn phí" — nó không bao giờ miễn phí — mà là "tôi chịu trả cái giá này ở đâu thì đỡ đau nhất". Và có một hệ quả sắc hơn ở kiểu hỏng 2: một cơ chế bảo vệ mà không chặn trần sẽ dời chi phí về đúng chỗ tệ nhất — cái khe giữ WAL để cứu bản sao lại làm sập máy chính; nên luôn đặt trần và chọn trước "mất bản sao còn hơn mất máy chính". Tự soát ngay:

select slot_name, active, wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) as wal_giu_lai
from pg_replication_slots;

show max_slot_wal_keep_size;

Hai câu hỏi: có khe nào active = false không (mỗi cái đang giữ WAL cho một bản sao không tồn tại), và max_slot_wal_keep_size có phải -1 không (nếu có, đĩa máy chính của bạn đang không có lưới an toàn nào).

Phần sau đo sao chép logic: nó khác sao chép luồng chỗ nào, chậm hơn bao nhiêu, và làm được gì mà sao chép luồng không làm được.