Từ đây sê-ri chuyển sang mảng ghi và độ bền. Nền tảng của cả hai là WAL (Write-Ahead Log) và checkpoint — cơ chế đảm bảo PostgreSQL không mất dữ liệu khi máy sập, nhưng cũng là nguồn gốc của phần lớn tải ghi ra đĩa. Hiểu chúng giúp bạn giải thích một hiện tượng gây bối rối: vì sao một UPDATE chạm vài byte mỗi dòng lại sinh ra hàng chục MB WAL? Bài này đo thật câu trả lời.

WAL: ghi nhật ký trước, sửa dữ liệu sau

Nguyên tắc write-ahead: trước khi PostgreSQL sửa một trang dữ liệu trong bộ nhớ, nó ghi bản ghi mô tả thay đổi đó vào WAL — một dòng ghi tuần tự, rất nhanh. Chỉ khi WAL đã an toàn trên đĩa, giao dịch mới được coi là bền. Nếu máy sập, khi khởi động lại PostgreSQL phát lại WAL từ checkpoint gần nhất để dựng lại mọi thay đổi chưa kịp ghi vào file dữ liệu. Đây là lý do một CSDL có thể chịu mất điện mà không hỏng.

SELECT wal_records, wal_fpi, wal_bytes FROM pg_stat_wal;

FPI: lần chạm trang đầu tiên sau checkpoint ghi cả trang 8KB

Đây là chi tiết giải thích "WAL phình to". Để chống torn page (trang bị ghi dở khi máy sập giữa chừng — một trang 8KB có thể chỉ ghi được nửa), PostgreSQL dùng cơ chế full-page image (FPI): lần đầu tiên một trang bị sửa sau mỗi checkpoint, WAL chép nguyên cả trang 8KB vào nhật ký, chứ không chỉ phần thay đổi. Các lần sửa sau đó (trước checkpoint kế) chỉ ghi phần delta nhỏ.

Ảnh chụp đoạn mã SQL nền tối minh hoạ WAL và checkpoint ghi trước để bền dồn ghi để dọn, WAL mọi thay đổi ghi vào nhật ký trước khi chạm trang dữ liệu Write-Ahead Log một UPDATE INSERT DELETE ghi bản ghi thay đổi vào WAL ghi tuần tự nhanh trước rồi mới sửa trang trong bộ nhớ nếu máy sập PostgreSQL phát lại WAL để khôi phục độ bền SELECT wal_records wal_fpi wal_bytes FROM pg_stat_wal, FPI lần chạm trang đầu tiên sau checkpoint ghi cả trang 8KB full_page_image để chống torn page trang rách khi sập lần đầu sửa một trang sau mỗi checkpoint WAL chép nguyên trang 8KB các lần sửa sau chỉ ghi phần thay đổi delta nhỏ hơn nhiều checkpoint càng dày FPI càng nhiều WAL càng phình, checkpoint dồn các trang bẩn ra file dữ liệu cho phép dọn WAL trang đã sửa trong bộ nhớ dirty checkpoint đẩy chúng ra đĩa đánh dấu WAL trước điểm đó an toàn để tái sử dụng xoá kích hoạt bởi checkpoint_timeout 5min hoặc max_wal_size 1GB SELECT checkpoints_timed checkpoints_req FROM pg_stat_bgwriter, chẩn đoán và chỉnh checkpoints_req nhiều hơn hẳn checkpoints_timed WAL đầy quá nhanh checkpoint bị ép liên tục quá dày nhiều FPI phình WAL ALTER SYSTEM SET max_wal_size 4GB giãn ra ít checkpoint hơn ALTER SYSTEM SET checkpoint_timeout 30min để timeout dẫn dắt ALTER SYSTEM SET checkpoint_completion_target 0.9 rải ghi tránh sốc I/O

