Hai cái công tắc cạnh nhau trên tường trông y hệt nhau, nhưng một cái chỉ chỉnh độ sáng đèn, cái kia cắt nguồn điện kết cấu của cả toà nhà. Dán nhãn cả hai là "tiết kiệm điện" rồi bật tắt tuỳ hứng là cách hỏng việc. Trong PostgreSQL, fsync và synchronous_commit là hai cái công tắc như vậy — cùng bị gọi là "chỉnh độ bền", mà thật ra là hai canh bạc khác hẳn nhau. Độ bền dữ liệu có giá, và giá đó đo được; phần này đo bốn mức, và tìm ra rằng tham số nguy hiểm nhất cũng là tham số vô ích nhất.

Bốn mức, tám kết nối

Cấu hình tps Độ trễ So với mặc định
Mặc định (cả hai bật) 25.645 0,312 ms 1,00×
synchronous_commit = off 87.714 0,091 ms 3,42×
fsync = off (nguy hiểm) 78.300 0,102 ms 3,05×
Cả hai tắt (rất nguy hiểm) 83.403 0,096 ms 3,25×

Kết quả quan trọng nhất nằm ở dòng thứ ba: tắt fsync không nhanh hơn tắt synchronous_commit — nó còn chậm hơn một chút.

Và dòng thứ tư cho thấy tắt cả hai cũng không hơn tắt riêng synchronous_commit.

Nghĩa là: fsync = off không mua được gì mà bán đi rất nhiều. Đó là một tham số không có trường hợp sử dụng hợp lý trên hệ thống thật.

Giá của một lần fsync

Một kết nối, không tranh chấp, để đo trực tiếp độ trễ mỗi lần commit:

Cấu hình tps Độ trễ
fsync=on, synchronous_commit=on 4.910 0,204 ms
fsync=on, synchronous_commit=off 12.283 0,081 ms
fsync=off, synchronous_commit=on 12.043 0,083 ms
0,204 − 0,081 = 0,123 ms

Đó là giá của độ bền, cho mỗi lần commit, trên máy đo này với SSD. Trên đĩa quay nó là vài mili giây; trên đĩa mạng có thể còn hơn.

Con số 0,123 ms giải thích cả bảng đầu tiên: với 8 kết nối, mỗi kết nối tiết kiệm 0,123 ms mỗi giao dịch thì tổng cộng ra 3,42 lần thông lượng.

Nó cũng giải thích vì sao chèn từng dòng một giao dịch chậm đến thế ở phần 37 — 7.278 dòng mỗi giây chính là giới hạn của việc fsync một triệu lần.

Hai tham số, hai loại rủi ro hoàn toàn khác nhau

Đây là điểm mà tôi nghĩ nhiều người gộp làm một, và chúng không giống nhau chút nào.

synchronous_commit = off: mất giao dịch gần nhất, cơ sở dữ liệu vẫn nhất quán.

PostgreSQL vẫn ghi WAL, vẫn fsync — nó chỉ không đợi việc đó xong trước khi báo commit thành công cho ứng dụng. Nếu máy mất điện, các giao dịch chưa kịp xuống đĩa biến mất.

Cửa sổ mất dữ liệu bị chặn bởi wal_writer_delay, mặc định 200 ms. Tiến trình ghi WAL đẩy dữ liệu xuống đĩa mỗi 200 ms, nên bạn mất tối đa khoảng ba lần con số đó.

Điều quan trọng: cơ sở dữ liệu vẫn nhất quán. Nó khởi động lại bình thường, mọi dữ liệu còn lại đọc được, không có gì hỏng. Bạn mất vài trăm mili giây cuối, không mất tính toàn vẹn.

fsync = off: cơ sở dữ liệu có thể hỏng.

Không có gì bảo đảm thứ tự ghi giữa WAL và tệp dữ liệu. Sau mất điện, chỉ mục có thể trỏ vào dòng không tồn tại, trang có thể rách nửa chừng, và cơ chế khôi phục không sửa được vì chính WAL cũng có thể chưa xuống đĩa.

Không có cách khôi phục ngoài bản sao lưu.

Tôi có thử dựng phép đo cho hiện tượng này — giết tiến trình bằng SIGKILL rồi đếm số giao dịch đã được xác nhận nhưng biến mất. Tôi không dựng được phép đo đáng tin: vòng lặp ghi của tôi vẫn tiếp tục sau tín hiệu giết, nên số liệu trước và sau không so được. Trong cả hai lần thử, cơ sở dữ liệu khởi động lại bình thường và không hỏng — nhưng đó không phải bằng chứng cho tính an toàn, chỉ là một lần thử không đủ khắc nghiệt.

Tôi ghi lại chỗ này thay vì bịa một con số, vì đây đúng là loại kết luận mà một phép đo tồi sẽ làm hại.

Cách dùng đúng: đặt riêng cho từng giao dịch

Đừng đổi giá trị toàn cục. Đặt cho đúng những giao dịch không cần độ bền:

begin;
set local synchronous_commit = off;
insert into nhat_ky_truy_cap values (...);
commit;

Đo được: trong giao dịch nó là off, sau commit nó tự trở lại on. Không có cách nào quên trả lại.

Đây là công cụ chính xác cho một tình huống rất thực: bảng nhật ký, bảng đo lường, bảng phiên đăng nhập — những nơi mất 200 ms cuối cùng chẳng ai quan tâm — trong khi bảng đơn hàng và bảng giao dịch tài chính vẫn giữ độ bền đầy đủ.

Đặt theo vai cũng được:

alter role ghi_log set synchronous_commit = off;

Năm mức, không phải hai

synchronous_commit không phải bật/tắt. Nó có năm giá trị:

