PostgreSQL 架构
问题
PostgreSQL 的整体架构是什么?与 MySQL 线程模型有什么区别?VACUUM 是什么?
答案
一、进程模型
PostgreSQL 采用 多进程 架构(MySQL 是多线程):
| 组件 | 说明 |
|---|---|
| Postmaster | 主进程,监听连接,fork 子进程 |
| Backend | 每个连接一个进程(非线程) |
| Background Writer | 定期将脏页写入磁盘 |
| WAL Writer | 将 WAL 缓冲区写入磁盘 |
| Autovacuum | 自动清理死元组 |
| Checkpointer | 执行检查点,刷脏页 |
进程 vs 线程
- PostgreSQL 多进程:稳定性好(一个进程崩溃不影响其他),但内存开销大,连接数受限
- MySQL 多线程:内存效率高,连接切换快,但线程安全问题复杂
实践:PostgreSQL 通常配合 PgBouncer 等连接池使用,避免频繁创建进程。
二、内存架构
| 内存区域 | 说明 | 类比 MySQL |
|---|---|---|
| Shared Buffers | 共享缓冲池,缓存数据页 | Buffer Pool |
| WAL Buffers | WAL 日志缓冲区 | Log Buffer |
| Work Mem | 排序、哈希操作的内存 | sort_buffer_size |
| Maintenance Work Mem | VACUUM、CREATE INDEX 内存 | — |
| Temp Buffers | 临时表使用的内存 | tmp_table_size |
三、MVCC 实现(与 MySQL 的关键区别)
PostgreSQL 的 MVCC
PostgreSQL 不使用 undo log。每次 UPDATE 都会 创建一个新的行版本,旧版本保留在原表中:
┌─────────────────────────────────┐
│ 堆表(Heap) │
│ id=1, name='张三', xmin=100, xmax=200 ← 旧版本(已死) │
│ id=1, name='李四', xmin=200, xmax=0 ← 新版本(活跃) │
└─────────────────────────────────┘
| 字段 | 含义 |
|---|---|
xmin | 创建该行版本的事务 ID |
xmax | 删除(或更新)该行版本的事务 ID |
ctid | 行的物理位置(页号, 偏移量) |
对比 MySQL 和 PostgreSQL 的 MVCC
| 对比项 | MySQL InnoDB | PostgreSQL |
|---|---|---|
| 旧版本存储 | undo log(单独空间) | 堆表中(与新数据同表) |
| 新版本 | 原地更新 | 写新行,标记旧行 |
| 清理机制 | purge 线程清理 undo | VACUUM 清理死元组 |
| 索引更新 | 非主键只需更新 undo 指针 | 所有索引都要更新(指向新行) |
| 优势 | UPDATE 密集型性能好 | 实现简单,快照隔离强 |
| 劣势 | 长事务导致 undo 膨胀 | 表膨胀,需要 VACUUM |
四、VACUUM 机制
由于旧版本行保留在表中,需要 VACUUM 来清理 死元组(Dead Tuples):
VACUUM 的类型
| 类型 | 操作 | 锁 | 说明 |
|---|---|---|---|
| VACUUM | 标记死元组空间可复用 | 不阻塞读写 | 空间不归还 OS |
| VACUUM FULL | 重建整张表 | 排他锁 | 空间归还 OS,但期间表不可用 |
| Autovacuum | 自动定期执行 VACUUM | 不阻塞 | 推荐开启 |
表膨胀
如果 VACUUM 跟不上 UPDATE/DELETE 的速度,或者有长事务阻止 VACUUM 回收旧版本,表会持续膨胀。
预防:
- 不要有长事务(尤其是空闲的
idle in transaction) - 合理配置 autovacuum 参数
- 监控
pg_stat_user_tables.n_dead_tup
五、WAL(Write-Ahead Logging)
PostgreSQL 的 WAL 等价于 MySQL 的 redo log,用于崩溃恢复:
数据修改 → 写 WAL buffer → fsync 到 WAL 文件 → 修改 Shared Buffer 中的页
WAL 还用于:
- 流复制:将 WAL 发送给从库实现主从复制
- PITR:基于 WAL 的任意时间点恢复
- 逻辑复制:解析 WAL 生成逻辑变更事件
常见面试问题
Q1: PostgreSQL 为什么需要 VACUUM 而 MySQL 不需要?
答案:
因为 MVCC 实现方式不同:
- MySQL:旧版本存在 undo log 中,purge 线程自动清理,主表数据不会膨胀
- PostgreSQL:旧版本直接存在堆表中,必须通过 VACUUM 回收空间
Q2: 为什么 PostgreSQL 选择多进程而不是多线程?
答案:
历史原因和设计哲学:
- 1996 年 PostgreSQL 项目启动时,线程模型还不成熟
- 进程隔离性更好,一个连接崩溃不会影响整个数据库
- 配合连接池(PgBouncer),进程模型在实际使用中性能也不差
Q3: shared_buffers 应该设置多大?
答案:
一般推荐为系统内存的 25%。例如 64GB 内存的服务器,设置 16GB。不建议超过 40%,因为 PostgreSQL 还依赖操作系统的文件缓存。