配置陷阱与生产最佳实践:11 个最危险的 GUC 和它们的正确设置
PG 的配置系统(GUC,Grand Unified
Configuration)是内核行为的外在控制面。postgresql.conf
里的每一个参数,背后都对应着源码中的特定分支、数据结构和算法路径。GUC
的核心理念是
“内置合理默认值,允许懂的人改”——但问题在于,“懂”的门槛远比多数人想象的高。
这套配置系统的坑不在于参数太多(PG 17 有 350+ 个
GUC),而在于参数之间相互联动、默认值的物理假设经常过时、以及错误配置的症状往往远离根因。你把
work_mem 调到了 256MB 想让查询跑快点,三天后
OOM killer
随机杀连接——这两件事之间的因果关系埋在进程模型、Hash Join
的内存分配路径和 Linux overcommit 三层机制之下。
本文不是 GUC 参数列表。本文从 11 个生产环境中最容易被误解和配错的参数出发,每条走这条路:这个参数在内核里控制什么 → 默认值在什么场景下是错的 → 配错后症状的内核机制解释 → 通过什么 SQL 和 shell 命令查验当前设置是否有问题。目标不是告诉你”设成 X 就好”,而是让你知道为什么、怎么验证。
本文涉及的很多内核机制在前置章节中有详细拆解,建议先通读系列基础篇(至少第 1、4、5、10 章)。后文不再重复基础概念,直接引用相应章节。
一、shared_buffers:不是越大越好
这个参数在内核里控制什么
shared_buffers 决定了 PG 的 Buffer Pool
大小——这是共享内存中最核心的缓存区域,所有 Backend
进程通过共享内存直接访问(参见第 5
章:Buffer Manager的详细拆解)。它被组织为
N = shared_buffers / 8KB 个 buffer slot,每个
slot 由 BufferDesc + 8KB 数据页组成。页面替换由
Clock Sweep 算法驱动:usage_count 从 5 递减到
0,被选中的页面如果脏了就写回磁盘,然后分配给新请求。
为什么默认值在你的场景下可能是错的
默认值(通常是 128MB)在现代硬件上偏小,但盲目调大的后果比调小更隐蔽。超过 ~8-10GB 后,三重反噬开始显现:
第一重:double buffering。 PG 的 shared_buffers 和 OS page cache 是两套独立的缓存系统。同一个 8KB 数据页可能同时存在于 shared_buffers 和 OS page cache 中——白白占用两倍物理内存。当 shared_buffers 占比过高时,挤占了 OS page cache 的空间,而 PG 大量依赖 OS 的文件 I/O(WAL 写入、数据文件读入 buffer pool、大表 Seq Scan 绕过 ring buffer 后的文件读取),OS page cache 被压缩的后果是这些 I/O 路径全部退化到物理磁盘。
第二重:checkpoint 写脏页量变大。
Checkpoint 发生时,所有 shared_buffers
中的脏页必须被刷到磁盘(参见第 4
章:WAL 内部机制)。shared_buffers 越大,checkpoint
时需要写入的脏页总量越大。PG 通过
checkpoint_completion_target
将这些写入摊平到整个 checkpoint
间隔中,但如果脏页量太大,即使摊平也无法避免瞬时 I/O
尖峰——结果就是 checkpoint 期间的 TPS
雪崩式下降,查询延迟尖刺。
第三重:Clock Sweep 扫描开销。 当大量
buffer 的 usage_count 都降到 0 时,Clock Sweep
需要扫描大量 buffer 才能找到可替换的
slot。freelist(空闲 buffer 链表)为空时,每次
StrategyGetBuffer() 调用都在等待 Clock Sweep
完成扫描——在高并发下,多个 Backend 同时等待 buffer
分配,BufferAlloc 路径上的 LWLock
争用显著上升。
配错后的症状
- 内存利用率异常:
free -h显示available极低但buff/cache极高——OS 层面的 page cache 已被 PG 的 shared_buffers 挤压。 - 即使所有查询都走
shared hit(EXPLAIN (BUFFERS)输出Buffers: shared hit=...),查询延迟波动仍与 checkpoint 时间点强相关——这是 checkpoint IO 尖刺的典型信号。 pg_stat_bgwriter中buffers_checkpoint的增量显著大于buffers_clean(bgwriter 的单次写入量),且maxwritten_clean频繁增加——说明 bgwriter 赶不上脏页生成速度。
查验方法
Step 1:观察 buffer 命中率的真实含义。
-- 查看 buffer 命中率(pg_stat_bgwriter 的口径)
SELECT
buffers_checkpoint,
buffers_clean,
buffers_backend,
buffers_alloc,
ROUND(100.0 * (buffers_checkpoint + buffers_clean + buffers_backend) /
NULLIF(buffers_checkpoint + buffers_clean + buffers_backend + buffers_alloc, 0), 2)
AS sync_io_pct
FROM pg_stat_bgwriter;
```text
`sync_io_pct` 越接近 0,说明命中率越高——但这个指标存在"命中率骗局":如果大量 shared_buffers 都用在重复访问同一小批热页面,而大表 Seq Scan 每次都读磁盘,命中率仍然可以接近 99% 但查询可能很慢。
**Step 2:用 `pg_buffercache` 观察 buffer 的实际分布。**
```sql
-- 安装扩展
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
-- 每个表占用了多少 shared_buffers
SELECT
c.relname,
COUNT(*) AS buffers,
pg_size_pretty(COUNT(*) * 8192) AS size
FROM pg_buffercache b
JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid)
WHERE b.reldatabase IN (0, (SELECT oid FROM pg_database WHERE datname = current_database()))
GROUP BY c.relname
ORDER BY COUNT(*) DESC
LIMIT 20;如果排名前几的表占用了 shared_buffers 的绝大部分,说明访问模式集中。关键判断:这些表的数据量是否远大于 shared_buffers?如果是,说明每次访问大量页面在反复淘汰 shared_buffers 里的内容——此时更大的 shared_buffers 可能有帮助。如果不是(比如热表只有 500MB,但 shared_buffers 设了 8GB),那剩下的 7.5GB 基本是浪费。
Step 3:对比 shared_buffers 和 OS page cache 的实际使用。
# 查看 OS 层面的 page cache
free -h
# 查看 PG 的共享内存段大小
ipcs -m | grep postgres
# 用 pg_buffercache 统计 shared_buffers 中脏页比例
```text
```sql
SELECT
isdirty,
COUNT(*) AS buffers,
pg_size_pretty(COUNT(*) * 8192) AS size
FROM pg_buffercache
GROUP BY isdirty;如果 free -h 的 available
低于物理内存的 20%,而 shared_buffers 占了物理内存的 50%
以上,说明 double buffering 已经挤压到其他 I/O
路径——你需要减小 shared_buffers,把内存还给 OS page
cache。
正确做法:shared_buffers
的合理范围通常是物理内存的 15%-25%,上限不超过
8-10GB(除非是纯 OLTP 且热数据集完全小于此值)。最终依据是
pg_buffercache 的实际观察,不是公式。
二、work_mem:最危险的 GUC
这个参数在内核里控制什么
work_mem 控制的是单个查询中每个 sort
/ hash / bitmap / materialize
操作各自的内存上限。注意三个关键词:“单个操作”(不是单个查询)、“各自独立”(一个查询有
3 个 sort + 1 个 hash join,就是 4 个 work_mem
配额)、“上限”(操作的内存可以低于此值,但不能超过)。
以 hash join 为例(参见第 12
章:执行器中对 ExecHashJoin()
的内存分配分析):PG 在构建 hash table 时,如果数据量超过
work_mem,会把超出部分写入临时文件(Hybrid Hash Join
的 spill 机制)。sort 同理——数据量超过 work_mem
时切换到 external merge sort。
PG 15 引入了 hash_mem_multiplier,将 hash
操作的内存上限从 work_mem 调整为
work_mem × hash_mem_multiplier(默认
2.0)。这意味着 PG 15+ 中 hash join 默认使用 2 倍
work_mem 的内存——这是对”hash table 写临时文件比
sort 写临时文件更痛苦”的补偿。
为什么默认值在你的场景下可能是错的
默认值 4MB 在 OLTP 场景(查询简单、操作少)可能够用。但
work_mem
的真正危险不是设太小——是设太大而你没有意识到它在连接数面前是乘法爆炸。
危险的算术:
\[\text{总内存} = \text{work\_mem} \times \text{每查询操作数} \times \text{平均并行度} \times \text{并发连接数}\]
拿一个典型的生产负载算一笔账:100
个并发连接,每个连接跑一个 parallel hash join(3 个 worker =
4 个进程),同时还有 sort + materialize。如果你把
work_mem 调到了 64MB:
- Hash join(hash_mem_multiplier=2.0):64MB x 2.0 x 4 进程 = 512MB 每个连接
- Sort:64MB
- Materialize:64MB
- 每个连接峰值的操作内存:512 + 64 + 64 = 640MB
- 100 个并发连接:640MB x 100 = 64GB
如果你的服务器只有 32GB 物理内存,这就是一个即将触发的
OOM 炸弹。Linux overcommit(默认开启)不会阻止
malloc() 成功,但当这些内存真正被触及时,OOM
killer 会随机选择进程杀掉。
更隐蔽的是:这个乘法中的”操作数”不是 EXPLAIN
里看到的第 1 行那几个——一个 CTE (WITH)
被多次引用时,每个引用生成独立的 Materialize 节点,各拿一份
work_mem。一个含多表 JOIN
的查询在生成中间结果时的 sort 操作也可能不止一个。
配错后的症状
- OOM:
dmesg -T | grep "Out of memory"看到postgres进程被 kill。被 kill 的 backend 不一定就是”吃内存最多的那个”——OOM killer 的oom_score基于页表占用等指标,可能是某个正常的 small query 的 backend 被杀。 - Postmaster 收到子进程异常退出信号 → 如果
restart_after_crash=on(默认),Postmaster 触发所有子进程重启——所有连接断开,不是”杀一个连一个重连就行”。 - 大量临时文件写入:
pg_stat_database.temp_files的增量突然升高——但这不是work_mem太大的症状,而是work_mem太小导致大量 spill 的症状。两者刚好相反。
查验方法
Step 1:观察临时文件生成。
SELECT datname, temp_files, temp_bytes,
pg_size_pretty(temp_bytes) AS temp_size,
stats_reset
FROM pg_stat_database
WHERE datname = current_database();
```text
如果 `temp_files` 增长很快,且你知道当前负载中有 hash join 或大 sort,说明 `work_mem` 太小——数据在 spill 到磁盘。
**Step 2:估计当前负载的实际内存需求。**
```sql
-- 当前有哪些查询在执行 hash join / sort
SELECT pid, query,
wait_event_type, wait_event,
state
FROM pg_stat_activity
WHERE state = 'active'
AND (query ILIKE '%hash%' OR query ILIKE '%sort%')
AND pid != pg_backend_pid();
-- 当前活跃连接数
SELECT COUNT(*) AS active_connections
FROM pg_stat_activity
WHERE state = 'active';-- 每个数据库的临时文件统计
SELECT datname, temp_files,
pg_size_pretty(CASE WHEN temp_bytes > 0
THEN temp_bytes / NULLIF(temp_files, 0) ELSE 0 END) AS avg_temp_file_size
FROM pg_stat_database;
```text
如果 `avg_temp_file_size` 很小但 `temp_files` 很多,说明操作经常只超出 `work_mem` 一点就 spill——稍微增大 `work_mem` 就能消除大部分 spill。但如果 `avg_temp_file_size` 很大,意味着部分操作的数据量远超 `work_mem`——需要从查询优化角度减少参与 hash/sort 的行数,而不是无脑增 `work_mem`。
**Step 3:`hash_mem_multiplier` 的联动检查。**
```sql
SHOW hash_mem_multiplier;
SHOW work_mem;-- 以人类可读的方式显示 hash 操作的实际内存上限
SELECT setting, unit,
CASE WHEN unit = 'MB' THEN setting::numeric * 1024 * 1024
WHEN unit = 'kB' THEN setting::numeric * 1024
WHEN unit = 'GB' THEN setting::numeric * 1024 * 1024 * 1024
ELSE setting::numeric
END AS bytes
FROM pg_settings
WHERE name = 'work_mem' OR name = 'hash_mem_multiplier';
```text
**正确做法**:`work_mem` 不要全局设大。用 `ALTER DATABASE`、`ALTER USER` 或连接池的 session 级别参数,为不同负载类型设定不同值:OLTP 报告查询 → 16-64MB,日常轻查询 → 4-16MB。上线前在 staging 环境用真实负载跑一轮,观察 `temp_files` 增量和物理内存占用,确认 `work_mem × max_connections × 平均并行度 × 操作数` 不超过物理内存的 60%。
---
## 三、effective_cache_size:不分配内存,只影响优化器的决策
### 这个参数在内核里控制什么
`effective_cache_size` 是优化器代价模型中用于估算 I/O 代价的参数。它本身**不分配任何内存**——它只是告诉优化器:"你可以假设查询用到的数据页中,有这么多能在这层缓存(shared_buffers + OS page cache)中被找到。"
在 `cost_index()` 函数(`src/backend/optimizer/path/costsize.c`)中,索引扫描的 I/O 代价计算依赖于一个关键假设:索引页中有一部分已经在缓存中,不需要物理 I/O。这个"已经在缓存中的比例"由以下公式估算:
$$\text{cache\_hit\_fraction} = \min\left(1, \frac{\text{effective\_cache\_size}}{\text{所有参与查询的表的页面数}}\right)$$
如果 `effective_cache_size` 远小于表大小,优化器认为大部分索引页和 heap 页需要从磁盘读取——于是索引扫描的估算代价偏高,优化器倾向于 Seq Scan。如果 `effective_cache_size` 大到能覆盖全部数据,优化器认为几乎所有页面都在缓存中——索引扫描代价显著降低,优化器倾向于使用索引。
(代价模型的完整拆解见[第 10 章:统计信息与代价模型](../10-statistics-cost/10-statistics-cost.html)。)
### 为什么默认值在你的场景下可能是错的
默认值 4GB 在内存只有 4-8GB 的系统上偏高,在内存 256GB 的服务器上则严重偏低。
关键误区:`effective_cache_size` 应该反映真实可用的缓存大小——包括 shared_buffers 和 OS page cache。在典型 PG 部署中,OS page cache 通常远大于 shared_buffers(因为 PG 开发者建议 shared_buffers 只占物理内存的 15-25%)。所以**真实的"缓存"主要是 OS page cache,不是 shared_buffers。**
设太小(如默认 4GB 在 128GB 服务器上)的后果:优化器不相信索引页能被缓存,高估索引扫描代价 → 对大于内存的表倾向于 Seq Scan → `EXPLAIN` 里看到大量不该出现 Seq Scan 的查询计划。
### 配错后的症状
- `EXPLAIN` 输出中,优化器对有明显索引可用且有过滤条件的查询仍然选择 Seq Scan,且代价估算的前几行就显示 `cost=0.00..XXXX`(Seq Scan 的启动代价为 0)。
- 查询响应时间比预期慢,且 `EXPLAIN (ANALYZE, BUFFERS)` 显示 Seq Scan 读取了大量 `shared read`(从磁盘读),而你知道索引存在且选择性很好。
- 使用 `SET enable_seqscan = off;` 后查询显著变快——说明优化器选 Seq Scan 不是因为索引差,而是代价估算失衡。
### 查验方法
```sql
-- 当前值
SHOW effective_cache_size;
-- 查看系统真实可用内存
-- 在 shell 中:free -h-- 检查某张表是否被优化器错误估算
EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT * FROM large_table WHERE indexed_column = 'rare_value';
```text
关注输出中的 `Settings: effective_cache_size`、`Buffers: shared hit/read` 对比,以及 `rows` 估算与实际行数的偏差——如果偏差一个数量级以上,问题可能不只在 `effective_cache_size`,也在统计信息(参见[统计信息漂移的诊断](../10-statistics-cost/10-statistics-cost.html))。
**正确做法**:设 `effective_cache_size` 为系统可用内存的 50-75%(不是物理总内存——要扣除其他应用和 OS 本身的开销)。例如 128GB 服务器上,减去 OS 和其他进程约 16GB,剩余 112GB 的 60% 约为 67GB——设 `effective_cache_size = 64GB` 是一个合理起点。最终依据是 `EXPLAIN` 输出的计划是否与实际最优计划一致。
---
## 四、random_page_cost:默认 4.0 在 NVMe 上严重高估随机 I/O
### 这个参数在内核里控制什么
`random_page_cost` 是优化器代价模型中的核心常量之一,表示从磁盘随机读取一个页面的相对代价。它的默认值 4.0,意味着优化器假定:随机读一页的代价是顺序读一页(`seq_page_cost`,默认 1.0)的 4 倍。
这个值直接影响所有索引扫描(Index Scan、Index-Only Scan、Bitmap Scan)的代价估算。索引扫描的本质是"索引页的随机读 + heap 页的随机读"——如果 `random_page_cost` 偏高,这两个操作的估算代价就偏高,优化器就倾向于放弃索引选择 Seq Scan。
历史上 4.0 对应的是机械硬盘(HDD)的寻道延迟(~4ms random seek vs ~1ms sequential read)。但 NVMe SSD 的随机读延迟通常在 0.1-0.3ms,与顺序读的差异很小——此时 `random_page_cost = 4.0` 意味着优化器认为索引扫描的代价是实际代价的 10-40 倍。
### 为什么默认值在你的场景下可能是错的
在 NVMe SSD(甚至高端 SATA SSD)上,默认值 4.0 严重偏差。`pg_test_fsync` 工具的实测数据可以帮助量化差距:
```bash
# PG 自带的 fsync 性能测试
pg_test_fsync
# 输出示例(NVMe SSD):
# Compare file sync methods using one 8kB write:
# open_datasync n/a
# fdatasync n/a (fsync 约为 0.1ms)
# 磁盘寻道时间的主导因子已经降到微秒级你也可以用 fio 做更精确的随机读 vs
顺序读对比:
fio --name=randread --ioengine=libaio --iodepth=1 --rw=randread \
--bs=8k --direct=1 --size=1G --numjobs=1 --runtime=10 \
--group_reporting
fio --name=seqread --ioengine=libaio --iodepth=1 --rw=read \
--bs=8k --direct=1 --size=1G --numjobs=1 --runtime=10 \
--group_reporting
```text
取随机读延迟与顺序读延迟的比值作为 `random_page_cost` 的基准。
### 配错后的症状
- 优化器在有索引且过滤条件选择性极好时仍然走 Seq Scan——与 `effective_cache_size` 偏小的症状类似,但根因不同。
- `EXPLAIN (ANALYZE, BUFFERS)` 强行走 Index Scan(`SET enable_seqscan = off`)后,实际执行时间远低于优化器为 Seq Scan 估算的代价——说明优化器对索引扫描的代价估算高了数倍。
- 多个小查询的 `mean_exec_time` 偏高,`pg_stat_statements` 中 `shared_blks_read` 远高于期望。
### 查验方法
```sql
-- 当前值
SHOW random_page_cost;
-- 强制索引扫描对比
SET enable_seqscan = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM large_table WHERE indexed_column = 'rare_value';
-- 记录实际执行时间
SET enable_seqscan = on;
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM large_table WHERE indexed_column = 'rare_value';
-- 对比两者的 actual time 和 optimizer cost如果强走索引的 actual time 远低于 Seq Scan
的 actual time,但优化器默认选择了 Seq
Scan,说明 random_page_cost 设高了。
-- 批量检查:pg_stat_statements 中哪些查询的 shared_blks_read 异常高
SELECT queryid, query,
shared_blks_read, shared_blks_hit,
mean_exec_time,
ROUND(100.0 * shared_blks_read /
NULLIF(shared_blks_read + shared_blks_hit, 0), 2) AS read_pct
FROM pg_stat_statements
WHERE shared_blks_read + shared_blks_hit > 100
ORDER BY shared_blks_read DESC
LIMIT 10;
```text
**正确做法**:NVMe SSD → `random_page_cost = 1.1`;高端 SATA SSD → `1.5-2.0`;SAN/NAS → 需要实测。如果用 `fio` 测得随机读延迟是顺序读的 1.5 倍,设 `random_page_cost = 1.5`。上线后用上面的强制索引扫描对比法验证。
---
## 五、fsync=off:最危险的配置
### 这个参数在内核里控制什么
`fsync` 控制 PG 是否调用 `fsync()` 系统调用来保证 WAL 和数据文件写入已落到持久化存储(磁盘或等效设备)。当 `fsync=on`(默认)时,每次 WAL 写入和 checkpoint 的数据文件写入最终都会通过 `fsync()`(或 `fdatasync()`,取决于 `wal_sync_method`)确保数据不只在 OS buffer 中。
关掉它意味着:`COMMIT` 时 WAL 数据写到了 OS buffer 但没有调用 `fsync()` 刷盘。操作系统会在它觉得方便时(通常在几秒到几十秒内)把脏页写回磁盘——但在那之前,如果掉电或内核 crash,这些数据就丢了。
(WAL 的完整写入路径和 `wal_sync_method` 的语义差异见[第 4 章:WAL 内部机制](../04-wal-internals/04-wal-internals.html)。)
### 为什么有人会动它
`fsync` 是 PG 中最大的性能开关之一——关掉它,所有 WAL 写入的 `fsync()` 系统调用延迟消失,`COMMIT` 延迟从 ~0.1ms(NVMe)降到 ~0.01ms。在批量数据导入(`COPY`、`pg_restore`)场景中,导入速度可能提升 10-50 倍。
但代价是:**crash 之后 PG 可能无法恢复,或者恢复后数据不一致。** 这不是丢"最近几秒"数据的问题——这是整个集群可能在 crash 之后不可用的问题。WAL 恢复的逻辑依赖 WAL 记录的持久化顺序和完整性——如果 OS buffer 中的 WAL 没有完整刷盘,crash recovery 时 `StartupXLOG()` 可能遇到截断的 WAL 记录、缺失的 checkpoint record、或者 WAL 序列号不连续,导致恢复失败,数据库无法启动。
### 唯一合法的使用场景
唯一能接受 `fsync=off` 的场景是:**你正在做一次性批量数据导入,导入后有完整的源数据可以重建整个集群,并且你在导入完成后用 `pg_dump` 重新导出并恢复到 `fsync=on` 的干净集群中。**
即使在这个场景下,也有更安全的替代方案:用 `synchronous_commit=off`(见下一节)代替 `fsync=off`——`synchronous_commit=off` 只影响事务提交的 WAL 刷盘,不影响 checkpoint 的 `fsync()`。crash 后丢的是已提交事务,但集群本身是可恢复的。
### 查验方法
```sql
SHOW fsync;如果结果是 off,立即查以下:
- 这个集群是不是只用于批量导入?如果不是,这是严重配置错误。
- 最近一次崩溃测试(crash recovery 测试)是否通过?如果没有做过,不能信任当前数据。
- 即使没有
crash,检查数据一致性:
pg_checksums --check(如果启用了 data checksums)。
正确做法:永远不要在生产环境关
fsync。批量导入场景用
synchronous_commit=off
替代。如果确实需要极高导入速度且不能接受
synchronous_commit=off 的速度,改用
pg_bulkload 或 COPY FREEZE 等绕过
WAL 的批量导入工具,而不是关 fsync。
六、synchronous_commit=off:最多丢 wal_writer_delay 的已提交事务
这个参数在内核里控制什么
synchronous_commit 控制事务
COMMIT 是否等待 WAL 刷盘。它有多个级别:
| 值 | 含义 |
|---|---|
on |
等待 WAL 刷到磁盘(默认) |
remote_apply |
流复制:等待 standby apply 了 WAL |
remote_write |
流复制:等待 standby 写到了 OS buffer |
local |
只等本地刷盘 |
off |
不等待 WAL 刷盘——COMMIT 成功只表示 WAL 已写入 OS buffer |
off 的关键机制:COMMIT 后 WAL record 被写入
WAL buffer → XLogFlush() 把 WAL buffer 写入 OS
buffer → 但不调用 fsync()。WAL
Writer 进程(walwriter)会定期(每
wal_writer_delay 毫秒,默认 200ms)执行
XLogBackgroundFlush(),把积累的 WAL
数据刷到磁盘。
为什么默认值在特定场景下是错的
对金融交易、支付、会计等需要”提交即持久化”语义的应用,synchronous_commit=off
意味着:如果 crash 发生在 COMMIT 之后、WAL Writer
下一次刷新之前,已提交的事务会在 WAL
恢复时丢失。
最坏情况下丢失的窗口是
wal_writer_delay(默认 200ms)+ OS
自身的脏页回写延迟 + WAL 从 OS buffer
刷到磁盘控制器缓存再刷到持久化介质的时间。在极端负载下(大量小事务,每个都更新同一行),这个窗口可能更大——因为
WAL Writer 一次刷盘的 WAL 量有上限。
但 synchronous_commit=off 和
fsync=off
有本质区别:前者丢的是”已提交事务”,后者可能导致”集群不可恢复”。synchronous_commit=off
下 crash 后 PG
能正常启动并恢复——恢复过程会找到最近一个完整刷盘的 WAL
checkpoint,从那里开始
REDO——丢失的事务会被静默忽略,不会导致数据损坏或不一致。
配错后的症状
- 没有显式症状——这正是它的危险。数据库正常运行,直到一次 crash。
- 如果你在 crash
后对照应用日志和数据库状态,会发现某些”已确认写入”的数据不存在——这些数据正好落在
crash 前
wal_writer_delay窗口内。
查验方法
SHOW synchronous_commit;
-- 查看当前 WAL writer 的配置
SELECT name, setting, unit FROM pg_settings
WHERE name IN ('synchronous_commit', 'wal_writer_delay', 'wal_writer_flush_after');
```text
```sql
-- 查看 WAL 写入速率——如果写入量极大,WAL writer 的刷新频率可能跟不上
SELECT pg_current_wal_lsn(), pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0') AS current_offset;
-- 两次查询间隔中,WAL LSN 的增量可以估算丢失窗口的数据量正确做法:对需要”提交即持久化”的应用,保持
synchronous_commit=on。对”丢了也能重做”的
session(如批量导入、日志写入、缓存预热),用
SET SESSION synchronous_commit = off; 在
session
级别关闭,而不是全局关。主从复制场景,如果同时需要持久性和低延迟,用
synchronous_commit = remote_write 代替
off——至少保证 standby 收到了 WAL。
七、huge_pages:Docker/K8s 中的静默退化
这个参数在内核里控制什么
huge_pages 控制 PG 是否使用 Linux 的 huge
pages(大页内存)来分配 shared_buffers。默认
2MB 的 huge page(或 1GB 的 gigantic page)能显著减少
TLB(Translation Lookaside Buffer)miss——对于
shared_buffers 通常是几 GB 到几十 GB
的场景,如果使用 4KB 小页,TLB entries
不够用,每次页面遍历都需要额外的页表访问。
PG 在 src/backend/port/sysv_shmem.c
中分配共享内存时,根据 huge_pages
设置选择分配方式:
off:标准 4KB 页。try:尝试用 huge page,如果失败就静默退化为 4KB 页。on:强制使用 huge page,分配失败则启动失败。
为什么默认值在你的场景下可能是错的
默认值 try 在物理机/VM 上通常工作良好——OS
如果配置了 huge page,try 会自动使用。但在
Docker 容器和 Kubernetes Pod 中,容器默认没有
SYSVIPC 相关的 huge page
权限,try 会静默退化为 4KB
页——你的 PG
开心地启动了,shared_buffers 也分配了
8GB,但全用的是 4KB 页,TLB miss 显著增加。
这种退化在运行一段时间后表现为 CPU 使用率异常偏高(5-15%
的额外开销),但你几乎不可能从应用层面定位到原因——pg_stat_statements
显示所有查询都正常,EXPLAIN 也没问题,就是 CPU
“莫名其妙”高。
配错后的症状
- CPU 利用率比预期高 5-15%,
perf top显示大量时间花在__handle_mm_fault、_page_fault、__walk_page_range等内核页表遍历函数上。 - PG 启动日志(
pg_log或journalctl)中没有提到 “huge page”——如果 huge page 成功分配,启动日志会打印类似allocated huge pages: X of Y的信息。 /proc/meminfo中HugePages_Total = 0但shared_buffers设了 8GB。
查验方法
# 查看系统大页状态
grep -E "HugePages|Hugepagesize" /proc/meminfo
# HugePages_Total: 0 <-- 系统没有配置大页
# HugePages_Free: 0
# Hugepagesize: 2048 kB
```text
```bash
# 查看 PG 启动日志中关于 huge page 的信息
# 如果是物理机部署:
grep -i "huge" /var/log/postgresql/postgresql-*.log
# 或用 journalctl
journalctl -u postgresql | grep -i huge# 查看容器的内存限制和 huge page 配置
# Docker:
docker inspect <container> | jq '.[0].HostConfig.Memory, .[0].HostConfig.MemoryReservation'
# K8s:
kubectl describe pod <pod> | grep -A5 -E "Limits|hugepage"
```text
输出为空或 "huge pages: 0" 即说明退化。
```bash
# 确认当前 PG 实际使用的页大小
# PG 没有直接 GUC 暴露此信息,但可以通过共享内存段大小推算
# 如果 shared_buffers=8GB 但 ipcs -m 显示多个小段,很可能没用大页
ipcs -m | grep postgres正确做法:
- 物理机/VM:OS 层面先分配 huge
page(
vm.nr_hugepages = shared_buffers / 2MB + 少量预留),然后设huge_pages = on。PG 启动时如果 huge page 不够会直接报错,不会退化。 - Docker:
docker run --shm-size=...是 POSIX shm,不是 huge page。要让容器用 huge page,需要挂载/dev/hugepages(-v /dev/hugepages:/dev/hugepages)并在宿主机上预先分配 huge page。 - K8s:Pod spec 中声明
resources.limits.hugepages-2Mi。不声明 → PG 无法使用 huge page(即使宿主机有)。
如果无法在容器中使用 huge page,评估 TLB miss
的实际影响:在无 huge page 的环境跑
perf stat -e dTLB-load-misses,dTLB-loads -p <pg_pid>
观察 TLB miss 率。如果 miss 率超过
1%,考虑换物理机部署或减少 shared_buffers。
八、maintenance_work_mem:慢 VACUUM 和慢 CREATE INDEX 的隐形瓶颈
这个参数在内核里控制什么
maintenance_work_mem
控制的是维护操作的内存上限,主要包括三类:
- VACUUM:用于扫描索引时维护 dead tuple 的 TID 列表。如果列表超过此值,VACUUM 需要分多趟扫描索引(每趟处理一批 dead tuple)——多趟扫描意味着多次索引扫描,这在有大索引的表上代价极大。
- CREATE INDEX:用于排序。如果数据量超过此值,切换到 external merge sort,把中间数据写临时文件——IO 密集,速度断崖式下降。
- ALTER TABLE(如 ADD COLUMN、SET DATA TYPE):依赖于表重写过程中的排序和 hash 表操作。
为什么默认值在你的场景下可能是错的
默认值 64MB 在大于 10GB 的表上执行 VACUUM 或
CREATE INDEX 时严重不足。
以 VACUUM 为例(详细流程见第 8 章:VACUUM 与 Freezing):VACUUM 扫描 heap 页面收集 dead tuple 的 TID → 对于每个索引,用维护工作内存中的数组批量查找并标记 dead TID → 如果数组装不下所有 dead tuple,就要多趟 scan index。一个大表(100GB)上的 VACUUM,如果产生了 2GB 的 dead tuple,以 64MB 数组的大小需要大约 32 趟索引扫描——耗时是单趟扫描的几十倍。
CREATE INDEX 的 external merge sort 阶段:PG
的 B-Tree 创建过程(_bt_spools_heapscan() →
tuplesort_performsort())需要排序所有
tuple。64MB 在面对几千万行时要 spill
大量临时数据到磁盘,索引创建时间可以差一个数量级。
配错后的症状
VACUUM VERBOSE输出中看到index scans: X,其中 X 远大于 1——说明 VACUUM 被迫多趟扫描索引。pg_stat_progress_vacuum中phase = 'scanning heap'和phase = 'vacuuming indexes'之间的切换次数多——因为每趟都要重新扫描索引。CREATE INDEX期间磁盘写入量异常高(写入的目标不仅是新索引文件,还有大量临时文件)。
查验方法
SHOW maintenance_work_mem;
-- 查看最近一次 VACUUM 是否有多趟索引扫描
-- 手动跑一次 verbose VACUUM 来诊断(在低峰期)
VACUUM (VERBOSE) target_table;
-- 关注输出中的:
-- "index scans: N" -- N>1 说明 maintenance_work_mem 不够
-- "pages: X removed, Y remain"
```text
```sql
-- 查看当前 VACUUM 的进度
SELECT
relid::regclass AS table_name,
phase,
heap_blks_total, heap_blks_scanned,
index_vacuum_count,
max_dead_tuples
FROM pg_stat_progress_vacuum;max_dead_tuples 是当前这一趟能装下的最大
dead tuple 数——如果表上的 dead tuple 远大于此值,VACUUM
需要多趟索引扫描。
-- 预估表上的 dead tuple 量
SELECT relname,
n_dead_tup,
n_live_tup,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 100
ORDER BY n_dead_tup DESC
LIMIT 10;
```text
**正确做法**:`maintenance_work_mem` 可以设得比较大(1GB-8GB 在 64GB+ 服务器上合理),因为 **`maintenance_work_mem` 不在每个连接的 work_mem 乘数公式中**——它只在执行维护操作的进程中使用,而 VACUUM worker 的数量由 `autovacuum_max_workers`(默认 3)限制。最坏情况是 `autovacuum_max_workers` 个 VACUUM worker 同时做索引清理,峰值内存为 `maintenance_work_mem × autovacuum_max_workers`——如果设 1GB × 3 = 3GB,在 64GB 服务器上完全可控。
也可以只增大 `autovacuum_work_mem`(PG 9.4+)——它只影响 autovacuum worker,不影响手动的 `VACUUM` 和 `CREATE INDEX`。
---
## 九、idle_in_transaction_session_timeout:为什么必须设
### 这个参数在内核里控制什么
`idle_in_transaction_session_timeout` 控制一个 session 在事务中但空闲(没有执行任何查询)多久后被自动终止。事务中空闲意味着:session 执行了 `BEGIN` 或前一条 SQL 开启了事务,但在那之后没有再发任何查询——通常是应用程序 `BEGIN` → 查询 → 在代码中做了非 DB 操作(调 API、读取文件、等待用户输入)→ 忘了 `COMMIT` → 事务就这么开着。
这个参数默认值是 0——表示禁用。这就是问题。
### 为什么默认值在你的场景下可能是错的
一个忘了 `COMMIT` 的 session(哪怕是开发者的 `psql` 连接)可以造成连锁灾难:
1. **持锁不释放。** 事务中获取的锁(行级锁、表级 `ACCESS EXCLUSIVE` 等)在事务结束前不会释放。如果这个事务曾经 `UPDATE` 了一行(哪怕没修改任何列值),它持有该行的 `RowExclusiveLock` 和对应的事务 ID。其他 session 想对同一行做 `UPDATE` 或 `DELETE` 会阻塞。更糟的是,如果这个事务曾执行过 `ALTER TABLE`(哪怕是 `ALTER TABLE t ADD COLUMN x INT DEFAULT 0`,它需要 `ACCESS EXCLUSIVE` 锁),所有后续对该表的任何 DML/DDL 都会被阻塞。
2. **阻止 VACUUM 回收死行。** VACUUM 只能回收那些"在所有活跃事务开始之前就 dead"的元组(参见[第 8 章:VACUUM 与 Freezing](../08-vacuum-freezing/08-vacuum-freezing.html))。一个长时间开着的 `idle in transaction` session 带着一个旧的 `xmin`——VACUUM 在那个 `xmin` 之后产生的 dead tuple 都不能被回收。一天不提交,一天的死 tuple 都在累积膨胀。
3. **阻止 HOT 更新。** HOT(Heap-Only Tuple)更新的前提是更新的列不在任何索引中,且更新后的 tuple 能放在同一页面。但即使满足这些条件,如果更新涉及的行被另一个事务锁定,HOT 优化也可能无法生效——从而产生额外的索引更新和膨胀。
### 配错后的症状
- `pg_stat_activity` 中出现大量 `state = 'idle in transaction'` 且 `xact_start` 时间很久的 session。
- `pg_locks` 中有大量 `granted = true` 但 `transactionid` 类型的锁——每个 `idle in transaction` session 持有一个事务 ID 锁,阻止 VACUUM 推进。
- 表膨胀加速——`pg_stat_user_tables.n_dead_tup` 持续增长,但 `last_autovacuum` 也正常在执行(说明 VACUUM 在跑,但无法回收死行)。
- DDL 操作(`ALTER TABLE`、`CREATE INDEX CONCURRENTLY`)hang 住,`pg_stat_activity.wait_event = 'Lock'`。
### 查验方法
```sql
SHOW idle_in_transaction_session_timeout;
-- 查找所有 idle in transaction 的 session 及其持锁情况
SELECT
pid,
usename,
application_name,
state,
xact_start,
NOW() - xact_start AS xact_duration,
query,
wait_event_type,
wait_event
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND pid != pg_backend_pid()
ORDER BY xact_start;-- 查找被 idle in transaction session 阻塞的查询
SELECT
blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query,
blocking.state AS blocking_state,
NOW() - blocking.xact_start AS blocking_xact_duration
FROM pg_stat_activity blocked
JOIN pg_locks bl ON blocked.pid = bl.pid AND NOT bl.granted
JOIN pg_locks lb ON bl.locktype = lb.locktype
AND bl.database = lb.database
AND bl.relation = lb.relation
AND lb.granted
JOIN pg_stat_activity blocking ON lb.pid = blocking.pid
WHERE blocked.wait_event_type = 'Lock';
```text
**正确做法**:`idle_in_transaction_session_timeout` 必须设一个非零值。典型值是 `5min`-`15min`(取决于业务中合法长事务的最大持续时间)。设置后,超时的 session 会被终止并释放其持有的所有锁和事务 ID——错误日志中会记录类似 `terminating connection due to idle-in-transaction timeout` 的信息,方便你回溯是哪个应用连接出了问题。
---
## 十、log_lock_waits + deadlock_timeout 的组合
### 这些参数在内核里控制什么
`deadlock_timeout`(默认 1s)有两个作用:
1. **死锁检测的触发间隔。** 死锁检测器(`DeadLockCheck()`,`src/backend/storage/lmgr/deadlock.c`)不是每发生一次锁等待就跑——那样太贵。它每隔 `deadlock_timeout` 被调度一次,检测当前等待图中是否有环。有环 → 选一个事务 abort。
2. **`log_lock_waits` 的报告阈值。** 当 `log_lock_waits = on`(默认 `off`)时,如果一个 session 等待某个锁超过 `deadlock_timeout`,PG 会在日志中写入一条 WARNING,记录等待信息(等待的锁类型、持锁者的 PID 等)。
关键细节:**你不是只能等满 `deadlock_timeout` 才知道死锁了。** 即使 `log_lock_waits = off`,死锁检测仍然按 `deadlock_timeout` 间隔运行——只是不会在日志中记录"非死锁"的锁等待超时。
### 为什么默认值在排查场景下是错的
`log_lock_waits = off`(默认)意味着:当一个 session 被锁住了很久,你不知道——直到用户投诉"系统卡住了"。而设了 `log_lock_waits = on` 之后,你可以在日志中看到两种关键信号:
- **锁等待超时但最终获取到锁**:说明存在临时的锁竞争——可能是某个 DDL 短暂锁住了表,或者某个 UPDATE 的事务持续时间稍长。这不是死锁,但可以帮你定位性能异常的源头。
- **锁等待超时且最终被死锁检测 abort**:说明检测到了死锁——日志中会先有锁等待超时的 WARNING,再有死锁检测的 ERROR。
`deadlock_timeout` 的默认值 1s 也值得重新评估:如果设太短(如 100ms),高并发下正常的短暂锁等待也会频繁触发日志写入,增加 I/O 开销且产生噪声。如果设太长(如 10s),死锁检测间隔太长,死锁的 session 要等 10s 才被 abort——这段时间持锁的 session 可能阻塞了一大片其他 session。
### 配错后的症状
- 用户报告"查询卡住",但你不知道是锁等待、慢 I/O 还是网络延迟——因为没有 `log_lock_waits` 日志。
- 死锁检测的 ERROR 日志出现频率高——但你需要它们之外的信息(哪个 session 持锁、持了多久)来定位根因。
### 查验方法
```sql
SHOW log_lock_waits;
SHOW deadlock_timeout;
-- 查看当前的锁等待情况
SELECT
l.pid, l.locktype, l.mode, l.granted,
a.usename, a.query, a.state,
NOW() - a.query_start AS query_duration,
a.wait_event_type, a.wait_event
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE NOT l.granted
AND a.pid != pg_backend_pid()
ORDER BY a.query_start;-- 追踪锁等待链
SELECT
blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocked.wait_event_type AS blocked_wait_type,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query,
blocking.state AS blocking_state,
NOW() - blocking.query_start AS blocking_duration
FROM pg_locks bl
JOIN pg_stat_activity blocked ON bl.pid = blocked.pid AND NOT bl.granted
JOIN pg_locks lb ON bl.locktype = lb.locktype
AND bl.database = lb.database
AND bl.relation = lb.relation
AND lb.granted
JOIN pg_stat_activity blocking ON lb.pid = blocking.pid
WHERE blocked.pid != pg_backend_pid();
```text
**正确做法**:`log_lock_waits = on`。`deadlock_timeout` 保持 1s 或适当缩短到 500ms——前提是你的日志系统能承受这个频率的写入。同时配 `log_line_prefix` 带 `%p`(PID)和 `%x`(事务 ID),以便在日志中关联锁等待事件与特定 session。
---
## 十一、log_min_duration_statement + auto_explain 的组合陷阱
### 这些参数在内核里控制什么
`log_min_duration_statement`(默认 `-1`,即禁用):执行时间超过此值(毫秒)的 SQL 会被记录到日志中。
`auto_explain`(PG 的 contrib 扩展)是一个自动 EXPLAIN 工具:在 `shared_preload_libraries` 中加载后,当查询的执行时间超过 `auto_explain.log_min_duration` 时自动记录查询的 EXPLAIN 输出。它的关键选项:
- `auto_explain.log_min_duration`:记录阈值(-1 = 禁用)
- `auto_explain.log_analyze`:是否实际执行查询并收集 `ANALYZE` 数据(`EXPLAIN ANALYZE`)
- `auto_explain.log_buffers`:是否统计 buffer 访问
- `auto_explain.log_timing`:是否记录每个节点的实际执行时间
- `auto_explain.log_level`:输出日志的级别
### 为什么组合在一起是陷阱
**陷阱 1:阈值设太低导致日志洪水。** `log_min_duration_statement = 0`(记录所有查询)和 `auto_explain.log_min_duration = 0`(为所有查询生成 EXPLAIN)——在你的本地开发环境可能无害(几百条查询/分钟),但在生产环境有 10000 QPS 时,日志写入 I/O 会吃掉存储带宽,而且多数日志是你不需要看的正常快速查询。
**陷阱 2:`auto_explain.log_analyze = on` + 低阈值的双重执行。** 注意 `auto_explain.log_analyze = on` 意味着 `auto_explain` 不仅输出查询计划——它需要**实际执行这个查询**来收集 `ANALYZE` 数据(`actual time`、`rows`、`loops`)。对于本身就是慢查询的操作,这等于**把慢查询跑了两次**——一次是用户请求的正常执行,一次是 `auto_explain` 的 ANALYZE 执行。一个本来要跑 5s 的慢查询,现在跑了两遍 5s,消耗翻倍。
**陷阱 3:`log_min_duration_statement` 和 `auto_explain.log_min_duration` 不一致。** 如果 `log_min_duration_statement = 1000`(1s)但 `auto_explain.log_min_duration = 5000`(5s),你会看到日志中有很多"慢查询"(1-5s)但没有对应的 EXPLAIN 输出——因为它们跑得还不够慢,未触发 `auto_explain`。排查时你不知道这些查询为什么慢。反过来,如果 `auto_explain.log_min_duration` 小于 `log_min_duration_statement`,你会看到 EXPLAIN 输出但看不到原始查询的日志行——日志中的信息断片了。
### 配错后的症状
- PG 日志文件快速增长,`pg_current_logfile()` 的大小持续增加。
- 观察到的查询延迟远高于 `pg_stat_statements` 中记录的值——可能是因为 `auto_explain.log_analyze` 的二次执行放大了延迟。
- `pg_stat_statements` 中 `calls` 计数比预期高一倍(正常查询 + auto_explain 的 ANALYZE 执行各计数一次——虽然 PG 15+ 中 auto_explain 的 ANALYZE 不再更新 `pg_stat_statements`,但 CPU 和 IO 的双重消耗仍然存在)。
### 查验方法
```sql
-- 当前设置
SHOW log_min_duration_statement;
-- 检查 auto_explain 扩展是否加载
SELECT * FROM pg_extension WHERE extname = 'auto_explain';
-- 如果加载了,检查其设置
SELECT name, setting FROM pg_settings WHERE name LIKE 'auto_explain%';
-- 如果没有加载但需要排查,在 session 中临时加载
LOAD 'auto_explain';
SET auto_explain.log_min_duration = 5000; -- 5s
SET auto_explain.log_analyze = true;
SET auto_explain.log_buffers = true;
SET auto_explain.log_timing = true;# 检查日志当前大小和增长速度
du -sh /var/log/postgresql/
```text
```sql
-- 从 pg_stat_statements 观察 overhead
SELECT query,
calls, mean_exec_time, max_exec_time,
ROUND(stddev_exec_time, 2) AS stddev_ms,
shared_blks_read, shared_blks_hit
FROM pg_stat_statements
WHERE query LIKE '%auto_explain%'
ORDER BY total_exec_time DESC
LIMIT 5;正确做法:
log_min_duration_statement设为 1000-5000ms(1-5s),根据你对”慢”的定义调整。不要设成 0。auto_explain.log_min_duration设为与log_min_duration_statement相同或略大的值。auto_explain.log_analyze = on谨慎使用——它让慢查询执行两次。建议只在需要深入排查某个特定问题时临时打开(ALTER SYSTEM SET auto_explain.log_analyze = on; SELECT pg_reload_conf();),排查完成后关闭。auto_explain.log_nested_statements = on配合使用——auto_explain默认只记录顶层查询,不记录函数内部的 SQL。如果你的慢查询来自plpgsql函数,打开它才能看到内部 SQL 的 EXPLAIN。
十二、总结:配置审计的思维框架
上面 11 个 GUC 都是具体的参数,但单独记每个参数的”正确值”意义不大——同一台服务器上的 OLTP 和 OLAP 数据库的配置完全不同,今天的正确值在负载变化后可能变成明天的错误值。比记住数值更重要的是审计框架。
做一次完整的 PG 配置审计,按这个顺序走:
内存安全边界检查——
shared_buffers+work_mem × max_connections × 平均操作数+maintenance_work_mem × autovacuum_max_workers的总和,是否在物理内存容量的 60-70% 以内?/proc/meminfo+pg_buffercache同时确认。优化器决策验证——不只看
EXPLAIN输出,要用SET enable_seqscan = off强制索引扫描对比实际执行时间。如果强制索引更快但优化器默认选了 Seq Scan,random_page_cost或effective_cache_size可能不准。持久性语义审计——
fsync必须是on。synchronous_commit按 session 级别区分:金融交易写 →on,日志上报 →off。不要全局关。维护操作效率检查——
VACUUM VERBOSE看index scans是否大于 1,pg_stat_progress_vacuum看max_dead_tuples是否太小。如果多趟索引扫描频繁,增大maintenance_work_mem。阻塞与超时防护——
idle_in_transaction_session_timeout是否设了非零值?deadlock_timeout是否保持在合理范围(500ms-2s)?log_lock_waits是否on?可观测性开销评估——
log_min_duration_statement是否设了合理的阈值(而不是 0 或 -1)?auto_explain.log_analyze是否在不必要时保持开启?
每一步都配查验 SQL 或 shell 命令(见上面各节),而不是凭感觉改参数。PG 的配置文件中每改一个参数,都应该能回答这三个问题:你为什么认为当前值有问题?你通过什么视图/日志确认了这个问题?你改完后通过什么指标验证改善?
上一章:大版本升级与迁移实战 系列索引:PostgreSQL 内核机制深度拆解
参考资料
官方文档
- PostgreSQL Documentation, Chapter 19: Server Configuration — 每个 GUC 的正式定义和语义
- PostgreSQL Documentation, Chapter 30: Reliability and
the Write-Ahead Log —
fsync、synchronous_commit、wal_sync_method的系统级解释 - PostgreSQL Documentation, Chapter 63: B-Tree Indexes —
maintenance_work_mem在 CREATE INDEX 中的使用细节
源码(PG 17)
src/backend/storage/buffer/bufmgr.c:BufferAlloc()、Clock Sweep 替换算法——理解shared_buffers的内核行为src/backend/optimizer/path/costsize.c:cost_index()、cost_seqscan()——random_page_cost和effective_cache_size在代价估算中的使用src/backend/access/transam/xlog.c:XLogFlush()、XLogBackgroundFlush()——fsync和synchronous_commit的写入路径src/backend/storage/lmgr/deadlock.c:DeadLockCheck()——deadlock_timeout的驱动逻辑src/backend/commands/vacuum.c、src/backend/access/heap/vacuumlazy.c:VACUUM 中的maintenance_work_mem使用src/backend/storage/ipc/shmem.c、src/backend/port/sysv_shmem.c:共享内存分配与 huge page 逻辑
相关章节
- 第 1
章:进程模型与共享内存 — Backend fork 的开销与
work_mem的内存炸弹原理 - 第 4
章:WAL 内部机制 — WAL 写入路径、
fsync和synchronous_commit的内核解释 - 第
5 章:Buffer Manager — Clock Sweep、double
buffering、
shared_buffers上限解释 - 第 6
章:锁管理器 —
死锁检测算法、
deadlock_timeout的驱动机制 - 第 8
章:VACUUM 与 Freezing — VACUUM
的内部流程、
maintenance_work_mem不足的多趟索引扫描 - 第 10
章:统计信息与代价模型 —
代价模型详解、
random_page_cost和effective_cache_size的完整上下文 - 第 12
章:执行器与表达式求值 — Hash Join / Sort 内存分配与
work_mem的关系
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【PG 内核】数据恢复与损坏应对:PITR、pg_resetwal 和页面损坏的边界
拆解 PostgreSQL 数据恢复路径的内部机制与操作边界:PITR 的三个关键窗口与 timeline fork 原理、pg_checksums 的校验粒度与盲区、pg_resetwal 的 hint bit 代价与 VACUUM FULL 陷进、pg_dump 并行调度的内部策略。重点在于每种操作做什么、不做什么、哪些后果不可逆。
【PG 内核】大版本升级与迁移实战:pg_upgrade --link 为什么快以及为什么没有回滚
拆解 pg_upgrade 的三种模式(--link 硬链接零拷贝、--clone CoW 快照、--copy 物理复制)的执行流程、内部机制和不可回滚的根本原因;逻辑复制跨版本迁移的低停机方案及序列/large object/DDL 三大盲区;四种常见坑的根因与应对;附带迁移方案决策树,从小库到大库选哪种方案一次说清。
【PG 内核】Buffer Manager:为什么 shared_buffers 不是越大越好
拆解 PostgreSQL Buffer Manager 的核心机制:shared_buffers 的内部组织(BufferDescriptors 数组、Buffer Table hash table、buffer pool)、Clock sweep 替换算法的完整源码路径、buffer 四态状态机与 pin/unpin 协议、bgwriter 的触发条件与脏页写入策略、BAS_BULKREAD/BAS_VACUUM ring buffer 的缓存隔离机制。用 pg_buffercache 实验观察 buffer 分布和 clock sweep 行为,解释为什么 shared_buffers 超过 8-10GB 后回报递减——double buffering、checkpoint IO 尖峰和 clock sweep 扫描延迟的三重反噬。
【PG 内核】查询规划器 — 统计信息与代价模型:优化器为什么选错了索引
拆解 PostgreSQL 查询优化器的决策基础:pg_statistic 中 MCV/histogram/correlation 的存储结构、ANALYZE 的采样流程与精度边界、clauselist_selectivity 如何逐层估算选择率、seq_page_cost 等代价常量的物理意义与调优依据、CREATE STATISTICS 解决多列相关性问题、以及统计信息漂移的诊断 SQL 与排查路径。读完你能回答:优化器为什么选 Seq Scan 而不是你建的索引,以及怎么定位根因。