Hai phần trước nhắc tới WAL như một thứ "kèm theo": basebackup cần nó để nhất quán, PITR cần nó để quay về một giây bất kỳ. Nhưng WAL không phải phụ kiện của sao lưu; nó là cách PostgreSQL ghi mọi thứ, và kích thước của nó, nhịp checkpoint cắt nó, quyết định cả hiệu năng ghi lẫn thời gian phục hồi sau crash. Phần này đo WAL ở ba độ phóng đại: byte cho mỗi dòng theo từng loại lệnh, byte cho một dòng ngay sau checkpoint so với lần hai, và hình dạng TPS từng giây khi đổi ba tham số checkpoint dưới cùng một tải.

Nhật ký trước, dữ liệu sau

Ảnh chụp đoạn mã nền tối WAL và checkpoint nhật ký trước dữ liệu sau với PostgreSQL 16 đo bằng pg_current_wal_lsn trước sau mỗi thao tác pg_stat_bgwriter đếm checkpoint pgbench P 1, Write-Ahead Log mọi thay đổi ghi vào nhật ký trước khi trang dữ liệu được ghi xuống đĩa gồm COMMIT là fsync bản ghi WAL tuần tự rẻ trang heap index bẩn nằm trong shared_buffers ghi sau, CHECKPOINT ghi hết trang bẩn xuống đĩa đánh dấu từ đây WAL cũ không cần nữa, crash thì replay WAL từ checkpoint gần nhất nên khoảng cách checkpoint bằng thời gian phục hồi cộng lượng WAL phải giữ, full_page_writes lần đầu chạm một trang sau checkpoint WAL chứa cả trang 8 KB vì ghi trang 8 KB xuống đĩa 4 KB một lần crash giữa chừng là trang rách WAL chỉ bản ghi không sửa nổi, hệ quả cùng một UPDATE ngay sau checkpoint tốn khoảng 8 KB WAL lần hai chỉ vài chục byte nên checkpoint càng dày WAL càng phình không chỉ I O checkpoint, ba nút chỉnh đều reload được không cần restart gồm checkpoint_timeout 5min checkpoint theo giờ timed, max_wal_size 1GB vượt là checkpoint bắt buộc requested không phải trần cứng, checkpoint_completion_target 0.9 rải I O checkpoint trong 90 phần trăm khoảng 0.1 là dồn cục, đo gì gồm A byte WAL mỗi dòng insert update update lần 2 insert có index update khoá index delete, B 1 dòng ngay sau checkpoint so với lần 2 full_page_writes off pgbench 10 giây on off ra MB WAL, C pgbench c 8 T 40 P 1 với 4 cấu hình mặc định max_wal_size 64MB timeout 30s cct 0.1 ra tps tps mỗi giây min max số checkpoint timed req trang ghi MB WAL

Hình 1: COMMIT chỉ fsync nhật ký; trang dữ liệu bẩn ghi sau, ở checkpoint. Lần đầu chạm một trang sau checkpoint, WAL chụp cả trang.

PostgreSQL không ghi trang dữ liệu khi bạn COMMIT. Nó ghi một bản ghi WAL mô tả thay đổi, fsync tệp nhật ký (ghi tuần tự, rẻ), và để trang heap hay index bẩn nằm trong shared_buffers. Trang bẩn xuống đĩa sau, theo hai đường: checkpoint ghi tất cả trang bẩn và đánh dấu "từ đây trở về trước WAL không cần nữa", hoặc backend tự ghi khi cần chỗ trong shared_buffers. Crash thì replay WAL từ checkpoint gần nhất — nên khoảng cách giữa hai checkpoint là thời gian phục hồi tối đa và lượng WAL phải giữ trong pg_wal.

