Khi bạn COMMIT một giao dịch và PostgreSQL trả về "thành công", bạn tin rằng dữ liệu đã bền — nếu server mất điện ngay sau đó, dữ liệu vẫn còn khi khởi động lại. Điều đảm bảo lời hứa này (durability — chữ D trong ACID) là WAL (Write-Ahead Log). Đây là cơ chế nền tảng nhất của độ bền trong PostgreSQL, và cũng là nền của replication và phục hồi sau sự cố.
Bài này (phần 10 loạt PostgreSQL) giải thích vì sao PostgreSQL ghi log trước khi ghi dữ liệu, và đo thật lượng WAL mỗi loại thao tác sinh ra trên pg-lab.
Nguyên tắc write-ahead
Ý tưởng cốt lõi: trước khi thay đổi trang dữ liệu (heap) trên đĩa, PostgreSQL ghi bản ghi mô tả thay đổi đó vào WAL — một log tuần tự, chỉ ghi thêm (append-only) — và fsync nó xuống đĩa. Trang dữ liệu thực được ghi sau, theo nhịp checkpoint. Nếu server crash giữa chừng, lúc khởi động PostgreSQL đọc lại WAL và áp lại (replay) các thay đổi đã commit nhưng chưa kịp ghi vào trang dữ liệu.

Hình 1: Write-ahead — ghi WAL (tuần tự) + fsync trước, ghi trang dữ liệu (ngẫu nhiên) sau. Crash → replay WAL để không mất dữ liệu đã commit. COMMIT chỉ chờ ghi WAL tuần tự (nhanh), không chờ ghi trang ngẫu nhiên. Đo WAL bằng pg_wal_lsn_diff.
-- LSN = vi tri hien tai trong WAL; do truoc/sau 1 thao tac de biet WAL sinh ra
SELECT pg_current_wal_lsn();
-- ... chay thao tac ...
SELECT pg_size_pretty(pg_wal_lsn_diff(lsn_sau, lsn_truoc)); -- byte WAL
Vì sao write-ahead nhanh dù phải ghi hai lần (WAL + trang dữ liệu)? Vì WAL ghi tuần tự (append-only) — đĩa rất nhanh với ghi tuần tự — còn trang dữ liệu ghi ngẫu nhiên (chậm) được gom lại và ghi sau theo checkpoint. COMMIT chỉ cần chờ ghi WAL tuần tự, không chờ ghi trang ngẫu nhiên. Đổi ghi-ngẫu-nhiên-ngay thành ghi-tuần-tự-ngay + ghi-ngẫu-nhiên-sau là một thắng lợi lớn.
Đo thật: WAL sinh ra bởi mỗi thao tác
Mình đo lượng WAL (pg_wal_lsn_diff) mỗi thao tác 10.000 hàng tạo ra trên pg-lab. Kết quả thật:

