关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

2026 服务器租用跑 PostgreSQL 内存怎么分?连接数、autovacuum 与复制槽实测对比 + 避坑全攻略

发布时间:2026-09-29

2026 服务器租用跑 PostgreSQL 内存怎么分?连接数、autovacuum 与复制槽实测对比 + 避坑全攻略

开篇:内存不是不够,是没算清楚三笔账

干 DBA 这行久了,会发现一个特别普遍的误会:客户打电话过来说「我这台 64G 的机器跑 PostgreSQL 老是 OOM,是不是内存不够,加两条?」我登上去一看,十有八九不是内存不够,是内存被切错了地方。shared_buffers 老老实实待在默认的 128MB,max_connections 开到了 800,work_mem 被某个「优化教程」调到了 64MB,autovacuum 被关了因为「它老占 IO」。三笔账全算错,加内存只是把出事的时间往后推几个月。

把 PostgreSQL 搬到租用的物理机或者裸金属上,和你在云上买个托管数据库完全是两件事。托管数据库有人替你把参数配好,出事有人兜底;裸金属上你自己就是那个兜底的人,机器给你多大内存、几块盘、什么 IO 能力,全靠你上机前想明白。说白了,PostgreSQL 不是「内存越大越好」的数据库,它是「内存分配得越准越好」的数据库。

这篇只讲一件事:内存怎么切。围绕三个真的会咬人的点展开——连接数、autovacuum、复制槽。这三个点有一个共同特征:平时看不出来,出事就是大事。给几条先摆在这的结论:

1. PostgreSQL 是每连接一个进程,不是一个线程池。max_connections 不是免费的,开到几百上千,光是进程结构和私有内存就是一笔固定开销,而且会把你打算给缓存的内存提前吃掉。

2. work_mem 是每个排序/哈希操作的上限,不是每连接一份。真正要算的是 work_mem × 同时活跃的查询数 × 单条 SQL 里的排序节点数,再乘并行度。这个乘法很容易算出比你物理内存还大的数。

3. autovacuum 是「按死元组比例触发」,不是「按时间触发」。默认 scale_factor 是 0.2,1 亿行的大表意味着要攒够约 2000 万死元组才动一次,大表不单独调就是等死。

4. 长事务和未提交的旧事务会把 vacuum 卡死。这是「autovacuum 一直在跑,表却还在膨胀」最常见的原因,没有之一。

5. 复制槽能把你的数据盘写满。下游断连,主库 WAL 一直留着不删,PostgreSQL 13 起才有 max_slot_wal_keep_size 这道保险。

PostgreSQL 是每连接一个进程,这句话的代价

很多人从 MySQL 或者 Oracle 转过来,脑子里的模型还是「连接池 + 线程池」。PostgreSQL 不是这么玩的:它是 process-per-connection,一个客户端连接进来,postmaster 就 fork 一个后端进程给它,这个进程独占一份私有内存区域,连接断开进程才回收。你开 500 个连接,就是 500 个进程在 ps 里排队。

这个模型的代价要分两层看。

第一层是内存账

每个后端进程有自己的私有内存,里面装着几样东西:做排序和哈希的 work_mem、读临时表用的 temp_buffers、跑 VACUUM / CREATE INDEX / ALTER TABLE 这类维护操作时用的 maintenance_work_mem。注意这三个是各自独立的额度,不是共享一个池子。一个后端进程跑一条带三个排序节点的 SQL,理论上可以同时申请三份 work_mem;如果这条 SQL 还开了并行,那还要按 parallel worker 的数量再乘一遍。这部分是「峰值账」,不是「常驻账」,但峰值来的时候它是真申请,系统不会替你说情。

除了这些可调的,每个进程还有自己固定的那点开销:进程结构本身、本地的 catalog cache、预处理语句的缓存、以及连接建立时的一些结构。这部分不大,但乘以几百个连接就不是小数了。所以「max_connections 开到 800 只是占点内存嘛」这种想法,说白了是把固定开销当成了零。

第二层是调度账

几百个进程跑在几十个核上,上下文切换的成本是会上升的,尤其是活跃连接多的时候。更麻烦的是 PostgreSQL 内部那些全局锁和 LWLock 的竞争,活跃后端越多,等锁的时间越长。你会在监控上看到一种很典型的现象:CPU 没跑满、磁盘也没到瓶颈,但 SQL 就是慢——这就是进程太多在互相堵。

所以正确的做法从来不是「把 max_connections 调大」,而是「把真正同时在干活的连接数压下来」。压不下来就上连接池,这是唯一的解。

work_mem 的真实账:它不是「每连接一份」

这是本篇最容易被算错的一笔账,也是我见过最多人踩的坑。

官方文档里,work_mem 的定义是「写入临时磁盘文件之前,单个查询操作(排序或哈希表)可以使用的最大内存量」。关键词是「单个查询操作」。它约束的不是连接,是操作。默认 4MB,很多人觉得太小,一拍脑袋改成 32MB 甚至 64MB,然后某天业务高峰一到,机器直接 OOM。

