Skip to content

硬件选型建议 ​

前面几节讲的都是「软件侧」的调优:连接池怎么配、WAL 与检查点怎么调、磁盘 I/O 怎么优化、统计信息怎么维护。但所有这些优化都有一个天花板——硬件规格。一台内存只有 8GB 的机器,无论怎么调 shared_buffers 也装不下 100GB 的热数据;一块百级 IOPS 的机械盘,无论怎么调 effective_io_concurrency 也扛不住高并发点查。

本节聚焦选型与容量规划:CPU 该选核多还是频率高、内存要买多大、NUMA 机器上要注意什么、网络带宽怎么估、云实例规格怎么映射到真实能力。存储介质、文件系统、RAID、内核参数这些与 I/O 行为直接相关的内容已在 磁盘 I/O 优化 中讲透,本节不再重复,只从「选型」角度给出取舍。

基础概念 ​

四大子系统与数据库负载的对应关系 ​

服务器的四类资源,在 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_connectionswork_mem、max_parallel_workers

把 OLAP 的机器拿来做 OLTP,或反过来,都会浪费一半预算。 选型前必须先明确这台机器的负载画像。

为什么数据库性能瓶颈很少在 CPU ​

很多团队的第一反应是「数据库慢 → 加 CPU」。但在大多数 PostgreSQL 生产环境里,CPU 是最后才会成为瓶颈的资源:

