AI时代首先数据库-PostgreSQL实战笔记 | 21 PostgreSQL进程与内存架构介绍

上一篇我们聊了物理备份与 PITR,理解了 WAL 如何让数据库回到过去。这一篇,我们退后一步,看看 PostgreSQL 的"运行态"到底是怎么组织的。当你执行一条 SQL,背后发生了什么?谁在接收连接、谁在解析查询、谁在把脏页刷盘、谁在写 WAL?理解进程与内存架构,是理解后续 MVCC、WAL、checkpoint、缓存调优的底层基础。

一、Postmaster:一切进程的"起点"

PostgreSQL 启动时,第一个被创建的是 postmaster 进程。它是整个实例的父进程和守护进程,负责初始化共享内存、加载配置、监听网络端口,并在 ServerLoop 中等待连接和信号。

Postmaster 的核心职责有三项:

第一,初始化共享内存。 启动时,它读取 shared_buffers、wal_buffers 等参数,计算共享内存段大小,然后分配这块内存。这些参数只能在启动时设置,修改后必须重启,原因就在于此。

第二,fork 后端进程。 每当一个客户端连接进来,postmaster 接受连接后,会 fork 一个独立的 backend 进程来处理这个连接。客户端断开时,backend 进程随之退出,postmaster 负责回收。

第三,监控与拉起辅助进程。 Postmaster 在 ServerLoop 中持续检查 checkpointer、background writer、walwriter 等辅助进程是否在运行。如果某个关键进程异常退出,postmaster 会启动恢复流程,确保实例最终处于一致状态。

一句话:postmaster 不直接执行 SQL,它负责"生"和"管"其他进程。

二、Backend:每条连接一个独立进程

PostgreSQL 采用 process-per-connection 模型。每个客户端连接对应一个独立的 backend 进程,这个进程负责该连接上的全部工作:读取 TCP 流、解析 SQL、生成执行计划、执行、返回结果。

Backend 进程拥有自己的私有内存 ,主要用于排序(sort)和哈希(hash)操作,受 work_mem 参数控制。这意味着,如果一个查询包含多个排序或哈希操作,每个操作都可以使用完整的 work_mem 额度,实际内存消耗可能是 work_mem × 操作数。在并发连接数很高时,这是内存风险的主要来源之一。

Backend 进程之间默认不共享数据,它们通过共享内存(shared_buffers、WAL buffers 等)交换信息。这种"进程隔离 + 共享内存协作"的设计,是 PostgreSQL 稳定性的重要基础。

三、辅助进程:后台的"分工协作"

除了 postmaster 和 backend,PostgreSQL 还有一组常驻或按需启动的辅助进程,各自承担明确的后台任务。

进程 核心职责
Checkpointer 定期执行检查点,将所有脏页刷盘,并回收/循环旧 WAL,创建一个"已知良好"的恢复点
Background Writer 渐进式地把脏页从 shared_buffers 刷到磁盘,维持足够数量的空闲 buffer,减少 backend 进程在需要 buffer 时被迫同步刷盘的压力
WAL Writer 将 WAL buffers 中的数据刷写到 WAL 文件,减少 commit 时的同步 I/O 开销
Startup Process 崩溃恢复时重放 WAL;在备库上持续重放从主库接收到的 WAL
Autovacuum Launcher 启动 autovacuum worker,回收死元组、更新统计信息
WAL Archiver 执行 archive_command,将已完成的 WAL 段归档到指定位置
WAL Sender / Receiver 主库发送 WAL、备库接收 WAL,支撑流复制

Checkpointer 和 Background Writer 的分工值得特别说明。两者都涉及"把脏页刷盘",但目标不同:Checkpointer 的目标是"创建一个一致性恢复点" ,它会在检查点时刷完所有脏页,并记录 checkpoint 位置,崩溃恢复从这里开始;Background Writer 的目标是"维持空闲 buffer 供给",它持续、渐进地刷脏页,避免 backend 在需要 buffer 时被迫自己刷盘。两者的协作,决定了写入性能的平滑程度。

四、内存架构:共享内存与私有内存

PostgreSQL 的内存分为两大块:共享内存 和进程私有内存。

共享内存:所有进程共同访问

共享内存中最核心的是 shared_buffers,它是 PostgreSQL 内部的 buffer cache,默认 128MB。数据页从磁盘读入后,首先进入 shared_buffers,后续查询可以直接在内存中访问,避免重复磁盘 I/O。

另一个重要区域是 wal_buffers,用于临时存放 WAL 数据,由 WAL Writer 定期刷写到 WAL 文件。

私有内存:每个 backend 独享

每个 backend 进程有自己的私有内存,主要包括:

  • work_mem:排序和哈希操作的工作内存,默认 4MB;
  • maintenance_work_mem:VACUUM、CREATE INDEX 等维护操作的内存;
  • temp_buffers:临时表的缓冲区,默认 8MB。

私有内存的"按进程分配"特性,决定了并发场景下的总内存消耗可能远超单个参数值。这就是为什么 work_mem 的调整必须结合 max_connections 和实际查询复杂度来考虑。

五、为什么 PostgreSQL 用多进程,而不是多线程?

这是 PostgreSQL 设计中最常被讨论的问题之一。官方开发者社区的共识是:多进程在稳定性和健壮性上的收益,大于连接启动成本带来的代价。

具体原因可以归纳为三点:

