☰
PostgreSQL生产环境配置参数调优实战:从shared_buffers到work_mem的避坑指南
2026/10/19 5:01:39 网站建设 项目流程

接手过不少 PostgreSQL 生产环境的性能问题,发现一半以上的故障,根子都出在配置参数上——要么是装完就丢在那用默认值,要么是从网上复制了一堆“最佳实践”猛灌进去。前两天处理了一个业务线的慢查询问题,最后发现只是 work_mem 设置不合理导致的排序下盘,前后耽误了两天。这篇把 PostgreSQL 里最值得关注的参数逐个拆开聊,讲清楚每个参数背后到底在控制什么、该怎么定初始值、改完之后有什么坑。适合刚接触 PG 的后端同学,也适合准备在生产环境上线的 DBA。

1. 参数调优的整体框架:先看明白这些,再动手改数

1.1 参数从哪里读的:搞清生效层级和生效方式

PostgreSQL 的参数体系比很多数据库要“透明”得多,但也正因为透明,新手容易在改参数时看花眼。首先你要知道参数可以从几个不同层级设置:实例级、数据库级、用户级、会话级,后者的优先级更高。最常见的修改方式是编辑 postgresql.conf 主配置文件,但如果你用 ALTER SYSTEM 命令改过参数,那么它写入的是 postgresql.auto.conf,这个文件的优先级高于 postgresql.conf。

配好参数后,另一个关键点是生效方式。你可以通过 pg_settings 视图的 context 列查看参数的生效时机:

SELECT name, setting, unit, context, vartype FROM pg_settings WHERE name IN ('work_mem', 'shared_buffers', 'max_connections', 'synchronous_commit');

context 是 postmaster 的参数必须重启实例才能生效,比如 max_connections、shared_buffers。context 是 sighup 的参数只需要 reload 即可,比如 work_mem、max_wal_size。reload 操作非常简单:

pg_ctl reload -D /path/to/data

或者在数据库里执行 SELECT pg_reload_conf();。我给团队定过一个规矩:任何参数改动,第一步先查 context,确认要不要重启。因为生产库重启一次涉及主备切换、连接闪断,很多人就是漏了这一步,改完发现“没生效”,然后慌慌张张去重启实例,实际上一个 reload 就能搞定。

1.2 为什么会有人说“PG 调参空间不大”

我经常听到一种说法:PostgreSQL 自调优能力很强,参数不用怎么管。这话对了一半。PG 的默认参数确实照顾了通用场景,很多值偏保守,比如 shared_buffers 默认只有 128MB、max_connections 默认 100,这在小内存开发机上完全够用,但放到生产环境,尤其是高吞吐的 OLTP 业务上,默认值就是用来“保底”的,不是用来“跑满”的。

但反过来说,调参也不是把内存参数拉满就完事了。我见过太多人一上来就把 shared_buffers 调到物理内存的 80%,结果系统卡成幻灯片。调参的第一步其实不是改参数,而是先搞清楚你面对的是什么瓶颈。数据库慢,究竟是 CPU 打满了,还是磁盘 IO 在等,还是锁等待堆积?这些通过 top、iostat、pg_stat_activity 就能快速判断。参数只是资源使用策略的调节旋钮,旋钮拧得再快,也解决不了 SQL 本身写成全表扫描的问题。

另外一个重要习惯:每次只改一个参数,并且记录修改前的基线值。我吃过一次亏,一次性改了六个参数,结果性能确实上去了,但过了一周又退化,回头排查时根本不知道是哪一项起了反作用。后来我给自己定了规矩:一个批次只动一个参数,或者最多一组强相关参数(比如 max_wal_size 和 checkpoint_completion_target),改完至少观察 24 小时再决定下一步。

2. 内存类参数:决定数据库“吃不吃得饱”

2.1 shared_buffers:缓冲区不是越大越好

shared_buffers 是 PostgreSQL 自己的数据页缓存区,所有进程读写数据页都要经过它。很多优化文章一上来就说“把它调到内存的 25%”,这个方向没错,但你要先搞清楚为什么不是越大越好。

