1 / 15
23:52:11 pg-1 OOM-killed → systemd restart postgresql · không failover — node tự đứng dậy bằng WAL · trả lời câu hỏi treo ở bước 44 journey ①
CLIENT app · libpq / JDBC psql, service, pool… PRIMARY · pg-1 — nhận toàn bộ WRITE postmaster listen :5432 · fork() backend process PID 4711 parser analyzer rewriter planner executor SHARED MEMORY shared_buffers WAL buffers pg_xact (CLOG) trạng thái transaction lock table pg_locks · LWLocks walwriter flush WAL nền bgwriter ghi dirty dần checkpointer flush + fsync all walsender ① stream WAL → pg-2 walsender ② stream WAL → pg-3 OS page cache write() dừng ở đây nếu chưa fsync DISK NVMe · EBS pg_wal/ WAL segments 16MB base/ data files · heap + index global/pg_control checkpoint · redo LSN STANDBY · pg-2 — hot standby (READ) walreceiver nhận · fsync WAL startup process REDO / replay pg_wal/ · disk WAL bền tại pg-2 shared_buffers page sau replay STANDBY · pg-3 — hot standby (READ) walreceiver nhận · fsync WAL startup process REDO / replay pg_wal/ · disk WAL bền tại pg-3 shared_buffers page sau replay
query / control WAL record ACK / feedback data page ô vuông cam = dirty page trong RAM

// Từ điển thành phần

Giải thích nhanh từng khối trên sơ đồ, bằng tiếng Việt dễ hiểu. Mẹo: di chuột (hoặc chạm) vào bất kỳ khối nào trong diagram cũng hiện đúng nội dung này ngay tại chỗ.

Client & kết nối

CLIENT · libpq

Ứng dụng của bạn + driver (libpq, JDBC, psycopg…). Nói chuyện với PostgreSQL bằng giao thức riêng trên TCP 5432. Trên production thường đứng sau connection pooler (PgBouncer) vì mở connection mới rất đắt.

postmaster

Process «mẹ» của cả PostgreSQL: lắng nghe cổng 5432, mỗi client đến thì fork() ra một backend con phục vụ riêng. Không xử lý query — chỉ làm bảo vệ kiêm bà đỡ.

backend process

Process con phục vụ đúng MỘT connection — chạy toàn bộ pipeline parse → plan → execute cho mọi câu lệnh của client đó. Mỗi dòng trong pg_stat_activity là một backend.

Pipeline xử lý câu lệnh

parser

Đọc chuỗi SQL, cắt thành token rồi ghép thành cây cú pháp. Chỉ kiểm tra ngữ pháp — chưa hề biết bảng có tồn tại hay không. Sai cú pháp (lỗi 42601) chết ngay tại đây.

analyzer

Gắn ngữ nghĩa vào cây cú pháp: tra catalog xem bảng/cột có thật không, kiểu dữ liệu gì, bạn có quyền không. Lỗi «relation does not exist» phát sinh ở đây.

rewriter

Biến đổi query theo rule: mở view thành bảng thật bên dưới, chèn điều kiện Row-Level Security. Query trên bảng thường đi qua gần như nguyên vẹn.

planner / optimizer

«Bộ não chiến lược» — cân nhắc mọi cách thực hiện query (quét kiểu gì, join kiểu gì, thứ tự nào) và chọn phương án có cost thấp nhất dựa trên thống kê. EXPLAIN chính là xem sản phẩm của nó.

executor

«Người thi công» — chạy đúng theo plan đã chọn, kéo từng dòng dữ liệu qua cây các node xử lý (scan, join, sort…). Mọi cú đọc/ghi page đều xuất phát từ đây.

Shared memory

Shared Memory

Vùng RAM dùng chung của mọi process PostgreSQL trên node — chứa shared_buffers, WAL buffers, lock table… Backend nào cũng nhìn thấy cùng một dữ liệu tại đây, đó là cách chúng phối hợp.

