WAL là nhật ký ghi trước — mọi thay đổi được ghi vào đó trước khi chạm tới tệp dữ liệu. Nó là lý do PostgreSQL sống sót sau khi mất điện. Phần này đo nó sinh ra bao nhiêu dữ liệu, và tìm ra một cấu hình phổ biến làm mọi thứ tệ đi theo cách ngược đời.
Bao nhiêu WAL cho mỗi thao tác
Bảng 500.000 dòng, 40 MB:
| Thao tác | Thời gian | WAL sinh ra |
|---|---|---|
SELECT (chỉ đọc) |
0,08 s | 0 byte |
TRUNCATE |
0,07 s | 2.368 byte |
VACUUM trên bảng sạch |
0,09 s | 332 kB |
REINDEX |
0,13 s | 9.973 kB |
DELETE tất cả |
0,25 s | 28 MB |
UPDATE một cột text |
1,39 s | 124 MB |
UPDATE một cột int |
1,06 s | 126 MB |
UPDATE gán chính giá trị cũ |
1,76 s | 183 MB |
Ba con số đáng dừng lại.
UPDATE một cột int sinh 126 MB WAL trên bảng 40 MB — gấp 3,2 lần kích thước bảng, để sửa bốn byte trong mỗi dòng. Phần 21 đã giải thích lý do: MVCC ghi lại cả dòng, không ghi delta. Cộng thêm ghi trang đầy đủ ở mục sau.
UPDATE gán chính giá trị cũ vẫn tốn 183 MB. update w set a = a không đổi gì cả, và PostgreSQL vẫn ghi ra 500.000 phiên bản dòng mới. Nó không so sánh giá trị trước khi ghi. Nếu mã của bạn cập nhật một bản ghi mà phần lớn lần không có gì đổi, thêm một mệnh đề where là tiết kiệm thật:
update w set a = :moi where id = :id and a is distinct from :moi;
TRUNCATE sinh 2.368 byte so với 28 MB của DELETE — ít hơn 12.000 lần. Nó không xoá từng dòng mà bỏ cả tệp và tạo tệp mới, nên WAL chỉ cần ghi lại đúng sự kiện đó. Phần 36 đã đo cùng hiện tượng với DROP phân vùng: 181 kB so với 312 MB.
Ghi trang đầy đủ
Đây là cơ chế giải thích phần lớn con số 126 MB, và nó ít được nhắc tới.
Cùng một câu UPDATE trên 100.000 dòng, ba vòng đo:
| Vòng 1 | Vòng 2 | Vòng 3 | |
|---|---|---|---|
Ngay sau CHECKPOINT |
35 MB | 37 MB | 37 MB |
| Lần thứ hai | 25 MB | 25 MB | 25 MB |
Chênh 48%, cho câu lệnh y hệt nhau.
Lý do: sau mỗi checkpoint, lần đầu tiên một trang 8 kB bị chạm tới, PostgreSQL ghi nguyên cả trang vào WAL, không chỉ ghi phần thay đổi. Những lần sau trong cùng chu kỳ checkpoint thì chỉ ghi delta.
Đây không phải lãng phí mà là biện pháp an toàn. Nếu máy mất điện giữa lúc hệ điều hành đang ghi một trang 8 kB, đĩa có thể chỉ ghi được một nửa — gọi là trang rách. Delta không sửa được một trang rách; chỉ bản sao đầy đủ của trang mới sửa được.
Tắt nó đi để đo (đừng làm trên máy thật):
alter system set full_page_writes = off;
ngay sau checkpoint, không ghi trang đầy đủ: 24 MB
Trùng khít với con số 25 MB của "lần thứ hai" — xác nhận đúng cơ chế.
Hệ quả: max_wal_size quá nhỏ tự làm hại
Đây là kết quả tôi thấy đáng nhớ nhất trong bài.
Cùng khối lượng công việc — 6 lần UPDATE toàn bảng:
max_wal_size |
Thời gian | WAL sinh ra | Số checkpoint |
|---|---|---|---|
| 128 MB | 7,2 s | 1.108 MB | 18 |
| 1 GB | 7,2 s | 856 MB | 2 |
Đặt max_wal_size nhỏ hơn tám lần gây ra chín lần số checkpoint, và WAL nhiều hơn 29%.
Vòng lặp là thế này: max_wal_size nhỏ nghĩa là PostgreSQL phải checkpoint thường xuyên hơn. Mỗi checkpoint mở lại một đợt ghi trang đầy đủ. Ghi trang đầy đủ sinh nhiều WAL hơn. Nhiều WAL hơn thì lại chạm max_wal_size sớm hơn.
Nghĩa là: đặt max_wal_size nhỏ để tiết kiệm đĩa lại làm tốn thêm đĩa.
max_wal_size không phải giới hạn cứng cho dung lượng pg_wal — nó là ngưỡng để kích hoạt checkpoint. Thư mục pg_wal trên máy đo của tôi lúc đó là 864 MB với 54 tệp 16 MB, vượt cả max_wal_size 128 MB, vì WAL chỉ được dọn sau khi checkpoint hoàn thành.
Với máy chủ ghi nhiều, 1–4 GB là dải hợp lý. Đổi lại là thời gian khôi phục sau sự cố lâu hơn — PostgreSQL phải phát lại nhiều WAL hơn.
wal_compression
| Chế độ | WAL | Thời gian |
|---|---|---|
off |
72 MB | 0,50 s |
pglz |
63 MB | 0,87 s |
lz4 |
64 MB | 0,55 s |
pglz giảm 12% WAL nhưng tốn thêm 74% thời gian. lz4 giảm 11% và chỉ tốn thêm 10%.
Kết luận rõ: nếu bản dựng của bạn có lz4, dùng nó; đừng dùng pglz.
Tỉ lệ nén 11–12% thấp vì dữ liệu thử của tôi là chuỗi md5 — gần như ngẫu nhiên, không nén được. Trên dữ liệu thật với nhiều khoảng trắng, giá trị lặp, và NULL, tỉ lệ thường tốt hơn nhiều.
Lưu ý wal_compression chỉ nén phần ghi trang đầy đủ, không nén phần delta. Nên lợi ích của nó lớn nhất đúng ở chỗ đau nhất — ngay sau checkpoint.
Vì sao WAL quan trọng ngoài chuyện đĩa
Ba thứ khác đều đi qua WAL:
Máy dự phòng. Mọi byte WAL phải truyền sang replica và phát lại ở đó. 126 MB cho một câu UPDATE là 126 MB qua mạng.
Sao lưu liên tục. Nếu bạn lưu trữ WAL, mọi byte đó nằm trong kho vĩnh viễn.
Khôi phục sau sự cố. Thời gian khởi động lại sau mất điện tỉ lệ thuận với lượng WAL kể từ checkpoint cuối.
Đó là lý do những con số ở phần 36 (DROP phân vùng 181 kB thay vì DELETE 312 MB) và phần 37 (bảng UNLOGGED 40 byte thay vì 72 MB) đáng nhớ hơn phần chênh lệch thời gian của chúng.
Theo dõi
select
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) as tong_wal_tu_dau,
(select checkpoints_timed from pg_stat_bgwriter) as checkpoint_theo_gio,
(select checkpoints_req from pg_stat_bgwriter) as checkpoint_do_wal_day,
(select round(checkpoint_write_time::numeric / 1000) from pg_stat_bgwriter) as thoi_gian_ghi_giay;
Con số cần nhìn là tỉ lệ giữa hai loại checkpoint.
checkpoints_timed là checkpoint theo lịch — do checkpoint_timeout. Đây là loại tốt: đều đặn, có thể dự đoán, và trải đều nhờ checkpoint_completion_target.
checkpoints_req là checkpoint bị ép vì WAL đầy max_wal_size. Loại này đến bất chợt và dồn dập.
Nếu checkpoints_req chiếm hơn một phần ba tổng số, max_wal_size của bạn quá nhỏ — và số đo ở trên nói rằng bạn đang trả thêm 29% WAL cho việc đó.
Đo WAL của một thao tác cụ thể
Cách tôi dùng suốt bài này, và nó hữu ích khi cần biết một câu lệnh tốn bao nhiêu:
select pg_current_wal_lsn() \gset truoc_
-- chạy thao tác của bạn ở đây
select pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), :'truoc_pg_current_wal_lsn'));
Từ PostgreSQL 13 còn có cách gọn hơn:
explain (analyze, wal) update w set a = a + 1 where id <= 1000;
-- WAL: records=1000 fpi=13 bytes=180000
fpi là số trang đầy đủ được ghi. Nếu con số đó lớn so với records, bạn đang chạy ngay sau một checkpoint.
Thử ba mươi giây
select checkpoints_timed, checkpoints_req,
round(100.0 * checkpoints_req / nullif(checkpoints_timed + checkpoints_req, 0), 1) as phan_tram_bi_ep
from pg_stat_bgwriter;
Trên 33% là dấu hiệu max_wal_size quá nhỏ. Nâng nó lên là thay đổi một dòng, áp dụng ngay không cần khởi động lại, và theo số đo ở bài này thì nó vừa giảm WAL vừa giảm số lần máy chủ phải dồn dập ghi đĩa.
Phần sau đo sao chép và máy dự phòng: độ trễ thật giữa máy chính và replica, và chuyện gì xảy ra khi replica tụt lại quá xa.