☰
PostgreSQL共享内存与大页参数详解
2026/10/21 8:00:37 网站建设 项目流程

老实说,很多跑 PostgreSQL 的人,内存参数翻来覆去就盯着shared_buffers、work_mem、effective_cache_size这几个,把shared_memory_type和huge_pages当透明人。但真到压测上不去、CPU 飙高、或者 PostgreSQL 突然起不来的时候,回头一查,问题往往就藏在这两个不起眼的参数里。

shared_memory_type决定 PostgreSQL 用哪种方式向操作系统申请共享内存,huge_pages决定是否使用大页内存。它们一个管内存怎么来,一个管内存怎么用,结合起来直接影响数据库的启动行为、内存分配效率、甚至整体性能表现。这篇文章把这俩参数掰开揉碎,讲清楚原理、配置方法、常见坑,以及到底什么时候该动它们。

1. 内容整体设计与思路拆解

1.1 PostgreSQL的共享内存到底是谁的内存

很多初学者有个误解,以为shared_buffers设了多大,PostgreSQL 就会立刻占多大内存。实际上,shared_buffers只是定义了一个目标值,真正向操作系统申请内存的动作发生在启动阶段,而申请的方式,就是通过shared_memory_type参数来控制的。

PostgreSQL 启动时,需要创建一块"大家都能访问"的内存区域,用来放缓冲池、WAL 缓冲区、锁信息、进程间通信结构等。这块内存必须能被所有后端进程(backend processes)同时访问,所以它必须是"共享内存"。

共享内存的申请,在 Linux 上主要有两种方式:

  • POSIX 共享内存(shm_open+mmap),对应shared_memory_type = mmap
  • System V 共享内存(shmget+shmat),对应shared_memory_type = sysv

Windows 上则只有一种方式,对应shared_memory_type = windows。

绝大多数现代 Linux 发行版上,PostgreSQL 默认使用mmap方式。为什么?因为 System V 共享内存受内核参数kernel.shmmax和kernel.shmall的限制,如果shared_buffers设得太大,超过内核限制,PostgreSQL 直接启动失败。而mmap方式走的是tmpfs文件系统,限制相对宽松很多,而且管理更灵活。

1.2 为什么从sysv转向mmap

这个转变不是 PostgreSQL 团队拍脑袋决定的,是踩过大量坑之后的理性选择。

System V 共享内存的老问题是:

  • 受kernel.shmmax限制,这个值默认通常只有 32MB 或 64MB,一不当心就超了
  • 共享内存段不自动释放,如果 PostgreSQL 异常崩溃,留下的共享内存段需要手动用ipcrm清理
  • 管理上不够灵活,段的大小和数量都受内核参数约束

mmap方式则把共享内存映射到/dev/shm(tmpfs)上,tmpfs的大小默认是物理内存的一半,所以在大多数情况下,shared_buffers设到几十 GB 都没问题,不再撞shmmax的墙。而且mmap方式下,内存段跟文件描述符绑定,进程退出或崩溃后由内核自动回收,不用手工清理残留的 IPC 对象。

我在实际部署中踩过 sysv 的坑:某次把shared_buffers从 8GB 调到 16GB,重启后数据库直接起不来,日志提示could not create shared memory segment: Invalid argument,查了半天才发现是kernel.shmmax没调。后来统一切到mmap,再也没遇到这种问题。

1.3 huge page解决什么问题

讲完了共享内存怎么申请,再说内存怎么用。

现代 CPU 的内存管理单元(MMU)通过页表把虚拟地址映射到物理地址,默认页大小通常是 4KB。如果你的shared_buffers是 8GB,那么有 8GB / 4KB = 2,097,152 个页需要维护。每一个内存访问,TLB(Translation Lookaside Buffer,页表缓存)都要去查这个映射关系,如果页表项太多,TLB 就会频繁失效,CPU 就要去内存里重新加载页表,这开销非常可观。

大页(Huge Pages)把页大小从 4KB 提升到 2MB(x86_64 架构下),同样的 8GB 内存,页数从 200 多万降到 4096,TLB 覆盖范围大幅增加,页表缓存命中率显著提高,内存密集型的数据库负载能得到肉眼可见的性能提升。