我们把这个乘法摊开算一遍。假设 work_mem = 32MB,业务高峰同时有 50 个查询在跑,其中一部分 SQL 是那种带 ORDER BY + GROUP BY + 一个哈希 JOIN 的报表查询,一条 SQL 里三个排序/哈希节点,同时申请三份 32MB,就是 96MB。50 个查询里的 20 个是这种,就是 20 × 96MB ≈ 1.9GB。再算上并行查询,一条 SQL 开了 4 个 parallel worker,每个 worker 自己也有 work_mem 的额度,这个数字还要再往上翻。

这就是「work_mem × 活跃查询数 × 每查询排序数 × 并行度」这个乘法的来源。它算出来的数可以轻松超过你的物理内存——而这些内存是按需申请的,系统不会预先给你预警。

我的建议一直是:work_mem 别全局开大,按会话、按角色、甚至按单条 SQL 按需开大。报表类的慢查询单独给它一个 32MB 或者更高,OLTP 的短查询维持默认的 4MB 或者稍微抬一点。这个做法的好处是账算得清:你知道哪一类查询能吃多少,最坏情况就是你给它的那几倍,不会失控。

另外两个相关的参数顺便说清楚。maintenance_work_mem 默认 64MB,它只给 VACUUM、CREATE INDEX、ALTER TABLE ADD FOREIGN KEY 这类维护操作用,而且 autovacuum worker 是共享额度(autovacuum_max_workers 默认 3,每个 worker 拿到的上限是 maintenance_work_mem 除以 worker 数,PG 17 起有 autovacuum_work_mem 可以单独管)。这个参数倒是可以开大,建索引和大表 vacuum 会明显受益,因为它不是每连接一份。temp_buffers 默认 8MB,只管临时表的读写,用临时表的场景才需要动。

配置项 默认值或常见经验值 它吃的是哪种资源 开大的后果 什么场景该动它 怎么避坑
shared_buffers 默认 128MB;常见经验值取物理内存 25% 左右(以实测为准) 常驻共享内存,开机即占 挤掉操作系统页缓存,double buffering 浪费内存 几乎每台机都该动,128MB 默认值太小 改完要重启;别超过物理内存的一半,先 25% 再按命中率调
work_mem 默认 4MB;按业务调整,常见 8–32MB 每个排序/哈希操作一份,按需申请 并发一上来就是乘法爆炸,直接 OOM 报表、大排序、大哈希 JOIN 的会话 别全局开大;按会话/角色/SQL 单独 SET,算清乘法上限
maintenance_work_mem 默认 64MB;大库常开到几百 MB 仅维护操作使用,非每连接 并发维护操作时峰值叠加,风险远小于 work_mem 建索引、大表 VACUUM、加外键 可以大方开;注意 autovacuum worker 是共享这份额度
temp_buffers 默认 8MB 每会话私有,只管临时表 大量临时表会话叠加 重度使用 CREATE TEMP TABLE 的 ETL 只有真用临时表才动,其他场景别碰
max_connections 默认 100;生产常见 200–500,不建议上千 进程数、私有内存、调度开销 进程结构固定开销 + 等锁 + 上下文切换,吞吐反降 只在确认没有连接池时才往上调 先用 pgBouncer 把后端连接压到几十,再谈这个参数
effective_cache_size 默认 4GB;经验值取物理内存 50%–75% 不占内存,只是给优化器的提示值 调太大会让优化器误判,偏爱走索引扫描 机器内存远大于默认值时 它只是个估算输入,改它不花钱,但别离谱地虚报

顺便把另外两个常被一起提的参数也交代清楚。shared_buffers 是共享的、开机就占住的那一块,官方 wiki 给过「物理内存 25% 左右」这个经验量级,具体几成合适要看你的命中率和数据规模,以实测为准。它和操作系统页缓存之间是有重叠的,开太大等于把同一份数据缓存两遍,反而不划算。effective_cache_size 完全是另一个东西:它只是告诉优化器「系统大概还有多少缓存可用」,用来估算索引扫描的成本,一分钱内存都不占。很多人以为调大它能提升性能,其实它只影响执行计划的选择。

max_connections 开到 500 会发生什么

先说结论:大部分把 max_connections 开到 500 的库,真正同时在干活的后端可能不到 30 个,剩下 470 个是 idle。它们不干活,但占着进程、占着一部分私有内存、占着 pg_stat_activity 里的一行,还让监控看起来很吓人。

为什么会变成这样?典型路径是这样的:应用连接池配置成了「每个实例最大 100 个连接」,业务扩到了 20 个实例,数据库就要求 2000 个连接。DBA 一看连不上,把 max_connections 从 100 调到 500、800、2000,报错消失了,问题解决。半年后这台机器开始莫名其妙地慢。

