Skip to content

PostgreSQL — WAL & Checkpoint

1. Bức tranh toàn cảnh — RAM nằm ở đâu

PostgreSQL có hai vùng RAM riêng biệt trước khi xuống disk:

                        RAM                          Disk
              ┌─────────────────────┐
              │   shared_buffers    │◄──── data pages (8KB/page)
              │   (buffer pool)     │────► pg_data/base/...
              └─────────────────────┘
                        ↑ ↓ read/write data pages

              ┌─────────────────────┐
              │    WAL buffers      │
              │   (wal_buffers)     │────► pg_wal/
              └─────────────────────┘
                      ↑ append-only

shared_buffers là page cache của PostgreSQL — cả đọc lẫn ghi đều đi qua đây, không có gì bypass nó.

WAL buffers là buffer riêng chứa log entries trước khi flush xuống WAL file trên disk.


2. Flow đọc

SELECT * FROM users WHERE id = 1

tìm page trong shared_buffers?
    ├── có → trả về ngay  (buffer hit)
    └── không → đọc từ disk → nạp vào shared_buffers → trả về  (buffer miss)

Không có result cache. PostgreSQL cache data pages, không cache kết quả query. Mỗi lần chạy SELECT count(*) WHERE ... PostgreSQL vẫn phải tính lại — dù data không đổi. Oracle có result cache để skip bước này, PostgreSQL không có.

Nếu muốn cache result ở tầng application, dùng Redis/Memcached ở ngoài.


3. Flow ghi và vai trò WAL

UPDATE users SET tier = 'gold' WHERE id = 1

[1] tìm/nạp page vào shared_buffers (nếu chưa có)

[2] sửa page trong shared_buffers  ← page trở thành "dirty"
[2] đồng thời ghi log entry vào WAL buffers

[3] COMMIT → flush WAL buffers → WAL file trên disk  ← phải xong trước khi COMMIT trả về

[4] background writer dần dần flush dirty pages xuống disk  ← diễn ra sau, async

Tại sao COMMIT chỉ cần flush WAL, không cần flush data page?

WAL chứa đủ thông tin để reconstruct mọi thay đổi. Nếu crash xảy ra giữa bước 3 và 4, data page trên disk còn cũ — nhưng PostgreSQL có thể replay WAL để đưa nó về đúng trạng thái.

Nếu crash xảy ra:

server restart

PostgreSQL đọc WAL từ checkpoint cuối cùng

replay lại từng log entry

dirty pages được reconstruct đúng

database sẵn sàng

2. Checkpoint

Checkpoint là gì

WAL sẽ tích lũy mãi nếu không có cơ chế dọn. Checkpoint là lúc PostgreSQL nói: "tất cả data pages đã được flush xuống disk đến điểm này rồi — WAL trước điểm này không cần nữa."

WAL timeline:
─────────────────────────────────────────────────────────────►
         [Checkpoint A]          [Checkpoint B]
              │                       │
              │  WAL segment 001-005  │  WAL segment 006-010
              │  (có thể xóa sau B)   │  (cần giữ cho đến C)

Tại sao checkpoint gây chậm

Khi checkpoint chạy, PostgreSQL phải flush một lượng lớn dirty pages xuống disk cùng lúc → I/O spike → query bị chậm đột ngột.

checkpoint_completion_target = 0.9 giải quyết điều này bằng cách trải đều việc flush ra 90% thời gian giữa 2 checkpoint thay vì flush dồn vào cuối.

checkpoint_completion_target = 0.9:

[Checkpoint A]────────────────────────────────────[Checkpoint B]
      flush ████████████████████████████████░░░░░░░░░
            ←─────────── 90% thời gian ──────────►

checkpoint_completion_target = 0.1 (xấu):

[Checkpoint A]────────────────────────────────────[Checkpoint B]
      flush ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░████
                                                ← spike

Khi nào checkpoint được trigger

  • Định kỳ: mỗi checkpoint_timeout giây (default 5 phút)
  • Khi WAL quá lớn: khi WAL vượt max_wal_size (default 1GB)
  • Manual: CHECKPOINT command

Nếu thấy checkpoint quá thường xuyên trong log, nghĩa là WAL đang tích lũy nhanh hơn mức bình thường — thường do bulk write hoặc long-running transaction.

sql
-- Xem thống kê checkpoint
SELECT checkpoints_timed,
       checkpoints_req,           -- checkpoint bị trigger sớm do WAL đầy
       checkpoint_write_time,
       checkpoint_sync_time,
       buffers_checkpoint
FROM pg_stat_bgwriter;

-- checkpoints_req cao → max_wal_size đang quá nhỏ

