进程模型(Postmaster / Backend)
PostgreSQL 采用「多进程(Multi-Process)」模型:一个主进程负责管理,每条客户端连接对应一个独立的后端进程。本节讲清它为什么不用多线程、主进程与各类辅助进程的分工、进程间通信的三种手段,以及如何用 pg_stat_activity 观察当前实例的进程全景。
前置知识
阅读本节前,建议先了解:PostgreSQL 架构概览
基础概念
PostgreSQL 的进程分为三类:
| 类别 | 英文名 | 诞生方式 | 数量 | 生命周期 |
|---|---|---|---|---|
| 主进程 | Postmaster | 服务启动 | 1 | 随实例 |
| 辅助进程 | Auxiliary / Background Process | 主进程 fork | 固定若干 | 随实例 |
| 后端进程 | Backend Process | 主进程 fork(每个连接一个) | 动态 | 随连接 |
| 工作进程 | Worker Process | 主进程 fork 或按需派生 | 动态 | 随任务 |
| 术语 | 英文名 | 说明 |
|---|---|---|
| 多进程模型 | Multi-Process Model | 每个连接一个操作系统进程 |
| 多线程模型 | Multi-Threaded Model | 每连接一个线程,共享同一地址空间 |
| 派生(分叉) | fork | 父进程复制自身创建子进程的系统调用 |
| 信号 | Signal | 进程间异步通知机制(SIGHUP、SIGTERM 等) |
| 闩锁 | Latch | PostgreSQL 内部的轻量同步原语,类似自旋锁 |
| 轻量锁 | LWLock, Lightweight Lock | 保护共享内存数据结构的锁 |
| 进程数组 | ProcArray | 共享内存中记录所有活跃进程的快照信息 |
| 后端类型 | backend_type | pg_stat_activity 中标识进程角色的列 |
多进程 vs 多线程
这是理解 PostgreSQL 架构的第一个关键决策点:
| 维度 | 多进程(PostgreSQL) | 多线程(MySQL / Oracle) |
|---|---|---|
| 地址空间 | 各进程独立,隔离性好 | 共享,一处越界影响全局 |
| 崩溃影响 | 单个后端崩溃通常不影响其他连接 | 单线程崩溃可能拖垮整个实例 |
| 创建开销 | 较高(fork + COW 页表复制) | 较低 |
| 数据共享 | 需借助共享内存,数据须可序列化 | 直接读写同一变量 |
| 调试难度 | 便于 gdb attach 单个进程 | 线程级调试更复杂 |
| 并发上限 | 进程数受系统限制(数千) | 线程更易上到数万 |
| 保护机制 | 由 OS 内存保护强制隔离 | 依赖程序员自觉 |
PostgreSQL 诞生于 1986 年,当时 Unix 线程实现尚不成熟且不可移植,因此选择多进程。这个决策带来两个直接后果:
- 连接池是刚需:
fork一个进程的成本远高于创建一个线程,高并发短连接场景必须用 PgBouncer 之类的连接池。 - 共享内存是核心:所有跨进程的数据(buffer、锁、事务状态)都必须放在共享内存里,且结构必须固定布局——这也是 PostgreSQL 大量使用固定大小数组和 OID 的原因。
为什么单个后端崩溃不会拖垮实例
fork 出的每个进程拥有独立地址空间,某个后端因 bug 段错误(Segmentation Fault)时,操作系统只会终止该进程。Postmaster 通过 waitpid() 感知到子进程非正常退出,会先终止所有其他后端(因为共享内存状态可能已被写坏),然后重建共享内存并进入恢复流程。所以「后端崩溃」的后果是全体重启 + 崩溃恢复,而不是数据静默损坏。
核心机制
主进程的职责
Postmaster 是唯一以 postgres 可执行文件形式常驻的进程(其他进程都是它的 fork 结果)。它的职责有四项:
| 职责 | 实现方式 |
|---|---|
| 监听连接 | select() / poll() 监听 TCP 端口与 Unix 套接字,同时监听自管道(self-pipe) |
| 派生后端 | 接受连接后 fork() 一个后端进程,把套接字文件描述符传给它 |
| 监控子进程 | 通过 waitpid() 收集子进程退出状态,异常退出则触发重启 |
| 崩溃恢复 | 重启时重放 WAL 到最近的检查点之后,恢复一致性 |
Postmaster 本身不处理 SQL。它只在启动阶段读取配置、初始化共享内存,之后进入「监听循环」——这是它保持极简与稳定的关键。
# 查看 Postmaster 的启动命令(关键是 -D 指定的数据目录)
ps -p $(head -1 /var/lib/postgresql/18/main/postmaster.pid) -o pid,ppid,user,args PID PPID USER COMMAND
1147 1 postgres /usr/lib/postgresql/18/bin/postgres -D /var/lib/postgresql/18/main -c config_file=/etc/postgresql/18/main/postgresql.conf# postmaster.pid 记录了实例身份信息(用于防止双启动)
head -5 /var/lib/postgresql/18/main/postmaster.pid1147 # 主进程 PID
/var/lib/postgresql/18/main # 数据目录
1774570332 # 启动时间戳
5432 # 端口
/var/run/postgresql # Unix socket 目录postmaster.pid 的危险操作
绝不要手工删除 postmaster.pid。如果实例实际在运行而 pid 文件被删除,再启动一个新实例会导致两个 Postmaster 同时写同一份数据文件 → 数据损坏。若确实存在残留(如断电后文件系统未同步),先确认没有 postgres 进程在运行,再删除。
辅助进程清单
PostgreSQL 18 的辅助进程(可用 pg_stat_activity.backend_type 观察到):
| backend_type | 是否常驻 | 职责 | 后续章节 |
|---|---|---|---|
checkpointer | 是 | 执行检查点,刷新所有脏页,缩短恢复时间 | Checkpointer |
background writer | 是 | 在后台渐进刷脏页,平滑 I/O | Background Writer |
walwriter | 是 | 把 WAL 缓冲刷入磁盘 | WAL Writer |
autovacuum launcher | 是 | 调度 autovacuum worker | AutoVacuum Worker |
autovacuum worker | 按需 | 对一个表执行 VACUUM / ANALYZE | AutoVacuum Worker |
archiver | 是(启用归档时) | 把完成的 WAL 段拷贝到归档目录 | Archiver / Stats Collector |
walreceiver | 从库上 | 接收主库的 WAL 流 | —(复制章节) |
walsender | 主库上按需 | 向从库发送 WAL 流 | —(复制章节) |
logical replication launcher | 是 | 启动逻辑复制 apply worker | —(复制章节) |
parallel worker | 按需 | 并行查询的工作进程 | —(查询优化章节) |
client backend | 按连接 | 处理客户端 SQL 请求 | Backend 后端进程 |
-- 查看当前实例所有进程角色及其状态
SELECT backend_type,
count(*) AS processes,
string_agg(DISTINCT state, ', ') AS states
FROM pg_stat_activity
GROUP BY backend_type
ORDER BY processes DESC, backend_type; backend_type | processes | states
--------------------------------+-----------+---------------------
client backend | 5 | active, idle
checkpointer | 1 |
walwriter | 1 |
background writer | 1 |
autovacuum launcher | 1 |
logical replication launcher | 1 |
(6 rows)每连接一进程
从客户端发起连接到拥有一个专属进程,完整流程如下:
客户端 Postmaster 新 Backend
│ │ │
│── TCP SYN / connect ─────►│ │
│ │ ① accept() 得到新 socket │
│ │ ② fork() ───────────────────►│
│ │ (继承监听 socket, │
│ │ 但立即关闭它) │
│ │ ③ 父进程 close(new socket) │
│◄─── Authentication ───────┼──────────────────────────────│
│─── StartupMessage ────────┼─────────────────────────────►│
│ │ │ ④ 检查 pg_hba.conf
│ │ │ ⑤ 认证
│◄─── AuthenticationOk ─────┼──────────────────────────────│
│─── Query ─────────────────┼─────────────────────────────►│ ⑥ 解析→计划→执行
│◄─── DataRow / ReadyForQuery┼──────────────────────────────│
│ │ │
│─── Terminate ─────────────┼─────────────────────────────►│ ⑦ 进程退出
│ │◄── SIGCHLD ──────────────────│
│ │ waitpid() 回收 │关键点:监听套接字由所有子进程继承,但子进程必须立即关闭它(否则会在子进程退出时产生竞态)。这解释了为什么 connection refused 与 too many connections 是两种不同的错误——前者是 Postmaster 未监听,后者是它无法再 fork。
-- 从内部确认「每连接一进程」:pg_backend_pid() 就是操作系统 PID
SELECT pg_backend_pid() AS os_pid,
current_user,
current_database(),
inet_server_port() AS port; os_pid | current_user | current_database | port
--------+--------------+------------------+------
1215 | postgres | demo | 5432
(1 row)# 在另一个终端核对:PID 1215 确实是我们的连接
ps -p 1215 -o pid,ppid,args PID PPID COMMAND
1215 1147 postgres: postgres demo [local] idle-- 连接数上限的三个约束:max_connections 与保留连接
SELECT current_setting('max_connections')::int AS max_conn,
current_setting('superuser_reserved_connections')::int AS reserved,
count(*) FILTER (WHERE backend_type = 'client backend') AS current_conn,
current_setting('max_connections')::int
- current_setting('superuser_reserved_connections')::int
- count(*) FILTER (WHERE backend_type = 'client backend') AS remaining; max_conn | reserved | current_conn | remaining
----------+----------+--------------+-----------
100 | 3 | 12 | 85
(1 row)进程间通信(IPC)
各进程拥有独立地址空间,通信必须借助外部机制。PostgreSQL 用到三种:
| 机制 | 用途 | 特点 |
|---|---|---|
| 共享内存(Shared Memory) | 数据交换:buffer、锁、事务状态、统计 | 最快,但结构固定、需加锁保护 |
| 信号(Signal) | 控制通知:reload、shutdown、崩溃 | 只能传一个整数,异步 |
| 闩锁(Latch) | 进程内与跨进程的等待唤醒 | 自旋 + 事件循环,主要用于等待 I/O |
Postmaster 与子进程之间的「控制通道」是一根自管道(self-pipe):信号处理函数只向管道写一个字节(信号在 handler 中只能异步安全操作),主循环再从管道读出并处理。这是避免在信号 handler 中调用非异步安全函数的经典手法。
信号对照表
Postmaster 接收的信号与含义:
| 信号 | 含义 | 是否重启 | 是否影响事务 |
|---|---|---|---|
SIGHUP | 重新加载配置文件 | 否 | 否 |
SIGTERM | 智能关闭(Smart Shutdown):等所有客户端断开后再停 | 否 | 否,等事务自然结束 |
SIGINT | 快速关闭(Fast Shutdown):回滚所有活跃事务并立即退出 | 是(退出) | 是,事务被回滚 |
SIGQUIT | 立即关闭(Immediate Shutdown):不清理,下次启动做崩溃恢复 | 是(异常退出) | 是,可能导致不一致 |
SIGUSR1 | 通知子进程「共享内存状态变化,请重新读取」 | 否 | 否 |
SIGUSR2 | 通知子进程「WAL 状态变化」 | 否 | 否 |
SIGCHLD | 子进程状态变化(崩溃或退出) | 视情况 | 视情况 |
# 用 pg_ctl 触发不同的关闭方式(不要手工 kill -9)
pg_ctl -D /var/lib/postgresql/18/main stop -m smart # 智能:等连接结束
pg_ctl -D /var/lib/postgresql/18/main stop -m fast # 快速:回滚并退出(推荐)
pg_ctl -D /var/lib/postgresql/18/main stop -m immediate # 立即:下次启动需恢复
# reload 配置(等价于 SIGHUP)
pg_ctl -D /var/lib/postgresql/18/main reloadSIGKILL 与 SIGQUIT
kill -9(SIGKILL)无法被进程捕获,Postmaster 会在下次启动时执行崩溃恢复,虽然数据不会丢(WAL 保证),但会带来一次完整的恢复过程。而 SIGQUIT 给后端的语义是「立即自杀」,用于 Postmaster 判定共享内存已被污染时强制全体退出。日常运维只用 pg_ctl stop -m fast 或 systemctl stop。
后端进程数量与内存的关系
每个后端进程(client backend)的基础内存开销包括:
| 内存区域 | 典型大小 | 说明 |
|---|---|---|
| 进程自身 | ~5MB | 可执行代码 + 基础数据结构 |
| catalog cache | 数 MB | 系统目录缓存,随访问到的目录数增长 |
work_mem | 每个排序/哈希节点 | 默认 4MB,可能同时有多个 |
temp_buffers | 默认 8MB | 临时表缓存 |
| 连接相关缓冲 | 数十 KB | 协议缓冲 |
-- 粗算:连接数 × 单连接基础内存,评估实例内存是否够用
SELECT count(*) FILTER (WHERE backend_type = 'client backend') AS conns,
pg_size_pretty(
(count(*) FILTER (WHERE backend_type = 'client backend'))::bigint * 5 * 1024 * 1024
) AS rough_base_memory
FROM pg_stat_activity; conns | rough_base_memory
-------+-------------------
12 | 60 MB
(1 row)这只是粗略下限。真实占用取决于
work_mem、复杂查询的排序哈希节点数量、以及 catalog cache 的命中范围。用pg_backend_memory_contexts可以看单个会话的内存明细。
详细说明
用 pg_stat_activity 观察进程全景
pg_stat_activity 是观察进程状态的最主要视图。18 中的关键列:
| 列名 | 含义 | 排障价值 |
|---|---|---|
pid | 进程 ID | 与操作系统 PID 一致,可直接 kill 定位 |
backend_type | 进程角色 | 区分辅助进程与客户端连接 |
state | 连接状态(见下表) | 判断「空闲占坑」还是「正在干活」 |
wait_event_type / wait_event | 等待类型与等待事件 | 定位瓶颈最关键的列 |
query | 当前或最近一条 SQL | 注意:idle 时显示的是上一条 |
state_change | 状态变更时间 | 算 idle 时长,判断连接是否泄漏 |
backend_start | 进程启动时间 | 判断连接池是否复用连接 |
xact_start | 事务开始时间 | 检测长事务的第一手线索 |
query_start | 当前查询开始时间 | 算查询执行时长 |
client_addr / client_port | 客户端来源 | 定位是哪个应用/机器 |
-- 连接状态 state 的取值与含义
SELECT unnest(ARRAY[
'active 当前正在执行查询',
'idle 空闲,等待客户端下发新命令(正常)',
'idle in transaction 在事务中空闲(危险,持有锁与快照)',
'idle in transaction (aborted) 事务已出错但未回滚(危险)',
'fastpath function call 正在执行 fast-path 函数',
'disabled 跟踪已被关闭'
]) AS state_meanings;| state | 含义 | 风险 |
|---|---|---|
active | 正在执行查询 | — |
idle | 空闲等待新命令 | 连接泄漏时表现为大量长时间 idle |
idle in transaction | 开启了事务但未提交/回滚 | 阻塞 VACUUM、持有锁、快照不释放 |
idle in transaction (aborted) | 事务报错后未回滚 | 同样危险,且应用可能已卡死 |
fastpath function call | 执行 fast-path 函数 | 少见 |
disabled | 该会话的统计跟踪已关闭 | — |
-- 综合巡检:按「是否值得警惕」排序,列出问题最严重的连接
SELECT pid,
usename,
datname,
state,
wait_event_type || '/' || coalesce(wait_event, '-') AS wait,
now() - xact_start AS xact_age,
now() - state_change AS state_age,
left(regexp_replace(coalesce(query, ''), '\s+', ' ', 'g'), 60) AS query_short
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY (state = 'idle in transaction') DESC NULLS LAST,
xact_start ASC NULLS LAST; pid | usename | datname | state | wait | xact_age | state_age | query_short
------+----------+---------+---------------------+---------------+------------+------------+----------------------
1301 | app_user | demo | idle in transaction | ClientRead | 00:12:44 | 00:12:41 | UPDATE orders SET ...
1215 | postgres | demo | active | IO/DataFileRead| 00:00:02 | 00:00:02 | SELECT count(*) FROM ...
1178 | postgres | demo | idle | ClientRead | | 00:31:02 | SELECT pg_sleep(60);
(3 rows)等待事件:定位瓶颈的钥匙
wait_event_type 表示等待的大类,wait_event 表示具体原因。生产排障几乎全靠这两列:
| wait_event_type | 典型 wait_event | 含义 | 常见诱因 |
|---|---|---|---|
LWLock | BufferContent、WALWrite | 等待轻量锁 | buffer 竞争、WAL 刷盘瓶颈 |
Lock | transactionid、tuple、relation | 等待重型锁 | 长事务、外键未建索引、DDL 阻塞 |
IO | DataFileRead、WALWrite | 等待磁盘 I/O | 缓存不足、磁盘慢 |
BufferPin | BufferPin | 等待 buffer 钉住释放 | VACUUM 与查询竞争 |
Client | ClientRead、ClientWrite | 等待客户端收发数据 | 应用处理慢、网络慢 |
IPC | WalSync、BgWorkerShutdown | 进程间等待 | 提交等待 WAL 落盘 |
Timeout | VacuumDelay、PgSleep | 主动等待 | autovacuum 限速、显式 sleep |
| `` (空) | — | 无等待(正常空闲或纯 CPU 执行) | — |
-- 当前等待事件分布:一眼看出瓶颈在锁、I/O 还是客户端
SELECT coalesce(wait_event_type, 'CPU/Running') AS wait_type,
coalesce(wait_event, '-') AS wait_event,
count(*) AS sessions
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY 1, 2
ORDER BY sessions DESC; wait_type | wait_event | sessions
---------------+----------------+----------
Client | ClientRead | 8
Lock | transactionid | 2
IO | DataFileRead | 1
LWLock | WALWrite | 1
(4 rows)锁等待的解读
wait_event_type = 'Lock' 且 wait_event = 'transactionid' 意味着「等待另一个事务结束」。这时用 pg_blocking_pids(pid) 可以直接找出阻塞者:
SELECT pid,
pg_blocking_pids(pid) AS blocked_by,
state,
left(query, 60) AS query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;后端进程的生命周期
一个后端进程从创建到销毁的完整阶段:
| 阶段 | 触发 | 状态(pg_stat_activity) |
|---|---|---|
| 1. 派生 | Postmaster fork() | 尚未进入记录(无 state) |
| 2. 初始化 | 读取共享内存、初始化本地内存 | 无 state |
| 3. 认证 | 读 pg_hba.conf、校验口令/证书 | 无 state |
| 4. 就绪 | 发送 ReadyForQuery | idle |
| 5. 执行查询 | 收到 Query 消息 | active(或等待事件) |
| 6. 事务中空闲 | 开启事务但未结束 | idle in transaction |
| 7. 退出 | 客户端断开或 pg_terminate_backend() | 记录消失 |
-- 观察 backend_start 与 state_change 的差值,判断连接是否被复用
SELECT pid,
backend_start,
state_change,
state,
now() - backend_start AS conn_lifetime,
coalesce(application_name, '-') AS app_name
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY backend_start; pid | backend_start | state_change | state | conn_lifetime | app_name
------+-------------------------------+-------------------------------+--------+---------------+----------
1178 | 2026-09-22 09:20:11.482931+08 | 2026-09-22 09:22:45.117204+08 | idle | 00:31:02 | psql
1215 | 2026-09-22 09:31:44.902117+08 | 2026-09-22 09:38:02.551200+08 | active | 00:19:29 | pgbench
1301 | 2026-09-22 09:25:03.117204+08 | 2026-09-22 09:25:03.200117+08 | idle in transaction | 00:26:10 | app-server
(3 rows)为什么必须用连接池
「每连接一进程」带来的成本:
| 成本项 | 数值(经验值) | 影响 |
|---|---|---|
fork() 延迟 | 0.1~1ms | 高 QPS 短连接场景下成为瓶颈 |
| 单连接基础内存 | 5~10MB(不含 work_mem) | 连接数直接吃内存 |
| 上下文切换 | 每连接一个可调度实体 | 连接数过高时 CPU 花在切换上 |
| 锁竞争 | 更多进程争抢 LWLock | buffer / WAL 锁竞争加剧 |
因此 PostgreSQL 社区的共识是:数据库侧连接数应控制在 100~300 的区间(与 CPU 核数相关),超出部分由连接池承担。pgbouncer 的 transaction 池模式可以让「1000 个应用连接」映射到「20 个数据库连接」。
应用 1000 连接 ──► PgBouncer(transaction 模式) ──► PostgreSQL 20 个后端进程连接池的代价
transaction 池模式下,同一个数据库连接会在不同应用请求间复用,因此会话级状态会泄漏:SET、PREPARE、临时表、游标、LISTEN/NOTIFY、会话级 advisory lock 都会在使用者之间串味。用 session 池模式可以避免,但复用率下降。应用若依赖会话状态,务必选择 session 模式。
辅助进程的启动与退出
辅助进程在实例启动时由 Postmaster 一次性 fork,并在整个实例生命周期内常驻。它们之间也有依赖顺序:
Postmaster 启动
├─► 初始化共享内存(shared_buffers / WAL buffer / ProcArray / 锁表)
├─► 执行崩溃恢复(若上次非正常关闭)
├─► fork checkpointer
├─► fork background writer
├─► fork walwriter
├─► fork archiver(若 archive_mode = on)
├─► fork autovacuum launcher
├─► fork logical replication launcher
└─► 进入监听循环,准备 accept 客户端连接-- 观察各辅助进程的启动时间,确认它们是否被重启过
SELECT backend_type,
pid,
backend_start,
now() - backend_start AS uptime
FROM pg_stat_activity
WHERE backend_type <> 'client backend'
ORDER BY backend_start; backend_type | pid | backend_start | uptime
----------------------------+------+-------------------------------+----------
checkpointer | 1149 | 2026-09-22 09:12:01.117204+08 | 00:39:12
background writer | 1150 | 2026-09-22 09:12:01.120117+08 | 00:39:12
walwriter | 1152 | 2026-09-22 09:12:01.135882+08 | 00:39:12
autovacuum launcher | 1153 | 2026-09-22 09:12:01.148201+08 | 00:39:12
logical replication launcher | 1154 | 2026-09-22 09:12:01.150117+08 | 00:39:12
(5 rows)从 uptime 发现异常重启
如果 uptime 明显小于实例的启动时间(对比 pg_postmaster_start_time()),说明该辅助进程曾异常退出并被 Postmaster 重建——这通常意味着它崩溃过,值得查日志。
SELECT pg_postmaster_start_time() AS instance_start,
now() - pg_postmaster_start_time() AS instance_uptime;实战示例
示例一:绘制实例的进程全景图
-- 一次查询输出进程分类统计与资源占用
SELECT backend_type,
count(*) AS cnt,
count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE state LIKE 'idle in transaction%') AS idle_in_xact,
min(backend_start) AS earliest_start
FROM pg_stat_activity
GROUP BY backend_type
ORDER BY cnt DESC; backend_type | cnt | active | idle_in_xact | earliest_start
--------------------------------+-----+--------+--------------+---------------------------
client backend | 12 | 3 | 1 | 2026-09-22 09:20:11.48+08
checkpointer | 1 | 0 | 0 | 2026-09-22 09:12:01.11+08
background writer | 1 | 0 | 0 | 2026-09-22 09:12:01.12+08
walwriter | 1 | 0 | 0 | 2026-09-22 09:12:01.13+08
autovacuum launcher | 1 | 0 | 0 | 2026-09-22 09:12:01.14+08
logical replication launcher | 1 | 0 | 0 | 2026-09-22 09:12:01.15+08
(6 rows)# 与操作系统对照:统计所有 postgres 进程及其内存占用(RSS)
ps -eo pid,ppid,rss,args --sort=-rss | grep '[p]ostgres' | head -10 PID PPID RSS COMMAND
1301 1147 28452 postgres: app_user demo 10.0.1.5(51234) idle in transaction
1215 1147 19884 postgres: postgres demo [local] SELECT
1178 1147 9120 postgres: postgres demo [local] idle
1147 1 6214 /usr/lib/postgresql/18/bin/postgres -D /var/lib/postgresql/18/main
1149 1147 3412 postgres: checkpointer
1150 1147 3388 postgres: background writer
1152 1147 3320 postgres: walwriter
1153 1147 3204 postgres: autovacuum launcherRSS 单位是 KB。可以看到:4 个辅助进程的内存占用合计不到 15MB,而一个
idle in transaction的客户端后端就占了约 28MB。这就是「连接数即内存」的直观体现。
示例二:定位并清理阻塞链
-- 第 1 步:找出正在等待锁的会话及其阻塞者
WITH blocked AS (
SELECT pid,
pg_blocking_pids(pid) AS blockers,
state,
wait_event,
now() - xact_start AS xact_age,
left(query, 50) AS query_short
FROM pg_stat_activity
WHERE backend_type = 'client backend'
)
SELECT b.pid AS blocked_pid,
array_to_string(b.blockers, ',') AS blocking_pids,
b.state,
b.wait_event,
b.xact_age,
b.query_short
FROM blocked b
WHERE cardinality(b.blockers) > 0; blocked_pid | blocking_pids | state | wait_event | xact_age | query_short
-------------+---------------+----------------------+----------------+-----------+--------------------
1240 | 1301 | active | transactionid | 00:00:12 | UPDATE orders SET ...
(1 row)-- 第 2 步:查看阻塞者 1301 的状态(典型的 idle in transaction)
SELECT pid, state, now() - xact_start AS xact_age, left(query, 60) AS last_query
FROM pg_stat_activity
WHERE pid = 1301; pid | state | xact_age | last_query
------+---------------------+----------+----------------------------------
1301 | idle in transaction | 00:12:44 | UPDATE orders SET status = 'paid'
(1 row)-- 第 3 步:先尝试温和取消(让应用自己处理错误)
SELECT pg_cancel_backend(1301); pg_cancel_backend
-------------------
t
(1 row)-- 第 4 步:若取消无效(应用不响应或已卡死),再终止该连接
SELECT pg_terminate_backend(1301);
-- 第 5 步:确认阻塞已解除
SELECT count(*) AS still_blocked
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0; still_blocked
---------------
0
(1 row)预防 idle in transaction
问题的根因是应用「开了事务却去干别的事」。两道防线:
idle_in_transaction_session_timeout:超时自动终止事务。设60s或更低。statement_timeout/lock_timeout:为单条语句与等锁设置上限。
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
ALTER SYSTEM SET statement_timeout = '30s';
ALTER SYSTEM SET lock_timeout = '5s';
SELECT pg_reload_conf();示例三:验证信号的实际效果
# 场景:模拟一次「共享内存被污染」后的自动重启
# 1) 记录当前主进程 PID
MASTER=$(head -1 /var/lib/postgresql/18/main/postmaster.pid)
echo "主进程 PID: $MASTER"
# 2) 杀掉一个后端进程的父级检测路径:用 SIGQUIT 让一个后端立即退出
psql -d demo -c "SELECT pg_backend_pid();" # 假设返回 1215
kill -QUIT 1215
# 3) 观察日志:Postmaster 会把这种情况视为崩溃信号,重启整个实例
tail -20 /var/log/postgresql/postgresql-18-main.logLOG: server process (PID 1215) was terminated by signal 3: Quit
LOG: terminating any other active server processes
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back the current
transaction and exit, because another server process exited abnormally
and possibly corrupted shared memory.
LOG: all server processes terminated; reinitializing
LOG: database system was interrupted; last known up at 2026-09-22 09:40:12 CST
LOG: database system was not properly shut down; automatic recovery in progress
LOG: redo starts at 0/16B3748
LOG: consistent recovery state reached at 0/16C0A28
LOG: database system is ready to accept connections-- 4) 恢复后核对:实例的"启动时间"已更新,但数据完好
SELECT pg_postmaster_start_time() AS restarted_at,
(SELECT count(*) FROM pg_stat_activity) AS backends; restarted_at
-------------------------------
2026-09-22 09:41:03.117204+08
(1 row)不要在生产模拟
上面这段仅用于理解机制。生产环境千万不要手工 kill -QUIT 后端进程——虽然 WAL 能保证数据不丢,但会中断所有连接并触发全量恢复。
注意事项
- 「集群」在 PostgreSQL 文档中指一个数据目录,不是多机集群,读文档时务必区分。
- 不要用
kill -9:会导致共享内存状态不一致,Postmaster 触发全体重启与崩溃恢复。终止查询用pg_cancel_backend(),终止连接用pg_terminate_backend()。 - 手工删除
postmaster.pid极其危险:若实例实际在运行,会造成双实例写同一份数据文件 → 数据损坏。删除前必须确认没有postgres进程。 SIGTERM是「等待」而SIGINT(fast)是「回滚退出」:pg_ctl stop -m smart在长事务存在时可能长时间挂住,生产建议用-m fast。idle与idle in transaction是两个完全不同的概念:前者无害,后者会阻塞 VACUUM 并持有锁。监控必须分开统计。- 连接数受双重限制:
max_connections减去superuser_reserved_connections才是普通用户可用的额度。 pg_stat_activity.query在idle时显示的是最后一条 SQL:不要误判为「正在执行」,要看state。- 权限限制:非超级用户只能看到自己连接的
query/client_addr等敏感列(其他连接显示为<insufficient privilege>)。监控账号需要pg_monitor角色。 - 辅助进程不可单独重启:它们由 Postmaster 统一管理,异常退出会被重建;若反复崩溃,Postmaster 会在达到阈值后停止实例。
- 并行查询会临时创建
parallel worker进程:这是正常现象,不要当成连接泄漏。
-- 不推荐:用超高的 max_connections 硬扛连接压力
-- ALTER SYSTEM SET max_connections = 2000; -- 不推荐
-- 推荐:连接池 + 合理的数据库侧连接上限最佳实践
- 生产必须部署连接池:用 PgBouncer(
transaction模式)把应用连接数与数据库后端数解耦,数据库侧连接数控制在 CPU 核数的 2~4 倍。 - 设置三道超时防线:
statement_timeout、lock_timeout、idle_in_transaction_session_timeout,把「卡死的会话」变成「自动失败的语句」。 - 给监控账号授
pg_monitor:否则看不到其他会话的query与client_addr,排查会失去最关键的信息。 - 把等待事件纳入监控:对
wait_event_type = 'Lock'且持续超过阈值的会话告警,能提前发现锁雪崩。 - 输出进程分类看板:按
backend_type统计连接数与状态分布,而不是只看「总连接数」这一个数字。 - 关闭时统一用
-m fast:smart容易被长事务卡住,immediate会引发恢复,fast是运维默认选择。 - 不要在生产手工杀进程:所有终止操作走
pg_cancel_backend/pg_terminate_backend,并记入变更日志。 - 长事务专项巡检:每天检查
xact_start超过 10 分钟的会话,逐个确认是否为应用 bug(未提交的事务是 VACUUM 与膨胀的最大元凶)。 - 辅助进程的异常重启要查日志:若某个辅助进程的 uptime 与实例不一致,说明它崩溃过,需从日志定位原因。
- 升级到 18 后更新进程名对照表:监控脚本里对
postgres: writer process之类的旧名称匹配应改为backend_type。
下一节