full_page_writes là chi tiết mà hầu hết người vận hành gặp lần đầu khi thấy WAL phình bất thường. Đĩa ghi theo khối 4 KB, trang PostgreSQL 8 KB; crash giữa hai nửa là một trang rách mà bản ghi WAL "đổi 100 byte ở offset X" không sửa nổi. Nên lần đầu tiên một trang bị chạm sau checkpoint, WAL chứa nguyên ảnh trang; những lần sau trong cùng khoảng checkpoint chỉ ghi bản ghi nhỏ. Hệ quả: checkpoint càng dày thì không chỉ I/O checkpoint tăng mà WAL cũng phình, vì "lần đầu chạm" xảy ra thường xuyên hơn.

Ba tham số điều khiển nhịp checkpoint đều đổi được bằng ALTER SYSTEM cộng pg_reload_conf(): checkpoint_timeout (tối thiểu 30 giây), max_wal_size (vượt là checkpoint bắt buộc — một mục tiêu mềm, không phải trần cứng), và checkpoint_completion_target (rải I/O của checkpoint trong bao nhiêu phần khoảng thời gian).

Đo thật ở ba độ phóng đại

Ảnh chụp output thật nền tối 1 dòng sau checkpoint 6160 byte lần hai 120 byte với pg-prim postgres 16 shared_buffers 128MB archive_mode on pgbench scale 30 8 client, A byte WAL cho mỗi dòng bảng w id int v text 100 byte 100000 dòng mỗi lệnh gồm insert không index 160 B mỗi dòng, insert có 1 index 226, update ngay sau checkpoint 381, update lần 2 không checkpoint 243, update đổi khoá index sau checkpoint 453, delete sau checkpoint 202, cùng một UPDATE sau checkpoint đắt hơn 57 phần trăm vì mỗi trang chạm lần đầu phải ghi nguyên 8 KB, B full_page_writes nhìn ở mức một dòng một trang gồm update 1 dòng ngay sau checkpoint 6160 B là ảnh cả trang nén khoảng trống cộng bản ghi, update cùng dòng lần 2 120 B chỉ bản ghi, update dòng khác cùng trang 120 B trang đã có ảnh không chụp lại, full_page_writes off 1 dòng sau checkpoint 120 B tắt là mất lưới an toàn trang rách, pgbench c 8 T 10 với full_page_writes on tps 10088 WAL 390 MB và off tps 11062 WAL 62 MB nên 84 phần trăm WAL của tải này là ảnh trang, C hình dạng checkpoint pgbench c 8 T 40 P 1 k-tps mỗi giây gồm mặc định 5min 1GB 0.9 tps 10632 min p10 max 7359 9180 11238 2 checkpoint req buffers ck 1999 backend 342714 WAL 1093 MB, max_wal_size 64MB tps 6483 min p10 max 3632 4598 8764 66 checkpoint req buffers ck 255171 backend 17847 WAL 2133 MB, timeout 30s 4GB 0.9 tps 10257 min p10 max 6388 9355 11283 1 timed buffers 289 và 359070 WAL 1015 MB, timeout 30s 4GB 0.1 tps 9933 min p10 max 2966 7871 11356 1 timed buffers 4941 và 314145 WAL 938 MB, cct 0.1 từng giây 10 9 11 10 9 10 8 9 10 10 6 8 11 11 11 10 11 11 11 11 11 11 11 11 11 10 10 11 10 10 9 7 6 8 5 2 10 8 11 10, 64MB từng giây 5 4 3 4 5 5 4 4 5 4 8 8 8 5 8 7 8 8 8 8 8 7 5 4 5 5 5 5 6 7 7 7 7 8 6 6 4 5 6 8, 64MB checkpoint mỗi 0,6 giây WAL gấp đôi vì ảnh trang liên tục 128MB shared_buffers nên backend tự ghi 340k trang, nói thẳng một lượt riêng lẻ có giây tụt về 0 tps không gắn với cấu hình nào ba lượt mặc định khác không tái hiện min 6090 tới 8087 shared_buffers 128MB mặc định quá nhỏ cho 613 MB đó là lý do backend ghi nhiều hơn checkpointer 170 lần