Mức Đợi tới đâu
off Không đợi gì — báo thành công ngay
local Đợi WAL xuống đĩa máy này, không đợi máy dự phòng
remote_write Đợi máy dự phòng nhận được
on Đợi máy dự phòng ghi xuống đĩa (mặc định)
remote_apply Đợi máy dự phòng đã áp dụng xong

Ba mức cuối chỉ có ý nghĩa khi bạn có sao chép đồng bộ. Không có synchronous_standby_names thì on và local giống hệt nhau.

Mức local là lựa chọn ít được biết mà rất hữu ích: giữ nguyên độ bền trên máy chính, chỉ bỏ phần đợi máy dự phòng. Với replica ở trung tâm dữ liệu khác, phần đợi đó có thể là hàng chục mili giây.

remote_apply là mức duy nhất bảo đảm rằng nếu bạn đọc từ replica ngay sau khi ghi thành công, bạn thấy dữ liệu vừa ghi. Nó cũng là mức đắt nhất.

wal_sync_method

Cách PostgreSQL yêu cầu hệ điều hành đẩy dữ liệu xuống đĩa. Trên máy đo của tôi mặc định là fdatasync, và có bốn lựa chọn: fsync, fdatasync, open_sync, open_datasync.

Đây là tham số hiếm khi đáng chỉnh. fdatasync là mặc định đúng trên Linux. Công cụ pg_test_fsync đi kèm PostgreSQL đo được từng phương thức trên chính đĩa của bạn nếu bạn muốn kiểm.

Bảng quyết định

Dữ liệu Nên đặt
Đơn hàng, thanh toán, tài khoản Mặc định — đừng đụng vào
Nhật ký truy cập, đo lường, phiên set local synchronous_commit = off
Bảng trung gian trong ETL Bảng UNLOGGED (phần 37) — rẻ hơn nhiều
Môi trường phát triển trên máy cá nhân fsync = off chấp nhận được
Bất cứ thứ gì trên máy chủ thật Không bao giờ fsync = off

Dòng thứ ba đáng nhấn mạnh. Phần 37 đã đo bảng UNLOGGED sinh 40 byte WAL thay vì 72 MB — tốt hơn nhiều so với việc hạ độ bền toàn cục, và rủi ro thì rõ ràng và có giới hạn: bảng đó bị cắt trắng sau sự cố, các bảng khác không sao.

Mẫu số chung

Hai cái công tắc ở đầu bài dạy một điều rộng hơn PostgreSQL: khi bạn đổi an toàn lấy tốc độ, câu hỏi đúng không phải "bật hay tắt" mà là "rủi ro sẽ rơi vào đâu". synchronous_commit = off đánh mất vài trăm mili giây cuối nhưng giữ nguyên tính nhất quán — mất mát có giới hạn, khôi phục được. fsync = off đánh cược cả tính toàn vẹn — hỏng là hỏng vĩnh viễn. Hai thứ cùng "hạ độ bền" mà cách nhau nguyên một hạng rủi ro, nên phải đọc xem mỗi cái từ bỏ gì, chứ không chỉ nó tiết kiệm bao nhiêu. Và lời giải hay nhất gần như không bao giờ là một công tắc toàn cục — mà là một ngoại lệ có phạm vi, thu bán kính vụ nổ về đúng phần dữ liệu chịu được: set local cho một giao dịch, UNLOGGED cho một bảng.

Đúng nguyên tắc "khoanh vùng cái tối ưu nguy hiểm" này áp ở mọi ngăn xếp:

  • Trong Java, bạn hạ mức cô lập hay đánh dấu @Transactional(readOnly = true) cho một phương thức, chứ không nới lỏng giao dịch cho cả ứng dụng; bạn hạ mức log cho một logger chứ không cho root. Nới đúng chỗ, không nới toàn hệ.
  • Trong Go, một context.WithTimeout đặt hạn cho một lời gọi thay vì bỏ hạn chờ toàn cục; một dòng //nolint tắt luật cho một dòng thay vì tắt linter cho cả repo. Cùng tư duy: ngoại lệ nhỏ nhất có thể.
  • Ngay trong cơ sở dữ liệu, set local, bảng UNLOGGED, hay đọc read committed cho một giao dịch đều là cách nới an toàn trong đúng một phạm vi — đối lập với fsync = off phủ lên mọi thứ, kể cả dữ liệu không bao giờ được phép mất.

Nên trước khi tắt bất kỳ cơ chế an toàn nào để lấy tốc độ, hãy hỏi hai câu: "cái này từ bỏ gì — mất có giới hạn hay hỏng không cứu được", và "tôi có thể khoanh nó vào đúng phần dữ liệu chịu được thay vì bật toàn cục không". Tự soát ngay:

select name, setting, source from pg_settings
where name in ('fsync','synchronous_commit','wal_sync_method','wal_writer_delay');

Nếu fsync không phải on, tìm xem ai đặt nó và vì sao — cột source cho biết nó đến từ tệp cấu hình hay dòng lệnh. Theo số đo ở bài này, đổi nó về on và thay bằng synchronous_commit = off cho ra thông lượng cao hơn với rủi ro thấp hơn hẳn. Và tìm những bảng đáng hạ độ bền:

select relname, n_tup_ins
from pg_stat_user_tables
order by n_tup_ins desc limit 10;

Bảng nào ở đầu danh sách mà nội dung là nhật ký hoặc số liệu đo lường thì đó là ứng viên cho set local synchronous_commit = off, hoặc cho bảng UNLOGGED.

Phần sau đo sao chép luồng: độ trễ thật giữa máy chính và máy dự phòng, và chuyện gì xảy ra khi replica tụt lại quá xa.