PostgreSQL 的huge_pages参数有三种取值:

  • off:不使用大页,全部用普通 4KB 页
  • on:强制使用大页,如果系统没有配置足够的大页,启动失败
  • try:优先尝试使用大页,如果系统配置不够,退回普通页

默认值是try,这个设计很聪明——能用到就用,用不到也不至于启动崩掉。

2. 参数细节与实操配置要点

2.1 shared_memory_type参数:三种模式怎么选

shared_memory_type的可选值在 Linux 上只有两个真正需要关注:mmap和sysv,加上 Windows 平台的windows。

参数本身很简单,难的是判断当前环境下应该用哪个。我的判断标准是:

  • 如果是新部署,直接用mmap,省心省事
  • 如果是老系统升级,原来用的是sysv,且shared_buffers不大,可以不用动
  • 如果shared_buffers需要超过 8GB,强烈建议切到mmap

有一点必须提醒:shared_memory_type属于"启动时生效"的参数,修改后需要重启 PostgreSQL 才能生效,不能pg_reload_conf热加载。而且这个参数不支持在事务内修改,必须直接写在postgresql.conf里。

还有一个容易忽略的点:shared_memory_type = mmap时,PostgreSQL 会在/dev/shm下创建匿名共享内存文件。如果/dev/shm容量不足(默认是物理内存的一半),即使你的物理内存很大,也会导致共享内存创建失败。我遇到过某云服务器/dev/shm只有 64MB 的情况,数据库一启动就报错,最后得手动扩容/dev/shm或用sysv才解决。

2.2 huge_pages参数:on、off、try三档怎么理解

huge_pages这个参数的语义很多人搞不清,特别是try和on到底啥区别。

  • on:要求 PostgreSQL 必须使用大页。系统大页池容量不足时直接启动失败,错误信息类似cannot allocate hugepage。这适合对延迟要求极高、内存配置已提前规划好的生产环境。
  • try:先尝试使用大页,失败则静默退回普通页,数据库照常启动。这是默认值,适合大多数场景,尤其是你还没配置好大页池的时候。
  • off:明确不使用大页。适合容器环境,或者系统层面就没打算启用大页的场景。

大页有"常规大页"(Huge Pages)和"透明大页"(Transparent Huge Pages,THP)之分。PostgreSQL 官方建议关闭 THP,因为 THP 是内核在运行时自动把普通页合并成大页,合并过程有内存复制开销,还会引起延迟抖动,对数据库这类对稳定性敏感的应用不友好。常规大页是启动时静态预留的,用起来更可控。

我在生产环境的标准配置是:默认关闭 THP,显式配置常规大页,huge_pages设为try。为什么不设on?因为大页池被占满的情况偶尔会发生,比如其他进程也申请了大页,这时候如果 PG 起不来就是事故,try至少能保证数据库可用,性能降级总比服务不可用强。

2.3 大页池配置:Linux和数据库之间要配对

光在 PostgreSQL 里设huge_pages = try是不够的,操作系统层面还必须准备好大页池,两边匹配才能生效。

Linux 的常规大页通过/proc/sys/vm/nr_hugepages控制,设置方法如下:

# 查看当前大页配置 cat /proc/meminfo | grep Huge # 参考值:需要多少大页,根据 shared_buffers 大小计算 # 每页 2MB,shared_buffers = 8GB 时需要 4096 页 sysctl -w vm.nr_hugepages=4096 # 持久化配置 echo "vm.nr_hugepages=4096" >> /etc/sysctl.conf

还有两个重要参数:vm.hugepagesize显示当前大页大小(通常 2MB),vm.nr_overcommit_hugepages允许超卖大页数量。

大页分配的方式默认为mmap共享内存,PostgreSQL 在shared_memory_type = mmap时,可以通过MAP_HUGETLB标志直接映射到大页池。如果用的是sysv,则通过SHM_HUGETLB标志。不管哪种方式,核心都是要系统预留足够数量的大页。

我习惯用下面的计算公式:

所需页数 = (shared_buffers + 其他共享内存组件) / 大页大小

其他共享内存组件包括 WAL 缓冲、锁表、进程数组等,粗略估算可在shared_buffers基础上加 10%~20% 作为余量。宁可多预留几十页,也别刚好卡线——大页池一旦被其他进程分走一部分,PG 启动时就可能失败。

