Kiến trúc PostgreSQL & WAL

4 câu hỏi~25 phútTừ khóa: postmaster, backend process, process per connection, shared buffers, dirty page, WAL, fsync, checkpoint, crash recovery, synchronous_commit, full_page_writes

Tiếp đi: kiến trúc PostgreSQL và WAL. Vì sao COMMIT xong mất điện mà dữ liệu vẫn còn?

Ở bài 6.1, chữ D trong ACID là Durability. Bài này mở PostgreSQL ra xem nó giữ lời hứa đó thế nào.

1. Bài toán

shop-app chạy 3 instance Spring Boot, mỗi instance có HikariCP 20 connection tới một PostgreSQL. Mỗi giây vài trăm order được tạo. Câu hỏi:

2. Vấn đề

Ghi trực tiếp từng page dữ liệu xuống đĩa ở mỗi COMMIT là random write vào nhiều file (bảng orders, order_items, các index...). Chậm. Nhưng chỉ giữ trong RAM thì mất điện là mất order đã báo thành công cho khách.

3. Giải pháp: các thành phần

                 Spring Boot (HikariCP: 20 connection)
                         │ │ │  TCP 5432
                         ▼ ▼ ▼
┌──────────────────── PostgreSQL server ────────────────────┐
│ postmaster (process cha): nhận kết nối, fork backend      │
│                                                           │
│ backend #1   backend #2   ... backend #60   ← 1 PROCESS / connection
│     │            │                │                       │
│     ▼            ▼                ▼                       │
│ ┌───────────── Shared memory ─────────────┐               │
│ │ shared_buffers (cache page 8KB)          │               │
│ │ WAL buffers                              │               │
│ │ lock table, ...                          │               │
│ └──────────────────────────────────────────┘               │
│ Background: checkpointer, background writer, walwriter,    │
│             autovacuum launcher/workers, WAL sender...     │
└─────────────┬─────────────────────────┬───────────────────┘
              ▼                         ▼
       pg_wal/ (WAL, ghi tuần tự)   base/ (data file, page 8KB)
Thành phầnVai trò
Backend processMỗi connection một process OS, chạy SQL của connection đó. Connection đắt (vài MB RAM) → cần pool (bài 6.8).
shared_buffersCache page dữ liệu trong RAM, dùng chung mọi backend. Page bị sửa mà chưa ghi xuống đĩa gọi là dirty page.
WAL (Write-Ahead Log)Nhật ký mọi thay đổi, ghi tuần tự vào pg_wal/. Luật: ghi log trước, ghi data sau.
CheckpointerĐịnh kỳ ghi hết dirty page xuống data file, đánh dấu "WAL trước điểm này không cần để recovery nữa".
Background writerGhi bớt dirty page dần dần để backend không phải tự ghi.

4. Từng bước: một COMMIT đi qua đâu

Bước 1: Xem các process

SELECT pid, backend_type, usename, application_name, state
FROM pg_stat_activity;
-- client backend, checkpointer, background writer, walwriter, autovacuum launcher...

Bước 2: Xem cấu hình liên quan

SHOW shared_buffers;        -- thường ~25% RAM
SHOW wal_level;             -- replica
SHOW synchronous_commit;    -- on
SHOW fsync;                 -- on (KHÔNG BAO GIỜ tắt trên production)
SHOW checkpoint_timeout;    -- 5min
SHOW max_wal_size;          -- 1GB

Bước 3: Spring Boot không cần làm gì đặc biệt. Durability nằm ở DB. Phía app chỉ cần coi "commit thành công" là mốc:

@Transactional
public Order placeOrder(CheckoutRequest req) {
    walletRepository.debit(req.userId(), req.total());   // UPDATE ... WHERE balance >= ?
    return orderRepository.save(Order.from(req));
}   // return bình thường → Spring COMMIT → PostgreSQL fsync WAL → mới trả OK

Bước 4: Quan sát WAL tăng lên

SELECT pg_current_wal_lsn();          -- ví dụ 0/3A00F2C8
-- bắn 1000 order bằng curl / k6
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/3A00F2C8'));

5. Cách hoạt động

INSERT INTO orders ...; COMMIT;

1. Backend tìm page của bảng orders trong shared_buffers (không có thì đọc từ đĩa)
2. Sửa page trong RAM → page thành DIRTY (chưa ghi data file)
3. Ghi bản ghi WAL mô tả thay đổi vào WAL buffer
4. COMMIT: ghi WAL record commit, rồi write + fsync WAL xuống pg_wal/
5. fsync xong → trả "COMMIT" cho Spring        ← Durability ở đây
   ...
6. Vài phút sau: checkpointer ghi dirty page xuống base/

Mất điện giữa bước 5 và 6:
  Khởi động lại → đọc checkpoint cuối → REPLAY WAL từ đó → page được dựng lại
  Transaction chưa có WAL commit → coi như chưa xảy ra (Atomicity)

Vì sao nhanh hơn: WAL là ghi tuần tự, nhỏ; nhiều transaction commit cùng lúc có thể chung một lần fsync (group commit). Ghi page data thì gom lại, làm sau.

6. So sánh

WALData file
Kiểu ghiAppend tuần tựRandom, theo page 8KB
Khi nào ghiTrước khi commit trả vềCheckpoint / background writer
Dùng đểRecovery, replication, PITR (bài 6.9, 6.10)Lưu trữ chính
Thiết lậpMất gì khi crashDùng khi
synchronous_commit = onKhông mất gì đã commitMặc định, tiền, order
synchronous_commit = offCó thể mất vài trăm ms transaction gần nhất, không hỏng dữ liệuLog, tracking, dữ liệu mất được (đặt theo transaction)
fsync = offCó thể hỏng cả databaseKhông bao giờ trên production

