硬件选型建议
前面几节讲的都是「软件侧」的调优:连接池怎么配、WAL 与检查点怎么调、磁盘 I/O 怎么优化、统计信息怎么维护。但所有这些优化都有一个天花板——硬件规格。一台内存只有 8GB 的机器,无论怎么调 shared_buffers 也装不下 100GB 的热数据;一块百级 IOPS 的机械盘,无论怎么调 effective_io_concurrency 也扛不住高并发点查。
本节聚焦选型与容量规划:CPU 该选核多还是频率高、内存要买多大、NUMA 机器上要注意什么、网络带宽怎么估、云实例规格怎么映射到真实能力。存储介质、文件系统、RAID、内核参数这些与 I/O 行为直接相关的内容已在 磁盘 I/O 优化 中讲透,本节不再重复,只从「选型」角度给出取舍。
前置知识
阅读本节前,建议先了解:磁盘 I/O 优化、shared_buffers 共享缓冲区、WAL 与检查点优化、并行配置参数
基础概念
四大子系统与数据库负载的对应关系
服务器的四类资源,在 PostgreSQL 里各自对应不同的「消耗者」:
| 子系统 | 谁在消耗 | 不足时的典型症状 |
|---|---|---|
| CPU | 查询执行、规划、压缩、校验和、并发连接的开销 | %usr 长期 > 80%,查询排队,pg_stat_activity 大量 active |
| 内存 | shared_buffers、work_mem、连接上下文、内核页缓存 | 命中率下降、临时文件暴增、换页(si/so)、OOM |
| 存储 | 数据读、脏页写、WAL 写、临时文件、归档 | iowait 高、await 上升、检查点抖动 |
| 网络 | 客户端连接、流复制、备份、归档 | 复制延迟、备份慢、连接超时 |
选型的第一步不是「买什么」,而是判断这台机器的主要瓶颈会落在哪一类。OLTP 交易系统瓶颈通常在存储(随机 IOPS)与内存;报表/分析系统瓶颈在存储吞吐与内存容量;高并发短连接场景则可能在 CPU(连接开销)与网络。
OLTP 与 OLAP 的硬件取向完全不同
同一个参数、同一份预算,在 OLTP 和 OLAP 下的最优分配可能完全相反:
| 维度 | OLTP(交易型) | OLAP(分析型) |
|---|---|---|
| CPU | 中等核数 + 高频率(单核性能优先) | 多核(并行扫描、并行聚合能吃到核数) |
| 内存 | 大容量(缓存热数据 + 高连接数开销) | 大容量(大 work_mem、哈希/排序) |
| 存储 | 高 IOPS 低延迟 NVMe | 高吞吐(顺序扫描为主) |
| 并发 | 高并发小事务 | 低并发大查询 |
| 关键参数 | shared_buffers、max_connections | work_mem、max_parallel_workers |
把 OLAP 的机器拿来做 OLTP,或反过来,都会浪费一半预算。 选型前必须先明确这台机器的负载画像。
为什么数据库性能瓶颈很少在 CPU
很多团队的第一反应是「数据库慢 → 加 CPU」。但在大多数 PostgreSQL 生产环境里,CPU 是最后才会成为瓶颈的资源:
一次点查的执行时间构成(NVMe 环境,缓存未命中)
解析 + 规划 ~0.1 ms (CPU)
shared_buffers 查找 ~0.001 ms (CPU)
物理读(若未命中) ~0.05~0.5 ms(存储,可差 10 倍)
执行 + 返回 ~0.1 ms (CPU)存储延迟的波动范围(微秒到毫秒)远大于 CPU(纳秒到微秒)。所以在排查性能问题时,应该先看存储与内存,而不是先看 CPU。只有在确认 CPU %usr 已接近饱和、且 pg_stat_statements 中耗时最高的是 CPU 密集型算子(排序、哈希、正则、JSON 解析)时,加 CPU 才是对的。
内存容量为什么比 CPU 频率更关键
PostgreSQL 是「内存换性能」的典型系统:
shared_buffers越大,物理读越少;- 剩余内存留给内核页缓存的双缓冲,进一步减少物理 I/O;
work_mem/maintenance_work_mem越大,排序与哈希越不容易落盘;- 索引全在内存时,几乎不产生随机读。
内存不足的代价是非线性的:一旦工作集装不下,命中率下降会导致物理 I/O 激增,进而拖慢所有查询,形成正反馈。因此「内存够大」几乎总是优先于「CPU 更快」。
NUMA:多路服务器上的隐形陷阱
NUMA(Non-Uniform Memory Access,非统一内存访问)是双路及以上服务器的内存架构:每个 CPU 有自己的本地内存,访问本地内存快,访问另一个 CPU 的内存(远端内存)慢,延迟通常差 1.5~2 倍。
双路 NUMA 服务器
┌─────────────┐ ┌─────────────┐
│ CPU 0 │ QPI/UPI │ CPU 1 │
│ + 本地内存 │ ◄────────► │ + 本地内存 │
└─────────────┘ └─────────────┘
▲ ▲
│ 本地访问(快) │ 远端访问(慢 1.5~2 倍)
└────────────────────────────┘对数据库的影响:shared_buffers 是一整块共享内存,如果被分散在两个 NUMA 节点上,一部分访问就会变成跨节点访问。这不是致命的,但会让尾延迟变差。PG18 之前 PostgreSQL 没有 NUMA 感知能力,无法观测共享内存落在哪个节点上;PG18 新增了两个视图填补这个空白(见下文)。
云实例规格与本地物理机的差异
云主机(EC2 / 阿里云 / 腾讯云等)的规格命名(如 c7i.8xlarge、g7.2xlarge)背后是「计算优化型 / 内存优化型 / 通用型」的定位,但必须搞清三件事:
- vCPU 是超线程,不是物理核。8 vCPU 可能是 4 物理核 + 超线程,实际并行计算能力不等于 8 个物理核。
- 网络与存储带宽随规格阶梯式变化。小规格实例的 EBS/云盘带宽被限流,即使盘本身很快也跑不满。
- 存在突发额度(burst)。CPU 积分、IOPS 突发额度用完后性能会断崖式下跌。
因此云上选型不能只看「几核几 G」,要同时看:基准 IOPS、基准网络带宽、是否支持增强网络、云盘类型(GP2/GP3/io2 的差异是数量级的)。
语法与用法
硬件相关参数总表(PostgreSQL 18)
下表汇总与硬件规格直接相关的参数(默认值以 PG 18 为准):
| 参数 | 默认值 | 作用域 | 说明 |
|---|---|---|---|
shared_buffers | 128MB | 全局,需重启 | 数据库自己的页缓存,起点为内存 25% |
huge_pages | try | 全局,需重启 | 是否使用大页(try / on / off) |
huge_page_size | 0(自动) | 全局,需重启 | 显式请求大页大小,如 2MB / 1GB |
shared_memory_type | mmap(Linux) | 全局,需重启 | 共享内存实现,Linux 上大页仅与 mmap 配合 |
max_worker_processes | 8 | 全局,需重启 | background worker 进程池总大小 |
max_parallel_workers | 8 | 全局,动态 | 池中留给并行操作的上限 |
max_parallel_workers_per_gather | 2 | 全局 / 会话,动态 | 单个 Gather 节点的并行度上限 |
max_parallel_maintenance_workers | 2 | 全局 / 会话,动态 | 维护类操作(建索引、VACUUM)的并行度 |
max_connections | 100 | 全局,需重启 | 最大连接数,直接决定内存与信号量开销 |
max_files_per_process | 1000 | 全局,需重启 | 每个后端可打开的文件数(PG18 起只限制后端) |
autovacuum_worker_slots | 16 | 全局,需重启 | autovacuum worker 槽位数(PG18 新增) |
num_os_semaphores | 运行时计算 | 只读 | 报告所需信号量数量(PG18 新增) |
effective_cache_size | 4GB | 全局,动态 | 优化器对「可用缓存总量」的估计 |
记住一条:只有
shared_buffers、huge_pages、shared_memory_type、max_worker_processes这几个是与硬件规格强相关且需要重启的。其余大部分可以动态调整。
查看实例当前的硬件与参数基线
数据库侧的参数只是「请求」,真实的硬件能力要从操作系统读:
-- 一次性查看与硬件相关的关键参数
SELECT name, setting, unit, context, pending_restart
FROM pg_settings
WHERE name IN (
'shared_buffers',
'huge_pages',
'huge_page_size',
'shared_memory_type',
'max_worker_processes',
'max_parallel_workers',
'max_parallel_workers_per_gather',
'max_parallel_maintenance_workers',
'max_connections',
'max_files_per_process',
'autovacuum_worker_slots',
'effective_cache_size'
)
ORDER BY name;pg_settings 的 context 决定了修改方式:
context = postmaster → 改后必须重启实例(shared_buffers、max_worker_processes 等)
context = sighup → 改后 reload 即可(max_parallel_workers 等)
context = superuser → 超级用户可 SET(会话级)
context = user → 普通用户可 SET(会话级)计算 background worker 池子的占用
max_worker_processes 不是一个「只给并行查询用」的参数,它是个共享池,以下功能都会从中取用:
-- 看一看当前池子里有多少 worker,各自被谁占用
SELECT type, count(*) AS workers
FROM pg_stat_activity
WHERE backend_type IN (
'background worker',
'parallel worker',
'logical replication worker',
'walsender'
)
GROUP BY type
ORDER BY workers DESC;
-- 池子总大小
SELECT current_setting('max_worker_processes')::int AS pool_size,
current_setting('max_parallel_workers')::int AS parallel_limit;一条经验规则:池子大小应该 ≥ 并行上限 + 其他消费者。如果同时使用逻辑复制、扩展(如 pg_cron、pglogical)与并行查询,max_worker_processes 需要高于默认的 8,否则逻辑复制可能拿不到 worker(因为并行查询先占满了)。
内存相关的参数与物理内存的对应
把「物理内存」拆解成数据库实际会用的几块,是内存选型的基本功:
-- 估算各内存参数的总占用(简化模型,单位 MB)
SELECT
current_setting('shared_buffers')::bigint * 8192 / 1024 / 1024 AS shared_buffers_mb,
current_setting('max_connections')::int
* current_setting('work_mem')::bigint * 8192 / 1024 / 1024 AS work_mem_worst_mb,
current_setting('maintenance_work_mem')::bigint
* current_setting('max_parallel_maintenance_workers')::int * 8192 / 1024 / 1024
AS maintenance_worst_mb; shared_buffers_mb | work_mem_worst_mb | maintenance_worst_mb
-------------------+-------------------+----------------------
8192 | 3200 | 1024
(1 row)注意
work_mem是每个操作节点可用的内存,不是每个连接。上表的work_mem_worst_mb是最保守的估算(每个连接只算一个节点),真实峰值可能是它的数倍,因为一个查询可能有多个排序/哈希节点。
详细说明
CPU:核数与频率怎么权衡
| 负载类型 | 优先级 | 推荐规格(经验值) | 说明 |
|---|---|---|---|
| 高并发 OLTP | 高频率 | 8~16 物理核,高主频(3.0GHz+) | 单条查询延迟由单核性能决定 |
| 复杂分析 / 报表 | 多核 | 32~64+ 核 | 并行查询能吃到核数,见 并行配置参数 |
| 混合负载 | 平衡 | 16~32 核 | 为并行查询留出空间,同时保证点查延迟 |
| 只读副本 | 多核 | 同主库或更多核 | 承接报表与备份 |
几条必须记住的原则:
- 不要盲目追多核。 PostgreSQL 为每个连接分配一个进程,连接数多时进程切换本身有开销。超过 64 核后,如果没有对应的高并发负载,多数核会闲置。
- 并行度受内存约束,不受核数约束。
max_parallel_workers_per_gather调高会成倍放大work_mem的占用,实战中应「按内存而不是按 CPU 核数」来定。 - 超线程的收益有限。 数据库负载通常已经吃满流水线,超线程带来的提升远低于 2 倍,把 vCPU 数直接当物理核数会高估能力。
- 注意
max_worker_processes的默认值8。 多核机器上如果想让并行查询真正跑起来,这个池子(需重启)往往要先调大。
内存:容量、ECC 与带宽
容量是第一位。一个可操作的经验公式:
所需内存 ≈ 工作集大小(热数据 + 索引)
+ shared_buffers 之外的页缓存(约 1~2 × shared_buffers)
+ 连接数 × 每连接基础开销(约 5~10MB)
+ 排序/哈希峰值(work_mem × 节点 × 并发)
+ 操作系统与监控代理(留 2~4GB)实践中常按「工作集能装进内存」来买:如果热数据是 200GB,那么 256GB 内存的机器能把它几乎全缓存住,性能会有质的提升。
ECC(Error-Correcting Code,纠错码)内存:数据库服务器应当使用 ECC 内存。非 ECC 内存上的位翻转(bit flip)可能悄无声息地损坏数据页,而 PostgreSQL 除非启用数据校验和(PG18 的 initdb 已默认启用),否则无法检测。
内存带宽与通道数:多通道内存(如 8 通道)比双通道的带宽高数倍。对聚合、排序这类内存带宽密集型负载,通道数填满比单条容量更大更重要。务必按主板手册「插满所有通道」,而不是只插两根大容量内存。
存储:容量规划与介质搭配
存储的具体调优见 磁盘 I/O 优化。这里只讲选型层面的三条:
| 用途 | 推荐介质 | 容量规划 |
|---|---|---|
| 数据盘(PGDATA) | 企业级 NVMe,RAID 10 | 预留 30% 以上空间,且 ≥ 预期 3 年增长量 |
WAL 盘(pg_wal) | 独立 NVMe(可低于数据盘规格) | ≥ max_wal_size 的 3~5 倍 |
| 备份/归档盘 | 大容量 HDD 或对象存储 | 按保留策略与恢复点目标(RPO)设计 |
| 临时文件盘 | 独立 SSD(可选) | ≥ 单次最大排序量的 2 倍 |
一条容易被忽视的规则:SSD 的性能与剩余空间强相关,盘越满写放大越大。容量选型时要按「可用空间」而非「标称容量」规划。
网络:复制与客户端连接的带宽规划
网络常被忽略,但在复制、备份与高并发连接场景下会成为瓶颈:
带宽需求的粗略估算
流复制(同步/异步) ≈ 写入速率 × 复制流数量
基础备份(pg_basebackup) ≈ 数据库大小 / 允许的备份时长
WAL 归档 ≈ 写入速率
客户端连接 ≈ 平均查询结果集大小 × QPS| 场景 | 推荐网卡 | 说明 |
|---|---|---|
| 单机(客户端与库同机) | 万兆(10GbE) | 本地回环基本不是瓶颈 |
| 主从复制 | 万兆或更高 | 复制带宽不足会直接累积复制延迟 |
| 备份与归档(独立网段) | 万兆,或独立链路 | 避免备份流量挤占复制与业务流量 |
| 高并发短连接 | 万兆 + 增强网络 | 连接建立/释放的往返开销对延迟敏感 |
注意:同步复制(synchronous replication)对网络延迟极其敏感——每次提交都要等备库确认,网络往返增加 1ms,提交延迟就增加 1ms。跨机房同步复制要用低延迟专线,而不是公网。
NUMA 感知与 PG18 的 NUMA 观测
PG18 之前,PostgreSQL 完全没有 NUMA 观测能力。PG18 新增了两个视图:
pg_buffercache_numa(由 pg_buffercache 扩展提供)—— 把每个共享缓冲区块映射到 NUMA 节点:
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
-- 查看共享缓冲区在各 NUMA 节点上的分布
SELECT numa_node,
count(*) AS buffers,
pg_size_pretty(count(*) * 8192) AS size
FROM pg_buffercache_numa
GROUP BY numa_node
ORDER BY numa_node; numa_node | buffers | size
-----------+---------+---------
0 | 524288 | 4096 MB
1 | 524288 | 4096 MB
(2 rows)该视图暴露的列包括 bufferid、relfilenode、reltablespace、reldatabase、relforknumber、relblocknumber、isdirty、usage_count、pinning_backends、os_page_num、numa_node。
官方警告:作为 NUMA 节点 ID 查询每个页面需要将内存页面分页,第一次执行此函数可能需要明显的时间。它由超级用户或
pg_monitor角色读取。
pg_shmem_allocations_numa —— 展示主共享内存段的各分配块落在哪些 NUMA 节点:
-- 查看共享内存分配在 NUMA 节点上的分布
SELECT name, numa_node,
pg_size_pretty(size) AS size
FROM pg_shmem_allocations_numa
ORDER BY size DESC
LIMIT 10;该视图的列只有三个:name(text)、numa_node(int4)、size(int8)。
官方警告:This view should not be queried by monitoring systems as it is very slow and may end up allocating shared memory in case it was not used earlier. 也就是说,不要把它放进监控采集脚本——它非常慢,且可能触发本未分配的共享内存分配。当前限制还包括:不显示匿名共享内存分配,且不含动态共享内存。
NUMA 节点不均衡不是「错误」
看到 pg_buffercache_numa 的分布不完全均匀,不代表有问题——Linux 的首次触碰(first-touch)策略会让共享内存落在初始化它的进程所在节点。真正需要关注的是远端访问比例是否过高。如果一台机器的 shared_buffers 有 60% 落在单节点上、而所有后端都跑在另一节点,那才有优化空间。常规做法是用 numactl 把 postgres 绑定到固定的节点,或直接使用单路机器。
内核资源与硬件上限
硬件规格决定了 PostgreSQL 会向内核申请多少资源,几个上限必须与硬件一起核对。以下内容来自官方「管理内核资源」一章。
System V IPC 参数(仅在 shared_memory_type = sysv 或非 Linux 平台相关):
| 参数 | 描述 | 运行一个实例所需的值 |
|---|---|---|
SHMMAX | 共享内存段最大大小(字节) | 至少 1kB,默认通常远高于此 |
SHMMIN | 共享内存段最小大小 | 1 |
SHMALL | 系统可用的共享内存总量 | 以字节计同 SHMMAX;以页计为 ceil(SHMMAX/PAGE_SIZE) |
SHMSEG | 每进程最大共享内存段数 | 只需 1 个 |
SHMMNI | 系统级最大共享内存段数 | 类似 SHMSEG |
SEMMNI | 最大信号量标识符数(集合) | 至少 ceil(num_os_semaphores / 16) |
SEMMNS | 系统级最大信号量数 | ceil(num_os_semaphores / 16) * 17 |
SEMMSL | 每个集合的最大信号量数 | 至少 17 |
SEMMAP | 信号量映射条目数 | 参见文档正文 |
SEMVMX | 信号量最大值 | 至少 1000(默认通常 32767,除非必要不要改) |
PG18 新增的 num_os_semaphores 是运行时计算参数,用来提前查看需要多少信号量:
# 在启动服务器之前查看所需信号量数量
postgres -D $PGDATA -C num_os_semaphores它由 max_connections、autovacuum_worker_slots(PG18 新增,默认 16)、max_wal_senders、max_worker_processes 等共同决定。
Linux 平台上不使用 System V 信号量(改用 POSIX 信号量),因此默认设置通常够用——除非显式把
shared_memory_type设为sysv。这也是「不要用sysv共享内存」的原因之一(另一原因是它往往需要非默认的内核设置)。
共享内存大小的系统限制(Linux):
# 允许 16GB 的 System V 共享内存(仅 sysv 模式需要)
sysctl -w kernel.shmmax=17179869184
sysctl -w kernel.shmall=4194304
# 写入 /etc/sysctl.conf 以持久化systemd RemoveIPC:这是从源码安装 PostgreSQL 时最容易踩的坑。systemd 默认会在用户完全注销时删除其 IPC 对象(含共享内存),而系统用户被豁免。如果 postgres 用户不是系统用户,并行查询的共享内存会被随机删除:
WARNING: could not remove shared memory segment "/PostgreSQL.1450751626": No such file or directory两种解法必须至少选一种:
# 解法一(推荐):把 postgres 创建为系统用户
useradd -r postgres
# 解法二:关闭 RemoveIPC
# 编辑 /etc/systemd/logind.conf
RemoveIPC=no官方明确警告:必须确保这两项中的至少一项,否则 PostgreSQL 服务器将非常不稳定。
资源限制(ulimit):
# 查看当前 shell 的限制
ulimit -n # 打开文件数(soft)
ulimit -Hn # 打开文件数(hard)
ulimit -u # 进程数
# BSD 类系统在 /etc/login.conf 中配置(-cur 为软限制,-max 为硬限制)
# :datasize-cur=256M:\
# :maxproc-cur=256:\
# :openfiles-cur=256:\| 层面 | 参数 | 说明 |
|---|---|---|
| 每进程打开文件数 | ulimit -n / max_files_per_process | PG18 起 max_files_per_process 只限制后端(此前 postmaster 打开的文件也计入) |
| 每用户进程数 | ulimit -u | 每个连接一个进程,至少要 ≥ max_connections + 系统其余需求 |
| 系统级打开文件数(Linux) | fs.file-max | sysctl -w fs.file-max=N |
| 连接队列长度(Linux) | net.core.somaxconn | 默认常为 128;高并发短连接场景必须调大 |
| 连接队列长度(新版 FreeBSD) | kern.ipc.soacceptqueue | 同上 |
| 连接队列长度(macOS / 其他 BSD) | kern.ipc.somaxconn | 同上 |
Linux 内存过量使用与 OOM 防护:
Linux 默认的虚拟内存过量使用(overcommit)行为会导致内存压力大时内核 OOM killer 杀掉 postgres postmaster 进程,典型内核消息:
Out of Memory: Killed process 12345 (postgres).官方给出两种缓解方式(可同时使用):
# 方式一:启用严格过量使用模式(不会完全阻止 OOM killer,但显著降低概率)
sysctl -w vm.overcommit_memory=2
# 并可配合调整 vm.overcommit_ratio
# 方式二:把 postmaster 的 OOM 分数调为 -1000,保证它不被 OOM killer 锁定
# 必须在调用 postgres 之前、以 root 身份执行
echo -1000 > /proc/self/oom_score_adj
# 同时设置这两个环境变量,让子进程以正常 OOM 分数(0)运行
export PG_OOM_ADJUST_FILE=/proc/self/oom_score_adj
export PG_OOM_ADJUST_VALUE=0官方说明:
PG_OOM_ADJUST_VALUE也可以省略,默认为零;如果不设置PG_OOM_ADJUST_FILE,子进程会继承 postmaster 的 OOM 分数(即-1000),这是不明智的——关键是要让 postmaster 有优先保护,同时子进程仍可被 OOM killer 处理。
Linux 大页:这是大内存实例(shared_buffers 较大)最值得做的硬件相关优化。它把共享内存从 4kB 页换成 2MB/1GB 页,大幅减少页表项数量与内存管理开销。
前提是内核带 CONFIG_HUGETLBFS=y 与 CONFIG_HUGETLB_PAGE=y。配置流程:
# 1. 查看需要多少个大页(在启动服务器之前)
postgres -D $PGDATA -C shared_memory_size_in_huge_pages
# 3170
# 2. 确认当前大页大小
grep ^Hugepagesize /proc/meminfo
# Hugepagesize: 2048 kB
# 3. 查看系统支持哪些大页尺寸
ls /sys/kernel/mm/hugepages
# hugepages-1048576kB hugepages-2048kB
# 4. 预留大页(本例需 3170 个;若机器上其他程序也用大页则要更多)
sysctl -w vm.nr_hugepages=3170
# 5. 写入 /etc/sysctl.conf 以便重启后仍生效配合数据库侧的参数:
# postgresql.conf
huge_pages = try # 推荐 try:申请失败自动退回普通页,不会启动失败
# huge_page_size = 2MB # 默认 0(自动);可显式请求 2MB 或 1GB
shared_memory_type = mmap # Linux 上大页仅与 mmap 配合生效不要设为 huge_pages = on 而不预留
huge_pages = on 表示必须用上大页,申请失败会导致实例直接启动不了。正确顺序是:先在 OS 层预留(vm.nr_hugepages),确认 huge_pages_status = on,再考虑从 try 改为 on。生产环境建议长期保持 try。
单机、主从与读写分离的硬件分配
同等的预算,如何在一组机器上分配,直接决定架构能撑到多大:
方案一:单机(成本最低)
[ 大规格主库 ] ← 所有读写都在这台
方案二:主从 + 只读副本(推荐)
[ 主库(写 + 关键读) ] ← 高频率 CPU + 大内存
│ 流复制
[ 只读副本(报表 + 备份) ] ← 多核 CPU + 大内存 + 高吞吐盘
方案三:读写分离 + 逻辑复制(复杂)
[ 写主库 ] → [ 读副本 A ](按业务分片)
→ [ 读副本 B ]| 角色 | CPU 取向 | 内存 | 存储 | 网络 |
|---|---|---|---|---|
| 写主库 | 高频率,中等核数 | 大 | 高 IOPS NVMe | 万兆 |
| 只读副本 | 多核 | 大 | 高吞吐 NVMe | 万兆 |
| 备份/归档节点 | 低 | 中 | 大容量 HDD | 万兆(独立链路更好) |
只读副本不是「垃圾桶」。 把报表、备份、pg_dump、ANALYZE 全丢到副本上,副本会先撑不住。备份与归档最好再独立一台,或走对象存储。
云实例规格映射表
把常见云厂商的规格族映射到数据库负载:
| 规格族(AWS 命名,其他云类似) | 定位 | 适合的 PostgreSQL 用途 |
|---|---|---|
m(通用型) | CPU:内存 = 1:4 | 中小型 OLTP、混合负载 |
c(计算优化型) | CPU:内存 = 1:2 | 高并发连接、CPU 密集(但数据库慎选,内存偏小) |
r(内存优化型) | CPU:内存 = 1:8 | 大型 OLTP、大工作集缓存(数据库首选) |
x(超大内存型) | 1:16 及以上 | 超大工作集(数百 GB 到 TB 级热数据) |
i / d(存储优化型) | 本地 NVMe | 需要本地盘低延迟(云盘替代) |
云盘类型的选择(AWS 为例):
| 盘类型 | 定位 | 数据库适用性 |
|---|---|---|
gp2 / gp3(通用 SSD) | 基准 IOPS 与容量挂钩 | 中小型库可用;注意 burst 额度 |
io1 / io2(预配 IOPS SSD) | 可独立预配 IOPS | 生产数据库首选 |
st1 / sc1(吞吐优化 HDD) | 大容量顺序场景 | 备份、归档、日志 |
云盘 IOPS 与容量绑定
gp2 的 IOPS 是「每 GB 3 IOPS」,一块 100GB 的盘只有 300 基准 IOPS——再快的数据库也会被卡住。选云盘时必须同时确认基准 IOPS、是否支持独立预配、以及突发额度的大小。
实战示例
场景一:为 OLTP 系统做整机选型
需求:日均 5000 万次事务、峰值 2000 TPS、热数据集约 150GB、要求 P99 延迟 < 20ms。
第 1 步:确定瓶颈优先级
2000 TPS × 平均 10 次随机读/事务 = 20000 随机读/s
→ 存储需要 ≥ 20k IOPS(留 3 倍余量 → 60k IOPS)
→ 热数据 150GB 全部进内存最理想(shared_buffers + 页缓存)
→ CPU 只要不是单核性能瓶颈即可第 2 步:按瓶颈配置
| 部件 | 选型 | 理由 |
|---|---|---|
| CPU | 16 物理核,高主频 | 单条事务延迟由单核决定 |
| 内存 | 256GB,ECC,插满所有通道 | 150GB 热数据 + 索引 + 连接开销 |
| 数据盘 | 企业级 NVMe,RAID 10,≥ 1TB | 需要 ≥ 60k IOPS |
| WAL 盘 | 独立 NVMe | 顺序写与随机写分离 |
| 网络 | 万兆 | 复制 + 客户端 |
第 3 步:落地为数据库参数
# postgresql.conf(256GB 内存,16 核机器)
shared_buffers = 64GB # 内存 25%
effective_cache_size = 192GB # 内存 75%
max_worker_processes = 24 # 池子调大,留给并行与复制
max_parallel_workers = 8
max_parallel_workers_per_gather = 2 # 按内存约束,不按核数
max_parallel_maintenance_workers = 4 # 建索引可利用多核
huge_pages = try-- 验证配置
SELECT name, setting, unit, pending_restart
FROM pg_settings
WHERE name IN ('shared_buffers', 'effective_cache_size', 'max_worker_processes', 'huge_pages');场景二:NUMA 机器上确认内存是否跨节点
第 1 步:确认机器是否为 NUMA 架构
# 查看 NUMA 节点数量
numactl --hardware
# available: 2 nodes (0-1)
# node 0 cpus: 0 1 2 3 ...
# node 1 cpus: 16 17 18 ...
# 只在 Linux 上可用
lscpu | grep -i numa第 2 步:从数据库侧观测共享缓冲区的 NUMA 分布(PG18 新增)
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
-- 共享缓冲区在各 NUMA 节点上的分布
SELECT numa_node,
count(*) AS buffers,
pg_size_pretty(count(*) * 8192) AS size
FROM pg_buffercache_numa
GROUP BY numa_node
ORDER BY numa_node;第 3 步:判断是否需要干预
若两个节点分布接近均匀,且后端进程分散在两个节点
→ 无需干预(操作系统已自动平衡)
若共享内存几乎全在一个节点,而大部分后端在另一个节点
→ 存在大量跨节点访问,考虑:
① 用 numactl 把 postgres 绑定到固定节点
② 或直接使用单路(single-socket)机器# 把 PostgreSQL 绑定到节点 0(需在启动脚本中配置)
numactl --cpunodebind=0 --membind=0 postgres -D $PGDATA监测 NUMA 分配前先看它的代价
pg_shmem_allocations_numa 用来观察共享内存段的节点分布,但它会触碰所有内存页,官方明确要求「不要用监控系统查询它」。它适合作为一次性的诊断工具,而不是例行巡检项。
场景三:核对硬件是否真的被数据库用上
选型完成后,必须验证「买的硬件确实在起作用」。以下脚本一次性采集关键证据:
-- 1. 大页是否真的生效
SELECT name, setting FROM pg_settings WHERE name IN ('huge_pages', 'huge_page_size');
SHOW huge_pages_status; -- on / off / unknown
-- 2. 共享内存实际大小
SELECT pg_size_pretty(
current_setting('shared_buffers')::bigint * 8192
) AS shared_buffers_actual;
-- 3. 缓存命中率(应长期 > 99%)
SELECT round(100.0 * sum(hits) / NULLIF(sum(hits) + sum(reads), 0), 3) AS hit_pct
FROM pg_stat_io WHERE object = 'relation';
-- 4. 并行查询是否真的在用(应能观察到 parallel worker)
SELECT backend_type, count(*) FROM pg_stat_activity
WHERE backend_type IN ('parallel worker', 'client backend') GROUP BY 1;#!/bin/bash
# check_hardware_usage.sh —— 核对硬件是否被 PostgreSQL 用上
echo "=== CPU ==="
lscpu | grep -E '^CPU\(s\)|Model name|Thread|Core|Socket'
echo "NUMA 节点数: $(numactl --hardware 2>/dev/null | grep -c 'node [0-9] cpus')"
echo "=== 内存 ==="
free -g
grep -E 'MemTotal|HugePages_Total|Hugepagesize' /proc/meminfo
echo "=== 存储 ==="
lsblk -d -o NAME,ROTA,SIZE,MODEL | grep -v loop
for dev in /sys/block/nvme*n1 /sys/block/sd*; do
[ -f "$dev/queue/rotational" ] && \
echo "$(basename $dev): rotational=$(cat $dev/queue/rotational)"
done
echo "=== 网络 ==="
ip -br link show | awk '{print $1, $3}'判断要点:
HugePages_Total = 0 → 大页没配,huge_pages = try 在「尽力而为」
rotational = 1 → 数据盘是机械盘,OLTP 会非常慢
MemTotal 远大于数据库需求 → 检查 effective_cache_size 是否同步调大
Socket = 2 且 NUMA 未绑定 → 检查跨节点访问是否严重场景四:为大页预留内存
背景:新上的 128GB 内存机器,shared_buffers = 32GB,希望用上大页以降低内存管理开销。
第 1 步:算出需要多少个大页
# 必须在停止/启动实例之前执行
postgres -D /var/lib/pgsql/data -C shared_memory_size_in_huge_pages
# 输出:16385(32GB / 2MB ≈ 16384,加少量开销)
grep ^Hugepagesize /proc/meminfo
# Hugepagesize: 2048 kB第 2 步:在 OS 层预留
# 预留略多于计算值的数量(给其他使用大页的程序留余量)
sysctl -w vm.nr_hugepages=17000
# 验证预留结果
grep -E 'HugePages_Total|HugePages_Free' /proc/meminfo
# 持久化
echo "vm.nr_hugepages=17000" >> /etc/sysctl.conf第 3 步:配置数据库并重启验证
# postgresql.conf
shared_buffers = 32GB
huge_pages = try
shared_memory_type = mmap-- 重启后确认大页真的用上了
SHOW huge_pages_status;
-- on → 成功使用大页
-- off → 退回普通页(检查 vm.nr_hugepages 是否足够、是否与其他程序冲突)# OS 侧交叉验证:HugePages_Free 应减少约 16385 个
grep -E 'HugePages_Total|HugePages_Free|HugePages_Rsvd' /proc/meminfo大页必须一次性预留,不能「按需增长」
大页在 OS 层是预先分配并锁定的,不参与换页。因此不能在实例运行时才申请——必须停机、预留、再启动。而且预留出来的大页会永久占用内存,即使 PostgreSQL 没在用。
注意事项
1. 不要在 NUMA 机器上盲目追求「大 shared_buffers」。 共享内存越大,跨节点访问的绝对量越大。先确认 NUMA 拓扑与绑定策略,再决定 shared_buffers 的规模。
2. max_worker_processes 需要重启,务必留足池子。 它同时被并行查询、逻辑复制、扩展(pg_cron、pglogical)使用。默认 8 在多核机器上往往不够,而修改它必须重启实例。
3. max_files_per_process 的语义在 PG18 变了。 现在它只限制后端进程打开的文件数,postmaster 打开的文件不再计入。如果你的运维脚本依赖旧语义做容量核算,需要更新。
4. Linux 上不要用 shared_memory_type = sysv。 Linux 默认使用 POSIX 信号量与匿名 mmap 共享内存,这是首选;sysv 需要非默认的内核设置,且会导致大页不可用。
5. 从源码安装时必须处理 RemoveIPC。 「把 postgres 创建为系统用户」与「设置 RemoveIPC=no」二者必居其一,否则并行查询的共享内存会被 systemd 随机删除,表现为偶发的共享内存警告与并行查询失败。
6. 没有 ECC 内存的机器上必须启用数据校验和。 PG18 的 initdb 已默认开启数据校验和(可用 --no-data-checksums 关闭)。在非 ECC 硬件上关闭校验和是不负责任的。
7. 大页预留后不能随意回收。 vm.nr_hugepages 预留的内存会一直占用,不参与其他用途。预留过多会挤占普通进程可用内存。
8. 不要用 vm.overcommit_memory=2 之后就忘记配 vm.overcommit_ratio。 严格过量使用模式下,可分配内存受 swap + RAM × overcommit_ratio 限制,配置不当会导致 PostgreSQL 启动失败(无法分配共享内存)。
9. 云主机的 vCPU 与物理核不是一回事。 超线程、CPU 积分、共享宿主机等都会让「标称规格」与实际能力有差距。压测时务必用接近生产的实例规格。
10. 本地 NVMe 与云盘的性能特征完全不同。 云盘存在 IOPS 突发额度耗尽、延迟抖动大、性能与容量绑定等特性。云环境的选型不能照搬本地经验。
11. 存储带宽与网络带宽要一起算。 流复制、备份、归档都会占用网络带宽;如果网络先打满,再快的盘也没用。
12. 硬件升级永远排在「消除读写放大」之后。 缺索引导致的全表扫描、fillfactor 不当导致的写放大、work_mem 不足导致的临时文件——这些都不是靠加硬件解决的。先做 磁盘 I/O 优化 中讲的软件侧优化,再考虑升级硬件。
最佳实践
1. 先画负载画像,再选型。
必须回答的问题:
① 是 OLTP 还是 OLAP,还是混合?
② 热数据集有多大?(决定内存下限)
③ 峰值 QPS / TPS 是多少?(决定存储 IOPS 与 CPU)
④ 可接受的 P99 延迟是多少?(决定介质等级)
⑤ 数据增长速度?(决定容量与扩容节奏)
没有这四个数字,任何选型都是猜。2. 按「工作集能装进内存」定内存容量。
黄金法则:内存 ≥ 热数据 + 索引 + 连接开销 + 排序峰值 + OS 预留
例:热数据 150GB + 索引 40GB + 连接开销 8GB + 排序峰值 16GB + OS 4GB
≈ 218GB → 买 256GB3. 存储按「基准 IOPS × 3 倍余量」选,不按容量选。
需求 IOPS = 峰值 TPS × 每事务随机读次数 × 3(安全系数)
注意:SSD 按可用空间规划,不要按标称容量规划;
盘满 80% 后写性能会显著下降。4. 内存插满所有通道,优先 ECC。
错误做法:2 条 32GB 内存(双通道,带宽受限)
正确做法:8 条 8GB 内存(八通道,带宽最大化)5. 多核机器上先调 max_worker_processes(需重启),再调并行度。
# postgresql.conf(32 核机器)
max_worker_processes = 32
max_parallel_workers = 16
max_parallel_workers_per_gather = 4 # 受 work_mem × 并发约束,不按核数
max_parallel_maintenance_workers = 46. 大内存实例一律走大页。
# 1. 先算出所需大页数
postgres -D $PGDATA -C shared_memory_size_in_huge_pages
# 2. OS 层预留(留 5% 余量)
sysctl -w vm.nr_hugepages=<上一步的值 × 1.05>
# 3. 写入 /etc/sysctl.conf 持久化# postgresql.conf
huge_pages = try
shared_memory_type = mmap-- 4. 重启后验证
SHOW huge_pages_status; -- 期望 on7. 把硬件相关的内核资源一次性配好,形成标准清单。
# /etc/sysctl.conf 中的标准条目
vm.swappiness = 1
vm.overcommit_memory = 2
vm.overcommit_ratio = 80
vm.nr_hugepages = 17000
vm.dirty_background_ratio = 5
vm.dirty_ratio = 15
fs.file-max = 2097152
net.core.somaxconn = 4096# /etc/security/limits.conf 或 systemd 服务单元中的资源限制
postgres soft nofile 65536
postgres hard nofile 65536
postgres soft nproc 65536
postgres hard nproc 655368. NUMA 机器用 PG18 的新视图做一次性诊断,不做例行监控。
-- 诊断用(慢,不要放进监控)
SELECT numa_node, count(*) AS buffers
FROM pg_buffercache_numa
GROUP BY numa_node ORDER BY 1;# 需要绑定时,在启动脚本中用 numactl
numactl --cpunodebind=0 --membind=0 postgres -D $PGDATA9. 优先保障内存与存储,CPU 放在最后。
预算分配建议(OLTP):
内存 40% → 存储 35% → CPU 20% → 网络 5%
预算分配建议(OLAP):
内存 40% → CPU 30% → 存储 25% → 网络 5%10. 上云时把「规格族 + 盘类型 + 网络能力」作为整体评估。
必须同时确认:
实例规格族的 CPU:内存比(m / c / r / x)
云盘类型与基准 IOPS(gp3 还是 io2)
基准网络带宽与是否支持增强网络
是否存在 CPU 积分 / IOPS 突发额度
跨可用区复制的网络延迟11. 建立硬件健康巡检,把本节的关键查询固化成脚本。
-- 一次查出硬件是否被用上的关键证据
SELECT 'huge_pages_status' AS metric, current_setting('huge_pages_status') AS value
UNION ALL
SELECT 'shared_buffers', pg_size_pretty(current_setting('shared_buffers')::bigint * 8192)
UNION ALL
SELECT 'effective_cache_size', current_setting('effective_cache_size')
UNION ALL
SELECT 'max_worker_processes', current_setting('max_worker_processes')
UNION ALL
SELECT 'cache_hit_pct',
round(100.0 * sum(hits) / NULLIF(sum(hits) + sum(reads), 0), 3)::text
FROM pg_stat_io WHERE object = 'relation';告警线参考:
huge_pages_status = off 且 shared_buffers > 8GB → 警告(大页未生效)
缓存命中率 < 99% → 警告(内存可能不足)
max_worker_processes = 默认值 8 且核数 ≥ 16 → 提示(并行能力受限)12. 每次硬件变更后重新校准规划器参数。
-- 换盘、换机型后都要重新评估这两个参数
ALTER SYSTEM SET effective_io_concurrency = <按新介质取值>;
ALTER SYSTEM SET random_page_cost = <按新介质取值>;
ALTER SYSTEM SET effective_cache_size = '<新内存的 75%>';
SELECT pg_reload_conf();下一节
本节完成后,「配置与性能优化」这一部分就全部结束了。接下来进入全文搜索与高级特性,从最基础的文本检索开始:全文搜索概述。
参考链接
- PostgreSQL 18 官方文档:管理内核资源(IPC、systemd RemoveIPC、资源限制、内存过量使用、大页)
- PostgreSQL 18 官方文档:资源消耗(
shared_buffers/huge_pages/huge_page_size) - PostgreSQL 18 官方文档:连接与认证(
max_connections) - PostgreSQL 18 官方文档:异步行为(
max_worker_processes/max_parallel_workers) - PostgreSQL 18 官方文档:预设参数(
huge_pages_status/shared_memory_size_in_huge_pages/num_os_semaphores) - PostgreSQL 18 官方文档:pg_buffercache(含
pg_buffercache_numa视图) - PostgreSQL 18 官方文档:pg_shmem_allocations_numa 视图
- PostgreSQL 18 官方文档:规划器代价常量(
effective_cache_size/random_page_cost) - PostgreSQL 18 官方文档:发布说明(AIO、并发 I/O 默认值、
num_os_semaphores、数据校验和) - PostgreSQL 18 官方文档:initdb(数据校验和默认开启)
- PostgreSQL 18 官方文档:服务器启动失败排查
- PostgreSQL 18 官方文档:表空间