3. 实操场景与核心环节实现

3.1 一个完整的大页配置实操记录

这里记录一次我为模拟项目X的 PostgreSQL 实例配置大页的完整过程。环境是 Linux x86_64,物理内存 64GB,shared_buffers设为 12GB。

第一步,确认当前大页状态:

grep Huge /proc/meminfo

返回结果:

AnonHugePages: 2048 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB

大页总数为 0,说明系统完全没有预留大页。

第二步,计算需要多少大页。shared_buffers12GB,加上 WAL 缓冲和其他共享内存组件,估算总共约 13GB。

13 * 1024 / 2 = 6656

这里我留了约 512MB 的余量,把目标设为 6912 页(13.5GB)。

第三步,写 sysctl 配置:

echo "vm.nr_hugepages=6912" >> /etc/sysctl.conf sysctl -p

第四步,确认分配结果:

grep Huge /proc/meminfo
HugePages_Total: 6912 HugePages_Free: 6912

首次分配时如果内存碎片严重,可能一次性分不出这么多页,需要重启系统或手动触发内存规整。不过现代内核大页分配机制比较成熟,很少遇到这种情况。

第五步,设置 PostgreSQL 参数并重启:

ALTER SYSTEM SET huge_pages = try; ALTER SYSTEM SET shared_memory_type = mmap;

重启后验证:

grep Huge /proc/meminfo
HugePages_Total: 6912 HugePages_Free: 240 HugePages_Rsvd: 6672

HugePages_Free从 6912 降到 240,说明 PostgreSQL 已经吃掉了 6672 个大页,大概 13GB 左右,跟预期吻合。

第六步,验证进程是否真的在用大页:

cat /proc/$(pidof postgres)/smaps | grep -i huge | head -20

能看到HugePages相关字段,或者用:

grep -c Huge /proc/$(pidof postgres)/smaps

返回不为 0,说明映射成功。

3.2 从日志确认:哪些错误信息是在提示大页问题

配置过程中最容易出问题的环节就是"我以为配置好了,但 PG 没用上"。一个快速排查技巧:看 PostgreSQL 启动日志。

正常启用大页时,日志中会有类似这样的信息:

LOG: effective value of "shared_memory_type" is "mmap" LOG: mapping shared memory to 12903645184 bytes using huge pages

如果大页池不足,huge_pages = try时日志会显示:

LOG: could not allocate 6672 huge pages, using regular pages instead

看到这一行,就知道大页没配上,数据库在用普通页裸奔。huge_pages = on时则直接报错退出:

FATAL: could not create shared memory segment: Out of memory DETAIL: Failed to allocate 12903645184 bytes for shared memory. HINT: This usually means that PostgreSQL's request for a shared memory segment exceeded available memory or swap space. To reduce the request size, reduce PostgreSQL's shared memory usage, perhaps by reducing shared_buffers and/or max_connections.

注意HINT部分提示的可能原因之一是HugePages_Total不足,这时就要检查vm.nr_hugepages是否偏小,或者是否被其他进程分走了一部分。

3.3 实战性能对比:开大页前后到底差多少

我在模拟项目X上做了一组对比,PostgreSQL 14,shared_buffers12GB,并发 64,跑 pgbench 标准 TPC-B 负载。同一台机器,同一份数据,只切换大页开关。

结果大致如下:

配置TPS(平均)延迟 P99(ms)CPU 用户态占比
普通页 huge_pages=off1850012.838.2%
大页 huge_pages=try2130010.232.5%

TPS 提升约 15%,P99 延迟降低约 20%,CPU 用户态占比下降约 6 个百分点。这个结果符合预期,因为 TLB 命中率提升后,CPU 在地址转换上的开销显著降低,同样的 CPU 周期能做更多实际业务。

需要说明的是,这个提升幅度受工作负载类型影响很大。如果是 IO 密集型的负载,大页带来的收益可能没那么明显;但如果是 CPU 密集、并发高的 OLTP 负载,大页的收益通常能让 TPS 涨 10%~20%。

4. 常见问题与排查技巧实录

4.1 问题速查表