PG 的数据页缓存和操作系统的 page cache 是两层缓存。PG 读一个数据页时,如果 shared_buffers 里没有,它要去 OS page cache 里找;OS 也没有,才会真正发起磁盘 IO。写到 dirty 页时,PG 要先把脏页刷给 OS,再由 OS 异步写盘。shared_buffers 设得过大,会导致两个问题:一是 PG 自身管理脏页、查找空闲 buffer 的开销变大;二是留给 OS page cache 的空间变小,而 OS page cache 对顺序扫描和 WAL 写入的加速作用很强,两边失衡会拖慢整体性能。

我给的初始参考值:物理内存 16GB 的机器,shared_buffers 给 4GB 起步;32GB 给 8GB 起步;超过 64GB 的机器可以给到 12GB-16GB,但不要盲目往 32GB 去怼。网上有些配置生成器推荐把 shared_buffers 设成内存的 40%,那通常只适用于专用数据库主机,而且还需要配合大页、关闭 overcommit 等系统调优,普通业务环境照抄很容易出事。

有一个和 shared_buffers 强相关的参数叫 effective_cache_size,它不代表真实缓存,而是告诉优化器“操作系统层面大概还能缓存多少数据”,优化器会用它估算走索引扫描和顺序扫描的成本。一般建议设置为物理内存的 50%-75%。比如 16GB 机器,可以设 10GB。很多人只调 shared_buffers 不调这个参数,结果索引明明更快,优化器却总选顺序扫描,就是因为 effective_cache_size 还在用默认的 4GB,优化器认为缓存太小,顺序扫描“可能更划算”。

2.2 work_mem:排序和哈希操作的内存,一不小心就 OOM

work_mem 是 PG 参数里最容易踩坑的一个。它的作用是控制单个排序、哈希连接、哈希聚合、去重等操作最多能用多少内存。注意,它不是全局共享池,而是“每个操作都可能独立占用这么多”。什么概念?一个复杂查询里有两个排序操作,就可能吃两份 work_mem;100 个会话同时跑排序查询,理论上就可能碰 200 份。

默认 work_mem 是 4MB,OLTP 业务够用,但如果一条 SQL 要对大结果集排序,4MB 根本不够,PG 会把中间结果写到磁盘临时文件,也就是“下盘”。下盘之后查询速度会慢一个数量级。我遇到过一个真实的订单查询, EXPLAIN (ANALYZE, BUFFERS) 里显示 Sort Method: external merge Disk: 342MB,临时文件写到磁盘,整个查询跑了 12 秒。把 work_mem 从 4MB 调到 64MB 之后,排序全程在内存完成,查询降到 200 毫秒。

但这里有个极大的坑:千万不要为了这种查询把 work_mem 调到 1GB 甚至 2GB。你想着有一天碰上大排序时能扛住,但实际上只要同时来几十个并发查询,内存立刻爆炸。我见过一个团队把 work_mem 设成 256MB,结果高峰期数据库直接 OOM,实例被系统杀掉。事后排查时算了一笔账:并发 80、每个会话平均两个排序操作,理论上内存占用就是 80 × 2 × 256MB = 40GB,而机器只有 32GB,不炸才怪。

合理的做法是:先定位需要大内存的 SQL,再为这类查询单独设置会话级参数,或者用资源组限制并发。全局值从 16MB-64MB 起步,边跑边观察。观察方法很简单,看慢查询日志或者 pg_stat_statements 中是否有大量 temp bytes,如果只有个别查询下盘,就针对性优化那条 SQL,而不是无脑调全局参数。

另外建议顺手把 temp_file_limit 设上,比如 8GB。这个参数限制会话产生的临时文件总量,防止某条变态 SQL 把磁盘写满。生产环境里我见过最夸张的一次,一条错误的聚合 SQL 生成了 600GB 临时文件,直接把数据盘写爆。设置 temp_file_limit 之后,超过限额的查询直接报错,好过拖垮整个实例。

2.3 maintenance_work_mem:维护操作要用,别和 work_mem 搞混