Hình 2: Kết quả thật. INSERT 861 kB, COPY (bulk) 357 kB cho cùng dữ liệu, UPDATE 1.645 kB (nặng nhất), DELETE 572 kB, SELECT 0 bytes. Mọi thay đổi sinh WAL; đọc thì không.
- INSERT 10.000 hàng: 861 kB — mỗi hàng mới sinh một bản ghi WAL.
- COPY 10.000 hàng (cùng dữ liệu): 357 kB — COPY sinh ít WAL hơn INSERT ~2,4 lần. Bulk load gom hiệu quả hơn. Đây là một lý do nữa (ngoài throughput) để dùng COPY/bulk khi nạp dữ liệu lớn.
- UPDATE 10.000 hàng: 1.645 kB — nặng nhất. Vì MVCC (bài 1), UPDATE tạo phiên bản mới và giữ bản cũ, nên WAL phải ghi cả thông tin cho cả hai, cộng có thể full-page write.
- DELETE 10.000 hàng: 572 kB — đánh dấu xóa.
- SELECT: 0 bytes — đọc không sinh WAL. Chỉ thay đổi mới cần ghi log; đọc là "miễn phí" về mặt durability.
Đánh đổi cần cân nhắc
synchronous_commit đổi độ bền lấy tốc độ COMMIT. Mặc định synchronous_commit=on nghĩa là COMMIT chờ WAL được fsync xuống đĩa mới trả về — đảm bảo dữ liệu đã bền. Đặt off thì COMMIT trả về ngay sau khi ghi WAL vào bộ đệm (chưa fsync), nhanh hơn nhiều, nhưng nếu crash trong khoảng ~vài trăm ms đó, vài giao dịch cuối có thể mất (dù database vẫn nhất quán, không hỏng). Đây là "bền mềm" — hợp cho dữ liệu chịu được mất một ít (log, analytics, dữ liệu dựng lại được), không cho giao dịch tài chính. Một núm chỉnh rất hiệu quả cho workload ghi nhiều mà chấp nhận rủi ro nhỏ.
fsync=off nhanh nhất nhưng nguy hiểm — gần như không bao giờ dùng trên dữ liệu thật. Tắt fsync khiến PostgreSQL không ép WAL/dữ liệu xuống đĩa vật lý, nhanh hơn đáng kể nhưng nếu crash, dữ liệu có thể hỏng (corrupt), không chỉ mất vài giao dịch. Chỉ dùng cho dữ liệu hoàn toàn dựng lại được (ví dụ đang nạp dữ liệu ban đầu mà sẽ làm lại từ đầu nếu hỏng), và phải bật lại sau. Khác với synchronous_commit=off (chỉ mất giao dịch cuối, không hỏng), fsync=off có thể phá hủy cả database.
full_page_writes bảo vệ chống "torn page" — tốn WAL nhưng cần thiết. Khi một trang bị sửa lần đầu sau checkpoint, PostgreSQL ghi toàn bộ trang vào WAL (không chỉ thay đổi nhỏ), để phòng trường hợp crash làm trang bị ghi dở (torn page — một phần cũ, một phần mới). Điều này làm WAL nhiều hơn (nhất là ngay sau checkpoint), và là một phần lý do UPDATE sinh nhiều WAL. Tắt full_page_writes giảm WAL nhưng chỉ an toàn nếu hệ thống lưu trữ đảm bảo ghi nguyên tử theo trang (một số SAN/filesystem). Mặc định on là an toàn; đừng tắt trừ khi chắc chắn về tầng lưu trữ.
Ba ý mang về
- WAL ghi log trước, ghi dữ liệu sau — đó là nền của độ bền và phục hồi. Mọi thay đổi vào WAL (tuần tự, fsync) trước khi vào trang dữ liệu; crash → replay WAL để không mất giao dịch đã commit. WAL tuần tự nhanh nên COMMIT không phải chờ ghi trang ngẫu nhiên. Đây cũng là nền của replication và point-in-time recovery.
- Mọi thay đổi sinh WAL; đọc thì không — và lượng WAL khác nhau theo thao tác. Đo thật (10.000 hàng): INSERT 861 kB, COPY 357 kB (bulk hiệu quả hơn 2,4×), UPDATE 1.645 kB (nặng nhất do MVCC ghi cả bản cũ+mới), DELETE 572 kB, SELECT 0 bytes. Dùng pg_wal_lsn_diff để đo.
- Cấu hình WAL là đánh đổi độ bền vs tốc độ. synchronous_commit=off nhanh hơn nhưng crash mất vài giao dịch cuối (không hỏng); fsync=off nhanh nhất nhưng crash có thể HỎNG dữ liệu (chỉ cho dữ liệu dựng lại được); full_page_writes chống torn page (tốn WAL nhưng an toàn). Mặc định (tất cả on) an toàn nhất — chỉ nới khi hiểu rõ rủi ro.
Nguồn
- PostgreSQL docs — Write-Ahead Logging (WAL): https://www.postgresql.org/docs/current/wal-intro.html
- PostgreSQL docs — WAL Configuration (synchronous_commit, fsync): https://www.postgresql.org/docs/current/runtime-config-wal.html
- PostgreSQL docs — Reliability and the Write-Ahead Log: https://www.postgresql.org/docs/current/wal.html
Phần sau ta dùng SKIP LOCKED để xây một hàng đợi công việc ngay trong SQL: nhiều worker lấy việc đồng thời mà không đụng nhau, không khóa chờ — đo thật nhiều worker cùng chạy.