text
一次点查的执行时间构成(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 倍。

text
双路 NUMA 服务器

  ┌─────────────┐            ┌─────────────┐
  │  CPU 0      │  QPI/UPI   │  CPU 1      │
  │  + 本地内存  │ ◄────────► │  + 本地内存  │
  └─────────────┘            └─────────────┘
       ▲                            ▲
       │ 本地访问(快)              │ 远端访问(慢 1.5~2 倍)
       └────────────────────────────┘

对数据库的影响:shared_buffers 是一整块共享内存,如果被分散在两个 NUMA 节点上,一部分访问就会变成跨节点访问。这不是致命的,但会让尾延迟变差。PG18 之前 PostgreSQL 没有 NUMA 感知能力,无法观测共享内存落在哪个节点上;PG18 新增了两个视图填补这个空白(见下文)。

云实例规格与本地物理机的差异 ​

云主机(EC2 / 阿里云 / 腾讯云等)的规格命名(如 c7i.8xlarge、g7.2xlarge)背后是「计算优化型 / 内存优化型 / 通用型」的定位,但必须搞清三件事:

  1. vCPU 是超线程,不是物理核。8 vCPU 可能是 4 物理核 + 超线程,实际并行计算能力不等于 8 个物理核。
  2. 网络与存储带宽随规格阶梯式变化。小规格实例的 EBS/云盘带宽被限流,即使盘本身很快也跑不满。
  3. 存在突发额度(burst)。CPU 积分、IOPS 突发额度用完后性能会断崖式下跌。

因此云上选型不能只看「几核几 G」,要同时看:基准 IOPS、基准网络带宽、是否支持增强网络、云盘类型(GP2/GP3/io2 的差异是数量级的)。

语法与用法 ​

硬件相关参数总表(PostgreSQL 18) ​

下表汇总与硬件规格直接相关的参数(默认值以 PG 18 为准):

参数默认值作用域说明
shared_buffers128MB全局,需重启数据库自己的页缓存,起点为内存 25%
huge_pagestry全局,需重启是否使用大页(try / on / off)
huge_page_size0(自动)全局,需重启显式请求大页大小,如 2MB / 1GB
shared_memory_typemmap(Linux)全局,需重启共享内存实现,Linux 上大页仅与 mmap 配合
max_worker_processes8全局,需重启background worker 进程池总大小
max_parallel_workers8全局,动态池中留给并行操作的上限
max_parallel_workers_per_gather2全局 / 会话,动态单个 Gather 节点的并行度上限
max_parallel_maintenance_workers2全局 / 会话,动态维护类操作(建索引、VACUUM)的并行度
max_connections100全局,需重启最大连接数,直接决定内存与信号量开销
max_files_per_process1000全局,需重启每个后端可打开的文件数(PG18 起只限制后端)
autovacuum_worker_slots16全局,需重启autovacuum worker 槽位数(PG18 新增)
num_os_semaphores运行时计算只读报告所需信号量数量(PG18 新增)
effective_cache_size4GB全局,动态优化器对「可用缓存总量」的估计

记住一条:只有 shared_buffers、huge_pages、shared_memory_type、max_worker_processes 这几个是与硬件规格强相关且需要重启的。其余大部分可以动态调整。

查看实例当前的硬件与参数基线 ​

数据库侧的参数只是「请求」,真实的硬件能力要从操作系统读:

sql
-- 一次性查看与硬件相关的关键参数
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 决定了修改方式:

text
context = postmaster    → 改后必须重启实例(shared_buffers、max_worker_processes 等)
context = sighup        → 改后 reload 即可(max_parallel_workers 等)
context = superuser     → 超级用户可 SET(会话级)
context = user          → 普通用户可 SET(会话级)

计算 background worker 池子的占用 ​

max_worker_processes 不是一个「只给并行查询用」的参数,它是个共享池,以下功能都会从中取用:

sql
-- 看一看当前池子里有多少 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(因为并行查询先占满了)。

内存相关的参数与物理内存的对应 ​

把「物理内存」拆解成数据库实际会用的几块,是内存选型的基本功:

sql
-- 估算各内存参数的总占用(简化模型,单位 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;
text
 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 与带宽 ​

容量是第一位。一个可操作的经验公式:

text
所需内存 ≈ 工作集大小(热数据 + 索引)
         + 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 的性能与剩余空间强相关,盘越满写放大越大。容量选型时要按「可用空间」而非「标称容量」规划。

网络:复制与客户端连接的带宽规划 ​

网络常被忽略,但在复制、备份与高并发连接场景下会成为瓶颈:

text
带宽需求的粗略估算

  流复制(同步/异步)  ≈ 写入速率 × 复制流数量
  基础备份(pg_basebackup) ≈ 数据库大小 / 允许的备份时长
  WAL 归档             ≈ 写入速率
  客户端连接           ≈ 平均查询结果集大小 × QPS
场景推荐网卡说明
单机(客户端与库同机)万兆(10GbE)本地回环基本不是瓶颈
主从复制万兆或更高复制带宽不足会直接累积复制延迟
备份与归档(独立网段)万兆,或独立链路避免备份流量挤占复制与业务流量
高并发短连接万兆 + 增强网络连接建立/释放的往返开销对延迟敏感

注意:同步复制(synchronous replication)对网络延迟极其敏感——每次提交都要等备库确认,网络往返增加 1ms,提交延迟就增加 1ms。跨机房同步复制要用低延迟专线,而不是公网。

NUMA 感知与 PG18 的 NUMA 观测 ​

PG18 之前,PostgreSQL 完全没有 NUMA 观测能力。PG18 新增了两个视图:

pg_buffercache_numa(由 pg_buffercache 扩展提供)—— 把每个共享缓冲区块映射到 NUMA 节点:

sql
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;
text
 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 节点:

sql
-- 查看共享内存分配在 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 是运行时计算参数,用来提前查看需要多少信号量:

bash
# 在启动服务器之前查看所需信号量数量
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):

bash
# 允许 16GB 的 System V 共享内存(仅 sysv 模式需要)
sysctl -w kernel.shmmax=17179869184
sysctl -w kernel.shmall=4194304

# 写入 /etc/sysctl.conf 以持久化

systemd RemoveIPC:这是从源码安装 PostgreSQL 时最容易踩的坑。systemd 默认会在用户完全注销时删除其 IPC 对象(含共享内存),而系统用户被豁免。如果 postgres 用户不是系统用户,并行查询的共享内存会被随机删除:

text
WARNING: could not remove shared memory segment "/PostgreSQL.1450751626": No such file or directory

两种解法必须至少选一种:

bash
# 解法一(推荐):把 postgres 创建为系统用户
useradd -r postgres

# 解法二:关闭 RemoveIPC
# 编辑 /etc/systemd/logind.conf
RemoveIPC=no

官方明确警告:必须确保这两项中的至少一项,否则 PostgreSQL 服务器将非常不稳定。

资源限制(ulimit):

bash
# 查看当前 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_processPG18 起 max_files_per_process 只限制后端(此前 postmaster 打开的文件也计入)
每用户进程数ulimit -u每个连接一个进程,至少要 ≥ max_connections + 系统其余需求
系统级打开文件数(Linux)fs.file-maxsysctl -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 进程,典型内核消息:

text
Out of Memory: Killed process 12345 (postgres).

官方给出两种缓解方式(可同时使用):

bash
# 方式一:启用严格过量使用模式(不会完全阻止 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。配置流程:

bash
# 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 以便重启后仍生效

配合数据库侧的参数:

ini
# 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。

单机、主从与读写分离的硬件分配 ​

同等的预算,如何在一组机器上分配,直接决定架构能撑到多大:

text
方案一:单机(成本最低)
  [ 大规格主库 ]  ← 所有读写都在这台

方案二:主从 + 只读副本(推荐)
  [ 主库(写 + 关键读) ]  ← 高频率 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 步:确定瓶颈优先级

text
2000 TPS × 平均 10 次随机读/事务 = 20000 随机读/s
  → 存储需要 ≥ 20k IOPS(留 3 倍余量 → 60k IOPS)
  → 热数据 150GB 全部进内存最理想(shared_buffers + 页缓存)
  → CPU 只要不是单核性能瓶颈即可

第 2 步:按瓶颈配置

部件选型理由
CPU16 物理核,高主频单条事务延迟由单核决定
内存256GB,ECC,插满所有通道150GB 热数据 + 索引 + 连接开销
数据盘企业级 NVMe,RAID 10,≥ 1TB需要 ≥ 60k IOPS
WAL 盘独立 NVMe顺序写与随机写分离
网络万兆复制 + 客户端

第 3 步:落地为数据库参数

ini
# 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
sql
-- 验证配置
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 架构

bash
# 查看 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 新增)

sql
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 步:判断是否需要干预

text
若两个节点分布接近均匀,且后端进程分散在两个节点
    → 无需干预(操作系统已自动平衡)

若共享内存几乎全在一个节点,而大部分后端在另一个节点
    → 存在大量跨节点访问,考虑:
      ① 用 numactl 把 postgres 绑定到固定节点
      ② 或直接使用单路(single-socket)机器
bash
# 把 PostgreSQL 绑定到节点 0(需在启动脚本中配置)
numactl --cpunodebind=0 --membind=0 postgres -D $PGDATA

监测 NUMA 分配前先看它的代价

pg_shmem_allocations_numa 用来观察共享内存段的节点分布,但它会触碰所有内存页,官方明确要求「不要用监控系统查询它」。它适合作为一次性的诊断工具,而不是例行巡检项。

场景三:核对硬件是否真的被数据库用上 ​

选型完成后,必须验证「买的硬件确实在起作用」。以下脚本一次性采集关键证据:

sql
-- 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;
bash
#!/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}'
text
判断要点:
  HugePages_Total = 0        → 大页没配,huge_pages = try 在「尽力而为」
  rotational = 1             → 数据盘是机械盘,OLTP 会非常慢
  MemTotal 远大于数据库需求  → 检查 effective_cache_size 是否同步调大
  Socket = 2 且 NUMA 未绑定  → 检查跨节点访问是否严重

场景四:为大页预留内存 ​

背景:新上的 128GB 内存机器,shared_buffers = 32GB,希望用上大页以降低内存管理开销。

第 1 步:算出需要多少个大页

bash
# 必须在停止/启动实例之前执行
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 层预留

bash
# 预留略多于计算值的数量(给其他使用大页的程序留余量)
sysctl -w vm.nr_hugepages=17000

# 验证预留结果
grep -E 'HugePages_Total|HugePages_Free' /proc/meminfo

# 持久化
echo "vm.nr_hugepages=17000" >> /etc/sysctl.conf

第 3 步:配置数据库并重启验证

ini
# postgresql.conf
shared_buffers = 32GB
huge_pages = try
shared_memory_type = mmap
sql
-- 重启后确认大页真的用上了
SHOW huge_pages_status;
-- on  → 成功使用大页
-- off → 退回普通页(检查 vm.nr_hugepages 是否足够、是否与其他程序冲突)
bash
# 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. 先画负载画像,再选型。

text
必须回答的问题:
  ① 是 OLTP 还是 OLAP,还是混合?
  ② 热数据集有多大?(决定内存下限)
  ③ 峰值 QPS / TPS 是多少?(决定存储 IOPS 与 CPU)
  ④ 可接受的 P99 延迟是多少?(决定介质等级)
  ⑤ 数据增长速度?(决定容量与扩容节奏)

没有这四个数字,任何选型都是猜。

2. 按「工作集能装进内存」定内存容量。

text
黄金法则:内存 ≥ 热数据 + 索引 + 连接开销 + 排序峰值 + OS 预留

例:热数据 150GB + 索引 40GB + 连接开销 8GB + 排序峰值 16GB + OS 4GB
    ≈ 218GB → 买 256GB

3. 存储按「基准 IOPS × 3 倍余量」选,不按容量选。

text
需求 IOPS = 峰值 TPS × 每事务随机读次数 × 3(安全系数)

注意:SSD 按可用空间规划,不要按标称容量规划;
      盘满 80% 后写性能会显著下降。

4. 内存插满所有通道,优先 ECC。

text
错误做法:2 条 32GB 内存(双通道,带宽受限)
正确做法:8 条 8GB 内存(八通道,带宽最大化)

5. 多核机器上先调 max_worker_processes(需重启),再调并行度。

ini
# postgresql.conf(32 核机器)
max_worker_processes = 32
max_parallel_workers = 16
max_parallel_workers_per_gather = 4    # 受 work_mem × 并发约束,不按核数
max_parallel_maintenance_workers = 4

6. 大内存实例一律走大页。

bash
# 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 持久化
ini
# postgresql.conf
huge_pages = try
shared_memory_type = mmap
sql
-- 4. 重启后验证
SHOW huge_pages_status;   -- 期望 on

7. 把硬件相关的内核资源一次性配好,形成标准清单。

bash
# /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
bash
# /etc/security/limits.conf 或 systemd 服务单元中的资源限制
postgres soft nofile 65536
postgres hard nofile 65536
postgres soft nproc  65536
postgres hard nproc  65536

8. NUMA 机器用 PG18 的新视图做一次性诊断,不做例行监控。

sql
-- 诊断用(慢,不要放进监控)
SELECT numa_node, count(*) AS buffers
FROM pg_buffercache_numa
GROUP BY numa_node ORDER BY 1;
bash
# 需要绑定时,在启动脚本中用 numactl
numactl --cpunodebind=0 --membind=0 postgres -D $PGDATA

9. 优先保障内存与存储,CPU 放在最后。

text
预算分配建议(OLTP):
  内存 40%  → 存储 35%  → CPU 20%  → 网络 5%

预算分配建议(OLAP):
  内存 40%  → CPU 30%  → 存储 25%  → 网络 5%

10. 上云时把「规格族 + 盘类型 + 网络能力」作为整体评估。

text
必须同时确认:
  实例规格族的 CPU:内存比(m / c / r / x)
  云盘类型与基准 IOPS(gp3 还是 io2)
  基准网络带宽与是否支持增强网络
  是否存在 CPU 积分 / IOPS 突发额度
  跨可用区复制的网络延迟

11. 建立硬件健康巡检,把本节的关键查询固化成脚本。

sql
-- 一次查出硬件是否被用上的关键证据
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';
text
告警线参考:
  huge_pages_status = off 且 shared_buffers > 8GB   → 警告(大页未生效)
  缓存命中率 < 99%                                   → 警告(内存可能不足)
  max_worker_processes = 默认值 8 且核数 ≥ 16        → 提示(并行能力受限)

12. 每次硬件变更后重新校准规划器参数。

sql
-- 换盘、换机型后都要重新评估这两个参数
ALTER SYSTEM SET effective_io_concurrency = <按新介质取值>;
ALTER SYSTEM SET random_page_cost = <按新介质取值>;
ALTER SYSTEM SET effective_cache_size = '<新内存的 75%>';
SELECT pg_reload_conf();

下一节

本节完成后,「配置与性能优化」这一部分就全部结束了。接下来进入全文搜索与高级特性,从最基础的文本检索开始:全文搜索概述。

参考链接 ​