前置知识: PostgreSQL

体系架构

6 min高级

PostgreSQL体系架构:进程模型、后台进程、共享内存、本地内存、查询处理流程与数据目录结构

1. 为什么先学架构

理解 PostgreSQL 的整体结构,是后续学习 VACUUM、锁、复制、调优的地基。 举例来说:为什么一个慢查询能拖垮整个数据库?为什么连接数不能开很大? 为什么统计信息在 PostgreSQL 15 前后行为不同?答案都在架构里。

一个可以先行建立的类比:把 PostgreSQL 想象成一家餐厅——

  • postmaster 是前台经理:迎客、为每桌客人安排服务员;
  • 每个后端进程(backend)是一名服务员:一对一服务一位客人(连接);
  • 共享内存是后厨公共操作台:所有服务员共享的备菜区;
  • 后台工作进程(checkpointer、walwriter 等)是保洁与仓储团队。

2. 进程模型:一连接一进程

2.1 与线程模型的对比

PostgreSQL 采用多进程架构(而非 MySQL 的多线程):每个客户端连接 由一个独立的操作系统进程服务。进程间内存天然隔离,一个后端崩溃不会 直接污染其他进程的内存,稳定性好;代价是进程创建开销大、每个连接的 基础内存占用高(数 MB 起步),因此生产环境通常配合 PgBouncer 等连接池。

flowchart TD
    C1[客户端 1] --> PM[postmaster<br/>主进程]
    C2[客户端 2] --> PM
    C3[客户端 N] --> PM
    PM --> B1[backend 1]
    PM --> B2[backend 2]
    PM --> B3[backend N]
    PM --> AV[autovacuum launcher/worker]
    PM --> CP[checkpointer]
    PM --> WW[walwriter]
    PM --> BW[background writer]
    PM --> WS[WAL summarizer 17+]
    PM --> LRL[logical replication launcher]
    subgraph 共享内存
        SM[Shared Buffers / WAL Buffer / 锁表 / ...]
    end
    B1 & B2 & B3 & AV & CP & WW & BW -.-> SM

2.2 连接流程

客户端发起连接 → postmaster 完成认证(pg_hba.conf)
→ fork 出一个 backend 进程 → 此后该连接的所有查询都由这个进程处理
→ 连接断开时, 进程退出
-- 验证"一连接一进程": 每个客户端连接在 pg_stat_activity 中对应一个 backend 进程
SELECT pid, usename, application_name, backend_type
FROM pg_stat_activity;
--     pid   | usename  | application_name | backend_type
--   --------+----------+------------------+------------------
--    16224  | postgres | psql             | client backend
--    16088  |          |                  | autovacuum launcher
--    16230  |          |                  | logical replication launcher
--    16085  |          |                  | background writer
--    ...(backend_type 列可区分客户端连接与后台进程)

-- 查看当前连接数(对应 backend 进程数)
SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';
--  count
-- ------
--      3

2.3 核心后台进程

进程作用相关参数
postmaster主进程,监听端口、认证、fork 后端port、max_connections
backend(client backend)处理单个客户端连接的全部查询—
checkpointer执行检查点,把脏页刷盘并写检查点记录checkpoint_timeout、checkpoint_completion_target
background writer(bgwriter)平滑刷写共享缓冲区中的脏页,减轻检查点压力bgwriter_delay
walwriter把 WAL 缓冲区刷写到磁盘wal_writer_delay
autovacuum launcher调度 autovacuum workerautovacuum、autovacuum_naptime
autovacuum worker实际执行自动清理/分析autovacuum_max_workers
WAL summarizer(PostgreSQL 17+)持续生成 WAL 块引用摘要,支撑增量备份summarize_wal
logical replication launcher按需拉起逻辑复制的 apply workermax_logical_replication_workers
WAL sender向备库/订阅端发送 WAL(按连接产生)max_wal_senders
WAL receiver / startup(备库)接收并回放 WALprimary_conninfo
stats collectorPostgreSQL 14 及更早:收集统计信息—
io worker(PostgreSQL 18+)异步 I/O 的工作进程(io_method = worker)io_method、io_workers