maintenance_work_mem 是给 VACUUM、CREATE INDEX、REINDEX、添加外键等维护操作使用的内存。它和 work_mem 的区别在于:维护操作通常只有少数几个在执行,所以这个参数可以给得比 work_mem 大很多,默认值是 64MB,生产环境我一般建议直接给 1GB。

为什么这个参数值得一提?因为 VACUUM 和建索引这类操作的速度直接受内存影响。比如给一张大表加索引,如果 maintenance_work_mem 太小,PG 会一边扫描一边反复排序,建索引时间成倍拉长。而 Vacuum 跑得慢,又会导致表膨胀、事务 ID 回收不及时,最终拖累正常业务。

从 PG 11 开始,还单独拆出了一个 autovacuum_work_mem,专门给自动 VACUUM 用,默认是 -1,也就是沿用 maintenance_work_mem。如果你的自动 VACUUM 经常跑不完,而手动 VACUUM 又很快,可以单独调高 autovacuum_work_mem,避免和手动维护操作打架。调的时候注意观察 autovacuum 进程的实际内存占用,别设得比机器空余内存还大。

3. WAL 与持久化参数:性能和安全之间怎么平衡

3.1 fsync、wal_level 和 full_page_writes:有些便宜不能贪

WAL(Write-Ahead Logging)是 PostgreSQL 崩溃恢复的基石。简单理解,所有修改先写 WAL 日志,再写数据页;崩溃后靠 WAL 重放来恢复。fsync 参数控制 PG 是否确保 WAL 日志真正落盘,默认是 on,表示每次事务提交时都强制刷盘。

我知道很多性能调优文章会建议把 fsync 改成 off,我不建议在任何生产环境这么做。fsync=off 的情况下,数据库会把 WAL 写入交给操作系统缓冲,数据库返回成功但数据可能还在内存里,机器断电或系统崩溃时,不仅最近已提交的事务可能丢失,整个数据目录甚至可能损坏到无法启动。这台机器上如果是可随时重建的测试环境或数据丢了也无所谓的中间层缓存库,可以开 off 来压榨性能;但只要有一点不想丢数据的业务,就别碰。

wal_level 是另一个需要认识的参数,常见取值有 minimal、replica 和 logical。minimal 写入的 WAL 最少,但因为它省略了一些恢复所需的信息,很多功能会受限,生产环境基本不考虑。replica 是生产环境标配,支持备库和 PITR 时间点恢复。logical 在 replica 基础上增加了逻辑解析所需的信息,用于逻辑复制、某些 CDC 工具,代价是 WAL 量更大。如果你只有一个单实例、不需要备库,也不做逻辑复制,wal_level=replica 依然是更稳妥的选择,因为后续加备库不需要再改参数重启。

full_page_writes 默认开启,表示每个 checkpoint 之后第一次修改的页要把整页写入 WAL,这是为了在崩溃恢复时避免“部分写”导致页损坏。关闭它会减少 WAL 量,但一旦出现硬件层面的半页写问题,数据就废了。这个参数通常不需要改,保持默认即可。

3.2 synchronous_commit:拿什么换延迟

synchronous_commit 控制的是事务提交时,WAL 刷盘到什么程度才向客户端返回成功。默认是 on,表示 WAL 必须刷到磁盘才返回成功。把它改成 off,PG 只需要把 WAL 写到操作系统缓冲区就返回成功,事务提交的延迟会大幅下降,尤其适合批量导入、日志写入等场景。

但代价是:如果数据库进程崩溃或操作系统宕机,已提交事务可能还没落盘,会丢最近一小段时间的数据。这个时间窗口通常很小,可能是几十毫秒到一两秒,具体取决于写入频率。对于可容忍极少数据丢失的内部系统或报表库,synchronous_commit=off 是合理的取舍;对于订单、支付这类核心账务系统,我不建议关。