Hình 2: Cùng một UPDATE tốn 6.160 byte hay 120 byte tuỳ có checkpoint vừa xảy ra; 84% WAL của pgbench là ảnh trang; 66 checkpoint trong 40 giây làm TPS giảm 39%.

phép đo kết quả
insert / insert có index / delete (byte mỗi dòng) 160 / 226 / 202
update sau checkpoint / lần 2 / đổi khoá index 381 / 243 / 453
1 dòng update ngay sau checkpoint / lần 2 / dòng khác cùng trang 6.160 / 120 / 120 B
pgbench 10 s, full_page_writes on / off 390 MB / 62 MB WAL, 10.088 / 11.062 TPS
mặc định (5min, 1GB, 0.9) 10.632 TPS, 2 checkpoint, 1.093 MB WAL
max_wal_size = 64MB 6.483 TPS, 66 checkpoint, 2.133 MB WAL
timeout 30 s, cct 0.9 / cct 0.1 10.257 / 9.933 TPS, giây thấp nhất 6.388 / 2.966

Mỗi dòng ghi tốn 160–453 byte nhật ký, và index là phần lớn khoản chênh. Insert 100 byte dữ liệu sinh 160 byte WAL không index, 226 byte với một index B-tree. Update thường 243 byte; update đổi khoá index 453 byte — vì nó là xoá cộng chèn trên index, không phải sửa tại chỗ. Delete 202 byte dù "không mang dữ liệu": bản ghi xoá cộng ảnh trang sau checkpoint. Một bảng có năm index thì mỗi UPDATE đổi cột có index là năm lần phần index này — đó là chi phí thật của "cứ thêm index cho chắc".

Một dòng sau checkpoint: 6.160 byte; cùng dòng đó lần hai: 120 byte. Đây là full_page_writes nhìn ở độ phóng đại cao nhất. Update đầu tiên sau CHECKPOINT ghi nguyên ảnh trang (8 KB nén bớt khoảng trống thành ~6 KB) cộng bản ghi; update thứ hai, hay update dòng khác trên cùng trang, chỉ 120 byte — trang đã có ảnh trong khoảng checkpoint này. Tắt full_page_writes thì 120 byte ngay lần đầu, nhưng tắt là bỏ lưới an toàn cho trang rách; chỉ hợp lệ trên hệ tệp hoặc đĩa đảm bảo ghi nguyên tử 8 KB (ZFS, một số NVMe doanh nghiệp). Ở mức tải: pgbench 10 giây sinh 390 MB WAL với FPW bật và 62 MB khi tắt — 84% WAL của tải này là ảnh trang, vì pgbench chạm ngẫu nhiên hàng chục nghìn trang và mỗi khoảng checkpoint lại chụp chúng từ đầu.

max_wal_size 64 MB: 66 checkpoint trong 40 giây, TPS giảm 39%, WAL gấp đôi. Tải sinh ~25 MB WAL mỗi giây nên 64 MB bị vượt mỗi 0,6 giây, mỗi lần là một checkpoint bắt buộc ghi hết trang bẩn (255.171 trang trong 40 giây). TPS 6.483 so với 10.632, và WAL 2.133 MB so với 1.093 — gấp đôi dù số giao dịch ít hơn, vì mỗi checkpoint làm mọi trang bị chạm sau đó phải chụp ảnh lại. Đây là vòng xoáy kinh điển: max_wal_size nhỏ để "tiết kiệm đĩa" lại sinh gấp đôi WAL. Log của máy chủ có 135 dòng checkpoints are occurring too frequently — nó nói đúng.

