PostgreSQL — Kiến trúc 9 lớp
Từ client xuống đĩa vật lý — 9 lớp kiến trúc nội bộ (PG18) kèm deep-dive replication.
Kiến trúc nội bộ của một PostgreSQL instance
Từ kết nối client xuống tận đĩa vật lý — chín lớp, mỗi lớp một nhiệm vụ. Tài liệu này mô tả chi tiết từng lớp kèm sơ đồ riêng, đã cập nhật theo PostgreSQL 18 và bối cảnh deploy trên CloudNativePG.
PostgreSQL 18 · cumulative statistics trong shared memorySơ đồ tổng quan — 9 lớp
Một request đi từ trên xuống: qua pooler, được postmaster fork thành backend, backend thực thi truy vấn, đọc/ghi qua shared memory, rồi xuống kernel và đĩa. Các tiến trình nền chạy song song để bảo trì. Bấm vào từng lớp bên dưới để xem chi tiết.
Client Applications
Ứng dụng (các pod Product Service dùng driver pgx) và dev/ops qua
kubectl exec mở kết nối TCP cổng 5432. Trong môi trường có tải,
chúng không nối thẳng vào Postgres mà đi qua PgDog ở chế độ transaction —
pooler này ghép nhiều kết nối client thành ít kết nối thật, và route read/write về đúng node.
SET, advisory locks).Postmaster — daemon giám sát
Postmaster là tiến trình cha duy nhất: nó lắng nghe cổng 5432, xử lý xác thực
(pg_hba.conf), ghi PID vào postmaster.pid, và fork một backend
riêng cho mỗi kết nối. Đây là đặc trưng quan trọng nhất của PostgreSQL —
mô hình process-per-connection, không phải thread.
max_connections cao (mặc định 100) tốn RAM và context-switch. Đây là lý do connection pooling gần như bắt buộc khi scale — đừng nâng max_connections lên hàng nghìn, hãy đặt pooler phía trước.Backend Processes
Mỗi backend phục vụ một kết nối, tự mình chạy trọn vòng đời truy vấn:
parse → plan → execute, và quản lý transaction của riêng nó. Backend là nơi
tiêu thụ work_mem — và lưu ý: work_mem được cấp cho mỗi
operation (mỗi sort, mỗi hash), nên một truy vấn phức tạp có thể dùng gấp nhiều lần con số đó.
Background Processes
Đây là các tiến trình postmaster fork lúc khởi động và chạy mãi. Chia thành các nhóm: writer (đẩy dữ liệu xuống đĩa), autovacuum (dọn dead tuple), replication (gửi WAL), và utility (log, archive). Từ PG15 trở đi, thống kê được giữ trong shared memory nên không còn stats collector process.
autovacuum_max_workers=3 nghĩa là 3 worker chạy song song trên các bảng khác nhau.bgwriter chỉ flush page bẩn — không ghi WAL. Chỉ checkpointer mới ghi checkpoint record vào WAL.Shared Memory
Vì các tiến trình tách biệt nhau, chúng phối hợp qua một vùng shared memory chung. Trái tim là shared buffers (cache các page 8KB), bên cạnh là WAL buffers, cache trạng thái commit (CLOG/SLRU), ProcArray (snapshot cho MVCC), lock table, catalog cache, và metadata buffer. Từ PG15, hệ thống thống kê cũng nằm ở đây.
pg_xact trong shared memory, không phải bản thân pg_xact trên đĩa. Và hộp Statistics màu lime là điểm mới — trước PG15 nó là một process riêng.Local Memory
Khác với shared memory, mỗi backend có vùng nhớ riêng không chia sẻ. Đây là nơi các tham số
work_mem, maintenance_work_mem, temp_buffers sống. Hiểu
đúng chỗ này cực kỳ quan trọng để ước lượng RAM tổng.
shared_buffers + (work_mem × số operation đồng thời trên tất cả backend). Vì work_mem cấp per-operation chứ không per-connection, đặt nó quá cao + nhiều kết nối = OOM. Đây là một trong những nguyên nhân OOM phổ biến nhất trên Postgres.OS Kernel
Postgres không ghi thẳng xuống đĩa mà qua syscall của kernel (read,
write, fsync, mmap). Kernel có page cache
riêng — và đây là nguồn gốc của hiện tượng double buffering: cùng một page có thể
nằm cả trong shared_buffers lẫn page cache của OS.
effective_cache_size không cấp bộ nhớ. Nó chỉ là con số để planner ước lượng chi phí index scan (giả định có bao nhiêu cache khả dụng). Đặt nó không làm Postgres dùng thêm RAM — chỉ ảnh hưởng cách chọn plan.Storage Hardware
Trên bare-metal, dưới kernel còn RAID controller (có write cache, thường battery-backed) và cache nội bộ trong ổ đĩa (SSD/NVMe/HDD). Các tầng này tăng tốc nhưng cũng là nơi dữ liệu có thể "mất" nếu mất điện mà chưa flush.
fsync + WAL + độ bền của EBS, không phải battery-backed cache vật lý. Với cloud, tầng L8 hơi "thừa" về mặt vận hành.Physical Storage — $PGDATA
Đáy cùng là thư mục dữ liệu trên PVC/EBS. Mỗi loại file có vai trò riêng: config, data
(base/), WAL (pg_wal/), trạng thái transaction (pg_xact/),
replication slot (pg_replslot/), và nhiều thư mục phụ.
pg_replslot/ giữ WAL lại đến khi consumer (replica/subscriber) xác nhận đã nhận. Nếu consumer chết hoặc chậm, slot không cho xóa WAL → pg_wal phình to đến đầy đĩa. Phải monitor pg_replication_slots (cột wal_status) và alert sớm.Replication — Physical & Logical
WAL không chỉ để recovery — nó còn là cơ sở của replication. Physical stream WAL byte-for-byte sang standby (cùng version, read-only). Logical decode WAL thành thay đổi mức dòng, gửi chọn lọc sang DB đích độc lập (khác version cũng được).
WAL & LSN — nền tảng của mọi replication
Mọi thay đổi trong Postgres được ghi vào WAL trước khi chạm data file.
Mỗi byte trong WAL có một địa chỉ duy nhất gọi là LSN (Log Sequence Number) —
một con số tăng dần, ví dụ 0/16B3748. LSN là "đồng hồ" chung mà mọi cơ chế
replication dùng để biết đã đồng bộ tới đâu.
Khoảng cách giữa LSN của primary và LSN mà replica đã xử lý chính là replication
lag. Có ba mốc LSN bạn sẽ gặp khi monitor: sent_lsn (đã gửi),
flush_lsn (replica đã fsync), replay_lsn (replica đã apply xong).
Physical replication — từng bước
Physical sao chép nguyên trạng vật lý: standby nhận WAL và replay y hệt, tạo ra bản sao byte-for-byte của primary. Toàn bộ cluster được copy — không chọn bảng được.
- Backend trên primary sinh WAL recordMọi thay đổi ghi vào WAL buffer rồi xuống
pg_wal/(fsync khi commit). - WAL Sender đọc WAL mớiMỗi standby kết nối tạo một process
walsendertrên primary (max_wal_senders). Nó theo dõi LSN và đẩy WAL qua TCP. - Replication slot ghi nhớ vị tríSlot lưu LSN mà standby đã nhận, để primary không xóa WAL standby chưa kịp lấy. Đây là khác biệt so với streaming không slot (dùng
wal_keep_size). - WAL Receiver trên standby nhận streamProcess
walreceiverghi WAL vàopg_wal/local của standby. - Startup Process replay WALĐọc WAL tuần tự, áp dụng từng record lên data file — giống hệt crash recovery, nhưng chạy liên tục.
- Hot standby phục vụ readNếu
hot_standby=on, standby nhận truy vấnSELECTread-only trong khi vẫn đang replay. - Standby gửi feedback LSN về primaryCho biết đã flush/replay tới đâu — dùng cho synchronous replication và
hot_standby_feedback.
hot_standby_feedback=on báo cho primary biết replica đang đọc tuple nào, để primary không vacuum sớm tuple đó (tránh "snapshot too old" / query conflict trên replica). Đánh đổi: primary giữ dead tuple lâu hơn → bloat tăng. Cân nhắc theo workload.Logical replication — từng bước
Logical không copy block vật lý mà giải mã WAL thành thao tác logic
("INSERT dòng này vào bảng users") rồi gửi sang một database đích độc lập, có thể ghi được,
khác version, khác schema. Cần wal_level = logical.
- Tạo Publication trên nguồn
CREATE PUBLICATION mypub FOR TABLE users, orders;— khai báo replicate bảng/cột/hàng nào. - Tạo Subscription trên đích
CREATE SUBSCRIPTION mysub CONNECTION '...' PUBLICATION mypub;— đích kết nối tới nguồn và tạo replication slot ở nguồn. - Initial snapshot (copy_data)Lúc tạo subscription, dữ liệu hiện có được
COPYsang đích trước, rồi mới stream thay đổi tiếp theo. Bảng lớn → bước này nặng. - Logical decoder đọc WAL qua slotPlugin
pgoutputđọc WAL từ replication slot, lọc theo publication, dịch thành luồng thay đổi row-level. - Stream tới đíchCác thay đổi (INSERT/UPDATE/DELETE) được gửi qua kết nối logical.
- Apply worker thực thi trên đíchMột background worker trên đích nhận và chạy các thay đổi đó như câu lệnh thật.
ALTER TABLE phải chạy tay ở cả hai bên, đúng thứ tự. (2) Sequence không sync giá trị — failover sang đích phải setval lại kẻo trùng ID. (3) Cần REPLICA IDENTITY (thường là primary key) để UPDATE/DELETE biết dòng nào; bảng không có PK phải set REPLICA IDENTITY FULL.Synchronous vs Asynchronous — quyết định RPO
synchronous_commit điều khiển khi nào COMMIT trả vềĐây là tham số quan trọng nhất về độ bền. Nó quyết định primary chờ tới mức nào trước khi báo COMMIT thành công cho client. Càng chờ xa → mất dữ liệu càng ít khi sự cố (RPO thấp), nhưng latency càng cao.
| Mức | Primary chờ đến khi | RPO | Latency |
|---|---|---|---|
off | không chờ cả fsync local | có thể mất vài giao dịch | thấp nhất |
on (không sync standby) | WAL fsync trên local | 0 nếu disk còn, >0 nếu mất node | thấp |
remote_write | replica ghi WAL vào OS cache | >0 (replica chưa fsync) | trung bình |
remote_flush / on+sync | replica đã fsync WAL | 0 | cao |
remote_apply | replica replay xong (query thấy) | 0 + read-after-write | cao nhất |
synchronous_standby_names trực tiếp mà khai báo spec.minSyncReplicas / maxSyncReplicas — operator tự sinh cấu hình. Đặt minSyncReplicas ≥ 1 để có RPO=0, nhưng nhớ: nếu số sync standby khả dụng tụt dưới mức min, primary sẽ chặn COMMIT để giữ đảm bảo — đánh đổi availability lấy durability.Failover — khi primary chết
Failover chỉ áp dụng cho physical (logical không có khái niệm này). Khi primary chết, một standby được chọn promote. Trên CNPG operator tự lo; trên EC2 thường là Patroni + etcd.
- Phát hiện primary chếtCNPG operator (hoặc Patroni qua TTL trong etcd) phát hiện primary không còn phản hồi.
- Chọn standby tốt nhấtStandby có
replay_lsnmới nhất (lag thấp nhất) được ưu tiên — để mất ít dữ liệu nhất. - Promote
pg_promote(): standby thoát recovery mode, mở read-write, trở thành primary mới với một timeline WAL mới. - Đổi routingCNPG đổi label để Service
-rwtrỏ sang pod mới; Patroni thì HAProxy health check tự phát hiện qua REST API. - Các standby còn lại re-attachCác replica khác đổi sang stream WAL từ primary mới (cần cùng timeline; dùng
pg_rewindnếu phân kỳ).
pg_rewind rồi join lại làm standby.Vận hành & sự cố thường gặp
| Triệu chứng | Nguyên nhân | Xử lý |
|---|---|---|
pg_wal phình to, sắp đầy đĩa | Replication slot của một consumer (replica/subscriber) chết hoặc chậm → slot giữ WAL không cho xóa | Kiểm tra pg_replication_slots.wal_status; nếu slot mồ côi thì pg_drop_replication_slot(); đặt max_slot_wal_keep_size để giới hạn |
| Replication lag tăng dần | Replica yếu hơn primary, hoặc network nghẽn, hoặc replay bị block bởi query dài (recovery conflict) | So flush_lsn vs replay_lsn; tăng tài nguyên replica; cân nhắc max_standby_streaming_delay |
| Query trên replica bị cancel | Recovery conflict: replay cần vacuum tuple mà query trên replica đang đọc | Bật hot_standby_feedback=on (đổi lấy bloat trên primary), hoặc nới max_standby_streaming_delay |
| Logical: apply worker dừng, lag tăng | Xung đột (duplicate key) trên đích, hoặc DDL lệch giữa hai bên | Xem pg_stat_subscription + log; sửa conflict trên đích; đồng bộ DDL thủ công đúng thứ tự |
| Sau failover, ID bị trùng (logical) | Sequence không được replicate sang đích | setval() các sequence trên đích trước khi cho ghi |
retained_wal của bất kỳ slot nào vượt ngưỡng (vd 50% dung lượng pg_wal PVC). Đây là nguyên nhân #1 khiến Postgres chết vì đầy đĩa — và nó âm thầm, không báo cho tới khi hết chỗ.Chọn loại nào cho use case nào
Physical streaming
- Toàn bộ cluster, byte-for-byte đơn giản
- RPO=0 nếu sync, có failover/HA
- Bắt buộc cùng major version
- Standby read-only hoàn toàn
- Không chọn lọc bảng được
Logical replication
- Chọn lọc bảng/cột/hàng linh hoạt
- Cross-version, cross-schema
- Đích ghi được (multi-master, gộp nguồn)
- DDL không replicate, sequence thủ công
- Không phải cơ chế HA