Máy dự phòng chỉ có giá trị nếu bạn chuyển sang được. Phần này giết một máy chính đang có ghi dở, thăng cấp máy dự phòng, và đo mọi thứ — kể cả phần khó hơn nhiều là đưa máy cũ quay lại.

Thời gian chuyển đổi, hai đầu não, và pg_rewind từ chối

Chuyển đổi

Ứng dụng đang ghi liên tục và ghi lại mỗi lần commit thành công. Tại thời điểm sự cố, nó đã xác nhận 2.426 dòng, id cuối cùng là 202447.

0,00 s   docker kill máy chính
0,24 s   pg_ctl promote xong,  pg_is_in_recovery = f
0,52 s   máy mới nhận ghi:  insert ... returning id  ->  202476

Nửa giây từ lúc máy chính chết tới lúc máy mới nhận ghi.

Và số giao dịch đã xác nhận bị mất: 0.

Có một chi tiết đáng chú ý trong con số. Id cao nhất còn lại trên máy mới là 202462, cao hơn id cuối cùng mà ứng dụng biết là 202447. Nghĩa là máy dự phòng có nhiều hơn những gì ứng dụng biết — đó là các giao dịch đã commit trên máy chính và đã truyền đi, nhưng xác nhận không kịp về tới ứng dụng trước khi máy chết.

Đây là hành vi đúng và đáng hiểu: sao chép bất đồng bộ có thể mất giao dịch đã xác nhận, nhưng nó cũng có thể giữ lại giao dịch chưa xác nhận. Ứng dụng nào coi "không nhận được xác nhận" là "chắc chắn không xảy ra" sẽ sai ở đây.

Con số 0,52 giây là của một cặp máy nhỏ trên cùng một máy chủ. Trên hạ tầng thật, phần lớn thời gian nằm ở việc phát hiện máy chính chết, không phải ở việc thăng cấp.

Khởi động lại máy cũ: hai đầu não

Đây là chỗ mọi thứ hỏng.

Máy chính cũ được khởi động lại mà không xử lý gì:

máy cũ    recovery = f    202.462 dòng    ghi được, cấp id 202476
máy mới   recovery = f    202.463 dòng    ghi được, cấp id 202476

Cả hai đều tự coi mình là máy chính. Cả hai đều nhận ghi. Cả hai cấp cùng một id cho hai dòng hoàn toàn khác nhau.

Không có gì trong PostgreSQL ngăn chuyện này. Máy cũ không biết nó đã bị thay thế; nó chỉ thấy mình vừa khởi động lại sau một lần dừng không sạch.

Nếu ứng dụng của bạn vẫn còn kết nối trỏ tới địa chỉ cũ — hoặc nếu có một máy chủ ứng dụng nào chưa kịp cập nhật — dữ liệu sẽ đi vào hai nơi và không có cách nào gộp lại.

Đây là lý do mọi hệ thống chuyển đổi tự động đều phải có một cơ chế bảo đảm máy cũ không thể quay lại làm máy chính: Patroni với đồng thuận qua etcd, repmgr với hàng rào, hoặc STONITH ở tầng hạ tầng. PostgreSQL không tự làm.

pg_rewind từ chối, và lý do đến từ quá khứ

Cách đúng để đưa máy cũ về làm bản sao là pg_rewind — nó tìm điểm hai dòng thời gian rẽ nhánh rồi chỉ sao lại phần khác nhau.

pg_rewind: connected to server
pg_rewind: error: target server needs to use either data checksums
           or "wal_log_hints = on"

Kiểm bằng pg_controldata:

wal_log_hints setting:        off
Data page checksum version:   0

Cả hai đều tắt, và không bật được sau khi sự cố đã xảy ra. wal_log_hints cần khởi động lại máy chủ — nhưng máy chủ cần khởi động lại chính là máy đã hỏng, và bật nó bây giờ cũng không sinh ra dữ liệu WAL của quá khứ.

Phương án duy nhất còn lại là dựng lại từ đầu:

pg_basebackup cho 38 MB dữ liệu  ->  78,9 giây

78,9 giây cho 38 MB. Trên cơ sở dữ liệu 500 GB, đó là hàng giờ — và trong suốt thời gian đó bạn chạy không có máy dự phòng.

Đối chiếu: pg_rewind chỉ sao lại phần khác nhau, thường là vài trăm megabyte dù cơ sở dữ liệu lớn cỡ nào.