shared_buffers

Cache dữ liệu chính — «bàn làm việc» của PostgreSQL: page 8KB nào cần đọc/sửa đều phải được đưa lên đây trước. Ô cam trên sơ đồ = page đã sửa trong RAM nhưng chưa ghi xuống disk (dirty page).

WAL buffers

Chỗ tập kết tạm các WAL record trong RAM trước khi ghi xuống file. Nhỏ thôi — mặc định wal_buffers=-1 tức 1/32 shared_buffers (kẹp trong khoảng 64kB–16MB) — vì được xả xuống disk liên tục, đặc biệt mỗi lần COMMIT.

pg_xact (CLOG)

«Sổ hộ tịch» của transaction: 2 bit trạng thái (committed/aborted…) mỗi transaction, lưu trên SLRU page trong thư mục pg_xact/. Tên cũ CLOG — đổi tên từ pg_clog ở PG10, cơ chế vẫn y nguyên trong PG18. MVCC tra sổ này để biết dữ liệu của transaction nào là «thật».

lock table

Bảng khoá trong shared memory — ai đang giữ khoá gì trên bảng/row nào, ai đang xếp hàng chờ. View pg_locks đọc trực tiếp từ đây.

Background processes

walwriter

Process nền chuyên xả WAL buffers xuống disk định kỳ (mỗi wal_writer_delay) — để backend lúc COMMIT thường chỉ phải chờ rất ít, hoặc «đi nhờ» cú flush của người khác.

bgwriter

Process nền «dọn trước»: ghi dần các dirty page sắp bị đuổi khỏi cache xuống OS cache, để backend khi cần chỗ trống không phải tự dọn (việc làm query khựng lại).

checkpointer

Định kỳ flush TOÀN BỘ dirty page xuống disk + fsync + đánh mốc checkpoint — điểm bắt đầu replay khi recovery. Chính nó quyết định «crash xong recovery mất bao lâu».

walsender

Mỗi standby có một walsender riêng trên primary: đọc WAL vừa ghi và stream sang standby đó qua network. Xem tình trạng từng anh trong pg_stat_replication.

Storage trên mỗi node

OS page cache

Cache của Linux kernel — lệnh write() của PostgreSQL chỉ đưa dữ liệu đến đây; phải fsync mới ép nó đi tiếp xuống disk thật. Dữ liệu bị cache hai lần (đây + shared_buffers) — gọi là double buffering.

DISK — NVMe/EBS

«Tủ hồ sơ» — thiết bị lưu trữ bền vững. Dữ liệu chỉ được coi là an toàn khi đã fsync xuống đây. Độ trễ fsync của disk là sàn latency của mọi COMMIT.

pg_wal/

Nhật ký mọi thay đổi (Write-Ahead Log), ghi tuần tự vào từng file 16MB. Là «sự thật» của database: mất data file vẫn cứu được từ WAL, mất WAL là hết chuyện.

base/ — data files

File dữ liệu thật của bảng và index. Được cập nhật «lười» — rất lâu sau WAL, qua tay checkpointer/bgwriter. Vì thế nó giống bản «vật chất hoá dần» của WAL hơn là bản gốc.

global/pg_control

File 512 byte quan trọng nhất cluster: ghi vị trí checkpoint cuối, timeline, trạng thái shutdown. PostgreSQL đọc nó ĐẦU TIÊN khi khởi động để biết phải replay từ đâu. Xem bằng pg_controldata.

Standby (pg-2 · pg-3)

Standby node (hot standby)

Bản sao đầy đủ của primary, replay WAL liên tục để đuổi theo. Phục vụ read query (giảm tải primary) và sẵn sàng được promote thành primary khi có sự cố.

walreceiver

Process trên standby nhận WAL stream từ primary: ghi + fsync vào pg_wal cục bộ, rồi báo cáo ngược «tôi đã nhận/flush/apply tới LSN nào» — nguồn số liệu replication lag.

startup process (REDO)