Hình 1: WAL ghi mọi thay đổi trước khi chạm trang dữ liệu (độ bền). FPI chép nguyên trang 8KB ở lần chạm đầu sau mỗi checkpoint (chống torn page). Checkpoint dồn trang bẩn ra đĩa, cho phép dọn WAL; kích hoạt bởi timeout hoặc max_wal_size.

Đo thật: cùng UPDATE, 48 MB hay 28 MB

Tôi chạy đúng một câu UPDATE pgbench_accounts SET abalance = abalance + 1 WHERE aid <= 100000 (100.000 dòng) trong hai tình huống, đọc pg_stat_wal:

Ảnh chụp bảng kết quả đo thật nền tối đo thật WAL cùng UPDATE 100k dòng 48 MB hay 28 MB tuỳ FPI pg_stat_wal UPDATE pgbench_accounts SET abalance bằng abalance cộng 1 WHERE aid nhỏ hơn bằng 100000 PostgreSQL 16, UPDATE ngay sau CHECKPOINT phải ghi full-page image CHECKPOINT reset thống kê WAL rồi UPDATE 100000 dòng wal_records 298383 wal_fpi 2606 wal_bytes 48 MB 2606 trang chép nguyên 8KB, UPDATE y hệt lần 2 chưa checkpoint 0 FPI wal_records 301007 wal_fpi 0 wal_bytes 28 MB các trang đã được image rồi chênh 20 MB chính là chi phí full-page image của lần đầu sau checkpoint, bảng cùng UPDATE 100k dòng lần 1 ngay sau CHECKPOINT wal_fpi 2606 WAL 48 MB lần 2 chưa checkpoint wal_fpi 0 WAL 28 MB, chẩn đoán checkpoint qua pg_stat_bgwriter cờ đỏ thật checkpoints_timed 73 checkpoints_req 170 buffers_checkpoint 435343 checkpoints_req 170 nhiều hơn hẳn checkpoints_timed 73 phần lớn checkpoint bị ép do WAL chạm max_wal_size không do timeout checkpoint quá dày nhiều FPI phình WAL nên tăng max_wal_size, một checkpoint đẩy trang bẩn ra đĩa từ log checkpoint complete wrote 16345 buffers 99.8 phần trăm 18 removed write 0.056 s sync 0.072 s distance 289869 kB UPDATE 500k dòng làm bẩn khoảng 16345 trang 127MB checkpoint flush hết pg_wal hiện tại 46 segment nhân 16MB bằng 736 MB gần max_wal_size 1GB

Hình 2: UPDATE 100k dòng ngay sau CHECKPOINT sinh 48 MB WAL với 2606 FPI; lặp lại y hệt (chưa checkpoint) chỉ 28 MB với 0 FPI. Chênh 20 MB chính là chi phí full-page image. checkpoints_req (170) ≫ checkpoints_timed (73) là cờ đỏ max_wal_size quá nhỏ.

  • UPDATE ngay sau CHECKPOINT: wal_fpi = 2606, wal_bytes = 48 MB. Vì mọi trang bị chạm đều là "lần đầu sau checkpoint", nên 2606 trang bị chép nguyên 8KB vào WAL.
  • UPDATE y hệt lần 2 (chưa checkpoint giữa chừng): wal_fpi = 0, wal_bytes = 28 MB. Các trang đó đã được image ở lần 1 rồi, nên lần này chỉ ghi delta.

Chênh lệch 20 MB chính là chi phí full-page image. Rút ra điều quan trọng: checkpoint càng dày, càng nhiều lần "chạm trang đầu tiên", càng nhiều FPI, WAL càng phình. Đây là mắt xích nối checkpoint với tải ghi.

Checkpoint và cách chẩn đoán

Checkpoint là điểm PostgreSQL dồn tất cả các trang bẩn (đã sửa trong bộ nhớ) ra file dữ liệu, rồi đánh dấu phần WAL trước điểm đó là an toàn để tái sử dụng. Nó được kích hoạt bởi một trong hai: checkpoint_timeout (mặc định 5 phút) hoặc max_wal_size (mặc định 1GB) — cái nào đến trước.