Đây là kiểu chi phí điển hình của việc bỏ qua một tham số lúc dựng hệ thống: nó không tốn gì trong hai năm, rồi tốn ba tiếng vào đúng lúc bạn không có ba tiếng.

Bốn việc phải làm trước

1. Bật wal_log_hints = on — hoặc bật data checksums lúc initdb, cách này còn tốt hơn vì nó cũng phát hiện hỏng dữ liệu.

wal_log_hints = on

Cần khởi động lại. Làm ngay hôm nay, không phải sau lần sự cố đầu tiên.

Chi phí là WAL nhiều hơn một chút, vì các thay đổi "gợi ý" trên trang cũng được ghi. So với 78,9 giây nhân theo kích thước cơ sở dữ liệu thì đó là đánh đổi dễ.

2. Có cơ chế chặn hai đầu não ở lớp ngoài. Patroni là lựa chọn phổ biến nhất; nó dùng etcd hoặc Consul làm trọng tài và bảo đảm chỉ một máy được làm chính.

3. Một chỗ duy nhất để ứng dụng hỏi "máy chính là ai". Địa chỉ IP nổi, một proxy như HAProxy hoặc PgBouncer, hoặc một dịch vụ khám phá. Nếu chuỗi kết nối của ứng dụng ghi thẳng địa chỉ máy chính, mỗi lần chuyển đổi là một lần phải sửa cấu hình ở mọi máy ứng dụng.

Từ libpq 10 trở lên còn có cách khai nhiều máy chủ trong một chuỗi kết nối:

postgresql://may1:5432,may2:5432/db?target_session_attrs=read-write

Driver tự thử từng máy và chọn máy nào nhận ghi. Đủ dùng cho hệ thống nhỏ, không thay thế được một proxy thật.

4. Diễn tập. Con số 0,52 giây là của một lần chuyển đổi đã chuẩn bị sẵn. Lần đầu tiên bạn làm việc này trên hệ thống thật, phần lớn thời gian sẽ nằm ở việc tìm câu lệnh đúng và nhớ ra máy dự phòng nằm ở đâu.

Kiểm tra trước khi cần

-- trên cả máy chính lẫn máy dự phòng
show wal_log_hints;
show data_checksums;

Nếu cả hai đều off, bạn đang ở đúng tình huống của bài này.

-- trên máy dự phòng: nó có thật sự bám sát không
select pg_is_in_recovery(),
       pg_last_wal_receive_lsn(),
       pg_last_wal_replay_lsn(),
       pg_last_xact_replay_timestamp();

receive_lsnreplay_lsn chênh nhau nhiều nghĩa là máy dự phòng nhận được WAL nhưng chưa phát lại kịp — nó sẽ mất thêm thời gian đó lúc thăng cấp.

-- trên máy chính: nó có thật sự đang gửi không
select client_addr, state, sync_state, replay_lag from pg_stat_replication;

Bảng rỗng nghĩa là không có máy dự phòng nào đang kết nối, dù bạn nghĩ là có.

Lệnh thăng cấp

Ba cách, cùng kết quả:

pg_ctl -D /var/lib/postgresql/data promote
select pg_promote();
touch /var/lib/postgresql/data/promote.signal    # PostgreSQL 12+

Cách thứ hai tiện nhất khi bạn đang ở trong psql. Cách thứ ba dùng được khi không có shell trên máy dự phòng.

Sau khi thăng cấp, dòng thời gian tăng lên và một tệp .history xuất hiện trong pg_wal — đó là dấu vết vĩnh viễn cho biết máy này từng được thăng cấp và tại điểm nào.

Thử ba mươi giây

show wal_log_hints;

Nếu là off, thêm vào postgresql.conf và lên lịch khởi động lại. Đó là một dòng, và nó là khác biệt giữa "vài phút" và "vài giờ" trong lần chuyển đổi tiếp theo.

Rồi hỏi câu thứ hai: ứng dụng biết máy chính là ai bằng cách nào? Nếu câu trả lời là "địa chỉ ghi trong tệp cấu hình", bạn có một việc nữa cần làm trước lần sự cố kế tiếp.

Phần sau đo pg_stat_statements: cách tìm truy vấn đắt nhất, và vì sao truy vấn chậm nhất thường không phải truy vấn đáng sửa.