7. Khi nào cần để ý

Process per connection thì khác gì thread per request của Tomcat? Mình mở 500 connection được không?

Bạn so sánh đúng hướng, chỉ cần chỉnh một điểm: Tomcat dùng thread trong một JVM (rẻ, chia sẻ heap), còn PostgreSQL dùng process riêng cho mỗi connection (mỗi cái có bộ nhớ riêng như work_mem cho sort/hash, catalog cache). 500 connection = 500 process. Phần lớn chúng idle nhưng vẫn tốn RAM, và khi cùng active thì tranh CPU, tranh lock trong shared memory, throughput còn giảm.

Số connection active hiệu quả ≈ vài lần số CPU core của DB server
  8 core → khoảng 20–40 active là hợp lý

3 instance × HikariCP 20 = 60      ✓
30 pod   × HikariCP 20 = 600       ✗ → giảm pool, hoặc đặt PgBouncer ở giữa

Chi tiết pool sizing nằm ở bài 6.8.

Có WAL rồi thì backup chỉ cần copy WAL là được đúng không?

Gần đúng, nhưng thiếu một nửa. WAL chỉ là các thay đổi, cần một điểm xuất phát. Backup kiểu PITR (point-in-time recovery) gồm:

base backup (pg_basebackup, lúc 02:00)  +  WAL archive (02:00 → 14:37)
                    │                              │
                    └────── restore ───────────────┘→ dữ liệu đúng như lúc 14:37

Cùng ý tưởng đó, replica nhận WAL stream từ primary để replay (bài 6.9). Backup và restore chi tiết ở bài 6.10.

Phỏng vấn hỏi kiến trúc PostgreSQL và WAL thì trả lời sao?

Q1. Mô tả ngắn kiến trúc PostgreSQL.

🇻🇳 Tiếng Việt:

PostgreSQL dùng mô hình process per connection: postmaster nhận kết nối và fork một backend process cho mỗi connection. Các backend chia sẻ shared memory gồm shared_buffers để cache page 8KB, WAL buffers và lock table. Ngoài ra có các process nền như checkpointer, background writer, walwriter, autovacuum. Vì mỗi connection là một process nên connection khá đắt, ứng dụng cần connection pool.

🇬🇧 English:

PostgreSQL uses a process-per-connection model: the postmaster accepts connections and forks a backend process for each one. Backends share memory that holds shared_buffers for caching 8KB pages, WAL buffers and the lock table. There are background processes such as the checkpointer, background writer, WAL writer and autovacuum. Since each connection is a process, connections are expensive, so applications need a connection pool.

Q2. WAL là gì, vì sao commit là durable?

🇻🇳 Tiếng Việt:

WAL là write-ahead log: mọi thay đổi được ghi vào log trước khi page dữ liệu được ghi xuống đĩa. Khi commit, PostgreSQL ghi bản ghi commit và fsync WAL rồi mới trả về cho client; page dữ liệu chỉ cần sửa trong shared buffers. Nếu crash, lúc khởi động nó replay WAL từ checkpoint cuối để dựng lại dữ liệu. Ghi WAL là ghi tuần tự nên nhanh hơn nhiều so với ghi ngẫu nhiên từng page.

🇬🇧 English:

WAL is the write-ahead log: every change is written to the log before the data page is written to disk. On commit, PostgreSQL writes a commit record and fsyncs the WAL before replying to the client; the data page only needs to change in shared buffers. After a crash it replays the WAL from the last checkpoint to rebuild the data. WAL writes are sequential, so they are much faster than random page writes.

Q3. Checkpoint làm gì?

🇻🇳 Tiếng Việt:

Checkpoint ghi tất cả dirty page trong shared buffers xuống data file và đánh dấu một điểm trong WAL. Khi recovery chỉ cần replay WAL từ điểm đó, và WAL cũ có thể được tái sử dụng hoặc xóa. Checkpoint quá dày gây nhiều I/O, quá thưa thì recovery lâu; điều chỉnh bằng checkpoint_timeout và max_wal_size.

🇬🇧 English:

A checkpoint flushes all dirty pages from shared buffers to the data files and marks a point in the WAL. Recovery only needs to replay WAL from that point, and older WAL can be recycled or removed. Too frequent checkpoints cause heavy I/O, too rare ones make recovery slow; we tune them with checkpoint_timeout and max_wal_size.

Cách nhớ ngắn gọn


Tự kiểm tra

PostgreSQL tạo gì cho mỗi connection?

Một backend process riêng, được fork bởi postmaster.

Lúc COMMIT trả về, page dữ liệu đã nằm trên đĩa chưa?

Chưa chắc. Chỉ WAL chắc chắn đã fsync; page có thể còn dirty trong shared buffers đến checkpoint.

synchronous_commit = off và fsync = off khác nhau thế nào?

Cái đầu có thể mất vài transaction gần nhất nhưng dữ liệu nhất quán; cái sau có thể làm hỏng database.

Vì sao ghi WAL nhanh hơn ghi page dữ liệu?

WAL ghi tuần tự, nhỏ, có group commit; page dữ liệu là ghi ngẫu nhiên vào nhiều file.

Muốn PITR cần những gì?

Một base backup và WAL archive liên tục từ thời điểm backup.