completion_target 0.1 không đổi TPS trung bình, nhưng đổi giây tệ nhất. Cùng checkpoint_timeout 30 giây: với 0.9, checkpoint rải I/O trong 27 giây và giây thấp nhất là 6.388; với 0.1, nó dồn vào 3 giây và chuỗi từng giây cho thấy rõ cú tụt … 7 6 8 5 2 10 … — 2.966 TPS ở giây 36, bằng 28% bình thường. Trung bình 9.933 so với 10.257 che hết chuyện này; chỉ -P 1 mới thấy. Người dùng cảm nhận giây tệ nhất, không cảm nhận trung bình.

shared_buffers 128 MB làm backend ghi nhiều hơn checkpointer 170 lần. Ở cấu hình mặc định, checkpointer ghi 1.999 trang còn backend tự ghi 342.714 trang để lấy chỗ trong bộ đệm 128 MB cho CSDL 613 MB. Nghĩa là trên cụm này, hình dạng checkpoint chỉ là một phần của I/O ghi; phần lớn là backend tự đẩy trang bẩn xuống đĩa ngay trên đường đi của giao dịch. Tăng shared_buffers lên 25% RAM là việc làm trước khi chỉnh checkpoint — không có nó, mọi tinh chỉnh ở đây chỉ chạm 1% I/O ghi.

Đánh đổi cần cân nhắc

Checkpoint thưa: ít I/O, ít WAL, nhưng phục hồi lâu và pg_wal to. Mỗi khoảng checkpoint là lượng WAL phải replay sau crash; 5 phút ở 25 MB/giây là 7,5 GB WAL phải giữ trong pg_wal và phải đọc lại khi khởi động. Chọn checkpoint_timeout theo thời gian phục hồi chấp nhận được, rồi đặt max_wal_size đủ lớn để checkpoint không bao giờ bị kích bởi kích thước trong tải bình thường — dòng too frequently trong log là chỉ báo.

Tắt full_page_writes tiết kiệm 84% WAL — và chỉ an toàn khi đĩa hứa ghi nguyên tử. Con số 390 → 62 MB rất hấp dẫn; cái giá là một lần mất điện với trang rách là CSDL hỏng mà không có WAL nào sửa được. Giảm WAL đúng cách là checkpoint thưa hơn, wal_compression = on (nén chính ảnh trang), và bớt index không dùng.

Phạm vi đo. Tải pgbench chạm ngẫu nhiên nên tỉ lệ ảnh trang cao bất thường; tải ghi tuần tự (log, time-series) có FPW nhỏ hơn nhiều. shared_buffers 128 MB mặc định của image Docker làm backend-write lấn át; máy chủ thật với 25% RAM sẽ thấy checkpoint rõ hơn. Một lượt chạy có giây tụt về 0 TPS không gắn với cấu hình nào và không tái hiện trong ba lượt mặc định khác (giây thấp nhất 6.090–8.087) — nhiễu I/O của máy ảo, ghi ra chứ không cắt.

Ba ý mang về

  1. WAL là giá thật của mỗi lần ghi: 160–453 byte mỗi dòng, index chiếm phần chênh, và lần đầu chạm một trang sau checkpoint là 6.160 byte thay vì 120. Với pgbench, 84% WAL là ảnh trang.
  2. max_wal_size nhỏ không tiết kiệm gì: 64 MB cho 66 checkpoint trong 40 giây, TPS −39%, WAL ×2. Đặt nó đủ lớn để checkpoint chỉ đến theo checkpoint_timeout, và chọn timeout theo thời gian phục hồi chấp nhận được.
  3. Nhìn từng giây, không nhìn trung bình: completion_target 0.1 giữ nguyên TPS trung bình và tạo cú tụt xuống 2.966. Và trước khi chỉnh checkpoint, kiểm buffers_backend: ở 128 MB shared_buffers, backend ghi 170 lần nhiều hơn checkpointer.

Nguồn

Phần sau dùng chính dòng WAL này cho việc nó sinh ra để làm: khôi phục về một giây trước câu DELETE nhầm. Dựng thật: xoá nhầm một bảng, lấy basebackup cộng WAL lưu trữ, recovery_target_time, và đếm số dòng trước — sau.