实际操作中,我更喜欢“临时关闭”的做法。比如做一次大规模数据导入,导入过程中 SET synchronous_commit = off;,导入完成后马上 SET synchronous_commit = on;。这样既享受了高吞吐,又不会让生产库长期处于有损风险的状态。如果你有主备库,还有 remote_write 和 remote_apply 两个选项,分别代表“备库收到 WAL 并写入系统缓存”和“备库已经应用完 WAL”,它们主要用来调节主备同步的可靠性等级,配合复制相关的参数一起用。

3.3 checkpoint 相关参数:数据库为什么周期性卡一下

很多 PG 用户都遇到过一种现象:数据库平时很稳,但每隔一段时间就会出现一次明显的 IO 尖峰,延迟跟着飙高,过几分钟又恢复正常。如果你在监控图上看到这种“锯齿状”的延迟曲线,大概率是 checkpoint 在集中刷脏页。

checkpoint 是 PG 周期性地把 shared_buffers 里的脏页刷到磁盘的过程。控制它的两个核心参数是 max_wal_size 和 checkpoint_completion_target。max_wal_size 不是指 WAL 文件的硬性上限,而是触发 checkpoint 的阈值,默认是 1GB。如果业务写入量大,1GB 的 WAL 可能几分钟就写满了,于是 checkpoint 频繁触发。checkpoint_completion_target 表示 checkpoint 的刷盘动作要尽量铺开到下一个 checkpoint 之前的时间段的百分之多少,默认是 0.9,也就是让刷脏页的动作尽量分散,避免瞬间拥堵。

我的习惯是先把 max_wal_size 设到 4GB-8GB,然后观察当前实例每小时产生多少 WAL。计算方法很简单:隔一小时查一次 pg_current_wal_lsn(),转换成字节数相减。目标是把 checkpoint 间隔拉到 15 分钟以上。如果 8GB WAL 也只够撑 10 分钟,说明写入量极大,这时候还要结合磁盘能力评估,单纯加大 max_wal_size 只会让崩溃恢复时间变长,因为恢复时需要重放的 WAL 也更多。

一个常见的配合用法是:同时调大 checkpoint_completion_target 到 0.9 左右,让 PG 把刷脏页的动作均匀铺开。但要注意,如果业务存在明显波峰,比如每天固定时段集中写大批量数据,那么在这些时段 checkpoint 还是可能集中发生。更细的优化方向是调整 bgwriter 的刷盘频率、使用更快的磁盘(SSD),或者开启多个 WAL 段以平滑写入,这些属于更高级的调优,先掌握核心参数再逐步深入。

4. 并发、超时与资源限制参数:给数据库系好安全带

4.1 max_connections:不是越大越好,反而可能是事故导火索

PostgreSQL 的架构是一个连接对应一个操作系统进程,这和很多线程模型的数据库不太一样。连接越多,进程越多,上下文切换开销越大,共享内存和锁的竞争也越激烈。更关键的是,应用层每个连接都可能执行查询,而每个查询又可能申请 work_mem 等内存,所以连接数增大时,内存压力是按倍数放大的。

默认 max_connections 是 100,很多团队一遇到“too many clients”报错,第一反应是把 max_connections 加到 500、1000,这是最危险的操作。之前有个线上系统就是这么处理的,加完确实不报连接数超限了,但运行两个小时后数据库内存被打爆,整库宕机。后来翻日志发现,业务后端实际只维持了几十个数据库连接,只是因为连接池配置不合理,应用重启时瞬间创建了三四百个连接,全堆到数据库上。

正确思路是:应用层必须用连接池(比如 HikariCP、Druid、PgBouncer),把数据库层的并发连接控制在几十到一两百左右。数据库层的 max_connections 设成 200-300 已经非常宽裕,同时等量的应用连接池足够支撑几千 QPS。如果你发现连接数经常打满,优先排查是不是应用层没有用连接池,或者连接池最小/最大空闲数配置不当,而不是无脑加数据库上限。

4.2 三个超时参数:给慢 SQL 和长事务套上缰绳

PostgreSQL 有非常实用的三个超时参数,很多生产事故如果提前设置了它们,完全可以避免。