Process «recovery vĩnh viễn» trên standby: đọc WAL và replay từng record vào shared_buffers, tái hiện chính xác những gì primary đã làm. Khi promote, chính nó replay nốt rồi kết thúc recovery.

pg_wal của standby

WAL nhận từ primary được fsync xuống disk cục bộ của standby. Đây là điều kiện để primary được «ack» trong synchronous replication — dữ liệu đã bền ở ≥2 máy.

shared_buffers của standby

Nơi startup process dựng lại các page sau replay. Read query trên standby đọc từ đây — cùng cơ chế buffer như primary, chỉ khác nguồn ghi là REDO thay vì executor.

MVCC & VACUUM (journey ④)

heap tuple (phiên bản row)

Một phiên bản vật lý của row: header 23 byte (xmin, xmax, ctid, infomask…) + data. UPDATE tạo phiên bản mới chứ không sửa đè — một row logic có thể có nhiều tuple, phần lớn là «xác» chờ VACUUM.

line pointer (ItemId)

Mảng «mục lục» đầu page: mỗi slot 4 byte trỏ tới vị trí tuple thật trong page. Index trỏ vào line pointer chứ không trỏ thẳng tuple — tầng gián tiếp này cho phép HOT redirect mà không phải sửa index.

PageHeader

24 byte đầu mỗi page 8KB: pd_lsn (LSN lần sửa cuối — mấu chốt WAL-before-data), checksum, và cặp con trỏ pd_lower/pd_upper đánh dấu ranh giới vùng trống giữa line pointer và tuple.

free space trong page

Vùng trống giữa pd_lower và pd_upper — nơi tuple mới được đặt vào. fillfactor < 100 chủ ý chừa vùng này rộng khi INSERT để UPDATE sau có chỗ đi đường HOT.

B-tree index

Cây index trỏ vào (page, line pointer) của heap. Entry trỏ dead tuple không tự mất — index cũng bloat, và chỉ REINDEX mới thu nhỏ được cấu trúc đã phình.

transaction & snapshot

Mỗi transaction nhìn database qua snapshot của mình: so xmin/xmax của tuple với snapshot + tra pg_xact để quyết định phiên bản nào «tồn tại». Hai transaction có thể thấy hai giá trị khác nhau của cùng một row — đó là MVCC.

autovacuum

Launcher thức dậy mỗi 60s, phái worker (mặc định 3) vacuum bảng vượt ngưỡng dead tuple (50 + 20% số row). Vacuum thường không chặn đọc/ghi — nó là người dọn rác sống chung với workload.

visibility map (_vm)

2 bit cho mỗi page: all-visible (không còn tuple chết — VACUUM skip, index-only scan khỏi chạm heap) và all-frozen (mọi tuple đã frozen — freeze sau skip). Fork riêng đuôi _vm.

free space map (_fsm)

Cây ghi xấp xỉ chỗ trống từng page — INSERT hỏi nó để tìm chỗ đặt tuple. Được VACUUM cập nhật đầy đủ; bảng bloat mà FSM cũ là lý do file cứ phình dù trong ruột rỗng.

vòng tròn XID 32-bit

Transaction ID quay vòng trong không gian 2³²: mỗi xid chỉ có 2³¹ «quá khứ» và 2³¹ «tương lai». Tuple không được freeze kịp sẽ bị vòng quay biến từ quá khứ thành tương lai — biến mất khỏi mọi query. Freeze + monitor age(datfrozenxid) là bắt buộc.

Locking (journey ⑥)

transaction (node trong wait-for graph)

Mỗi transaction là một node; mũi tên nét đứt = «đang chờ lock của ai». Chu trình mũi tên khép kín = deadlock. Sơ đồ này chính là thứ deadlock detector dựng mỗi lần thức dậy.

bảng + table lock