两个重要的版本演进:

  • PostgreSQL 15:统计信息从 stats collector 进程的周期性文件写入, 改为保存在共享内存中,查询 pg_stat_* 视图实时且更轻量; 独立的 stats collector 进程从此消失。
  • PostgreSQL 17:为支撑增量备份,新增常驻的 WAL summarizer 后台进程。
# 在操作系统层面直接观察 PostgreSQL 进程树(Linux)
ps -ef | grep postgres
# postgres  16085     1  0 ... postgres: checkpointer
# postgres  16087     1  0 ... postgres: walwriter
# postgres  16088     1  0 ... postgres: autovacuum launcher
# postgres  16224  16085  0 ... postgres: postgres postgres [local] idle

3. 共享内存:所有进程的公共操作台

3.1 主要共享内存区域

区域参数默认值说明
Shared Buffersshared_buffers128MB数据页缓存(缓冲池)
WAL Bufferwal_buffers-1(自动)WAL 日志缓冲区
Clog(pg_xact)——事务提交状态(提交/中止/进行中)
Lock Table——重型锁(表锁等)的锁表
ProcArray——所有活动进程与事务的信息,快照的依据
复制/逻辑槽——复制槽状态
统计信息(15+)——pg_stat_* 视图的数据源

3.2 缓冲池工作方式

backend 读取数据时:先在 Shared Buffers 找目标页(命中则直接用), 未命中则从磁盘读入缓冲池(必要时按时钟扫描淘汰旧页)。 修改数据时只改缓冲池中的页(成为”脏页”),由 bgwriter/checkpointer 异步刷盘——这就是”写数据不等于立刻落盘”,崩溃恢复依赖 WAL。

-- 缓冲池常用诊断
SELECT count(*) FROM pg_buffercache;  -- 需 CREATE EXTENSION pg_buffercache;

-- 某表占用了多少缓冲页
SELECT c.relname, count(*) AS buffers
FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid)
WHERE c.relname = 'orders'
GROUP BY c.relname;
# postgresql.conf:生产环境一般设为物理内存的 25% 左右
shared_buffers = '4GB'      # 以 16GB 内存服务器为例
# 修改后需要重启

4. 本地内存:每个进程私有

区域参数默认值说明
Work Memwork_mem4MB排序、哈希连接等操作的内存
Maintenance Work Memmaintenance_work_mem64MBVACUUM、CREATE INDEX 等
Temp Bufferstemp_buffers8MB临时表缓冲区

关键认知:work_mem 是”每个排序/哈希节点”的配额,不是连接总配额。 一条复杂查询可能同时使用多个 work_mem,一个高并发系统里实际内存 = 连接数 x 每连接节点数 x work_mem,这是内存溢出(OOM)的常见根源。

-- 会话级临时调大(只影响当前连接, 用于个别大报表查询)
SET work_mem = '256MB';
SELECT ... ORDER BY ... LIMIT ...;
RESET work_mem;

-- 验证排序是否溢出到磁盘(external merge Sort 说明 work_mem 不够)
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM big_table ORDER BY payload;
-- Sort Method: quicksort   Memory: 25MB       -- 内存内完成
-- Sort Method: external merge  Disk: 180MB     -- 溢出到磁盘, 考虑调大 work_mem

5. 一条查询的旅程

把前面的组件串起来,一条 SELECT 从进入数据库到返回要经过:

客户端 → 认证(pg_hba.conf)→ backend 进程
  → 解析器(Parser)    : 生成语法树
  → 分析器(Analyzer)  : 解析对象名、检查权限
  → 重写器(Rewriter)  : 展开视图、应用规则
  → 规划器(Planner)   : 依据统计信息生成最优执行计划
  → 执行器(Executor)  : 按计划树逐节点取元组
      → 缓冲池读取数据页(未命中则读磁盘)
      → 依据 MVCC 可见性规则过滤元组版本