pg_stat_bgwriter cho ta chẩn đoán quý giá: đo thật trên lab này, checkpoints_timed = 73 nhưng checkpoints_req = 170. checkpoints_req ≫ checkpoints_timed nghĩa là phần lớn checkpoint bị ép do WAL chạm max_wal_size, chứ không phải do hết checkpoint_timeout. Đây là cờ đỏ kinh điển: WAL sinh ra quá nhanh so với max_wal_size, khiến checkpoint dồn dập — mà mỗi checkpoint lại làm tăng số FPI ở đợt ghi kế → phình WAL → vòng luẩn quẩn. Trong log, một checkpoint thủ công cho thấy nó đẩy 16.345 trang bẩn (~127MB) ra đĩa; pg_wal đang giữ 46 segment × 16MB = 736 MB, gần chạm max_wal_size 1GB.

Cách chỉnh:

ALTER SYSTEM SET max_wal_size = '4GB';              -- giãn ra, checkpoint thưa hơn
ALTER SYSTEM SET checkpoint_timeout = '30min';      -- để timeout dẫn dắt, không phải WAL đầy
ALTER SYSTEM SET checkpoint_completion_target = 0.9; -- rải ghi khắp 90% chu kỳ, tránh sốc I/O
SELECT pg_reload_conf();

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

Checkpoint thưa hơn = ít FPI, nhưng thời gian phục hồi lâu hơn. Tăng max_wal_size và checkpoint_timeout giảm tải ghi (ít FPI, ít dồn trang bẩn), nhưng nếu máy sập, PostgreSQL phải phát lại nhiều WAL hơn để khôi phục — thời gian khởi động sau sự cố dài hơn. Đây là cân bằng giữa hiệu năng lúc chạy và thời gian phục hồi (RTO).

checkpoint_completion_target rải ghi để tránh sốc I/O. Mặc định 0.9 nghĩa là checkpoint cố hoàn tất việc ghi trang bẩn trong 90% khoảng cách tới checkpoint kế, thay vì ghi ồ ạt một lúc. Điều này làm mượt tải đĩa — quan trọng trên hệ thống ghi nặng. Hầu như luôn nên để 0.9.

WAL cũng cần cho replication và PITR. Đừng chỉ nghĩ WAL là chi phí. Nó là nền tảng của streaming replication (bản sao đọc WAL để đồng bộ) và Point-in-Time Recovery (khôi phục về đúng thời điểm). Chỉnh max_wal_size, wal_keep_size phải cân nhắc cả các nhu cầu này.

Ba ý mang về

  1. Mọi thay đổi ghi vào WAL trước khi chạm trang dữ liệu (write-ahead, đảm bảo độ bền và phục hồi sau sập) — WAL ghi tuần tự nhanh, còn checkpoint dồn các trang bẩn ra file dữ liệu để cho phép dọn/tái dùng WAL.
  2. Full-page image làm WAL phình ở lần chạm trang đầu sau mỗi checkpoint: đo thật, cùng UPDATE 100k dòng sinh 48 MB (2606 FPI) ngay sau checkpoint nhưng chỉ 28 MB (0 FPI) khi lặp lại — nên checkpoint càng dày, WAL càng nhiều.
  3. checkpoints_req ≫ checkpoints_timed là cờ đỏ max_wal_size quá nhỏ: đo thật 170 so với 73 — checkpoint bị ép do WAL đầy, nên tăng max_wal_size/checkpoint_timeout (đánh đổi: phục hồi sau sập lâu hơn) và giữ checkpoint_completion_target=0.9 để tránh sốc I/O.

Phần sau ta đi sâu vào hai công tắc liên quan trực tiếp tới FPI vừa đo: Phần sau mổ xẻ wal_compression và full_page_writes — nén full-page image để giảm WAL, và vì sao gần như không bao giờ nên tắt full_page_writes.