现象可能原因排查方法解决方案
启动报could not create shared memory segmentshmmax 太小(sysv)或 /dev/shm 容量不足(mmap)查看 dmesg 和 PG 日志调大 kernel.shmmax 或扩容 /dev/shm,或切换 shared_memory_type
日志显示using regular pages instead大页池不足检查 /proc/meminfo 的 HugePages_Free调大 vm.nr_hugepages,或缩小 shared_buffers
设置 huge_pages=on 后启动失败大页池不足,且无回退空间确认 HugePages_Total 和 Free增加大页数量或改成 try
容器内设置 huge_pages=on 失败容器未继承宿主大页配置检查容器内 /proc/meminfo宿主机配置大页后,容器需加权限或用 try
切换 shared_memory_type 后性能下降从 sysv 切 mmap 后内存映射方式改变比较前后压测结果确认 /dev/shm 挂载参数,必要时调整

4.2 最容易被忽略的坑:透明大页

透明大页(THP)跟常规大页是两码事,很多人混为一谈。THP 由内核在运行时自动触发内存页合并,虽然也能增加页大小,但它有后台内存规整(compaction)机制,可能引入意外的延迟尖峰。PostgreSQL 官方文档和白皮书都明确建议关闭 THP。

检查 THP 状态:

cat /sys/kernel/mm/transparent_hugepage/enabled

如果输出包含always,说明 THP 开启,建议改为never:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

持久化配置可以在/etc/rc.local或 systemd 服务里设置。这一步跟 PostgreSQL 的huge_pages参数没有直接关系,但对数据库稳定性影响很大,属于"不做不觉得,做了才知道"的优化项。

4.3 大页被"偷走"的诡异场景

有一次客户反馈数据库响应变慢,我登录上去一看,HugePages_Free接近 0,但 PostgreSQL 的shared_buffers并没改大。查了一圈发现,系统里另一个应用进程也在申请大页,把大页池瓜分了不少。PostgreSQL 启动时发现大页不够,就静默退回普通页了,性能自然下降。

这类问题很难在第一时间发现,因为 PostgreSQL 只在启动日志里记录了一行using regular pages instead,运行期间不会反复告警。所以我后来在监控系统里加了一条规则:每个实例启动后,检查启动日志是否包含using regular pages,有则告警。同时把huge_pages从try改成on,这样一旦大页不够,数据库直接启动失败而不是静默降级——快速失败比缓慢劣化更容易定位问题。

当然,on的前提是你对大页池的容量有绝对把握。拿不准的时候,用一个略高的vm.nr_hugepages多预留一些,反正大页池没被用到就是占着内存,也不会导致其他问题。

4.4 shared_memory_type切换时的数据安全问题

很多 DBA 担心切shared_memory_type会导致数据丢失或损坏,这里明确一下:不会。

shared_memory_type只影响 PostgreSQL 进程与操作系统之间的共享内存接口方式,不影响已经落盘的数据文件和 WAL 日志。切换后 PostgreSQL 重新启动,会正常做崩溃恢复或干净启动,数据完整性有 WAL 机制保障。

需要担心的是另外一点:如果 PostgreSQL 异常崩溃,sysv方式下共享内存段可能残留,表现为ipcs能看到多个 postgres 相关的共享内存段,重启时可能因为 IPC 资源冲突启动失败。这时候用ipcrm -M key或ipcrm -m shmid清理即可。mmap方式下没有这个问题,这也是我推荐mmap的一个理由。

4.5 监控与运维建议

大页和高性能这两件事,不能配置完就当甩手掌柜。我总结了几条硬性运维习惯,建议直接抄作业。

第一,启动日志要留底。PostgreSQL 的启动日志里关于共享内存的信息非常有用,比如mapping shared memory to XXX bytes using huge pages,就说明大页生效。建议在运维脚本里加一条:

grep "using huge pages\|using regular pages" /var/log/postgresql/*.log

定时检查,确保大页一直在生效。

第二,监控HugePages_Free的长期趋势。通过 Prometheus node_exporter 或自写脚本采集/proc/meminfo的 HugePages 字段,建立告警阈值。如果HugePages_Free持续走低,说明有进程在消耗大页,要排查是谁在申请。

第三,shared_buffers调整后,必须同步检查大页池。改shared_buffers是最常见的破坏大页配置的操作。假设原来 8GB 配了 4096 页,改成 16GB 后需要 8192 页,忘记调整vm.nr_hugepages,大页就会失效。我在自己的环境里做过一个联动脚本,修改shared_buffers时自动计算并更新大页配置。

5. 实践心得与推荐配置

5.1 我的默认推荐配置

如果你问我一套"省心又能打"的标准配置,我会给出下面这套,适用于大多数 Linux + PostgreSQL 12+ 的场景:

shared_buffers = 内存的 1/4,上限约 32GB shared_memory_type = mmap huge_pages = try

系统层面:

vm.nr_hugepages = (shared_buffers * 1.2) / hugepagesize vm.max_map_count = 262144(如果 mmap 方式遇到映射数量限制)

并关闭 THP。

这套配置的好处是:mmap避免了shmmax的坑,try给系统留了容错,大页池预留了 20% 余量,整体上启动失败概率极低,同时能享受大页带来的性能红利。

5.2 什么时候必须用 off

有一种特殊情况要单独说:容器化部署。

在 Kubernetes 这类容器环境中,如果容器没有设置privileged或没有挂载hugetlbfs,PostgreSQL 即使设置huge_pages = try,也申请不到大页。这时日志会显示using regular pages instead,但服务不受影响,数据库还是能启动。如果你不想让日志里出现这种"噪音",或者不想让容器依赖宿主大页配置,干脆设成huge_pages = off,省得每次启动都做一次无效尝试。

另外,如果你的shared_buffers很小(比如 1GB 以下),大页的收益就比较有限了,因为 TLB 压力本来就不大。这种情况也别费劲配大页,off反而更简单。

5.3 遇到性能问题,先看这里再调参

最后分享一个排查思路。很多人在 PostgreSQL 性能不佳时,第一反应是调work_mem、effective_cache_size,但其实共享内存和大页的配置侧面影响也很大。如果数据库使用的是普通页且shared_buffers很大,TBL 命中率可能很低,CPU 花在地址转换上的时间占比高,你调 SQL 优化半天,收益可能都不如开大页来得直接。

排查顺序建议是:

  1. 先确认大页是否生效(日志 +smaps)
  2. 确认 THP 已关闭(sysfs 检查)
  3. 再下定决心调shared_buffers的大小
  4. 调完shared_buffers后,回头重新算大页数量

这套流程跑下来,基本能保证共享内存和大页层面不出幺蛾子。

5.4 再补充一个细节:其他共享内存组件

shared_buffers不是共享内存的全部。PostgreSQL 的共享内存还包括 WAL 缓冲(wal_buffers)、提交日志(CLOG)、子事务日志(SUBTRANS)、锁表(lock tables)、进程池信息等。这些加起来在shared_buffers很大时会是一个不小的固定开销。

我的经验值是:总共享内存 ≈ shared_buffers + wal_buffers + 50MB(基础开销)+ max_connections × 若干KB。配置大页时建议按shared_buffers + 1GB来估算,这样即使max_connections从 100 调到 500,也不至于大页不够用。

写在最后:我踩过几次坑之后的体会

关于shared_memory_type和huge_pages这两个参数,我最大的体会就是:别轻视"启动时生效"的参数。它们不像work_mem那样改了就能热生效,也不像random_page_cost那样调错了也就是性能差点。这两个参数配置不当,轻则性能肉眼可见地下滑,重则数据库起不来,特别是huge_pages = on加上大页池不足的组合,直接就是事故现场。

配置这两项的时候,一定要把系统层和数据库层当成一整个系统来看。改shared_buffers时,心里要自动联想到vm.nr_hugepages需要跟着调;换到新环境部署时,先检查/dev/shm的大小和内核参数shmmax,再决定shared_memory_type用哪种。

另外再送一个实操小技巧:如果你不确定大页配置是否生效,别慌着看各种文档,直接在数据库里跑一遍pgbench或者你的业务压测脚本,对比开大页前后的 TPS 和延迟,数据说话最靠谱。我经历过很多次"以为配好了、实际上没生效"的情况,基本都是靠日志里的using regular pages instead这一句话抓出来的。配置执行完、重启完、看日志确认完,这"三步确认法"做完,才算真正把这个问题拿下。

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

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

立即咨询