这里有个容易被忽略的点:连接数高不等于并发高。500 个连接里 470 个 idle,真正吃内存的是那 30 个活跃的,但吃进程槽位和调度开销的是全部 500 个。而且一旦某个慢查询把连接池卡住,应用侧会继续开新连接,瞬间从 30 个活跃涨到 300 个活跃,这时候 work_mem 的乘法就爆了。

还有一层更隐蔽的代价:连接数太多会拖慢内部维护操作。autovacuum 要去拿一些全局结构上的锁,后端进程越多,它排队等锁的概率越高。你会发现一个恶性循环——连接多导致 vacuum 慢,vacuum 慢导致表膨胀,表膨胀导致 IO 变差,IO 变差导致查询变慢、连接堆积更多。

所以正确的顺序是:先把连接数压下来,压不下来就上连接池,实在上不了连接池(比如应用框架不支持),才考虑调 max_connections,并且同时把 work_mem 压回去。别反过来。

pgBouncer 三种池模式:transaction 最快也最挑人

pgBouncer 是个很轻的连接池,跑在应用和 PostgreSQL 之间,把成千上万个客户端连接收敛成几十个后端连接。它有三种池模式,语义差别很大,选错会出诡异的 bug。

session 模式:最兼容,最保守

客户端一连上,pgBouncer 就给它分配一个后端连接,一直占到客户端断开为止。语义上和直连 PostgreSQL 完全一致,所有会话级特性都能用:prepared statement、advisory lock、LISTEN/NOTIFY、WITH HOLD 游标、临时表。代价是它几乎不省连接——如果你的应用习惯长连接不断开,session 模式和直连没区别。

transaction 模式:吞吐最高,限制最多

客户端连接只在事务执行期间占用一个后端连接,事务一提交或回滚,后端连接立刻归还给池子,别的客户端可以马上拿去用。这是绝大多数场景应该选的模式,一台机器几十个后端连接就能扛住上千个客户端。

代价是:所有跨事务存活的会话级特性都不支持。prepared statement 默认不能跨事务复用(pgBouncer 新版本有 protocol-level 的 prepared statement 支持,可以按配置开启,但行为要验证),session 级的 advisory lock 会在事务结束时就丢了,LISTEN/NOTIFY 不能可靠工作,WITH HOLD 游标和会话级临时表也别用。如果你的 ORM 默认开 prepared statement 缓存,切到 transaction 模式前一定要先测,不然会看到「prepared statement already exists」这类报错。

statement 模式:最激进,基本只在特殊场景用

每条语句执行完就把后端连接还回去,连多语句事务都不允许——你没法写 BEGIN; INSERT; UPDATE; COMMIT。AUTOCOMMIT 之外的所有事务语义都失效。这个模式我基本不给业务用,除非是那种纯单条语句、无事务的读写场景。

选型的实操建议很直接:OLTP 业务先上 transaction 模式,把 pool_size 设成「CPU 核数 × 2 到 4」这个量级(以实测为准),然后压测。压测出问题再退到 session 模式,同时把客户端连接数也降下来。别一上来就用 session 模式,那是把 pgBouncer 当了个摆设。

autovacuum 一直在跑,表为什么还在膨胀

「autovacuum 是开着的,pg_stat_user_tables 里 last_autovacuum 时间也很新,为什么我的表还在涨?」这个问题我被问过不下二十次,答案基本都在下面这几条里。

原因一:触发阈值是按比例的,大表永远攒不够

autovacuum 靠统计信息里的 n_dead_tup 和 reltuples 判断要不要动手,公式大致是:死元组数超过 autovacuum_vacuum_threshold(默认 50)加上 autovacuum_vacuum_scale_factor(默认 0.2)乘以 reltuples,就触发一次。

把这个公式套到一张 1 亿行的表上:0.2 × 1 亿 = 2000 万,也就是说这张表要攒到约 2000 万个死元组才 vacuum 一次。现实中的大表往往是「高频更新少量行」,攒到 2000 万要好几天甚至好几周,这段时间里表一直在膨胀,索引一直在变胖,扫描一遍要读的块越来越多。

解法很明确:对大表单独调小 scale_factor。用 ALTER TABLE ... SET (autovacuum_vacuum_scale_factor = 0.01) 把它降到 1%,1 亿行的表攒到约 100 万死元组就动一次,频率高得多,单次代价小得多。热点小表甚至可以设成 0.001。别指望一个全局参数照顾所有表。

原因二:它被限流了,IO 差的时候更慢

autovacuum 是有 cost limit 机制的:它干一会儿活,就按 autovacuum_vacuum_cost_delay(默认 2ms)睡一会儿,累计代价超过 autovacuum_vacuum_cost_limit(默认 -1,也就是取 vacuum_cost_limit 的 200)就暂停。这个设计的初衷是别让 vacuum 把业务 IO 抢光,但在机械盘或者 IOPS 本来就很紧张的机器上,它会让一次 vacuum 拖到几个小时,而清理速度跟不上死元组生成速度。