第一,故障隔离。 在多线程模型中,一个线程的严重错误(如内存越界、空指针)可能破坏整个进程的地址空间,导致所有连接一起崩溃,甚至数据损坏。在多进程模型中,每个 backend 是独立的操作系统进程,一个 backend 崩溃,通常只影响它自己处理的那一个连接,postmaster 可以回收它,其他连接和共享内存不受影响。

第二,共享状态的可控性。 多进程模型天然要求通过显式共享内存来共享数据,而不是隐式共享整个地址空间。这降低了意外修改共享状态的风险,也让内存管理更可预测。PostgreSQL 使用 "shared-nothing by default + explicit shared memory" 的模型,每个进程的私有内存与共享内存边界清晰。

第三,历史与工程权衡。 PostgreSQL 诞生于 1980 年代末,当时多线程编程模型和操作系统线程支持尚不成熟。选择多进程是当时更稳健的工程决策。虽然今天线程的成本已经很低,但 PostgreSQL 的整个代码库、扩展生态和运维工具都是围绕进程模型构建的,迁移到线程模型是一项巨大的工程,收益并不足以抵消风险。

代价是什么? 每个连接一个进程,意味着连接建立的成本高于线程模型,内存占用也更高。这就是为什么生产环境几乎总是推荐在应用和数据库之间加一层连接池(如 PgBouncer) ,用少量后端进程服务大量应用请求,而不是让 max_connections 直接随应用并发数线性增长。

六、内存架构如何影响性能:双缓冲的现实

PostgreSQL 的内存架构中,一个最容易被误解的关系是:shared_buffers 和操作系统缓存是两层独立缓存,数据可能同时存在于两边。

当 PostgreSQL 读取一个数据页时,如果 shared_buffers 中没有,它会向操作系统发起文件读取。操作系统可能从自己的 page cache 中直接返回,也可能触发磁盘 I/O。读入的数据页会被放入 shared_buffers。操作系统并不感知 shared_buffers 的存在,它独立地缓存文件页 。这就是所谓的 double buffering:同一个数据块,可能同时被 PostgreSQL 和 OS 各缓存一份。

这直接决定了 shared_buffers 的调优逻辑:它不是"越大越好" 。官方文档的指导是,专用数据库服务器上 shared_buffers 的合理起始值是系统内存的 25% ,而超过 40% 的分配不太可能比更小的值表现更好,因为 PostgreSQL 同时依赖 OS 缓存,过大的 shared_buffers 会挤压 OS 缓存,而 OS 缓存在 checkpointer 刷盘、WAL 写入、文件系统元数据等方面仍然需要充足的内存。

一句话:shared_buffers 是 PostgreSQL 的"第一层缓存",OS page cache 是"第二层缓存"。两者协作,而不是替代。理解双层缓存,才能理解为什么"把内存全给数据库"往往不是最优解。

七、总结

主题 核心结论
Postmaster 父进程,负责初始化共享内存、fork backend、监控辅助进程
Backend 每连接一个进程,负责解析、计划、执行;私有内存受 work_mem 控制
辅助进程 Checkpointer 建恢复点;Background Writer 维持空闲 buffer;WAL Writer 刷 WAL
多进程 vs 多线程 选择多进程是为了故障隔离和稳定性,代价是连接成本高,需连接池
共享内存 shared_buffers + wal_buffers,所有进程共同访问
私有内存 work_mem、maintenance_work_mem,按进程分配,并发时总量可能放大
双缓冲 shared_buffers 与 OS page cache 独立缓存同一数据;shared_buffers 建议 25%,超过 40% 收益递减

一句话:PostgreSQL 的进程模型用"隔离"换稳定性,用"连接池"补成本;内存模型用"双层缓存"换灵活性,但调优的核心是让两层各司其职,而不是让一层吃掉另一层。

下一篇,我们进入实例与集群:一个 PostgreSQL 实例可以承载多个数据库,集群、实例、数据库、模式、表空间之间到底是什么关系?表空间如何用于性能隔离?


思考题:

你知道自己生产环境的 shared_buffers 设成了多少吗?有没有因为把 shared_buffers 调得过大,导致 OS 缓存被挤压、写入性能反而下降的经历?另外,你的应用是直连数据库,还是通过 PgBouncer 之类的连接池?欢迎在评论区聊聊。

相关推荐
SelectDB7 小时前
让变更流出 Doris:Apache Doris 5.0 基于增量计算的 CDC 技术解析
数据库
这个DBA有点耶7 小时前
MCP隔离层+行级语义审计+输出校验:AI访问数据库的安全边界拆解
数据库·人工智能·ai编程
这个DBA有点耶8 小时前
从反向扫描O(n)到正向扫描O(1):降序索引的物理机制与混合排序匹配规则
数据库
2501_933923259 小时前
解读Redis:单线程模型
数据库·redis·缓存
柳鲲鹏9 小时前
内斗学概论:43、平级内斗
笔记
Tapdata10 小时前
被忽略的第三类上下文:Agent 进入真实业务后,还缺什么?
数据库·人工智能·架构
小科先生11 小时前
Java springboot调用数据库的基本流程
java·数据库·spring boot
星恒随风11 小时前
MySQL 从零入门(四):表的增删改查与基础查询详解
linux·服务器·数据库·笔记·mysql
Z21-X13 小时前
云原生运维学习笔记:ECShop全链路容器化部署与Docker底层资源隔离深度剖析
运维·笔记·学习·云原生
Pniubi13 小时前
MySQL靠什么实现事务的?事务隔离级别有哪些?
java·数据库·mysql