PostgreSQL Cgroup Memory Management in Cloud Environments
Based on Liu Zhilong's presentation at HOW 2026. Liu is a PG database expert at Ping An Technology.
1. Cgroup Memory Management Overview
1.1 Cgroup Basics
Cgroup (Control Group) is a Linux kernel mechanism for process group resource management — quota limits, isolation, and usage statistics for CPU, physical memory, I/O, and network. In cloud environments, multiple PG instances run on the same physical host, each managed via Cgroup — standard practice across cloud providers. Container technologies also rely on Cgroup for resource constraints.
1.2 The Fundamental Difference: CPU vs Memory Management
Cgroup's CPU and memory management logic is fundamentally different:
-
CPU: Core mechanism is time allocation via timeslice rotation. Even at the CPU limit, tasks just run slower — they still execute.
-
Memory: Core mechanism is page counting. Memory must be immediately available; working memory occupied by one task cannot be reused by another (except shared memory). When memory hits the limit, there's no "slow down" option — only reclamation or death.
The critical difference: hitting the CPU cap means slower execution. Hitting the memory cap means reclamation or termination.
1.3 Overselling in Cloud Environments
On shared cloud hosts, multiple Cgroup instances run concurrently. Sum all Cgroup limits and they may far exceed physical memory — "overselling." But for PG, overselling doesn't necessarily mean actual memory shortage — because Cache is reclaimable.
Actual memory usage falls into three categories: private memory (non-reclaimable), shared memory (PG Shared Buffers, shared by multiple processes, non-reclaimable), and Cache (file cache, reclaimable). When multiple instances have significant idle or reclaimable Cache space, real usage may not exceed limits even if the sum of quotas exceeds physical memory.
But scaling up or adding instances can push OS memory watermark thresholds, triggering aggressive Cache reclamation. Whose Cache gets reclaimed becomes a "luck of the draw" — potentially impacting critical instances and causing performance jitter.
1.4 PG Memory Layers on OS
PG process memory decomposes into three layers:
- RSS (Resident Set Size): Divided into private and shared memory, generally non-reclaimable
- Page Cache: File cache, reclaimable
- Kernel: Kernel's own memory overhead
At the Cgroup level, each instance has an independent LRU list; reclamation only affects the local Cgroup, achieving resource isolation.
2. Page Counting & Statistical Pitfalls
2.1 Key Interface Files
Core files in Cgroup memory management:
- memory.stat: Detailed statistics for all memory categories — the primary observation entry point
- memory.usage_in_bytes: Not recommended for direct use; accurate usage requires manual calculation
- cgroup.procs: Must include all PG processes (postmaster, checkpointer, bgwriter, walwriter, backend, etc.)
- memory.limit_in_bytes: Defines the Cgroup memory cap
2.2 Core memory.stat Metrics
Using a real example: PG instance with shared_buffers=64GB, shared_memory_type=mmap, ~800 clients. Key metrics in memory.stat:
cache: Page Cache sizerss: Anonymous pages and Swap memory — different from OS process RSS!mapped_file: File shared memory size — PG shared memory is counted herepgpgin / pgpgout: Charged/uncharged page counts for RSS+Cacheinactive_anon / active_anon: LRU distribution for anonymous pages and swap cacheinactive_file / active_file: LRU distribution for file cache
2.3 Core Statistical Deviations
Manual calculation reveals Cgroup v1's statistical confusion:
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
- Cgroup v1's RSS does not include file map shared memory
- PG shared memory (whether mmap or sysv) is classified under
mapped_file - With huge pages, shared memory doesn't appear in ANY statistical metric (including
rss_huge)
2.4 Process RSS vs Cgroup RSS
Sort PG processes by RSS: checkpointer and bgwriter typically have the largest RSS (60GB+ range) — because they frequently operate on Shared Buffers and occupy significant shared memory. Postmaster's RSS is much smaller since it only needs to create the shared memory virtual address space for child processes.
Examining /proc/[pid]/smaps reveals: checkpointer, bgwriter, and postmaster have identical shared memory virtual addresses, but different RSS. Forked child processes inherit the same virtual address mapping, but RSS only grows for processes that actually access corresponding physical pages.
2.5 How to Correctly Reproduce Cgroup OOM
Due to PG's Double Buffer architecture (Shared Buffers + Page Cache), with shared_buffers = 1/4 × cg_mem as typical config, Page Cache can occupy up to 3/4 of Cgroup memory. Normal business operations don't consume much private memory, so even at Cgroup memory saturation, the system can reclaim from Page Cache — the instance may not OOM.
To effectively trigger Cgroup OOM, simply create many sessions consuming private memory (large sorts, Hash Joins, etc.), rather than flooding Page Cache with writes. The latter only triggers Cache reclamation, not actual memory exhaustion.
3. Cgroup OOM Behavior & Impact
3.1 OOM Trigger Mechanism
Two scenarios:
OOM Killer On: Kernel selects the highest OOM Score process (typically a Backend user process) and sends SIGKILL. PG's postmaster detects abnormal child termination, and if shared memory is intact, auto-restarts a new process. Brief service interruption but instance-wide recovery possible. Logged in dmesg and PG logs.
OOM Killer Off: No kernel kill, but PG processes may hang due to memory allocation failure (state D, wait event mem_cgroup_oom). Critical processes like walwriter, if unable to obtain memory, can crash the entire instance. Even with OOM Killer off, the instance may still fail — not killed, but dead by resource starvation.
3.2 Key Distinctions
- Cgroup OOM ≠ System VM OOM. System-level VM OOM is independently judged by
vm.overcommit - With Cgroup v1's
memory.oom_controloff, OOM Killer doesn't start, but processes may still hang or crash due to memory shortage
4. Cgroup v1 Deficiencies & v2 Improvements
4.1 v1's Monitoring Blind Spots
- No Page Table statistics: High process counts can consume tens of GB in Page Tables, invisible in v1
- No Slab statistics: Kernel Slab memory overhead not tracked
- No HugePage statistics: Huge pages are a complete statistical black hole
- Cannot distinguish async/sync reclamation:
pgscan_kswapdvspgscan_directindistinguishable at Cgroup granularity - Shared memory statistical chaos: shmem and file_mapped mixed together
- RSS statistical inconsistency: Cgroup RSS ≠ process RSS
4.2 v2 Improvements
Cgroup v2 (Linux 4.5+) provides significant memory management enhancements:
Management layer: Clearer hierarchy, more intuitive configuration; new memory.min/low/high watermarks for fine-grained reclamation control; better spike handling; removes direct OOM Killer disable, replaced with gentler control; adds memory_hugetlb_accounting — finally tracking huge pages.
Observation layer: Adds Slab, Page Table, pgscan_kswapd/direct, pgsteal_kswapd metrics; new Socket, Vmalloc, Transparent Huge Pages, Zswap, zero-page swap dedicated statistics; shmem and file_mapped indicators now separated; huge page info finally observable.
5. Huge Pages: Management & Challenges
5.1 Advantages
Huge pages benefit PG stability and performance:
- Slightly improved TPS
- Reduced TLB (Translation Lookaside Buffer) refresh pressure, lower CPU cache contention
- Significantly reduces Page Table size in main memory — saves memory and accelerates virtual-to-physical address translation during reclamation
- Physical contiguity improves memory access locality
- Non-huge shared memory is theoretically OS-reclaimable; huge pages are not
5.2 Shared Buffers & Huge Pages Configuration
Setting shared_buffers to 1/4 of Cgroup memory is industry standard, but reality is more complex. Smaller shared_buffers slightly increases Page Cache (larger total cache); larger shared_buffers slightly reduces total cache but improves Shared Buffers hit rate. Both directions have trade-offs.
Based on actual stress testing and production experience, a more reliable recommendation:
- Without huge pages:
shared_buffers = min(1/4 × total_memory, 20GB) - With huge pages:
shared_buffers = min(1/4 × total_memory, 60GB)
Read-heavy scenarios can increase, but must synchronously adjust bgwriter parameters (bgwriter_delay, bgwriter_lru_maxpages).
5.3 Management Challenges
Statistical black hole: In Cgroup v1, huge pages completely escape all metrics. An instance's Cgroup limit set to 20GB may actually occupy 30GB physical memory — monitoring shows everything normal. RDS monitoring memory metrics may be completely unreliable with huge pages.
Resource planning difficulty: Huge pages must be pre-allocated at boot, can't dynamically scale. Problems: mixed PG/MySQL deployments on same host have different huge page strategies; unknown instance count and specs; over-allocation means resources can't meet new instances' needs; under-allocation wastes memory.