statement_timeout 控制单条 SQL 的最大执行时间,默认是 0,表示不限制。生产环境我强烈建议设置,比如 30 秒或 60 秒,具体根据你的业务查询类型来定。没有这个限制,一条烂 SQL 慢查询可以把 CPU 跑满,把连接占住,把其他正常的查询拖死。

idle_in_transaction_session_timeout 控制“事务里啥也不干但不提交也不回滚”的会话能存活多久。这种会话危害很大,因为它可能持有表锁,阻塞其他会话的 DDL 甚至 DML,还会阻止 VACUUM 清理死元组,导致表膨胀。我把这个参数设成 30 秒后,很多曾经需要人工杀会话的问题都自动消失了。

lock_timeout 控制一条 SQL 等待获取锁的最长时间,默认也是不限制。没有它,当多个事务互相等待锁时,会形成锁等待链,越堆越长。设成几秒到几十秒,让等待锁的业务快速失败然后重试,远比让它挂在那里等半天下不去好。

有一个真实案例:某个后台系统每天固定时间点出现大量“慢查询”,排查发现不是查询本身慢,而是某个长事务一直没有提交,导致其他事务在等它的行锁。通过 pg_stat_activity 看到 wait_event_type=Lock,再用 pg_blocking_pids() 找到阻塞源头,就是一个开发人员在测试环境用一个事务跑了外部接口,事务挂了两分钟。设置 idle_in_transaction_session_timeout 之后,这类会话被自动断开,问题彻底消失。

4.3 参数初设速查表:照着设,别乱抄

每次写调优相关的文章,都会有人问“能不能给个直接抄的配置”。我会给出一个初始值参考表,但请记住,这只是起点,不是终点。不同业务的写入模式、查询模式、数据量、机器规格差别很大,最终值要靠监控和测试来修正。

参数名作用建议起始值注意事项
shared_buffers数据页共享缓存物理内存的 25%,16GB 机器给 4GB不是越大越好,留足 OS page cache
effective_cache_size告诉优化器系统可用缓存物理内存的 50%-75%影响优化器选索引还是顺序扫描
work_mem排序/哈希操作内存OLTP 4MB-16MB,OLAP 64MB-256MB每个操作独立占用,小心并发放大
maintenance_work_mem维护操作内存1GB配合 autovacuum_work_mem 使用
max_connections最大连接数100-200,配合连接池不要靠它扛应用连接数膨胀
statement_timeoutSQL 最大执行时间30s-60s0 表示不限制,生产环境别用 0
idle_in_transaction_session_timeout事务内空闲超时30s防止长事务阻塞清理和锁等待
lock_timeout锁等待超时5s-30s让等待锁的请求快速失败重试
max_wal_size触发 checkpoint 的 WAL 阈值4GB-8GB结合每小时间 WAL 量观察
checkpoint_completion_targetcheckpoint 刷盘分散度0.9配合 max_wal_size 一起调

这张表放在这里不是让你照抄完就不管了,而是给你一个安全的起点。改完参数之后,一定要通过监控确认没有副作用,再往前走。

5. 常见问题排查实录:这些“参数坑”我基本都踩过

5.1 改了参数不生效?可能被 auto.conf 覆盖了

有一个很经典的问题:在 postgresql.conf 里把 max_connections 从 100 改成了 300,重启后 SHOW max_connections; 一看还是 100。折腾半天,最后发现之前用 ALTER SYSTEM SET max_connections = 100; 写过这个参数,它的值被记录在 postgresql.auto.conf 里,而 postgresql.auto.conf 的优先级高于 postgresql.conf。所以 postgresql.conf 里怎么改都会被 auto.conf 压住。

排查这类问题的方法很简单:

SELECT name, setting, source FROM pg_settings WHERE name IN ('max_connections', 'shared_buffers');

source 列会告诉你当前生效值来自哪,是 configuration file、auto.conf、database 还是 session。如果是 auto.conf,用 ALTER SYSTEM RESET 来清除:

ALTER SYSTEM RESET max_connections;

然后 reload 或重启。这个小问题排查清楚,能省掉好几个人加一个晚上的时间。