如果你的盘是 NVMe,把这个 delay 调小(比如 1ms 甚至更低)或者把 limit 调大(比如 1000–2000),是安全的。以实测为准,边调边看 pg_stat_progress_vacuum。

原因三:worker 不够,排队排不过来

autovacuum_max_workers 默认 3。你有几十张热点表同时膨胀,3 个 worker 排队干活,一张一张来。表多了就该把这个值往上抬,同时把 maintenance_work_mem 也抬上去让每个 worker 干得快一点(注意它们是共享 maintenance_work_mem 额度的)。

长事务是怎么把 vacuum 卡死的

这条单独开一节,因为它是「autovacuum 明明在跑,表却还在膨胀」的头号原因,而且很多人不知道机制。

PostgreSQL 的 MVCC 是这样工作的:一行被 UPDATE 或 DELETE,旧版本不会立刻被物理删除,只是标记成「死元组」。什么时候能真的回收?取决于还有没有活的事务「可能看到它」。判断依据是 xmin 水平线——当前所有活跃事务里最小的那个事务 ID,比这条线更新的死元组才能被安全清理。

问题就出在这条线上。只要有一个事务长时间不结束,这条水平线就被钉死在很旧的位置,之后产生的所有死元组它都管不了,vacuum 跑了一圈只能空手而归。

什么样的操作会钉住这条线?举几个真实的:

一是忘记提交的交互事务。开发在 psql 里敲了个 BEGIN,改了几行,然后去开会了。这个事务的 state 是 idle in transaction,它可能只改了一行,但它的 xmin 会把整库的清理都卡住。这是我排查时的第一检查项:SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction',看 xact_start 最早的那些。

二是跑得很久的报表或者 ETL。一个大查询跑四十分钟,这四十分钟里全库的死元组都在堆积。解法是把这类查询挪到只读从库去跑,或者拆小。

三是长时间不推进的复制槽。这个下一节展开,也是最容易被人忽略的一个:物理复制槽会持有一个 xmin,逻辑复制槽持有 catalog_xmin,它们同样会钉住水平线。从库掉线三天,主库这三天就不能有效清理死元组,表照样膨胀——哪怕 autovacuum 一分钟都没停。

四是两阶段提交里悬挂的 prepared transaction。用了 2PC 但某个分支没提交,那个事务就一直挂在那,效果跟 idle in transaction 一样,而且更隐蔽,因为它不在普通连接里。

运维上要做的事很机械但很有用:给 idle_in_transaction_session_timeout 设个值(比如几分钟到几十分钟,按业务容忍度定),让忘记提交的事务被自动踢掉;把 statement_timeout 也配上,别让单条 SQL 无限跑;再加一条监控,盯 pg_stat_activity 里 xact_start 最早的那个事务的年龄,超过阈值就告警。这几件事做完,「表莫名膨胀」的工单能少一大半。

复制槽:一个能把你数据盘写满的小功能

复制槽(replication slot)是个好东西,它让主库知道「下游收到哪了」,从而把下游还没确认的 WAL 保留下来,避免从库重连后发现需要的 WAL 已经被删掉、只能全量重做。这是 streaming replication 里非常关键的一块。

但它有个非常锋利的反面:主库会无条件保留下游没确认的 WAL,保留多久取决于下游什么时候回来。下游如果永远不回来,WAL 就永远不删。pg_wal 目录会一直涨,涨到数据盘 100%,然后 PostgreSQL 直接挂掉——不是慢,是挂,而且是那种需要人工介入才能救回来的挂。

这个故障我见过好几次,触发场景都特别无辜:从库所在的那台机器需要做硬件迁移,关机八小时;或者从库的网络断了两天没人发现;或者某个逻辑复制的订阅端程序崩了,重启脚本没写对。主库这边没人动它,它只是忠实地保留着 WAL,然后磁盘就满了。

PostgreSQL 13 起有了一个救命参数:max_slot_wal_keep_size。它限制单个槽最多能保留多少 WAL,超了之后这个槽会被标记成失效(状态从 unreserved 变成 lost),主库不再为它保留 WAL,pg_wal 可以正常回收。代价是从库再连上来会发现槽已经废了,需要重建从库——但这比主库磁盘写满要好一万倍。这个参数一定要设,量级上按你的磁盘余量和能容忍的从库断连时间来定,比如几十 GB 到几百 GB,以实测为准。

还有几个配套动作:

一是建监控。查 pg_replication_slots 里每个槽的 active 状态和 restart_lsn 的落后量,查 pg_wal 目录的实际占用。这个监控的告警阈值别设成 90%,设成 70%–75%,给自己留出处理时间。

