The cgroups v2 memory limit is enforced strictly at the kernel level, meaning that once a process group hits the memory.max threshold, the kernel triggers an Out-Of-Memory (OOM) kill or forces allocation failures. To resolve artificial memory load apps exceeding limits, you must either adjust the memory.high and memory.max values in the cgroup hierarchy, ensure the app releases allocated pages correctly, or verify that the system does not have insufficient swap space configured to handle overcommit scenarios.
Understanding cgroups v2 Memory Enforcement
In Linux cgroups v2, memory accounting is unified under a single controller, replacing the separate memsw and memory controllers of v1. The key files governing this behavior are located in the cgroup directory for your specific application. The memory.max file sets the hard limit; any attempt to allocate memory beyond this point will fail with ENOMEM or trigger an OOM event if the kernel decides to reclaim aggressively. The memory.high file sets a soft limit. When usage exceeds this value, the kernel throttles the processes in the cgroup by increasing their priority for page reclamation, but it does not immediately kill them. This distinction is critical when running artificial load tests, as the goal is often to sustain high usage without crashing.
If your application is designed to hold a large amount of data in RAM, it may be hitting the memory.max boundary. Unlike traditional Unix systems where overcommit might allow processes to promise more memory than physically available, cgroups v2 enforces strict accounting. If the sum of all allocations within the cgroup exceeds memory.max, the kernel will refuse further allocations. This behavior protects the host system from being overwhelmed by a single misbehaving or testing container, but it requires careful configuration for legitimate workloads that need predictable upper bounds.
Configuring Hard and Soft Limits
To address the proximity limit warning, first check the current settings using cat /sys/fs/cgroup/. If the value is set too low for your test scenario, you can increase it. However, simply raising the limit might mask underlying issues in your application's memory management. A better approach is to use memory.high to apply pressure without causing immediate termination. For example, setting memory.high to 80% of memory.max allows the kernel to start reclaiming pages before hitting the hard cap, simulating a constrained environment more realistically.
- Navigate to the cgroup directory associated with your application.
- Read the current limit:
cat memory.max. - Set a new soft limit:
echo "8589934592" > memory.high(for 8GB). - Set the hard limit:
echo "10737418240" > memory.max(for 10GB).
This configuration ensures that the application feels the pressure of limited resources early on, allowing you to observe how it handles allocation failures or retries, which is often the intended purpose of artificial load generation.
Diagnosing Allocation Failures vs. OOM Kills
When an app exceeds its limit, the symptom depends on whether it hit the soft or hard limit. If it hits the hard limit, the kernel may send a SIGKILL to the process, resulting in a sudden crash. You can detect this by checking the system logs for messages containing cgroup: memory control or Out of memory in dmesg. If the app continues to run but starts failing system calls like mmap or brk, it has likely hit the memory.high throttle. In this case, the application should handle the error gracefully, perhaps by reducing its footprint or waiting for reclamation.
For artificial load applications, it is common to see spikes in memory usage due to temporary buffers or caching mechanisms. These spikes might temporarily exceed the limit even if the average usage is well within bounds. To mitigate this, you can tune the kernel's reclaim behavior. The memory.swap.max setting also plays a role; if swap is enabled and memory.swap.max is not set, the kernel may use swap to satisfy allocations, effectively bypassing the RAM limit. To enforce strict RAM limits, you should explicitly set memory.swap.max=0 to prevent swapping, ensuring that the RAM limit is the only boundary.
Checking Swap Configuration
Verify if swap is interfering with your cgroup limits. Run cat /sys/fs/cgroup/. If the output is max, the cgroup can use unlimited swap. Change this to 0 to disable swap for the cgroup, forcing all memory pressure to remain within the physical RAM limits defined by memory.max. This is essential for accurate testing of memory-constrained environments.
Application-Level Adjustments
While system-level configurations are powerful, the root cause may lie in how the application allocates memory. Artificial load apps often pre-allocate large blocks of memory to simulate stress. If the app uses mmap with MAP_ANONYMOUS, these pages are accounted towards the cgroup limit immediately upon mapping, even if they are not yet touched (demand paging). This can lead to rapid exhaustion of the limit. Consider using posix_memalign or standard malloc with explicit population of the memory to control the timing of accounting.
Furthermore, ensure that the application properly frees memory when it is no longer needed. Leaks in artificial load generators can accumulate quickly, leading to unexpected OOM kills after prolonged execution. Use tools like valgrind or heaptrack to profile the application outside of the cgroup constraint to identify leaks. Once fixed, reintroduce the cgroup limits to validate stability.
Another consideration is the interaction with other cgroup features. If CPU limits are also enforced, a CPU-throttled process may hold onto memory longer than expected because it cannot make progress to free it. Ensure that CPU and memory limits are balanced appropriately for your workload.
Configure Amount of Reserved Memory for Linux Kernel explains how kernel reservations can impact the available memory for user-space cgroups, which is relevant when tuning total system memory budgets.Monitoring and Validation
After adjusting limits, monitor the cgroup metrics in real-time. Use systemd-cgtop or read from memory.current and memory.stat to observe usage trends. Look for patterns where usage consistently hovers near the limit, indicating a need for fine-tuning. If the application frequently triggers reclaims, consider increasing the buffer size between memory.high and memory.max to reduce thrashing.
Validation involves running the load test for an extended period and checking for stability. Record the peak memory usage and compare it against the configured limits. If the application behaves predictably and handles allocation errors gracefully, the configuration is successful. If it crashes unexpectedly, review the kernel logs for detailed OOM reports, which provide stack traces and memory state information crucial for debugging.
Frequently Asked Questions
Why does my app get killed even though I set a high memory.max?
Your app may be hitting the system-wide OOM killer rather than just the cgroup limit if the entire node runs out of memory. Check the kernel logs for oom-kill messages to distinguish between cgroup-specific enforcement and global system exhaustion. Additionally, ensure that memory.swap.max is not inadvertently allowing swap usage that masks the true RAM pressure.
How can I prevent swap from affecting my cgroup memory limits?
Set memory.swap.max=0 in the cgroup configuration file to completely disable swap usage for that specific cgroup. This forces the kernel to rely solely on physical RAM and page cache reclamation, ensuring that your memory.max limit strictly controls RAM consumption.
What is the difference between memory.high and memory.max?
memory.max is the hard limit; exceeding it causes allocation failures or OOM kills. memory.high is the soft limit; exceeding it throttles the process by prioritizing page reclamation but does not immediately terminate it. Use memory.high to simulate resource contention without crashing the application.
Can I dynamically change memory limits while the app is running?
Yes, you can write new values to memory.max and memory.high while the application is active. The changes take effect immediately, and the kernel will adjust its accounting and throttling policies accordingly. This is useful for dynamic scaling scenarios where resource availability fluctuates.
Why is my cgroup showing higher memory usage than the sum of its processes?
Cgroups account for all memory used by tasks within the hierarchy, including shared libraries, page cache, and slab allocations. If multiple processes share memory, it is counted once but attributed to the cgroup. Additionally, kernel structures associated with the cgroup's I/O or network operations may contribute to the total memory footprint.