Mọi lệnh chạm bảng đều lấy một trong 8 mức table lock, giữ tới hết transaction. Hai lệnh chỉ chặn nhau khi ma trận tương thích nói vậy — SELECT không chặn SELECT, nhưng ALTER chặn tất cả.

row lock

Khoá một dòng = ghi xid vào t_xmax của tuple + cờ infomask (không tốn lock table). Bốn mức: FOR KEY SHARE < FOR SHARE < FOR NO KEY UPDATE < FOR UPDATE — phân mức tinh vi để foreign key check không chặn UPDATE thường.

deadlock detector

Backend chờ lock quá deadlock_timeout (1s) sẽ thức dậy, dựng wait-for graph từ lock table và tìm chu trình. Có chu trình → chính nó tự abort làm victim với ERROR: deadlock detected.

8 mức table lock

ACCESS SHARE → ROW SHARE → ROW EXCLUSIVE → SHARE UPDATE EXCLUSIVE → SHARE → SHARE ROW EXCLUSIVE → EXCLUSIVE → ACCESS EXCLUSIVE. Càng sang phải càng độc quyền; tên có chữ ROW vẫn là khoá cả bảng (tên lịch sử).

Backup & PITR (journey ⑦)

timeline

Một «dòng lịch sử» của cluster. PITR hoặc promote tạo timeline mới (số tăng dần — chính là 8 hex đầu của tên file WAL) để WAL của hai lịch sử không bao giờ trộn lẫn.

base backup

Bản chụp toàn bộ cluster ĐANG chạy, lấy qua walsender (pg_basebackup). Tự nó chưa nhất quán — phải kèm WAL để replay vá lại; backup PostgreSQL luôn là cặp (base backup + WAL).

backup_label

File toạ độ của bản backup: CHECKPOINT LOCATION, START WAL LOCATION (REDO point). Khi restore, PostgreSQL đọc checkpoint từ đây thay vì pg_control — đừng bao giờ xoá nó trong bản restore.

WAL archive

Kho lưu mọi segment WAL (archive_command → S3/NFS) — dòng lịch sử liên tục của database. Archive fail thì pg_wal phình và primary sập; alert archive ngang hàng alert disk.

recovery target

Mốc dừng replay: theo thời gian (recovery_target_time — so timestamp trên commit record), LSN, xid, hoặc tên restore point đặt sẵn. Replay dừng ngay trước tai nạn.

restore cluster

Cluster mới dựng từ base backup + recovery.signal + restore_command. Chạy đúng cỗ máy recovery của crash recovery, chỉ khác nguồn WAL (kho archive) và có điểm dừng.

timeline history file

File .history ghi từng dòng: timeline cha · LSN rẽ nhánh · lý do. Vừa là gia phả của cluster, vừa là chỉ dẫn để PITR lần sau đi đúng nhánh WAL qua nhiều timeline.

incremental backup (PG17)

WAL summarizer theo dõi block nào đã đổi; pg_basebackup --incremental chỉ chụp block đó (file INCREMENTAL.oid). Restore: pg_combinebackup ghép full + chuỗi incremental thành bản đầy đủ.

Logical / CDC (journey ⑧)

PUBLICATION

Khai báo phía publisher: bảng nào (kèm row filter/column list) được phát hành. Metadata trong pg_publication; bảng publish phải có replica identity để UPDATE/DELETE phát lại được.

SUBSCRIPTION

Khai báo phía subscriber: nhận publication nào, qua connection nào. CREATE SUBSCRIPTION tự tạo slot logical trên publisher và khởi động bộ máy apply.

walsender (logical)

Cùng process walsender của streaming replication nhưng chạy chế độ logical: decode WAL vật lý bằng snapshot lịch sử của catalog để dịch ngược thành thay đổi row.

ReorderBuffer

Vùng nhớ gỡ rối WAL: hash by_txn gom các thay đổi xen kẽ về đúng transaction, chỉ phát đi khi gặp commit (abort thì vứt). Vượt logical_decoding_work_mem thì spill/streaming.

pgoutput