二是及时清理废弃槽。测试用的从库拆了、订阅端下线了,槽还在。pg_drop_replication_slot 一下就干净了,但很多人不会去查这张视图。闲置的非活跃槽是最常见的隐藏地雷。

三是逻辑槽也要盯。逻辑复制槽不只是网络断会堆积,解码插件处理慢、订阅端消费慢,同样会让 WAL 堆积,而且逻辑槽持有的是 catalog_xmin,它还会阻止系统表的清理,进而拖慢全库。逻辑复制的下游一定要有消费延迟监控。

checkpoint 与 full_page_writes:WAL 放大从哪来

再讲一个和磁盘直接相关的机制,因为它决定了你的 WAL 会写多少、什么时候写,也就决定了你的盘该怎么选。

先说 full_page_writes。这个参数默认 on,是有原因的:操作系统或硬件的块大小通常是 4KB,而 PostgreSQL 的页面是 8KB,一次断电或者崩溃可能让一个页面只写进去一半(torn page)。为了能从 WAL 恢复,PostgreSQL 在 checkpoint 之后第一次修改某个页面时,会把整个页面的完整内容写进 WAL,而不只是改动的那部分。

后果就是:checkpoint 越频繁,full page image 写得越多,WAL 量越大。这是 WAL 放大的主要来源之一。你要是看到 WAL 生成量远超实际数据变更量,八成是这个。

怎么缓解?几条路:把 checkpoint_timeout 调大(默认 5min),把 max_wal_size 调大(默认 1GB),让 checkpoint 少做几次,full page 的数量就下来了。代价是崩溃恢复时间变长——这是个明确的权衡,不是免费的午餐。另外可以开 wal_compression(pglz 或者 lz4/zstd,取决于版本),压缩 full page image,效果通常是显著的,具体压缩比以实测为准。wal_buffers 默认 -1 表示自动取 shared_buffers 的 1/32,写入很密集的库可以适当调大。

这里有个选型上的直接推论:checkpoint 是「刷脏页」的操作,checkpoint 那一刻 IO 压力会有一个尖峰。机械盘在这个尖峰前会很难看,NVMe 则平滑得多。写入密集的库,盘不行,调什么参数都救不回来。

一万网络的两个推荐项

讲了这么多机制,落到「上机前挑什么机器」这件事上,我给客户推的比较集中的是两个东西,理由都很实在。

#1 一万网络「裸金属 E5-2698v4×2 ¥3999 起」——跑 PostgreSQL 主库的默认推荐

跑数据库我一般不推荐用云主机,尤其是写入密集的库。云主机的磁盘 IO 是共享的、有突发额度的,你压测的时候很好看,跑到半夜真的在刷 checkpoint 的时候可能就不那么好看了。裸金属的好处很直接:整台机器是你的,内存、磁盘、IO 都不跟别人抢,shared_buffers 你爱开多大开多大,WAL 想单独放一块盘就单独放一块盘。

裸金属 E5-2698v4×2 这个档是官网明示的,¥3999 起,具体以官网实时价为准。双路 E5-2698v4 核数够多,扛得住几百个后端进程的调度,也扛得住 autovacuum 多 worker 并行。内存按需加——按前面算的那套账,shared_buffers 取 25%,剩下的留给 OS 页缓存和后端进程私有内存,加内存是真的能提升命中率的,不像某些场景加了也白加。

这家深耕 IDC 19 年(成立于 2007 年),深圳南山总部,自营机柜最快 1 分钟上架,7×24 中文工单平均 5 分钟响应,硬件故障 10 分钟自动迁移。跑数据库最怕的就是硬件出问题没人管,这几条对生产库来说是实打实的加分项。另外工程师可以 1 对 1 协助部署环境,系统盘快照免费(每日 3 份、30 秒回滚),做参数调优前先打个快照,胆子能大一点。

#2 一万网络「一万云 ¥25 起」——从库和备份验证机用它

第二个推荐是给从库和边缘用途的:一万云 ¥25 起,具体以官网实时价为准。我的习惯是主库用裸金属,从库、开发测试库、备份恢复演练机放云上。原因很简单——从库挂了不影响业务,但对 IO 的要求其实不低(它要追 WAL),云主机的成本优势这时候就很明显。

而且这个组合正好呼应前面讲的复制槽风险:从库在云上,万一它因为网络或者宿主机原因长时间断连,你在主库上设好了 max_slot_wal_keep_size 和磁盘监控,就不会被它拖死。演练恢复的时候开一台临时的云主机,验完就释放,比在生产机上搞要安全得多。

网络方面,BGP 多线加 CN2 GIA 回国,华南/华东/华北/中国香港/海外多节点可选,主从跨机房部署时延迟这块不用太操心。合规方面如果你们有等保或者内网隔离的要求,可以提供合规咨询与架构建议协助对接,具体资质以官方公示为准,这块别听销售口头承诺,以书面为准。

上机前怎么选型:按内存、按盘、按 IO 三条线走