→ 结果返回客户端

写操作(INSERT/UPDATE/DELETE)在此基础上多两步:先把变更记录写入 WAL 缓冲区(并按 synchronous_commit 约定刷盘),再修改缓冲池中的数据页。 这条”WAL 先行”的原则是崩溃恢复与复制的基础。

6. 数据目录结构

flowchart TD
    T0["$PGDATA/"]
    T1["base/           — 每个数据库一个子目录(表与索引文件)"]
    T2["global/         — 集群级共享系统表"]
    T3["pg_wal/         — WAL 预写日志"]
    T4["pg_xact/        — 事务提交状态(PG 10 前叫 pg_clog)"]
    T5["pg_multixact/  — 多事务 ID 状态"]
    T6["pg_stat/        — 统计信息持久化(14 及更早)"]
    T7["pg_wal/summaries — WAL 摘要(PG 17+, 支撑增量备份)"]
    T8["postgresql.conf — 配置文件"]
    T9["pg_hba.conf     — 客户端认证配置"]
    T10["postmaster.pid — 主进程 PID 与状态"]
    T11["standby.signal  — 存在即以备库模式启动"]
    T0 --> T1
    T0 --> T2
    T0 --> T3
    T0 --> T4
    T0 --> T5
    T0 --> T6
    T0 --> T7
    T0 --> T8
    T0 --> T9
    T0 --> T10
    T0 --> T11
-- 定位某张表在磁盘上的物理文件(relfilenode)
SELECT pg_relation_filenode('orders') AS filenode,
       pg_relation_filepath('orders') AS filepath;
--   filepath
--  -------------------------------------------
--  base/16384/24596
--(base/数据库OID/表文件节点号, 表超过 1GB 会拆成 24596.1、24596.2 ...)

7. 实战场景

7.1 场景一:连接数打满

现象: 应用报 FATAL: sorry, too many clients already
原因: 每连接一进程, max_connections 有上限, 且每个进程消耗数 MB 内存
处置: 短查询密集场景引入 PgBouncer(transaction 池化模式),
      长事务/慢查询则优先优化查询而不是扩连接数

7.2 场景二:统计信息查询突然变慢/变快

现象: 升级到 PostgreSQL 15 后, pg_stat_user_tables 的读数"实时"了
原因: 15 前统计由 collector 进程定期写文件(有延迟与丢失),
      15 后存于共享内存, 读到的就是当前值

8. 常见陷阱与调试

  • 以为 shared_buffers 越大越好:超过内存 25-40% 通常收益递减, 因为操作系统页缓存(double buffering)也在缓存数据页。
  • 在高并发下把 work_mem 设成 256MB:内存需求是乘法关系, 先观察 Sort Method: external merge 再针对性调整。
  • 直接 kill 后台进程:postmaster 检测到子进程异常退出会重启整个 实例(所有连接断开);运维操作应使用 pg_terminate_backend(pid)。
  • 以为删了表磁盘立刻释放:被删除的文件句柄可能仍被进程持有, 长事务、复制槽、prepared 事务都会拖住空间回收(详见 VACUUM 一文)。

小结

  • 初学者要点:PostgreSQL 是”一连接一进程”的 C/S 架构;backend 进程负责 你的所有查询;数据先写 WAL、再改内存页、最后异步落盘;shared_buffers 是公共缓存,work_mem 是私有工作区。
  • 进阶注意:统计信息在 15 后存于共享内存;17 新增 WAL summarizer 支撑 增量备份;18 新增异步 I/O(io_method = worker/io_uring)。理解 “进程模型 x 内存配额 x 后台进程分工”这三件事,是读懂 VACUUM、锁、 并行查询与复制文档的前提。