Output plugin mặc định: serialize transaction đã gom thành chuỗi message Begin/Relation/Insert/Update/Delete/Commit. Debezium cũng nói chuyện bằng đúng format này.

apply worker

Process phía subscriber: nhận message, mở transaction local với session_replication_role=replica, thi hành qua executor (ghi WAL local!), đóng dấu origin để resume/chống vòng lặp, rồi ACK ngược.

Debezium → Kafka

Consumer thay thế subscriber: đọc slot logical qua pgoutput, đẩy mỗi thay đổi thành sự kiện Kafka — cách chuẩn đưa dữ liệu PostgreSQL ra search index, cache, data lake.

logical replication slot

Như slot physical (giữ WAL cho consumer chậm) cộng thêm: gắn một database, mang plugin, và catalog_xmin giữ catalog khỏi VACUUM. Slot bỏ hoang = pg_wal phình + catalog bloat.

table sync worker

Worker tạm thời làm initial sync: COPY dữ liệu sẵn có của từng bảng, khớp mốc LSN với dòng decode để không trùng không sót, xong việc tự giải tán.

logical replication launcher

Process nền phía subscriber: theo dõi pg_subscription và đẻ apply worker / table sync worker khi cần.

Parallel Query (journey ⑨)

leader backend

Backend của chính bạn — dựng plan, đóng gói hành trang vào DSM, phái worker, và (mặc định) cũng xắn tay nhận block trong lúc chờ gom kết quả (parallel_leader_participation).

parallel worker

Background worker process được fork riêng cho query này: đọc DSM để dựng lại plan con (chỉ các node parallel_safe), tự nhận dải block để quét, chạy Partial Aggregate rồi đẩy kết quả qua tuple queue. Xong việc tự giải tán.

Gather / Gather Merge

Node ranh giới: dưới nó chạy song song ở worker, trên nó chạy một luồng ở leader. Gather nhận tuple không theo thứ tự; Gather Merge giữ sort order bằng cách merge các dòng đã sort từ từng worker.

Finalize Aggregate

Tầng gộp cuối ở leader: nhận trạng thái trung gian (sum, count…) từ các Partial Aggregate và ráp thành kết quả chính xác từng bit — mô hình map-combine-reduce trong một câu SQL.

DSM — dynamic shared memory

Vùng nhớ chung cấp phát riêng cho query song song: GUC, transaction snapshot (mọi worker cùng thế giới MVCC), plan đã serialize, trạng thái scan chung, và cả shared hash table của Parallel Hash Join.

tuple queue (shm_mq)

Băng chuyền một chiều từ mỗi worker về leader, nằm trong DSM. Chi phí chuyển tuple qua đây chính là parallel_tuple_cost — lý do kiến trúc luôn cố aggregate sớm để gửi ít dòng.

quyết định của planner

Parallel chỉ đáng khi bảng đủ lớn (min_parallel_table_scan_size 8MB) và lợi vượt chi phí setup + chuyển tuple (parallel_setup_cost/parallel_tuple_cost). Bảng nhỏ không bao giờ parallel — đúng thiết kế.

khi nào không parallel

Câu lệnh ghi, cursor, hàm PARALLEL UNSAFE (mặc định khi CREATE FUNCTION!), hay SERIALIZABLE trước PG12 — dính một cái là cả query rớt về một luồng, âm thầm không warning.

chia block động

Bảng không bị cắt tĩnh: trạng thái scan chung nằm trong DSM, worker nào rảnh nhận dải block kế tiếp — cân bằng tải tự nhiên, worker nhanh tự gánh nhiều hơn.

ba tầng trần worker

max_parallel_workers_per_gather (mỗi Gather, mặc định 2) ⊂ max_parallel_workers (pool song song, mặc định 8) ⊂ max_worker_processes (mọi bgworker — đổi phải restart). Workers Launched < Planned nghĩa là pool đang cạn.

Isolation & SSI (journey ⑩)

transaction lane