3. WAL và Replication

WAL streaming replication hoạt động đơn giản: primary liên tục stream WAL sang replica, replica replay lại.

Primary                         Replica
──────                          ───────
INSERT/UPDATE/DELETE

ghi WAL  ──── stream WAL ────►  nhận WAL
      ↓                               ↓
data page                       replay → data page

wal_level kiểm soát lượng thông tin trong WAL:

LevelDùng khi
minimalKhông replication, không backup
replicaStreaming replication (default nên dùng)
logicalLogical replication, CDC, pglogical

Thực tế: luôn set replica kể cả không dùng replication ngay — cost không đáng kể và giúp mọi công cụ backup hoạt động được.

Monitor replication lag

sql
-- Trên primary: xem replica lag
SELECT client_addr,
       state,
       sent_lsn - write_lsn   AS write_lag_bytes,
       write_lsn - flush_lsn  AS flush_lag_bytes,
       flush_lsn - replay_lsn AS replay_lag_bytes
FROM pg_stat_replication;

-- Trên replica: tự xem lag của mình
SELECT now() - pg_last_xact_replay_timestamp() AS replication_lag;

4. PITR — Point-in-Time Recovery

PITR cho phép restore database về bất kỳ thời điểm nào trong quá khứ, không chỉ về backup gần nhất.

Cần:

  1. Base backup — snapshot của data directory tại một thời điểm
  2. WAL archive — tất cả WAL segments từ sau backup đó đến hiện tại

Recovery flow:

base backup (T=0)  +  WAL T=0..T=5  →  restore về T=3
                                         (replay WAL đến T=3 rồi dừng)

Khi nào BE dev thực sự cần biết điều này:

  • Khi thiết kế backup strategy cho production
  • Khi cần giải thích tại sao cần lưu WAL archive chứ không chỉ daily snapshot

Còn setup cụ thể (pg_basebackup, archive_command, restore_command) thường là việc của DBA/DevOps.


5. Các tình huống hay gặp

App chậm từng đợt không rõ nguyên nhân

Có thể là checkpoint I/O spike. Kiểm tra:

sql
-- Bật log checkpoint
-- postgresql.conf:
-- log_checkpoints = on

-- Sau đó xem trong log:
-- LOG: checkpoint complete: wrote 12453 buffers (5.3%); ...
-- Nếu buffers cao và duration dài → I/O đang bị ảnh hưởng

Fix: tăng checkpoint_completion_target lên 0.9, tăng max_wal_size để checkpoint xảy ra ít thường hơn.

Disk đầy đột ngột ở thư mục pg_wal

WAL có thể tích lũy khi:

  1. Long-running transaction — PostgreSQL giữ WAL từ trước khi transaction bắt đầu
  2. Replica bị lag — primary phải giữ WAL cho đến khi replica catch up
  3. WAL archiver bị stuckarchive_command fail liên tục
sql
-- Tìm long-running transaction
SELECT pid, now() - xact_start AS duration, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY duration DESC
LIMIT 5;

-- Xem replica có bị lag không
SELECT client_addr, state, sent_lsn - replay_lsn AS lag_bytes
FROM pg_stat_replication;

-- Xem WAL archiver status
SELECT last_failed_wal, last_failed_time, last_archived_time
FROM pg_stat_archiver;

full_page_writes — bao giờ cần tắt

Mặc định on. Khi checkpoint xảy ra, lần đầu tiên một page được modify sau checkpoint → PostgreSQL ghi cả page 8KB vào WAL (thay vì chỉ ghi delta). Lý do: bảo vệ trường hợp crash giữa chừng khi đang ghi một 8KB page (partial write).

Tắt được nếu storage của bạn đảm bảo atomic write (ZFS, EBS, một số SAN). Thường không cần tắt — WAL compression bù lại phần lớn overhead.


6. Config thực tế

ini
# WAL
wal_level = replica                    # luôn set, không để minimal
max_wal_size = 4GB                     # tăng nếu checkpoint quá thường xuyên
min_wal_size = 1GB                     # giữ sẵn để tránh allocate liên tục
wal_compression = on                   # nên bật, giảm I/O và disk

# Checkpoint
checkpoint_completion_target = 0.9    # trải đều I/O, tránh spike
checkpoint_timeout = 5min             # giữ default

# Logging để debug
log_checkpoints = on

Một điểm về work_mem và WAL: work_mem lớn giúp sort/hash trong bộ nhớ thay vì spill ra disk — gián tiếp giảm lượng WAL vì ít temp file operations hơn. Nhưng tổng RAM dùng = work_mem × connections × operations_per_query → đừng set quá cao.

Personal notes by thanhlt