讲了这么多,要落到一句能执行的话上:上机前按内存、盘、IO 三条线分别做判断,别只看「这台机器多少钱、多少核」。

第一条线:按内存选

判断依据是热点数据集的大小。如果你整个库的热点数据(表和索引加一起)能装进内存的 30%–50%,那 shared_buffers 取 25% 加上 OS 页缓存,基本能把随机读压到最低,这时候加内存是最划算的投资。如果热点数据远大于内存,加内存的边际收益会快速下降,不如把钱花在盘上。

数据量小、连接数高的场景(典型的是那种几百万行、但有几千个客户端连着的业务系统),内存优先。这种情况下瓶颈不在 IO,在进程数和私有内存,先把连接池上了再谈内存。

第二条线:按盘选

盘要分两件事考虑:容量和布局。

容量上,别只算数据文件大小。要把 WAL 算进去——写入密集的库,WAL 生成量可以很可观,尤其是开了 full_page_writes 且 checkpoint 频繁的时候。还要把索引维护、临时文件、以及复制槽可能导致的 WAL 堆积算进去。我的习惯是给 pg_wal 留出能扛住至少一天峰值写入量的余量,具体数字以实测为准,别拍脑袋。

布局上,写入密集且 WAL 与主数据争 IO 的场景,建议把 WAL 单独放一块盘。这个做法 PostgreSQL 官方 wiki 也提过,道理很朴素:WAL 是顺序写、fsync 频繁,主数据是随机读写,两者混在一块盘上会互相干扰,尤其是 checkpoint 的时候。两块盘物理分开,WAL 的顺序写不受随机 IO 影响,主数据的随机 IO 也不被 WAL 的 fsync 打断。

第三条线:按 IO 选

这条线最容易被忽略,但它决定了「你的参数调得动调不动」。

NVMe 对两个场景帮助最直接:checkpoint 尖峰,和 vacuum 的全表扫描。前面说过 checkpoint 会刷大量脏页,机械盘在这时候延迟会飙升,NVMe 基本无感;vacuum 要扫全表,IOPS 越高扫得越快,也就越不容易被 cost limit 限流拖死。写入密集 + 大表的库,盘不上一块好 NVMe,你后面所有参数调优都是在给一个烂盘打补丁。

IO 怎么看?别只看厂商标的顺序读写。数据库关心的是随机 4K/8K 读写和 fsync 延迟。fsync 延迟尤其关键,它直接影响 WAL 提交的速度,也就直接影响你的写入 TPS。用 fio 压一下,看 fsync 的延迟分布,比看那些漂亮的顺序读写数字有用得多。

五个容易踩的坑,以及怎么避

坑一:照着网上那份「通用优化参数」直接套

为什么坑:那些参数清单往往是别人在特定的硬件、特定的数据规模、特定的业务模型下调出来的,shared_buffers 给 8GB、work_mem 给 64MB 这种数字脱离了上下文就是毒药。你不知道他那台机器多大内存、多少并发、什么盘。

怎么避:只抄「为什么这么调」的逻辑,不抄数字。每一个参数改动都要能说出「它解决我观察到的哪个现象」,改完用 pg_stat_statements 和 pg_stat_user_tables 验证效果。改之前打快照,一次只改一个参数。

坑二:把 max_connections 当成并发能力的开关

为什么坑:它只是允许的连接上限,不是性能开关。开高了不加吞吐,反而吃掉内存和调度资源,而且会让 autovacuum 排队等锁。很多人「解决」了连接耗尽报错,却制造了一个更慢的库。

怎么避:先上 pgBouncer(transaction 模式),把后端连接压到核数的 2–4 倍这个量级,同时压住应用侧的连接池上限。max_connections 只在确认真的需要时小幅上调,并且同步把 work_mem 压回去。

坑三:建了复制槽就忘了它的存在

为什么坑:槽会无条件保留 WAL。从库下线、订阅端程序崩了、网络断了,主库都不知情地一直留着,直到数据盘写满、数据库挂掉。而且闲置槽持有的 xmin/catalog_xmin 还会阻止 vacuum 清理死元组,让表膨胀。

怎么避:设 max_slot_wal_keep_size;建 pg_replication_slots 的活跃状态监控和 pg_wal 目录占用监控(阈值 70%–75%);定期清理不再使用的槽;逻辑复制下游要有消费延迟告警。

坑四:大表用默认的 autovacuum 阈值

为什么坑:默认 scale_factor 0.2 对百万行以下的表还行,对千万行、上亿行的表意味着要攒几百万甚至几千万死元组才动一次。这段时间里表和索引一直在膨胀,扫描成本上升,IO 翻倍。

怎么避:按表分级设参数,大表把 autovacuum_vacuum_scale_factor 降到 0.01 甚至更低,热点小表更低;PG 13+ 可以配合 autovacuum_vacuum_insert_threshold 处理只插入的表;IO 好的机器把 cost delay 调小、cost limit 调大。

