PostgreSQL 源码漫游(一):编译与进程架构
(PostgreSQL 源码漫游, Part 1)
前言
最近打算系统地读一读 PostgreSQL 的源码。PostgreSQL 大概是世界上最成熟的开源关系数据库了,代码库积累了三十多年,注释详尽、架构清晰,很适合作为学习数据库内核的材料。
这个系列我会从编译开始,逐步深入到各个子系统。第一篇先把全貌过一遍——怎么编译、有哪些进程、查询是怎么流转的。
从源码编译
PostgreSQL 的源码可以从 GitHub 镜像获取:
git clone https://github.com/postgres/postgres.git
cd postgres
PostgreSQL 支持两套构建系统:传统的 Autoconf/Make 和较新的 Meson。日常开发用 Autoconf 就好:
# 1. 配置(检测平台、依赖、生成 Makefile)
./configure --prefix=$HOME/pgsql
# 2. 编译
make -j$(nproc)
# 3. 安装
make install
编译依赖不多,主要是:
- C11 编译器(GCC 或 Clang)
- Bison 2.3+(不是 yacc)
- Flex(不是 lex)
- Perl 5.14+
- readline 或 libedit(可选,但 psql 交互体验要靠它)
- zlib(可选,压缩支持)
./configure 会在各个子目录生成 Makefile,核心的全局配置在 src/Makefile.global.in。整个源码树大致是这样:
postgres/
├── src/
│ ├── backend/ # 服务端核心(这是重头戏)
│ ├── bin/ # 客户端工具:psql, pg_dump, initdb 等
│ ├── include/ # 头文件
│ ├── interfaces/ # 客户端库 libpq
│ ├── pl/ # 过程语言:PL/pgSQL, PL/Python 等
│ ├── common/ # 前后端共用代码
│ ├── port/ # 平台兼容层
│ ├── test/ # 回归测试
│ └── tools/ # 开发辅助工具
├── contrib/ # 官方扩展(pg_stat_statements 等)
└── doc/ # 文档
后续文章基本都会泡在 src/backend/ 里。
多进程架构
PostgreSQL 采用的是经典的 process-per-connection 模型——每个客户端连接由一个独立的操作系统进程服务。这和 MySQL 的多线程模型是不同路线的选择。
整个系统的进程可以分为三类:
1. Postmaster(进程管理器)
Postmaster 是 PostgreSQL 启动后的主进程,代码在 src/backend/postmaster/postmaster.c。它做的事情很直接:
- 监听 TCP 端口(默认 5432)
- 有新连接进来时 fork 一个子进程(Backend)去处理
- 管理所有子进程的生命周期
- 创建和维护共享内存
Postmaster 自己不访问共享内存,这是有意为之——万一某个子进程把共享内存搞坏了,Postmaster 不会被连带卡死,还能做 crash recovery。
程序的入口在 src/backend/main/main.c,它根据命令行参数决定走哪条路:
- 无特殊参数 →
PostmasterMain()(正常的多用户服务器) --boot→BootstrapModeMain()(初始化系统表)--single→PostgresSingleUserMain()(单用户模式,调试用)
2. 后台辅助进程
Postmaster 启动后会依次拉起一组辅助进程,各司其职:
| 进程 | 代码位置 | 职责 |
|---|---|---|
| Startup | access/xlogrecovery.c |
启动时做 crash recovery,回放 WAL |
| Background Writer | postmaster/bgwriter.c |
后台把脏页刷到磁盘,减轻 checkpoint 压力 |
| Checkpointer | postmaster/checkpointer.c |
定期做 checkpoint,确保数据持久化 |
| WAL Writer | postmaster/walwriter.c |
周期性把 WAL 缓冲区刷盘 |
| Autovacuum Launcher | postmaster/autovacuum.c |
自动触发 VACUUM 和 ANALYZE |
| Archiver | postmaster/pgarch.c |
把完成的 WAL 段拷贝到归档目录 |
| WAL Summarizer | postmaster/walsummarizer.c |
汇总 WAL 信息,用于增量备份 |
| Syslogger | postmaster/syslogger.c |
收集所有进程的 stderr,写日志文件 |
这些进程启动有先后顺序。Postmaster 状态机的流转大致是:
- PM_STARTUP:先拉起 Startup 进程做恢复,同时启动 Checkpointer 和 Background Writer 辅助恢复
- PM_RUN:恢复完成,进入正常运行状态,启动 WAL Writer、Autovacuum Launcher、Archiver 等
- 开始接受客户端连接
3. Backend(用户后端进程)
每个客户端连接对应一个 Backend 进程。Postmaster 收到连接请求后 fork 子进程,子进程先走认证流程(BackendMain()),通过后进入查询处理主循环(PostgresMain()),代码在 src/backend/tcop/postgres.c。
这个主循环做的事情就是:读命令 → 执行 → 返回结果 → 读下一个命令,直到客户端断开。
用 ps 看一个运行中的 PostgreSQL 实例,大概是这样:
postgres: postmaster
postgres: checkpointer
postgres: background writer
postgres: walwriter
postgres: autovacuum launcher
postgres: logical replication launcher
postgres: user1 mydb [local] idle
postgres: user2 mydb 192.168.1.5(43210) SELECT
最后两行就是 Backend 进程,能看到是谁连的、连的哪个库、当前在干什么。
查询处理流水线
一条 SQL 从客户端发出到结果返回,在 Backend 进程内部经过五个阶段。这条流水线是 PostgreSQL 最核心的骨架:
SQL 字符串
│
▼
┌──────────┐
│ Parser │ 词法分析 + 语法分析 + 语义分析
└────┬─────┘
│ Parse Tree
▼
┌──────────┐
│ Rewriter │ 规则重写(视图展开等)
└────┬─────┘
│ Query Tree
▼
┌──────────┐
│ Planner │ 生成执行路径,选择代价最低的方案
└────┬─────┘
│ Plan Tree
▼
┌──────────┐
│ Executor │ 按需拉取模型,逐行产出结果
└────┬─────┘
│ Result Rows
▼
返回客户端
分别来看。
Parser(解析器)
代码在 src/backend/parser/。分两步:
- 词法 + 语法分析:
scan.l(Flex)做词法分析,gram.y(Bison)做语法分析,产出原始语法树(Raw Parse Tree) - 语义分析:
analyze.c负责把名字解析成实际的数据库对象——表名查 catalog、列名做类型检查,产出 Query 结构体
Rewriter(规则重写)
代码在 src/backend/rewrite/。如果表上定义了 RULE(比如视图本质上就是一个 SELECT 规则),在这一步展开。大部分普通查询走过这步不会有变化。
Planner/Optimizer(计划器/优化器)
代码在 src/backend/optimizer/,这是最复杂的部分。它要回答的问题是:用什么方式访问表、用什么算法做 JOIN、按什么顺序连接多张表。
工作流程:
- 路径生成(
optimizer/path/):对每张表列出所有可能的访问方式(顺序扫描、各种索引扫描),对每种 JOIN 列出可能的算法(Nested Loop、Hash Join、Merge Join) - JOIN 排列:用动态规划找最优的连接顺序。如果表太多(默认阈值 12 张),切换到 GEQO(遗传算法)避免指数爆炸
- 计划生成(
optimizer/plan/):选出代价最低的路径,转化为 Plan 树
PostgreSQL 的优化器是基于代价的(cost-based),会估算每种方案的 I/O 和 CPU 开销。
Executor(执行器)
代码在 src/backend/executor/。拿到 Plan 树后开始实际执行。
执行器采用 demand-pull(按需拉取)模型——父节点调用 ExecProcNode(子节点) 获取下一行,子节点要么返回一行数据,要么返回 NULL 表示没了。这种设计让流水线式处理成为可能,不需要把中间结果全部物化。
举个例子,一个 Merge Join 的执行树:
MergeJoin
├── Sort
│ └── SeqScan (table_a)
└── Sort
└── IndexScan (table_b)
MergeJoin 交替从左右两个 Sort 节点拉数据,Sort 节点先把下层的全部数据缓存并排序,然后逐行返回。
存储层
执行器下面还有一层存储层,分布在 src/backend/storage/ 和 src/backend/access/:
- Buffer Manager(
storage/buffer/):管理共享缓冲池,所有对磁盘页的读写都经过它 - WAL(
access/transam/):Write-Ahead Logging,保证事务的持久性和 crash recovery - Lock Manager(
storage/lmgr/):行锁、表锁、咨询锁等 - Table Access Method(
access/heap/):堆表的物理存储格式,PostgreSQL 12 之后支持可插拔存储引擎 - Index Access Method(
access/nbtree/,access/hash/,access/gin/等):B-tree、Hash、GIN、GiST、BRIN 等索引实现
小结
PostgreSQL 的架构可以归纳为几个关键设计决策:
- Process-per-connection:每个连接一个进程,通过共享内存通信。简单可靠,一个 Backend 崩溃不会拖垮整个系统
- Postmaster 不碰共享内存:保证了主进程的健壮性
- 五阶段查询流水线:Parser → Rewriter → Planner → Executor → Storage,层次分明,每层有清晰的输入输出
- Demand-pull 执行模型:让流水线式处理成为可能,内存友好
- WAL + Checkpoint:经典的预写日志方案,兼顾性能和可靠性
后面的文章会逐个深入这些子系统。下一篇可能从 Parser 开始,看看一条 SQL 是怎么变成语法树的。