1. 先搞清楚:PostgreSQL的共享内存到底有几块
很多DBA一上来就盯着shared_buffers调参,以为共享内存就是那一块buffer池。实际上PostgreSQL从9.3、9.4开始,共享内存被拆成了两块完全不同的东西:静态共享内存和动态共享内存。这两个概念是理解shared_memory_type和huge page的前提,我先用一张对比的思路讲透。
1.1 静态共享内存:从postmaster启动那一刻就分配好
静态共享内存是在postmaster进程启动时一次性分配的,大小基本固定。它由一组核心参数共同决定,包括shared_buffers、max_connections、max_prepared_transactions、wal_buffers、max_locks_per_transaction等。你改完这些参数,必须重启数据库实例才能生效,因为这块内存从启动开始就要把所有后端进程的映射关系建好。
这块静态内存里装的东西比较杂:
- shared_buffers对应的缓冲池,也就是数据页、索引页、WAL页在内存里的缓存位置
- WAL buffer,用于redo日志的临时缓冲
- CLOG、SUBTRANS等事务状态管理结构
- 锁管理器用的内存结构,包括常规锁表、谓词锁表
- 进程间通信用的信号量、槽位信息
在System V风格的实现里,这块静态内存对应内核里的一个共享内存段,用ipcs -m能看到,权限为------,owner是启动数据库的操作系统用户。很多老DBA习惯用ipcs -m来判断数据库是否残留了共享内存段,就是因为那个时代的PG确实主要跑在sysv上。
1.2 动态共享内存:并行查询的"临时仓库"
动态共享内存是PostgreSQL引入并行查询之后才出现的机制。它的核心特点是:按需分配、用完即释放,大小不固定。并行查询执行时,协调进程要把任务拆给若干个worker进程,这些worker进程之间需要共享一批中间结果集。典型场景是一个大表并行扫描时,每个worker扫描各自的分区或者数据块,产生的tuple放在一块共享的内存队列里,协调进程从队列里拉数据再返回给客户端。这个队列就是动态共享内存。
除了并行查询,国内很多团队用PostgreSQL跑数据仓库类业务,动态共享内存的占用会非常明显。还有几个场景也会用到:
- 并行hash join、并行聚合时,多个worker之间交换散列桶。某个worker构建完hash表后,其他worker要能读它,这些hash桶放在DSM里。
- 两阶段提交时涉及全局事务信息的一些传递。
- 一些第三方扩展,如果文档里写了"requires dynamic shared memory",那它启动时也会申请DSM。
动态共享内存的另一个特点是,它不像static共享内存那样由数据库参数直接决定上限,而是受dynamic_shared_memory_type参数控制实现方式,同时在实际运行中由查询并行度、work_mem设置共同影响。很多人在pg_stat_activity里看到某个查询的state=active但内存占用一直涨,最后OOM,不少就是并行worker的DSM没有被及时释放。
1.3 容易混淆的两个参数:shared_memory_type和dynamic_shared_memory_type
很多初学者会把这两个参数搞混,我在这里专门区分一下:
shared_memory_type控制的是静态共享内存的实现方式,取值可以是mmap、sysv、windows,Linux上主要关注前两个。dynamic_shared_memory_type控制的是动态共享内存的实现方式,取值可以是posix、sysv、windows,Linux上默认是posix。
两者的底层实现完全不同,修改参数时要搞清楚你改的是哪一块。实际工作中我见过有人把dynamic_shared_memory_type改成了sysv,结果并行查询莫名报错,排查半天才发现是这里的问题。
2. shared_memory_type参数解读:mmap和sysv的取舍
2.1 参数说明与三种模式
shared_memory_type参数在PostgreSQL 9.3中引入,取值有三个:
| 取值 | 底层机制 | 适用场景 |
|---|---|---|
mmap | 使用mmap系统调用创建匿名映射文件 | Linux默认值,推荐使用 |
sysv | 使用System V共享内存 | 老版本配合huge page使用 |
windows | Windows平台专用 | Windows下强制使用 |
在Linux上,默认值是mmap。很多人知道这个参数但不知道为什么它是这么设计的,也不知道什么时候该切到sysv。我先把两种模式的核心差异说清楚。
2.2 mmap模式的底层逻辑与优点
mmap模式下,PostgreSQL调用mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0)创建一段匿名映射区域。这段区域映射到进程虚拟地址空间,所有fork出来的后端进程通过继承的页表访问同一段物理内存。这个过程不需要经过System V IPC层,不占用kernel.shmmax、kernel.shmall这些内核参数限制的资源。
mmap模式最大的好处,我实际体会下来有两点:
第一,内存段生命周期干净利落。sysv模式下,如果数据库非正常退出,比如kill -9把postmaster杀了,甚至操作系统直接宕机,共享内存段不会自动释放,需要使用ipcrm -m 段ID手动清理。否则下次启动时可能报could not create shared memory segment之类的错误。mmap模式下,匿名映射随进程退出自动释放,几乎不会留下垃圾。对运维来说,这一点很实用。
第二,不受kernel.shmmax限制。sysv模式需要向内核申请共享内存段,段大小不能超过kernel.shmmax。很多默认安装的Linux发行版kernel.shmmax只配了32MB或者64MB,如果你把shared_buffers设成8GB,sysv模式直接就启动失败了。mmap模式完全没有这个限制,这也是一段时间内很多人发现"为什么我的PostgreSQL启动报错,把shared_memory_type改成mmap就好了"的根本原因。
2.3 sysv模式的价值:huge page的老搭档
既然mmap这么好,为什么还要保留sysv?因为在PostgreSQL 14之前的版本里,huge page支持跟sysv模式是绑定的。那个时代的PG在mmap模式下无法使用huge pages,只有sysv模式才能调用shmget并向内核申请大页块。也就是说,如果你在主流的CentOS 7、Ubuntu 18.04上跑PG 12或者PG 13,想让shared_buffers跑在2MB大页上,必须把shared_memory_type设置成sysv。
sysv模式还有一个实际好处:便于用ipcs -m命令直观地看到共享内存段。对DBA来说,排查问题的时候,ipcs -m | grep postgres一下能看到段ID、大小、attached的进程数,信息透明度高一些。
2.4 PG14之后的行为变化
我在实际升级过程中发现,PostgreSQL 14对这块做了重要调整:mmap模式也开始支持huge pages。也就是说,从PG14开始,shared_memory_type=mmap和huge_pages=on/try可以同时使用,不再强制要求sysv。这个变化对生产环境是利好,我们既要huge page的性能,又想要mmap的干净生命周期,两全其美。
技术细节上,PG14之后mmap模式分配huge page的方式是:在mmap时指定MAP_HUGETLB标志,并且使用MAP_HUGE_2MB指定页大小为2MB。如果内核支持,就直接映射到大页上。分配失败时,根据huge_pages参数设置选择报错还是回退普通页。
所以现在看不同PG版本,行为差异很明显:
- PG 12/13:要huge page,必须sysv + huge_pages=on
- PG 14+:mmap + huge_pages=on就可以
- PG 15+:进一步优化了huge page分配失败时的日志信息
我的建议很简单:如果你还在用PG12/13,并且想用huge page提升性能,老老实实切sysv。如果你是PG14以上,默认mmap即可,但huge_pages一定要单独配置。这两个参数在低版本上是耦合的,在高版本上是解耦的,很多人没有意识到这个版本差异,直接照搬网上老文章,结果参数配置和预期行为对不上。
3. Huge Page:shared_buffers性能的关键
3.1 为什么要用大页:TLB的威力
操作系统管理内存的基本单位是页,Linux默认页大小是4KB。进程访问内存时,CPU要先查页表把虚拟地址翻译成物理地址。为了加速这个翻译过程,CPU里有一个叫TLB(Translation Lookaside Buffer)的缓存,专门缓存最近使用的虚拟地址到物理地址的映射关系。
问题来了:TLB的条目数量是固定的,通常只有几十到几百个。如果shared_buffers设得很大,4KB小页导致页表条目数量巨大,TLB命中率就会下降。CPU每次访问内存都要在页表里走一趟,这个开销是很大的。
用2MB大页替换4KB小页后,同样的内存区域需要的页表条目数减少了512倍。比如shared_buffers=1GB,小页需要262144个页表条目,大页只需要512个。TLB的命中率自然显著提升。
我拿一次实际优化案例来说:某业务库把shared_buffers从256MB调到4GB,TPS在高峰期并没有按预期线性增长,后来用perf top一看,native_flush_tlb内核函数的占比高得吓人。配置huge page之后,同样的压测场景,CPU软中断和锁等待大幅下降,性能提升接近20%。大页不是心理安慰,是实打实的性能优化手段。
3.2 huge_pages参数三档:try、on、off
PostgreSQL的huge_pages参数是数据库层面的开关,三个取值:
| 取值 | 行为 | 推荐场景 |
|---|---|---|
try | 先尝试使用大页,失败则自动回退普通页 | 生产环境默认推荐,保证启动不失败 |
on | 必须使用大页,分配失败直接启动失败 | 验证大页是否配置成功的利器 |
off | 强制不使用大页 | 排查问题时为了排除大页干扰 |
生产环境我习惯用try。有人会说try不是会导致"没有大页也能启动",从而掩盖配置问题吗?我的观点是:生产环境最重要的是服务可用性。如果因为大页配置问题导致数据库起不来,那是事故。try保证了即便大页池被其他进程占满,PG还能自动fallback到普通内存,数据库不会挂。
但调试时一定要用on。我在新服务器部署时,会先把huge_pages设为on,然后故意重启数据库。如果启动失败,日志里会明确告诉你还需要多少大页,这个数字可以直接参考。看到"database system is ready to accept connections"才算真正验证通过。验证完再改回try收工。
3.3 操作系统层的配置:vm.nr_hugepages
在PG层面做任何设置之前,操作系统必须预留好大页池。大页池大小由/proc/sys/vm/nr_hugepages控制,这个值表示预留多少个2MB大页。
查询当前状态:
cat /proc/meminfo | grep -i huge重点看这几个字段:
HugePages_Total:系统预留的大页总数HugePages_Free:未使用的空闲大页数HugePages_Rsvd:已保留但尚未被进程访问的大页数HugePages_Surp:超出vm.nr_hugepages设置的部分(通常为0)Hugepagesize:大页大小,通常是2048kB
临时修改:
sysctl -w vm.nr_hugepages=1024永久修改:
echo 'vm.nr_hugepages = 1024' >> /etc/sysctl.conf sysctl -p预留大页需要连续物理内存,系统运行一段时间产生内存碎片后,申请大页可能失败。建议在系统启动时通过内核参数hugepages=N预留,放在/etc/default/grub的GRUB_CMDLINE_LINUX里,例如GRUB_CMDLINE_LINUX="hugepages=1024",然后更新grub并重启。这样最稳妥,避免内存碎片问题。
3.4 如何验证PG是否真的用上了大页
很多人在系统里看到HugePages_Total不为零,就以为PG在用大页了。其实大页池preallocated了,进程可能根本没用。验证其实不难。
方法一:查看进程smaps
找到postmaster的主进程PID:
ps aux | grep postgres | grep -v grep假设主进程PID是12345,查看该进程的映射信息:
grep -i huge /proc/12345/smaps如果输出中有大量大小以2048kB为单位的映射区域,就说明大页确实映射进来了。mmap模式下,PG分配的大页映射会出现在这段信息里,这是最直接的证据。
方法二:看HugePages_Rsvd的变化
启动数据库之前先记录HugePages_Rsvd,启动后再看。如果PG在跑,Rsvd会明显大于0,因为内核为大页映射保留了物理内存。Free下降是正常的,Rsvd上涨说明有进程申请了大页。
方法三(sysv模式专属):ipcs -m
sysv模式下可以通过ipcs -m看段的权限。如果能以postgres用户身份执行ipcs -m,会看到对应段的shm_segsz大小与shared_buffers接近。如果大页配置生效,段大小会比较规整,跟Hugepagesize的整数倍对得上。
4. 从参数到实战:一组完整的配置验证流程
4.1 环境因素与计算大页数量
先说大页数量的计算方法。不同版本、不同参数的PG启动时占用的静态共享内存大小不完全一样,所以最稳妥的方法是让PG自己告诉我们。
我推荐的做法分三步:
第一步,先把shared_buffers等核心参数按生产规划设置好,huge_pages先设为try,启动数据库,正常运行几分钟。
第二步,用ps查看postmaster进程的RSS(Resident Set Size,实际驻留内存):
ps -eo pid,rss,cmd | grep postgres | grep -v grep | head -1这里显示的RSS单位是KB。假设显示为2200000,也就是2.2GB左右。
第三步,计算需要多少个大页:
echo $(( (2200000 * 1024) / (2 * 1024 * 1024) + 50 ))注意,RSS值单位是KB,乘1024变成字节,因为Hugepagesize是2MB也就是2097152字节,除以得到页数,再额外加上几十页余量用于缓冲。比如2200000KB除以2048KB约1075页,加50页余量,设为1125页。
实际的数字会因为版本、参数不同而不同,但思路是通用的。一切以实际观测为准,不要凭感觉。
4.2 配置大页完整步骤
配置流程我用命令列出来,重点解释每一步为什么这么做。
第一步,配置系统大页池:
sysctl -w vm.nr_hugepages=1125 echo 'vm.nr_hugepages = 1125' >> /etc/sysctl.conf这个数字如果不够,PG启动日志会提示建议值。多留几十页是为了稳妥,多了不浪费多少,少了要再改一次配置文件并重启系统才生效。
第二步,配置PG参数:
shared_memory_type = mmap huge_pages = on注意如果你的PG版本是14以下,这里必须写shared_memory_type = sysv。14以上保持mmap即可。为了确认配置是否生效,这里先设on。
第三步,重启PG:
pg_ctl -D /data/pgdata restart或者用systemd管理的话:
systemctl restart postgresql-14如果启动成功,日志里能看到类似:
LOG: database system was shut down LOG: database system is ready to accept connections说明PG已经用上大页。如果启动失败,日志会提示类似:
FATAL: could not map anonymous shared memory: Cannot allocate memory DETAIL: Failed system call was mmap(...) with parameters (..., MAP_SHARED|MAP_ANONYMOUS|MAP_HUGETLB, ...) HINT: This error usually means that PostgreSQL's request for a huge page allocation failed...同时日志里一般会带一个建议值,比如:
LOG: static shared memory segment is 2210500 bytes HINT: Total memory for a huge page pool should be at least 2210500 bytes.拿到建议值后重新计算vm.nr_hugepages,修改后需要先释放已占用的大页。这里有个细节:已经分配给PG的大页在PG进程退出后会释放回系统大页池。所以流程是停PG,改sysctl,再启动。
第四步,验证。用前面说的smaps方法:
grep -i huge /proc/PID/smaps | head就能看到类似:
huge-2048kB 20480 kB这样的映射条目,确认无误后,把huge_pages改回try,再重启一次,生产环境收工。
4.3 常见问题速查表
我整理了实际运维中遇到频率最高的问题和对应解法,直接列表对照:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 设置huge_pages=on后启动失败,日志提示mmap失败 | vm.nr_hugepages设置过少,或大页池被其他应用占用 | 停掉占用大页的应用,调大vm.nr_hugepages,重新启动 |
| 大页配了,但/proc/meminfo里HugePages_Free基本没变化 | PG处于mmap模式且实际没有映射大页,或huge_pages参数未生效 | 确认huge_pages=on,用smaps检查进程实际映射 |
| sysv模式下换了大页后,ipcs看不到段 | 静态共享内存实际用的是mmap,或shared_memory_type被改回去了 | 查看当前参数SHOW shared_memory_type; |
| 大页配置生效,但性能没有明显提升 | 大页主要用于缓解TLB压力;如果shared_buffers很小,收益有限 | shared_buffers配置过小时大页优化有限,需要结合实际场景评估收益 |
| 系统里AnonHugePages数值很大 | 透明大页THP在工作,不是普通大页 | 数据库环境建议echo never > /sys/kernel/mm/transparent_hugepage/enabled |
| 内存碎片导致无法分配大页 | 长时间运行的服务器内存碎片化 | 通过/sys/kernel/mm/hugepages/或重启设内核参数hugepages=N预留 |
4.4 透明大页THP与普通大页的本质区别
Linux从内核2.6.38开始引入了透明大页(Transparent Huge Pages, THP)。它跟普通大页最核心的区别在于:普通大页是显式预留、进程主动申请;而THP是内核在后台动态地将相邻的普通页合并成2MB大页,对进程透明。
数据库这种高并发低延迟的应用,最怕的就是这种黑盒操作。THP在运行时进行内存整理和合并时,可能触发分配阻塞和额外的CPU开销。很多大厂在部署数据库时都有"关掉THP"的经验,PG社区官方也有类似建议。操作方法:
echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag永久生效需要在/etc/rc.local或者systemd服务里追加执行,因为重启后/sys目录下的配置会恢复默认。判断THP是否开启:
cat /sys/kernel/mm/transparent_hugepage/enabled输出中带有[never]就是已关闭。很多云主机镜像默认是[always],需要特别注意。
5. 深入理解:相关视图与易混淆参数
5.1 pg_shmem_allocations视图:查看共享内存分配明细
PostgreSQL从9.4开始提供了pg_shmem_allocations视图,它把进程内静态共享内存的每一块分配都列出来了。查询:
SELECT name, size, allocated_size FROM pg_shmem_allocations ORDER BY size DESC;输出里会有shared_buffers、max_connections对应的一块,还有wal_buffers等。这些名字对应的是初始化共享内存时注册的内存块。对排查大页计算问题很有用,可以确认实际静态共享内存总数:
SELECT SUM(allocated_size) AS total_bytes FROM pg_shmem_allocations;计算出来之后除以大页大小再加余量,就能得到建议的大页数量,比看RSS更精确。
注意PG14之前的这个视图中还有一个type字段,用来区分shared(静态共享内存)和dsm(动态共享内存)两种类型。PG14之后这个字段在某些情况下对动态共享内存的可见性做了调整,但应用场景不大,这里不展开了。
5.2 常见性能优化误区
我在社区和实际项目中见过很多关于大页的误区,这里值得单独列一下。
第一个误区是"大页配了性能就一定好"。大页优化的核心收益来自TLB命中率提升。如果你shared_buffers只有几百MB,内存访问模式本身没有太多TLB压力,大页带来的收益可能只有几个百分点。很多小内存实例为了配大页折腾半天,性价比很低。
第二个误区是"大页越多越好"。大页池预留的内存是固定的,不能被常规进程使用。vm.nr_hugepages设置过大等于浪费内存。比如一台32GB的机器,你预留了20GB大页给PG,但PG实际只用8GB,剩下12GB就白白空着,其他进程全部挤在剩余12GB里,反而影响整体性能。这也是我坚持用方法让PG自己反馈所需页数的原因,而不是拍脑袋设一个很大的值。
第三个误区是"huge_pages=try说明大页一定生效了"。这是误解。try只是允许PG使用大页,如果系统大页池不够,它会静默回退到普通内存。生产环境如果发现性能跟预期不符,一定要用前面说的smaps方法验证。
5.3 DSM的huge page情况
最后说一个很多人没注意到的细节:动态共享内存DSM也可以使用大页。这需要设置参数huge_pages之外,还要在dynamic_shared_memory_type配合场景下看。不过DSM使用大页的条件相对苛刻,因为DSM的分配比较频繁,不像static共享内存只在启动时分配一次。如果一个并行查询只需要几百MB的DSM,每次都去申请大页,反而增加开销。PG对DSM的大页支持在不同版本上有差异,PG14之前的posix模式DSM实际上不自动使用huge page,PG14之后引入了一些优化。
对多数业务来说,把静态共享内存的大页配好,已经能拿到80%的收益。DSM上的大页,除非你的并行查询非常频繁且每次DSM内存都很大,否则先不动。
6. 经验性结论
我做了几年PostgreSQL运维和性能优化,大页这个事踩过的坑不少。从我实践来看,最实用的几条经验是:
第一,搞大页之前先确定PG版本。PG13以下,切sysv + huge_pages=on;PG14以上,保持mmap + huge_pages=try。这两个组合是经过大量生产环境验证的稳妥选择。
第二,验证永远放在配置之后。光看/proc/meminfo不够,还要看/proc/<pid>/smaps或者HugePages_Rsvd。不看实际映射数据,配置可能就是心理安慰。
第三,把透明大页关掉。这事儿和PG参数没有关系,但影响内存性能。很多数据库性能问题排查到最后,发现是THP在后台默默捣乱。标准数据库服务器的初始配置里就该把THP关掉。
最后再分享一个小技巧:如果数据库启动时大页分配失败,但已经来不及重启系统扩容,可以先用huge_pages=try让业务先起来,等运维窗口再调整系统配置。业务连续性永远排在性能前面。这个降级开关是PG设计得比较聪明的点,我们生产环境充分利用它就是。