本文整理于 HOW 2026 演讲内容,演讲者:刘智龙,平安科技PG数据库专家。
Cgroup(Control Group)是Linux内核提供的进程组资源管理机制,可对一组进程的CPU时间、物理内存、IO、网络等资源进行配额限制、隔离与使用统计。在云环境中,同一物理主机上通常运行多个PG实例,每个实例通过Cgroup进行资源管理——这是各家云厂商的常见做法,容器技术同样基于Cgroup实现资源约束。

Cgroup对CPU和内存的管理逻辑有根本性的不同:
关键区别在于:CPU上限打满,任务还能跑,只是慢一些;内存上限打满,任务面临的是回收或死亡。
在共享云主机上,多个Cgroup实例并发运行。若将所有Cgroup的限额简单相加,可能远超主机的物理内存,即"超卖"。但对于PG来说,超卖并不必然意味着实际内存不足——因为内存中的Cache部分是可以回收的。
实际内存占用可分为三类: 私有内存(不可回收)、 共享内存(如PG的Shared Buffers,由多进程共享,不可回收)、 Cache(文件缓存,可回收)。多个实例的Cache存在大量空闲或可回收空间时,即使限额之和超过物理内存,实际使用量可能并未超限。
但若进行扩容或新增实例,导致OS内存水位线超标,系统会频繁回收Cache。此时回收谁的内存成了"运气问题",可能波及关键实例,引发性能抖动。
PG进程的内存占用可分解为三层结构:

在Cgroup层面,各实例拥有独立的LRU列表,回收时仅影响本Cgroup,实现了资源隔离。
Cgroup内存管理涉及的核心文件包括:
以一个实际案例为例:PG实例的
shared_buffers=64GB,
shared_memory_type=mmap,客户端约800个。
memory.stat中的关键指标包括:
cache:Page Cache的大小rss:匿名页和Swap内存,
注意:这里的RSS与OS进程的RSS含义不同mapped_file:文件共享内存大小,
PG的共享内存在此项中统计pgpgin / pgpgout:RSS+Cache的charge/uncharge页数inactive_anon / active_anon:匿名页和Swap缓存的LRU分布inactive_file / active_file:文件缓存的LRU分布通过手动计算可以发现Cgroup v1中的统计混乱问题:
shared_mem_mapped = inactive_anon + active_anon - rss cache - shared_mem_mapped = inactive_file + active_file rss + mapped_file = inactive_anon + active_anon inactive_file + active_file + rss + mapped_file = rss + cache
mapped_file下rss_huge)查看PG各进程的RSS排序,通常checkpointer和bgwriter的RSS最大(均达60GB级别),这是因为这些进程频繁操作Shared Buffers,实际占用了大量共享内存。postmaster的RSS则小得多,因为它只需开辟共享内存的虚拟地址空间,fork给子进程使用。
查看
/proc/[pid]/smaps会发现:checkpointer、bgwriter和postmaster的共享内存虚拟地址完全一致,但RSS不同。fork出的子进程虽然继承了相同的虚拟地址映射,但只有实际访问了对应物理页面的进程,其RSS才会增长。
由于PG采用Double Buffer架构(Shared Buffers + Page Cache),在
shared_buffers = 1/4 × cg_mem的典型配置下,Page Cache最多可占3/4的Cgroup内存。正常业务中私有内存占用不会太多,即使Cgroup内存打满,系统也可从Page Cache中回收内存,因此实例未必会OOM。
想要有效触发Cgroup OOM,最直接的方法是创建大量占用私有内存的会话(如大量排序、Hash Join等),而非通过持续写入填满Page Cache。 后者只会触发Cache回收,不会导致真正的内存耗尽。
Cgroup OOM可分为两种情况:
OOM Killer On:内核根据OOM Score选择得分最高的进程终止(通常是Backend用户进程),发送SIGKILL信号。PG的postmaster检测到子进程异常终止后,若共享内存未被破坏,会自动拉起新进程,业务可能短暂中断但实例整体可恢复。系统日志(dmesg)和PG日志中均有相关记录。
OOM Killer Off:不触发内核Kill,但PG进程可能因内存分配失败而Hang住(状态变为D,等待事件为
mem_cgroup_oom)。关键进程如walwriter若无法获得内存,实例可能直接崩溃。这种情况下,即使OOM Killer关闭,实例仍然可能挂掉——不是因为被杀,而是跑死了。
vm.overcommit机制独立判断memory.oom_control关闭时,OOM Killer不启动,但进程仍可能因内存不足而Hang或崩溃pgscan_kswapd与
pgscan_direct在v1中无法按Cgroup粒度区分Cgroup v2(Linux 4.5+正式发布)在内存管理上提供了显著增强:
管理层面:
memory.min/low/high水位线参数,可更精细地控制回收行为memory_hugetlb_accounting,终于将大页纳入统计观测层面:
pgscan_kswapd/
pgscan_direct/
pgsteal_kswapd等指标大页对PG的稳定性和性能有多方面好处:
对于内存碎片和Cgroup内直接内存回收问题,大页有非常好的缓解效果。
将
shared_buffers设置为Cgroup内存的1/4似乎是行业标准,但实际情况更为复杂。调小
shared_buffers可略微增加Page Cache,即增大整体缓存容量;调大
shared_buffers则略微减小整体缓存,但提升了Shared Buffers命中率。两种方向各有利弊。
若
shared_buffers太小,PG自身可用的工作内存不足,相当于把内存管理责任推给OS,而OS回收Page Cache本身就有性能开销;若
shared_buffers太大,不仅挤占Page Cache,还需同步调整bgwriter相关参数(如
bgwriter_delay、
bgwriter_lru_maxpages),否则刷脏效率跟不上写入负载。
基于实际压测与生产运维经验,一个更靠谱的建议值(至少比简单的1/4公式更可靠):
shared_buffers = min(1/4 × 总内存, 20GB)shared_buffers = min(1/4 × 总内存, 60GB)读多写少的场景可适当调大,但必须同步调整刷脏参数。
统计黑洞:在Cgroup v1中,大页完全不进入任何统计指标。一个实例的Cgroup限额设为20GB,若使用大页,实际可能已占用30GB物理内存,但监控显示一切正常。RDS等监控系统的内存指标在大页场景下可能全面失真。
资源规划困难:大页需在主机启动时预先分配,无法按需动态扩展。问题在于:

示例场景:第一个PG实例获得充足大页,第二个实例要上线时大页已耗尽,无法满足需求。若试图为一个大实例分配过多大页,则主机上只能运行这一个实例,资源利用率极低。
内存问题大致可分为四个层面:
操作系统层面:内存碎片、Swap换入过高。注意:启用大页可缓解碎片问题,但可能略微加剧Swap换入问题,需权衡。
PG内核层面:元数据膨胀、版本Bug等。
Cgroup层面:Cgroup内内存回收引发的性能抖动,通常由Cgroup打满触发。启用大页对此有显著改善效果。
私有内存层面:个别Backend进程占用过多私有内存(如大结果集排序、Hash Join),导致Cgroup整体内存紧张。

/proc/buddyinfo查看碎片;
/proc/meminfo的
si/
so(Swap In/Out)判断换入换出压力;
/proc/vmstat中的
pgscan_kswapd和
pgscan_direct区分异步/同步回收memory.stat中的各项指标,注意v1和v2的统计差异ps查看各进程RSS/PSS,计算私有内存;关注进程等待事件(如
mem_cgroup_oom)pg_stat_bgwriter的checkpoint和buffers分配;等待事件中的IPC类事件Buffer命中率方面,Read次数比Hit Rate更值得关注。Hit Rate很高但Read也很高时,意味着即使命中率高,仍然产生了大量Buffer读取操作(可能因扫描范围太大),这是无效的缓存竞争,仅看命中率会被误导。

常见的前置信号包括:
page allocation failure、
kswapd长时间高CPU、
direct reclaim频繁触发、进程状态频繁切换为D(不可中断睡眠)、
malloc相关等待事件增多等。OOM则属于最终结果,往往伴随着明显的进程消失和日志记录。

云环境下PG的Cgroup内存管理比单机场景复杂得多。几个核心结论:
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70043484/viewspace-3176106/ ,如需转载,请注明出处,否则将追究法律责任。