5.2 shared_buffers 调大之后反而更卡,内存去哪了

有台机器是 16GB 内存,为了“充分利用”,把 shared_buffers 调成了 12GB,结果数据库频繁出现卡顿,free -g 显示内存几乎耗尽,还开始用 swap。这其实是两个问题叠加:一是 12GB 的 shared_buffers 留给操作系统和其他进程的空间太少,OS page cache 几乎被挤没了,而 PG 很多操作依然依赖 OS page cache 加速,结果整体变慢;二是大缓冲区意味着有更多脏页需要在 checkpoint 时处理,如果刷盘策略没跟上,IO 压力反而更大。

后来把 shared_buffers 降到 4GB,把 effective_cache_size 提到 10GB,数据库稳定下来,性能还反而比 12GB 时更好。另一个思路是给 PG 和系统预留足够的内存,比如机器上还有别的高内存进程时,shared_buffers 只给总内存的 20% 左右比较稳妥。调参不是比谁参数拉得高,而是让各个组件都有合适的资源可用。

5.3 WAL 写满,磁盘 IO 周期性尖峰

某个业务高峰期,监控图上每 10 分钟左右出现一次 IO 尖峰,持续三四秒,数据库延迟从 20ms 飙到 1 秒多。看 pg_stat_bgwriter 视图,发现 checkpoints_timed 一直在涨,说明 checkpoint 是按时间触发的,刷脏页的动作集中爆发。

我先把 max_wal_size 从默认 1GB 加到 8GB,并把 checkpoint_completion_target 保持 0.9,观察了一周,IO 尖峰变得平缓很多。计算了一下,该业务高峰期每小时大约产生 6GB WAL,8GB 差不多能撑 80 分钟才触发一次 checkpoint,足够把刷盘压力分散开。如果磁盘空间紧张或崩溃恢复时间不能接受太长,还需要综合考虑,但正常情况下把 checkpoint 间隔拉到 15-30 分钟是一个合理目标。

还要提一个细节:WAL 目录所在磁盘的 IO 性能是硬瓶颈。如果业务写入量极大,checkpoint 参数调得再好,机械盘的写延迟也会让 WAL 成为瓶颈。这时候唯一有效的办法是升级硬件、拆分写入、或者减少不必要的 WAL 生成(比如适当关闭 long_query 的 statement logging 等)。调参解决“怎么排队”,不解决“能力不够”。

5.4 锁等待堆积,不一定靠改并发参数

锁等待是 PG 里最容易让人误判的问题。表面现象是“数据库慢”“CPU 高”“查询超时”,一查 pg_stat_activity,wait_event_type 全是 Lock,blocking_pid 指向另一个会话。这时候如果你去调 work_mem、调 shared_buffers,一点用都没有。

正确做法是先找到锁的源头,分析持锁会话在干什么。常见的源头就是前文说的长事务、松散的自动化任务、外部接口调用不释放连接。处理手段包括:设置 idle_in_transaction_session_timeout 杀掉空闲悬挂事务;设置 lock_timeout 让后续等待快速失败;对可重入的业务加锁超时逻辑。参数能兜底,但应用层的事务设计才是根本。

我排查过最极端的一个例子:某业务代码在事务里调用了第三方接口,接口超时了 90 秒,事务一直没提交,直接导致所有涉及同一行数据的更新全部锁等待,整个表的操作堆积了几百个会话。最终处理是杀掉那个会话、给事务内的外部调用改成事务外发起、再配置 idle_in_transaction_session_timeout=30s。从那以后这类问题再没复发过。

最后分享一个我个人的工作习惯:每次调参之前,先把当前所有非默认参数导出存档,形如 SELECT name, setting, source FROM pg_settings WHERE source <> 'default';。因为很多时候一周后出问题,你根本想不起来自己改过什么,有这个基线文档,排查效率会高很多。PostgreSQL 的参数系统并不复杂,但它需要你尊重它的规则:先分清生效方式,再弄清楚资源模型,最后小步调整、持续观察。掌握了这种思路,80% 的参数问题都能在十分钟内定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询