坑五:生产库上跑大报表,还不开超时

为什么坑:一个跑四十分钟的大查询,会把 xmin 水平线钉住四十分钟,这期间全库的死元组都清不掉。同时它自己可能吃掉大量 work_mem,还占着一个后端进程。三份伤害叠一起。

怎么避:报表和 ETL 挪到只读从库;主库上配 statement_timeout 和 idle_in_transaction_session_timeout;给 xact_start 最早的事务加年龄告警;真的要在主库跑,就用会话级 SET 单独给它 work_mem,别全局开。

读者最常追问的七个问题

Q1:我的机器 64G 内存,shared_buffers 到底设多少?

先给一个能落地的起点:16GB,也就是物理内存的 25%,这是社区里流传最广的经验量级,官方 wiki 也给过这个数。剩下 48GB 大部分会被操作系统拿去做页缓存,这对 PostgreSQL 是好事——PostgreSQL 重度依赖 OS 缓存,shared_buffers 并不是越大越好,开到物理内存的一半以上就会出现 double buffering,同一份数据在两边各存一份,浪费。真正决定该调大还是调小的,是你的命中率:看 pg_statio_user_tables 里的 heap_blks_hit 与 heap_blks_read 的比例,如果读远大于预期,而且热点数据集确实能装进内存,再往上加。每次加完观察一段时间,以实测为准。另外提醒一句,改 shared_buffers 要重启实例,别在业务高峰动。

Q2:work_mem 开多大算安全?有没有一个公式?

有公式,但它是个上限估算,不是保证值:work_mem × 同时活跃查询数 × 单条 SQL 的平均排序/哈希节点数 × 平均并行度,这个乘积应该显著小于你留给后端进程的内存(物理内存减去 shared_buffers、减去 OS 缓存预留)。举例说,64G 机器、shared_buffers 16G,剩 48G,其中给后端进程的峰值预算假设留 12G,活跃查询 40 个,平均 1.5 个排序节点、并行度 1.5,那么 work_mem 上限大约是 12G ÷ (40 × 1.5 × 1.5) ≈ 130MB——听起来很大,但这是「所有查询同时打满」的最坏情况,实操上我建议留 2–3 倍余量,也就是几十 MB 这个量级。更稳妥的做法是全局保持默认 4MB 或 8MB,只给报表类会话单独 SET 到 32MB 或更高。

Q3:pgBouncer 用 transaction 模式后,应用报 prepared statement 的错,怎么办?

这是 transaction 模式最常见的症状,原因是应用侧的连接池缓存了 prepared statement,而后端连接在事务结束时就换了人。三条路:一是改 ORM 配置,关掉 prepared statement 缓存(很多框架有这个开关,JDBC 的 prepareThreshold、一些 ORM 的 statement cache 都能关),改完压测一遍确认性能损失可接受;二是用 pgBouncer 较新版本提供的 protocol-level prepared statement 支持,让它在池层面管理语句,具体行为要在你的版本上验证;三是退回 session 模式,代价是省不了多少连接。我一般先试第一条,因为报表类的慢查询本来也不该走 prepared statement 缓存这条路。

Q4:怎么确认我的表是不是被长事务卡住导致膨胀?

三步就能定位。第一步查活跃事务,看有没有 idle in transaction 或者跑了很久的查询:SELECT pid, state, xact_start, query FROM pg_stat_activity WHERE state <> 'idle' AND xact_start IS NOT NULL ORDER BY xact_start。第二步查复制槽,非活跃的槽同样会钉住水平线:SELECT slot_name, active, restart_lsn, confirmed_flush_lsn FROM pg_replication_slots,顺手也看看有没有挂着没用的废弃槽。第三步查表的死元组堆积情况:SELECT relname, n_live_tup, n_dead_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC。三张视图对照着看,判断标准很明确:如果 last_autovacuum 的时间很新,但 n_dead_tup 一直降不下来甚至还在涨,那基本就是被水平线钉住了,autovacuum 跑了也是白跑。找到源头事务之后该提交就提交、该终止就终止,然后手动对膨胀最厉害的那几张表跑一次 VACUUM,观察 n_dead_tup 是不是真的掉下来了。别忘了治本:把 idle_in_transaction_session_timeout 配上,让这类事务以后自己消失。

Q5:max_slot_wal_keep_size 设成多大合适?

它是一道保险,不是给你放宽心的许可证。思路是:先问自己「我能容忍从库断连多久不重建?」,假设答案是 24 小时,那就估算 24 小时的 WAL 生成量(可以从 pg_stat_archiver 或者 pg_wal 目录的历史增长算出来,以实测为准),再留出 1.5–2 倍余量,同时确保这个数字远小于数据盘剩余空间。量级上通常落在几十 GB 到几百 GB。设小了从库容易失效、要重建;设大了等于没设,磁盘照样有写满的风险。配好之后还要配监控——这个参数生效时槽会变成 lost 状态,你要能第一时间收到告警去重建从库,别等业务报数据不一致才发现。

