03 - 架构原理
第 03 章 · 架构原理
理解 PostgreSQL 的内部架构,是做好性能调优和故障排查的基础。本章将深入讲解其进程模型、内存结构、存储机制和核心子系统。
3.1 整体架构总览
PostgreSQL 采用 客户端/服务器 架构,核心由以下组件构成:
┌──────────────────────────────────────────────────────┐
│ 客户端应用 │
│ (psql / pgAdmin / 驱动程序) │
└──────────────┬───────────────────────────┬────────────┘
│ TCP/Unix Socket │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ Postmaster │ │ Backend Process │
│ (主守护进程) │ │ (每个连接一个进程) │
│ PID 1 │───→│ 执行查询、返回结果 │
└──────────┬───────────┘ └────────────┬──────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────────┐
│ Shared Memory │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Shared │ │ WAL │ │ CLOG │ │ Other │ │
│ │ Buffers │ │ Buffers │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
└──────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Background Workers │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ BG Writer│ │Checkpointer│ │ Autovacuum│ │WAL Writer│
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Stats │ │WAL Sender│ │Logical │ │
│ │ Collector│ │ │ │Rep Worker│ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ 磁盘存储 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Data │ │ WAL │ │ CLOG │ │ Other │ │
│ │ Files │ │ Files │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
└──────────────────────────────────────────────────────┘
3.2 进程模型
PostgreSQL 使用 多进程模型(而非多线程),每个客户端连接会 fork 一个独立的 Backend 进程。
核心进程
| 进程 | 作用 | 数量 |
|---|---|---|
| Postmaster | 主守护进程,监听端口,fork 子进程 | 1 |
| Backend | 处理客户端查询的进程 | 每连接 1 个 |
| Background Writer (BG Writer) | 将脏页从 Shared Buffers 写入磁盘 | 1 |
| Checkpointer | 执行检查点,确保数据持久化 | 1 |
| WAL Writer | 将 WAL 缓冲区写入 WAL 文件 | 1 |
| Autovacuum Launcher | 启动自动清理工作者 | 1 |
| Autovacuum Worker | 执行 VACUUM 和 ANALYZE | 可配置(默认 3) |
| WAL Archiver | 归档 WAL 文件(用于 PITR) | 0 或 1 |
| WAL Sender | 流复制:向从库发送 WAL | 每个从库 1 个 |
| WAL Receiver | 流复制:从主库接收 WAL(从库) | 0 或 1 |
| Stats Collector | 收集统计信息 | 1(PG 16+ 融入主进程) |
| Logical Replication Worker | 逻辑复制工作进程 | 按需 |
查看进程
-- 查看所有后端进程
SELECT pid, usename, datname, client_addr, state, query, backend_start
FROM pg_stat_activity;
-- 查看各类进程数
SELECT backend_type, count(*)
FROM pg_stat_activity
GROUP BY backend_type;
-- 终止某个连接
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname = 'mydb' AND pid <> pg_backend_pid();
连接开销
每个 Backend 进程约占用 5-10MB 内存,大量连接时资源开销显著。这正是为什么生产环境需要连接池(如 PgBouncer)。
1000 连接 × 10MB = ~10GB 仅连接开销
使用 PgBouncer 后:50 活跃连接 × 10MB = ~500MB
⚠️ 注意事项:PostgreSQL 的多进程模型意味着 max_connections 不宜设置过大(通常不超过 500),应通过连接池复用连接。
3.3 内存结构
Shared Memory
共享内存是所有进程共享的内存区域,在 PostgreSQL 启动时分配。
| 组件 | 参数 | 作用 | 推荐大小 |
|---|---|---|---|
| Shared Buffers | shared_buffers | 数据页缓存 | 总内存的 25% |
| WAL Buffers | wal_buffers | WAL 写入缓冲 | 64MB(自动计算) |
| CLOG Buffers | — | 事务提交状态缓存 | 自动管理 |
| Lock Space | — | 锁信息存储 | 按需 |
| Proc Array | — | 进程状态数组 | 按需 |
Per-Session Memory
每个 Backend 进程独立使用的内存:
| 组件 | 参数 | 作用 | 推荐大小 |
|---|---|---|---|
| Work Mem | work_mem | 排序、哈希、Merge Join | 4-64MB |
| Maintenance Work Mem | maintenance_work_mem | VACUUM、CREATE INDEX | 256MB-1GB |
| Temp Buffers | temp_buffers | 临时表缓存 | 8MB |
📌 关键概念:work_mem 是每个操作的内存限制,不是每个连接。一条复杂查询可能同时使用多个 work_mem(例如多个排序操作)。总内存消耗可能 = work_mem × 操作数 × 并发连接数。
3.4 存储系统
数据目录结构
$ ls -la /var/lib/postgresql/17/main/
PG_VERSION # 版本标识
postgresql.conf # 主配置
pg_hba.conf # 认证配置
postgresql.auto.conf # ALTER SYSTEM 配置
base/ # 数据库文件(每个子目录对应一个数据库)
global/ # 共享系统表(如 pg_database)
pg_wal/ # WAL 文件
pg_xact/ # 事务提交状态(CLOG)
pg_tblspc/ # 表空间符号链接
pg_stat_tmp/ # 临时统计文件
pg_log/ # 日志(可能在其他位置)
数据文件组织
base/
├── 1/ # template1 数据库
├── 13751/ # template0 数据库
├── 16384/ # postgres 数据库
└── 16385/ # mydb 数据库
├── 1234 # 表的数据文件(relfilenode)
├── 1234.1 # 表的第一个扩展段
├── 1234_fsm # Free Space Map
├── 1234_vm # Visibility Map
└── 1234_init # 未初始化的表
每个表对应一个或多个文件(1GB 为单位的扩展段),文件名为该表的 relfilenode。
-- 查看表的 relfilenode
SELECT relfilenode FROM pg_class WHERE relname = 'mytable';
-- 查看表的物理大小
SELECT pg_size_pretty(pg_total_relation_size('mytable'));
-- 查看表的文件路径
SELECT pg_relation_filepath('mytable');
页(Page / Block)
PostgreSQL 的基本 I/O 单位是 页,默认大小为 8KB(编译时可选 1-32KB)。
┌─────────────────────────────────────────────┐
│ Page (8KB) │
├─────────────────────────────────────────────┤
│ PageHeaderData (24 bytes) │
│ - pd_lsn (最后修改的 WAL 位置) │
│ - pd_checksum (校验和) │
│ - pd_lower (空闲空间起始) │
│ - pd_upper (空闲空间结束) │
│ - pd_special (特殊空间起始) │
│ - ... │
├─────────────────────────────────────────────┤
│ ItemIdData (行指针数组) │
│ [1] offset, length, flags │
│ [2] offset, length, flags │
│ ... │
├─────────────────────────────────────────────┤
│ Free Space │
├─────────────────────────────────────────────┤
│ Tuple Data (行数据,从下往上增长) │
│ ... │
│ [Tuple 2] │
│ [Tuple 1] │
├─────────────────────────────────────────────┤
│ Special Space (索引页专用) │
└─────────────────────────────────────────────┘
3.5 MVCC(多版本并发控制)
MVCC 是 PostgreSQL 实现高并发的核心机制。每个事务看到的是数据的一个快照(Snapshot),而非当前实时数据。
实现原理
每行数据有隐藏的系统字段:
| 字段 | 含义 |
|---|---|
xmin | 插入该行的事务 ID |
xmax | 删除/更新该行的事务 ID(0 表示未删除) |
ctid | 行的物理位置(页号, 行号) |
-- 查看隐藏系统字段
SELECT xmin, xmax, ctid, * FROM mytable;
-- xmin | xmax | ctid | id | name
-- -------+------+-------+----+------
-- 1000 | 0 | (0,1) | 1 | Alice ← 活跃行
-- 1000 | 1001 | (0,2) | 2 | Bob ← 已被事务 1001 删除
UPDATE 的实现
PostgreSQL 中的 UPDATE 实际上是 DELETE + INSERT:
UPDATE users SET name = 'Alice_v2' WHERE id = 1;
-- 实际操作:
-- 1. 旧行 (id=1, name='Alice') 的 xmax 设为当前事务 ID(标记为删除)
-- 2. 在同一页或其他页插入新行 (id=1, name='Alice_v2')
-- 3. 更新索引指向新行
这意味着 UPDATE 会产生死元组(Dead Tuple),需要 VACUUM 清理。
⚠️ 注意事项:频繁的 UPDATE 会导致表膨胀(Table Bloat),这是 PostgreSQL 性能下降的常见原因之一。需要定期 VACUUM 或使用 pg_repack。
3.6 WAL(Write-Ahead Logging)
WAL 是 PostgreSQL 保证数据持久性和一致性的核心机制。
基本原理
事务提交时:
1. WAL 记录先写入 WAL Buffer
2. WAL Buffer 刷入 WAL 文件(fsync)
3. 返回客户端 "COMMIT 成功"
4. 后续才将数据页写入数据文件
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Backend │────→│ WAL │────→│ WAL │
│ Process │ │ Buffer │ │ File │ ← 先写这里
└──────────┘ └──────────┘ └──────────┘
│ │
▼ ▼ (延迟写入)
┌──────────┐ ┌──────────┐
│ Shared │ │ Data │
│ Buffers │────────────────────→│ File │ ← 后写这里
└──────────┘ └──────────┘
WAL 配置
-- WAL 级别
SHOW wal_level;
-- replica - 支持流复制和 PITR(默认)
-- logical - 支持逻辑复制(需要时开启)
-- WAL 相关参数
SHOW max_wal_size; -- 自动 checkpoint 的 WAL 上限(默认 1GB)
SHOW min_wal_size; -- WAL 文件保留的最小空间(默认 80MB)
SHOW wal_buffers; -- WAL 缓冲区大小(默认 -1 自动计算)
SHOW checkpoint_timeout; -- 自动 checkpoint 间隔(默认 5min)
SHOW checkpoint_completion_target; -- checkpoint 平滑写入比例(默认 0.9)
查看 WAL 信息
-- 当前 WAL 位置
SELECT pg_current_wal_lsn();
-- WAL 使用情况
SELECT
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) AS total_wal,
count(*) AS wal_file_count
FROM pg_ls_waldir();
-- WAL 文件列表
SELECT name, size, modification
FROM pg_ls_waldir()
ORDER BY modification DESC
LIMIT 10;
3.7 Checkpoint(检查点)
检查点是将 Shared Buffers 中的脏页强制写入数据文件的操作,确保数据持久化。
检查点触发条件
| 条件 | 参数 | 默认值 |
|---|---|---|
| WAL 积累超过阈值 | max_wal_size | 1GB |
| 距上次检查点超时 | checkpoint_timeout | 5min |
| 手动触发 | CHECKPOINT 命令 | — |
| 关闭数据库 | pg_ctl stop -m fast | — |
检查点对性能的影响
检查点期间会产生大量磁盘 I/O,可能导致性能抖动。
-- 监控检查点活动
SELECT
checkpoints_timed, -- 超时触发的检查点次数
checkpoints_req, -- 手动/WAL 积累触发的次数
buffers_checkpoint, -- 检查点写入的缓冲区数
buffers_backend, -- 后端进程直接写入的缓冲区数
checkpoint_write_time, -- 检查点写入耗时 (ms)
checkpoint_sync_time -- 检查点 fsync 耗时 (ms)
FROM pg_stat_bgwriter;
💡 技巧:如果 checkpoints_req 远大于 checkpoints_timed,说明 max_wal_size 设置过小,应该增大。理想情况是检查点由超时触发(checkpoints_timed),而非由 WAL 积累触发。
3.8 共享缓冲区(Shared Buffers)读写流程
读取数据:
1. Backend 进程请求某个数据页
2. 先检查 Shared Buffers 中是否有该页(缓存命中)
├── 命中 → 直接读取(内存操作,极快)
└── 未命中 → 从磁盘读入 Shared Buffers → 返回
3. 后续读取同一页面即可缓存命中
写入数据:
1. Backend 修改 Shared Buffers 中的数据页(标记为脏页)
2. 不会立即写入磁盘
3. 由以下进程负责写入:
- Checkpointer:检查点时批量写入
- BG Writer:后台持续写入(LRU 策略)
- Backend:无法分配缓冲区时被迫写入(最后手段)
-- 查看缓存命中率(应 > 99%)
SELECT
sum(blks_hit) AS hits,
sum(blks_read) AS reads,
round(sum(blks_hit)::numeric / NULLIF(sum(blks_hit) + sum(blks_read), 0) * 100, 2) AS hit_ratio
FROM pg_stat_database
WHERE datname = current_database();
3.9 进程间通信
| 机制 | 用途 |
|---|---|
| 共享内存 | 数据页、WAL、锁信息等 |
| 信号量 | 进程同步、轻量锁 |
| LWLock | 轻量级锁(保护内部数据结构) |
| Spinlock | 自旋锁(极短等待) |
| LISTEN/NOTIFY | 应用层通知机制 |
-- LISTEN/NOTIFY 示例
-- 会话 1:监听
LISTEN my_channel;
-- 会话 2:发送通知
NOTIFY my_channel, '{"event": "user_created", "id": 42}';
-- 会话 1:接收通知
-- 收到异步通知
3.10 架构小结
┌─ 客户端层 ──────────────────────────────────┐
│ psql / 驱动程序 / ORM │
└─────────────────────────────────────────────┘
│ 连接(TCP / Unix Socket)
▼
┌─ 连接层 ────────────────────────────────────┐
│ Postmaster → fork → Backend Process │
│ (建议使用连接池 PgBouncer 减少 fork) │
└─────────────────────────────────────────────┘
│
▼
┌─ 查询处理层 ────────────────────────────────┐
│ Parser → Analyzer → Rewriter │
│ → Planner/Optimizer → Executor │
└─────────────────────────────────────────────┘
│
▼
┌─ 存储层 ────────────────────────────────────┐
│ Shared Buffers ←→ Disk (Data Files) │
│ WAL Buffer ←→ Disk (WAL Files) │
│ CLOG ←→ Disk (CLOG Files) │
└─────────────────────────────────────────────┘
业务场景
| 场景 | 需关注的架构组件 |
|---|---|
| 高并发 OLTP | 连接池 + MVCC + Shared Buffers 调优 |
| 大量写入 | WAL 配置 + Checkpoint 调优 + BG Writer |
| 数据恢复 | WAL 归档 + PITR 流程 |
| 性能瓶颈排查 | Shared Buffers 命中率 + 进程状态 + 锁等待 |
| 主从复制 | WAL Sender/Receiver + 流复制延迟 |
扩展阅读
- PostgreSQL 内部架构 — 154 页 PPT
- The Internals of PostgreSQL
- PostgreSQL WAL Internals — blog.postgresql.org
- 《PostgreSQL 查询引擎源码技术探析》— 张树杰