Dòng thời gian của một transaction SERIALIZABLE: các ô là thao tác đọc/ghi, mũi tên nét đứt giữa hai lane là rw-antidependency («tôi đọc thứ anh ghi đè»). Hai mũi tên chéo nhau = mầm chu trình.

thao tác đọc/ghi

Mỗi lần ĐỌC trong SERIALIZABLE để lại SIREAD lock (dấu chân); mỗi lần GHI được đối chiếu với dấu chân của người khác — trúng thì ghi nhận một rw-conflict (reader, writer, object).

phán quyết lúc COMMIT

First-committer-win: người commit trước đi qua; người sau, nếu dangerous structure đã khép, nhận ERROR could not serialize access (SQLSTATE 40001) — loại lỗi app bắt buộc phải retry.

SIREAD lock — predicate lock manager

Dấu chân đọc trong shared memory (pg_locks mode=SIReadLock), KHÔNG chặn ai. Được thăng cấp tuple → page → relation để tiết kiệm RAM; seq scan tạo relation-level ngay — nguồn false positive chính.

dangerous structure

Heuristic của SSI: hai cạnh rw-antidependency liên tiếp trong đồ thị ưu tiên = có thể không serializable → abort một victim. Rẻ hơn tìm chu trình đầy đủ, đổi bằng vài vụ bắt nhầm.

ba isolation level

Khác nhau ở NHỊP chụp snapshot: READ COMMITTED chụp mỗi câu lệnh; REPEATABLE READ chụp một lần cho cả transaction; SERIALIZABLE = REPEATABLE READ + theo dõi rw-antidependency (SSI).

// Tài liệu tham khảo

Nội dung chuẩn theo PostgreSQL 18, đối chiếu docs chính thức; feature mới đều ghi rõ version (PG16/17/18) để vẫn đúng với cụm cũ hơn. Muốn đào sâu từng phần, đọc theo map dưới đây.

The Internals of PostgreSQL — Hironobu Suzuki

Sách internals miễn phí chuẩn mực nhất, cập nhật liên tục (đã có Asynchronous I/O §8.5). Map chương ↔ journey: Ch.1 Cluster & Heap ↔ ①GĐ4 · Ch.2 Process & Memory ↔ ①GĐ1 · Ch.3 Query Processing ↔ ①GĐ2–3 + ③ + ⑨ (§3.7 Parallel Query) · Ch.5 Concurrency Control ↔ ④ MVCC + ⑩ (§5.5–5.6 Snapshot/Visibility, §5.9 SSI) · Ch.6 VACUUM ↔ ④ · Ch.7 HOT & Index-Only Scan ↔ ③GĐ5 + ④GĐ3 · Ch.8 Buffer Manager ↔ ①GĐ4 + ③GĐ4 · Ch.9 WAL & Checkpoint ↔ ①GĐ5,7 · Ch.10 PITR & Timeline ↔ ②GĐ3–4 · Ch.11 Streaming Replication ↔ ①GĐ6 + ② · Ch.12 Logical Decoding ↔ ⑧.

PostgreSQL 18 Official Documentation

Nguồn đối chiếu chính. Đọc kèm: Release Notes 18 (AIO, skip scan, checksums mặc định), chương High Availability, WAL Configuration, và Monitoring (pg_stat_io, pg_aios).

PostgreSQL Bloat Is a Feature, Not a Bug — Roger Welin

Bài viết thực chiến đi cùng journey ④: theo dấu một row từ page/tuple → MVCC → index bloat → VACUUM, kèm demo tự chạy được (ctid/xmin/xmax, pgstattuple, bt_page_items, REINDEX CONCURRENTLY) và ngưỡng bloat thực dụng cho production.

Công cụ tự khám phá

pg_waldump đọc từng WAL record · pageinspect mổ xẻ page 8KB · pg_buffercache soi shared_buffers · pg_controldata đọc pg_control — dùng để tự chạy lại từng bước trên database thật.