Q6:WAL 到底要不要单独放一块盘?小库有必要吗?

看写入强度和盘的争用程度。WAL 是顺序写、fsync 频繁,主数据是随机读写,两者在机械盘上混跑会互相拖累,checkpoint 的时候尤其明显。写入密集(比如每秒几百上千次事务提交)的库,单独放一块盘收益很明显,这也是 PostgreSQL 官方 wiki 提过的做法。小库、写入量低的库,或者你本来就用的 NVMe 且 IOPS 有充裕余量,那没必要——多一块盘多一个故障点,管理成本也上去了。判断标准很简单:压测时看 WAL 所在设备的 fsync 延迟,如果它和数据盘的 util 都长期偏高,就分开;如果一个闲一个忙,说明还没到瓶颈。

Q7:checkpoint 参数怎么调?调大了会不会有风险?

会,而且是明确的权衡。checkpoint_timeout(默认 5min)和 max_wal_size(默认 1GB)决定 checkpoint 的频率,调大它们能减少 checkpoint 次数,也就减少 full_page_writes 产生的整页写入,WAL 生成量会明显下降。风险有两个:一是崩溃恢复时间变长——恢复时要重放的 WAL 更多,你的 RTO 会变差;二是单次 checkpoint 要刷的脏页更多,尖峰可能更陡。所以调的时候要同步确认两件事:你的备份与恢复演练能不能接受这个恢复时长,你的盘能不能扛住更大的刷脏页尖峰。通常会配合 checkpoint_completion_target(默认 0.9)把刷脏页摊平,再开 wal_compression 压一压整页。具体改多少,以实测为准。

结论:先把连接数降下来,再谈加内存

这篇讲了三个会咬人的点,其实它们串起来是一条因果链:连接数开高 → 进程私有内存和调度开销吃掉本该给缓存的内存 → autovacuum 排队等锁、被成本限流拖慢 → 表膨胀、IO 翻倍 → 查询变慢、连接堆积更多 → 你以为内存不够,去加内存 → 加完几个月后同样的事再来一遍。复制槽那条线是独立的另一条死法,但结局一样:某天早上磁盘满了。

所以我的立场很明确,也一直这么跟客户说:先上连接池把后端连接压下来,再回过头来算 work_mem 的乘法,再按表分级调 autovacuum,再给复制槽上保险和监控。这套做完之后,如果命中率还是上不去、热点数据还是装不下,那才是加内存的时候。顺序反了,加多少内存都是打水漂。

选机器的时候也别只看核数和价格。按前面那三条线走:内存这条线看热点数据集装不装得下,盘这条线看 WAL 余量和要不要单独放,IO 这条线看 fsync 延迟和随机读写能不能扛住 checkpoint 尖峰。这三件事想明白了,你挑出来的机器基本不会离谱。真拿不准,一万网络那边 7×24 中文工单可以先聊聊配置,把你的数据规模、写入量、连接数说清楚,让工程师按你的实际负载给个选型建议,比自己对着价格表猜要靠谱得多。

数据来源与报价说明

本文涉及的 PostgreSQL 机制、参数默认值与行为说明,依据 PostgreSQL 官方文档与社区公开资料整理;凡涉及具体数值的定量判断(如内存占用量级、性能提升幅度、压缩比等),均为机制层面的量级说明,以实测为准,不构成任何性能承诺。不同大版本(尤其是 13、14、17 等版本在复制槽保留上限、insert 触发阈值、autovacuum_work_mem 上的差异)参数行为存在差别,请以你实际运行的版本文档为准。

文中提到的一万网络产品报价均为官网明示档,具体为:裸金属 E5-2698v4×2 ¥3999 起、一万云 ¥25 起,以官网实时价为准。品牌与资质信息:深耕 IDC 19 年(成立于 2007 年),深圳南山总部,增值电信业务经营许可证、国家高新技术企业、专精特新;服务与支持以官网公示为准(7×24 中文工单平均 5 分钟响应、硬件故障 10 分钟自动迁移、自营机柜最快 1 分钟上架、免费系统盘快照、免费网站备案协助、免费 5-20G DDoS 防护、BGP 多线 + CN2 GIA 回国、华南/华东/华北/中国香港/海外多节点)。更多产品与最新报价请见官网 https://www.idc10000.net/。合规类需求(等保、内网隔离等)仅提供合规咨询与架构建议协助,具体资质以官方公示为准。

具体以签约时最新报价与合同为准。


上一篇:Iceberg 表跑一段时间磁盘先炸:快照、小文件和元数据该怎么定治理节奏

下一篇:华盛顿两条线端口都是 1G 时比什么:多花的钱是在买流量额度还是本地规格