PostgreSQL 架构概览
PostgreSQL 是一个客户端-服务器(Client-Server)架构的关系型数据库。本节从整体视角俯瞰它的进程、内存与存储三大组成部分,讲清它与 SQLite 这类嵌入式数据库的本质差异,以及「一个实例管理多个数据库」的单机多库结构,为后续深入进程模型、内存结构与磁盘结构打下全局认知。
前置知识
阅读本节前,建议先了解:SSL 连接初始化
基础概念
要理解 PostgreSQL 的架构,先要理清一组容易混淆的名词。它们的层级关系是:实例(Instance)→ 集群(Cluster)→ 数据库(Database)→ 模式(Schema)→ 对象(Object)。
| 术语 | 英文名 | 说明 |
|---|---|---|
| 实例 | Instance | 一个运行中的服务端,由一组进程 + 一块共享内存构成,监听固定端口 |
| 集群 | Cluster | 一个数据目录(PGDATA)下的全部数据;一个实例对应一个集群 |
| 数据库 | Database | 集群内的逻辑隔离单元,彼此不能跨库查询(除非用 dblink / FDW) |
| 模式 | Schema | 数据库内的命名空间,默认 public |
| 表空间 | Tablespace | 数据文件的物理存放位置,可跨磁盘 |
| 后端进程 | Backend Process | 每个客户端连接对应一个服务端进程 |
术语陷阱
PostgreSQL 官方文档中「集群(Cluster)」指的是一个数据目录,而不是「多台机器组成的集群」。多机方案叫「复制(Replication)」或「高可用(High Availability)」。初学时务必区分,否则读文档会严重误解。
PostgreSQL 采用进程(Process)模型而非线程模型:每条客户端连接都由一个独立的操作系统进程服务。这与 MySQL 的「单进程 + 多线程」形成鲜明对比。
三大组成部分
整个 PostgreSQL 实例可以拆解为三块:
| 组成 | 内容 | 生命周期 |
|---|---|---|
| 进程(Processes) | 主进程 Postmaster、后端进程、后台辅助进程(bgwriter、walwriter、checkpointer、autovacuum 等) | 服务启动到停止 |
| 内存(Memory) | 共享内存区(shared_buffers、WAL buffer、CLOG、锁表)+ 每进程本地内存(work_mem、temp_buffers 等) | 共享内存随实例;本地内存随会话 |
| 存储(Storage) | 数据目录 PGDATA 下的表文件、索引文件、WAL 文件、事务状态文件等 | 持久化 |
一个典型生产实例的组件协作关系如下:
┌───────────────────────────────────────────────────────────────┐
│ 客户端(libpq / JDBC / psql) │
└───────────────────────────┬───────────────────────────────────┘
│ TCP 5432 / Unix Socket
┌───────────────────────────▼───────────────────────────────────┐
│ Postmaster 主进程 │
│ (监听连接、派生后端、监控子进程、崩溃恢复) │
└──────┬────────────────────────────────────┬───────────────────┘
│ fork │ fork
┌──────▼──────────┐ ┌──────────▼────────────────────┐
│ Backend 后端 │ │ 后台辅助进程(Background) │
│ 每连接一进程 │ │ bgwriter / walwriter / │
│ 解析、计划、执行 │ │ checkpointer / autovacuum / │
│ │ │ archiver / walsender ... │
└──────┬──────────┘ └──────────┬────────────────────┘
│ │
┌──────▼────────────────────────────────────▼───────────────────┐
│ 共享内存(Shared Memory) │
│ shared_buffers │ WAL buffers │ CLOG │ lock table │ proc array │
└──────┬────────────────────────────────────────────────────────┘
│ 读写
┌──────▼────────────────────────────────────────────────────────┐
│ PGDATA 数据目录(磁盘) │
│ base/ │ global/ │ pg_wal/ │ pg_xact/ │ pg_tblspc/ │ postgresql.conf │
└───────────────────────────────────────────────────────────────┘与嵌入式数据库的区别
SQLite 常被拿来与 PostgreSQL 对比,但二者架构截然不同:
| 维度 | PostgreSQL(客户端-服务器) | SQLite(嵌入式) |
|---|---|---|
| 运行形态 | 独立服务进程,客户端通过网络连接 | 一个库文件,进程内直接读写 |
| 并发模型 | 多进程 + MVCC,读写不阻塞 | 文件锁,写串行 |
| 网络访问 | 原生支持,多客户端共享 | 无网络层,需自己封装 |
| 权限体系 | 角色、模式、行级安全(RLS) | 无(依赖文件系统权限) |
| 事务隔离 | 完整 ACID + 多种隔离级别 | ACID,写并发受限 |
| 典型场景 | 后端服务、多用户、大数据量 | 移动端、桌面应用、单机工具 |
选择的关键在于:是否需要多客户端并发、是否需要网络访问、是否需要细粒度权限。三者只要有一个是「是」,就应选 PostgreSQL。
为什么 PostgreSQL 不用线程
PostgreSQL 诞生于 1986 年,当时 Unix 的线程实现尚不成熟且不可移植,因此选择多进程。多进程的好处是隔离性强——某个后端进程崩溃不会拖垮整个实例(Postmaster 会检测到并重建共享内存);代价是进程创建开销高于线程,且共享内存只能通过内核共享内存段传递。这也是连接池在 PostgreSQL 中尤其重要的原因。
核心机制
客户端-服务器通信
PostgreSQL 使用自己定义的前端/后端协议(Frontend/Backend Protocol)在 TCP 或 Unix 域套接字上通信。协议是消息(Message)驱动的:客户端发送 Query、Parse、Bind 等消息,服务端回复 RowDescription、DataRow、CommandComplete、ReadyForQuery 等消息。
| 传输通道 | 配置项 | 说明 | 适用 |
|---|---|---|---|
| TCP/IP | listen_addresses、port | 可跨主机访问,需 pg_hba.conf 授权 | 应用服务器连接 |
| Unix 域套接字 | unix_socket_directories | 仅本机,路径即身份,性能略优 | 本机管理、同机应用 |
| SSL/TLS | ssl、证书文件 | 在 TCP 之上加密 | 跨网络安全连接 |
-- 查看服务端监听与连接相关配置
SELECT name, setting, unit, short_desc
FROM pg_settings
WHERE name IN ('listen_addresses', 'port', 'unix_socket_directories',
'max_connections', 'superuser_reserved_connections')
ORDER BY name; name | setting | unit | short_desc
-----------------------------+------------------------+------+----------------------------------------------------
listen_addresses | localhost | | Sets the host name or IP address(es) to listen to.
max_connections | 100 | | Sets the maximum number of concurrent connections.
port | 5432 | | Sets the TCP port the server listens on.
superuser_reserved_connections | 3 | | Sets the number of connections reserved for superusers.
unix_socket_directories | /var/run/postgresql | | Sets the directories where Unix-domain sockets will be c
(5 rows)superuser_reserved_connections
max_connections = 100 并不意味着普通用户能用满 100 个连接。其中 superuser_reserved_connections(默认 3)个连接被保留给超级用户,防止 DBA 在连接耗尽时无法登录救火。
单机多库结构
一个 PostgreSQL 实例(一个数据目录)内可以创建多个数据库。这一点与 MySQL 不同:MySQL 中 database 约等于 schema,而 PostgreSQL 中 database 与 schema 是两级独立的概念。
-- 列出当前实例中的所有数据库
SELECT datname,
pg_size_pretty(pg_database_size(datname)) AS size,
datallowconn,
datistemplate
FROM pg_database
ORDER BY pg_database_size(datname) DESC; datname | size | datallowconn | datistemplate
-------------+---------+--------------+---------------
demo | 4832 MB | t | f
postgres | 8621 kB | t | f
template1 | 7601 kB | t | t
template0 | 7489 kB | f | t
(4 rows)关键规则:
- 不能跨库查询:连接到
demo库时,无法在一条 SQL 里访问postgres库的表。这是与 MySQLdb.table语法的最大差异。 template0不可连接:它作为「干净的原始模板」,datallowconn = f,用于以指定编码创建新库。template1是默认模板:CREATE DATABASE newdb实际是复制template1。往template1里装扩展会让所有新库都带上它。- 系统目录是全局共享的一部分:
pg_database、pg_authid等共享目录存放在global/目录,所有库共用;而pg_class、pg_attribute等是每库一份。
-- 数据库内的 schema 层次:一个库下可挂多个 schema
SELECT nspname AS schema_name,
pg_get_userbyid(nspowner) AS owner
FROM pg_namespace
WHERE nspname NOT LIKE 'pg_%'
ORDER BY nspname; schema_name | owner
-------------+----------
public | postgres
app | app_user
reporting | analyst
(3 rows)对象的完整限定名是 database.schema.object,但由于同库内才能引用,实际使用时写作 schema.object:
-- 限定名引用,避免依赖 search_path
SELECT count(*) FROM app.orders;
-- 跨库访问必须借助扩展(dblink / postgres_fdw),不能直接 JOIN
-- 例如 postgres_fdw:
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
CREATE SERVER remote_db FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '10.0.2.11', port '5432', dbname 'reporting');详细说明
进程组成一览
启动一个 PostgreSQL 实例后,用 ps 可以看到完整的进程树。所有进程都以主进程为父进程:
# 查看实例的全部进程(RPM/YUM 安装路径为 /usr/pgsql-18/bin/postgres)
ps -ef | grep '[p]ostgres'postgres 1147 1 0 09:12 ? 00:00:03 /usr/lib/postgresql/18/bin/postgres -D /var/lib/postgresql/18/main
postgres 1149 1147 0 09:12 ? 00:00:00 postgres: checkpointer
postgres 1150 1147 0 09:12 ? 00:00:01 postgres: background writer
postgres 1152 1147 0 09:12 ? 00:00:01 postgres: walwriter
postgres 1153 1147 0 09:12 ? 00:00:00 postgres: autovacuum launcher
postgres 1154 1147 0 09:12 ? 00:00:00 postgres: logical replication launcher
postgres 1178 1147 0 09:20 ? 00:00:00 postgres: postgres postgres [local] idle
postgres 1201 1147 0 09:22 ? 00:00:00 postgres: app appdb 10.0.1.5(51234) idle| 进程 | 类型 | 职责 | 后续章节 |
|---|---|---|---|
postgres -D ... | 主进程 | 监听、派生、监控、崩溃恢复 | Postmaster 主进程 |
checkpointer | 后台 | 定期写检查点,缩短恢复时间 | Checkpointer |
background writer | 后台 | 提前刷脏页,平滑 I/O | Background Writer |
walwriter | 后台 | 把 WAL 缓冲刷入磁盘 | WAL Writer |
autovacuum launcher | 后台 | 调度 autovacuum worker | AutoVacuum Worker |
logical replication launcher | 后台 | 启动逻辑复制工作进程 | —(复制章节) |
postgres: <user> <db> <host> | 后端 | 处理一条客户端连接 | Backend 后端进程 |
从命令行识别进程类型
ps 输出中的 postgres: 前缀后的内容就是进程的「命令行标签」。客户端后端会显示 <用户名> <数据库名> <来源地址>,而辅助进程只显示名称。这是排查时最快的分类依据。
PostgreSQL 10 之前,辅助进程的名字与现在不同(如 wal writer process),升级后 ps 输出格式变化属于正常现象。
内存两级结构总览
内存分为共享内存(Shared Memory)与本地内存(Local Memory)两级:
| 层级 | 分配时机 | 内容 | 是否所有进程可见 |
|---|---|---|---|
| 共享内存 | 实例启动时一次性分配 | shared_buffers、WAL buffers、CLOG buffer、锁表、进程数组 | 是 |
| 本地内存 | 会话/操作开始时 | work_mem、maintenance_work_mem、temp_buffers、catalog cache | 否,进程私有 |
-- 关键内存参数速查(setting 单位见 unit 列)
SELECT name, setting, unit, context
FROM pg_settings
WHERE name IN ('shared_buffers', 'wal_buffers', 'work_mem',
'maintenance_work_mem', 'temp_buffers', 'effective_cache_size')
ORDER BY name; name | setting | unit | context
------------------------+---------+------+-----------
effective_cache_size | 524288 | 8kB | user
maintenance_work_mem | 65536 | kB | user
shared_buffers | 16384 | 8kB | postmaster
temp_buffers | 1024 | 8kB | user
wal_buffers | -1 | 8kB | postmaster
work_mem | 4096 | kB | user
(6 rows)
shared_buffers = 16384的单位是 8KB 块,即 128MB(16384 × 8KB)。wal_buffers = -1表示「自动」,由服务端按shared_buffers推算(约 1/32,上限 16MB)。
存储目录总览
PGDATA 目录下既有数据文件,也有配置文件与状态文件:
| 目录 / 文件 | 内容 | 是否可跨库共享 |
|---|---|---|
base/ | 每个数据库一个以 OID 命名的子目录,存放表与索引文件 | 否,每库独立 |
global/ | 全局共享的系统目录(pg_database、pg_authid 等)与 pg_control | 是 |
pg_wal/ | WAL 预写日志段文件 | 是 |
pg_xact/ | 事务提交状态(CLOG)文件 | 是 |
pg_tblspc/ | 用户自定义表空间的符号链接 | 是 |
pg_multixact/ | 多事务(MultiXact)状态文件 | 是 |
postgresql.conf | 主配置文件(Debian 系除外) | — |
pg_hba.conf | 认证规则 | — |
PG_VERSION | 数据目录格式版本号 | — |
# 观察典型 PGDATA 目录
ls -1 /var/lib/postgresql/18/main/PG_VERSION
base
global
pg_commit_ts
pg_dynshmem
pg_hba.conf
pg_ident.conf
pg_logical
pg_multixact
pg_notify
pg_replslot
pg_serial
pg_snapshots
pg_stat
pg_stat_tmp
pg_subtrans
pg_tblspc
pg_twophase
pg_wal
pg_xact
postgresql.auto.conf
postgresql.confPGDATA 权限
PGDATA 目录权限必须为 0700 且属主为运行 PostgreSQL 的系统用户。PostgreSQL 启动时会检查,权限过宽会拒绝启动(报 data directory has invalid permissions)。这是为了保护数据文件不被其他系统用户直接读取——数据库内的权限控制无法防御绕过服务端的文件级访问。
架构演进时间线(版本相关)
PostgreSQL 的核心架构多年来保持稳定,但有几个关键变化需要知道:
| 版本 | 架构变化 |
|---|---|
| 9.6 | 引入并行查询,后端进程可派生并行工作进程 |
| 10 | 逻辑复制落地;pg_wal 从 pg_xlog 改名 |
| 12 | 引入 pg_stat_progress_* 进度视图;分区表性能大幅改进 |
| 13 | 移除 pg_stat_tmp 的统计临时目录依赖 |
| 14 | 引入 pg_stat_wal;B-tree 索引去重 |
| 15 | 移除 stats collector 进程,改为共享内存统计;引入 pg_stat_io(16) |
| 16 | 引入 pg_stat_io 视图,I/O 统计按 backend type 细分 |
| 17 | pg_stat_bgwriter 拆分出 pg_stat_checkpointer、pg_stat_io 增强 |
-- 确认当前实例版本与关键编译选项(架构相关能力由编译期决定)
SELECT version();
SELECT name, setting
FROM pg_settings
WHERE name IN ('block_size', 'wal_block_size', 'segment_size',
'max_connections', 'max_worker_processes', 'max_parallel_workers')
ORDER BY name;PostgreSQL 18.0 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.2.0, 64-bit
name | setting
------------------------+---------
block_size | 8192
max_connections | 100
max_parallel_workers | 8
max_worker_processes | 8
segment_size | 1GB
wal_block_size | 8192
(6 rows)block_size 与 segment_size 是 internal 上下文参数——只能在编译时指定,运行期不可更改(./configure --with-blocksize=8)。默认 8KB 页与 1GB 段文件是几乎所有生产环境的实际选择。
实战示例
示例一:绘制你手上实例的架构全貌
-- 一次性输出:版本、数据目录、关键资源参数
SELECT version() AS version,
current_setting('data_directory') AS data_dir,
current_setting('config_file') AS config_file,
pg_size_pretty(pg_size_bytes(current_setting('shared_buffers'))) AS shared_buffers,
current_setting('max_connections') AS max_conn,
current_database() AS current_db,
pg_backend_pid() AS my_backend_pid;-[ RECORD 1 ]--+-------------------------------------------------------------
version | PostgreSQL 18.0 on x86_64-pc-linux-gnu, compiled by gcc 13.2.0, 64-bit
data_dir | /var/lib/postgresql/18/main
config_file | /etc/postgresql/18/main/postgresql.conf
shared_buffers | 128 MB
max_conn | 100
current_db | demo
my_backend_pid | 1201-- 统计当前实例的进程类型分布
SELECT backend_type,
count(*) AS cnt,
string_agg(DISTINCT coalesce(usename, '-'), ', ') AS users
FROM pg_stat_activity
GROUP BY backend_type
ORDER BY cnt DESC; backend_type | cnt | users
----------------------+-----+-----------------------
client backend | 6 | app_user, postgres
checkpointer | 1 | -
background writer | 1 | -
walwriter | 1 | -
autovacuum launcher | 1 | -
logical replication launcher | 1 | -
(6 rows)示例二:验证「不能跨库查询」
# 在 demo 库中尝试访问 postgres 库的表 → 报错
psql -d demo -c "SELECT count(*) FROM postgres.pg_catalog.pg_class;"ERROR: cross-database references are not implemented: "postgres.pg_catalog.pg_class"
LINE 1: SELECT count(*) FROM postgres.pg_catalog.pg_class;-- 正确做法一:切换到目标库
-- \c 是 psql 元命令,不是 SQL
-- \c postgres
-- SELECT count(*) FROM pg_catalog.pg_class;
-- 正确做法二:用 postgres_fdw 做跨库联邦查询
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
CREATE SERVER pg_self FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'reporting');
CREATE USER MAPPING FOR CURRENT_USER SERVER pg_self
OPTIONS (user 'postgres');
CREATE FOREIGN TABLE ft_report (
id bigint,
name text
) SERVER pg_self OPTIONS (schema_name 'public', table_name 'report');
SELECT * FROM ft_report LIMIT 5;同一实例内的跨库查询也可以用 postgres_fdw
即使两个库在同一个实例、同一个数据目录,也不能用 db.schema.table 直接引用。必须通过 postgres_fdw、dblink,或者把对象放到同一个库的不同 schema 里。
示例三:从进程角度确认「每连接一进程」
# 开 3 个后台 psql 会话(保持连接)
for i in 1 2 3; do
(psql -d demo -c "SELECT pg_sleep(60);" >/dev/null 2>&1 &)
done
sleep 1
# 统计 client backend 进程数
ps -ef | grep 'postgres: ' | grep -v grep | wc -l-- 从数据库内部对照:每条连接一个 pid
SELECT pid,
backend_start,
state,
coalesce(client_addr::text, 'local') AS client,
application_name
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY backend_start; pid | backend_start | state | client | application_name
------+-------------------------------+--------+---------+------------------
1178 | 2026-09-22 09:20:11.482931+08 | idle | local | psql
1201 | 2026-09-22 09:22:03.117204+08 | active | 10.0.1.5| psql
1215 | 2026-09-22 09:31:44.902117+08 | active | local | psql
1216 | 2026-09-22 09:31:44.903551+08 | active | local | psql
1217 | 2026-09-22 09:31:44.904882+08 | active | local | psql
(5 rows)pg_stat_activity.pid 与操作系统 ps 中的进程号一一对应,这就是「每连接一进程」最直接的证据。
注意事项
- 「集群」不是「分布式集群」:PostgreSQL 文档里的 Cluster 指一个数据目录,不代表多机。多机要看复制与高可用章节。
- 同实例跨库不能直接查询:
db.schema.table语法不存在,必须切换连接或使用postgres_fdw/dblink。 template1会被复制:在template1里创建对象或安装扩展,会影响此后所有CREATE DATABASE的结果。保持template1干净是最佳实践。- 连接数不是越高越好:每条连接一个进程,且各占约 5~10MB 本地内存。
max_connections = 1000的实例在连接风暴下可能因内存与上下文切换而雪崩。生产应配合连接池。 - PGDATA 权限必须为
0700:权限过宽会导致启动失败,且数据文件可被同机其他用户直接读取,绕过所有数据库权限控制。 block_size与segment_size不可运行期修改:需要重新编译,不要轻信「改参数就能调页大小」的说法。- 不要用
kill -9杀掉后端进程:这会导致共享内存状态不一致,Postmaster 会触发全体重启。要终止查询请用pg_cancel_backend()/pg_terminate_backend()。 - 超级用户保留连接:
superuser_reserved_connections一定要留有余量,否则连接打满后无法登录排查。
-- 不推荐:把 max_connections 调到极大值来解决「连接不够」
-- ALTER SYSTEM SET max_connections = 2000; -- 不推荐
-- 正确做法:引入连接池(PgBouncer),让应用侧连接数远小于数据库侧的并发需求最佳实践
- 架构认知先行:排查性能问题前,先明确「这是连接数问题、内存问题还是 I/O 问题」,分别对应进程/内存/存储三个维度。
- 为不同用途划分数据库:生产库与报表库分实例或分库,避免分析型查询抢占事务型资源。
- 用 schema 做逻辑隔离:多租户场景优先用 schema(
tenant_a.orders),比「每租户一个数据库」更省连接与维护成本。 - monitoring 与业务分离:监控账号用独立的数据库连接,并保留
superuser_reserved_connections应急通道。 - 把关键参数纳入配置管理:
shared_buffers、max_connections、wal_level等属于架构级决策,应写入配置文件并纳入版本控制,而不是用ALTER SYSTEM临时设置。 - 定期巡检进程与连接:把
pg_stat_activity的连接数、空闲事务数纳入监控告警。 - 理解内存两级结构再调优:
shared_buffers是全局的(重启生效),work_mem是每操作的(可动态改),二者误用会导致 OOM。 - 升级前阅读版本说明:架构相关的视图与进程名在版本间会变化(如 PG15 移除 stats collector、PG17 拆分
pg_stat_bgwriter),升级脚本与监控看板需要同步更新。
下一节