61 comments

[ 0.19 ms ] story [ 129 ms ] thread
Reminder: if overcommit as a concept seems distasteful, the real ire should be directed at the Unix fork syscall, an API that will always fail on a process using over 50% of the available memory without overcommit. Ideally apps would use vfork or posix_spawn instead, but that isn't the world we live in. Overcommit is a sensible way to help apps that use a lot of memory "just work". You can always turn it off if you don't need apps using a lot of memory to be able to fork.
There are also legitimate uses of overcommit for reserving huge areas of virtual memory and relying on demand paging to allocate physical memory, although strictly speaking enabling overcommit isn’t necessary if you separate the “reserve” (mmap(PROT_NONE)) and “commit” (mmap(MAP_FIXED, PROT_READ | PROT_WRITE)) stages (which is what Windows forces you to do).
I’m not sure how this works on Linux exactly but in principle you don’t need to support overcommit to avoid that problem. On illumos for example anonymous memory allocations come from swap, which is a virtual resource that includes both disk and memory. You do still have this problem if something is using more than half of swap, but (1) that’s much larger, and (2) you can augment it with more disk space, which won’t actually be used unless stuff actually uses all the physical memory. This has its own tradeoffs but it’s useful to keep in mind that there are other ways to do things.
If available memory means physical + swap then you're saying the same thing as pcwalton.
Right, available memory includes swap. (That's why I didn't say "available RAM".)
I thought fork was copy on write nowadays.
It is, but “write” doesn’t have an error code it can return on out-of-memory.

The whole crux of overcommit is that the affected process doesn’t get a chance to gracefully fall back or shutdown on OOM conditions at defined places in the code (like at invocations of malloc).

Fork is copy-on-write. The problem is that there's no way for fork() to know how much memory the child process needs. It might call exec() immediately, using very little memory. It might keep running, mostly sharing pages with the parent. It might touch every memory page, requiring a lot of memory.
That explains why Windows, which has a process model based on spawning and not forking, never had overcommit.
However Windows has stacks that commit on-demand, which is kinda similar to overcommit. It can result in programs terminating on any random function call (if that call requires allocating a new page of stack). https://learn.microsoft.com/en-us/windows/win32/procthread/t...

However this is very rare to happen in practice -- stack growth isn't very frequent in typical applications; so usually a malloc() will fail first in low-memory situations.

A Windows program can avoid the risk of getting terminated on stack growth by specifying equal commit and reserve sizes for the stack. So in least in theory, it's possible to write a windows program that is reliable in low memory situations.

Also I would suggest using a Windows machine which seems to run out of memory despite hundreds of gigabytes of swap and yet still ~70% of RAM full. I don't know what's causing the bug but it affects multiple systems I use and it's a right nuisance with a nearly full disk to have >10% of it sitting there idle just in case whatever's over-allocating memory decides to actually use it (I mean, I guess I could also direct my ire at whatever broken accounting fails to actually tell me what's causing it as well, but I can be mad at two things at once).
yup. i recall first being exposed to this while trying to understand why a memory-hog backend service was being oom-killed in production while attempting to spawn a resource-light subprocess via fork+exec, while there was still a good portion of free memory to use.

the more you read about linux oom killer, overcommit, linux memory accounting heuristics, copy-on-write, fork+exec as a mechanism to spawn an unrelated process, the more surprising it is that anything works at all even some of the time.

If you want to optimize for throughout one option is to overcommit resources (CPU/MEM/IO/NET) but utilize backpressure mechanisms to reduce load during times of saturation.

Kubernetes does this through node pressure eviction but it is pretty easy to hook into the pressure stall information and have the application handle this as well (for example, start returning 429 HTTP responses when PSI goes over a certain level).

At the end of the day the optimal overcommit is workload dependent — good metrics and an iterative approach is needed.

You probably also want to do somewhat intelligent load shedding, where you basically make sure you drop the most expendable items first.

That's a basic fact of life for CPU cycles even for consumers on their machines, we call that just CPU priorities or so. But it's also useful for the other resources; but there it's a bit more disruptive, because we generally need to disrupt a program to eg claim back the disk space it uses.

Is any of this still applicable in 2024?

One has to doubt...

Doubt - based on what?
Knowledge, GP probably thinks that the kernel world moves like the userspace (probably js) world.

Which clearly isnt true. If OS development worked like userspace/web dev we'd all be in much more pain.

As then as now, you can manage it.

vm.overcommit_memory == 0 heuristic overcommit

vm.overcommit_memory == 1 (full overcommit) allows allocating more memory than there is ram + swap.

vm.overcommit_memory == 2 never overcommit. There must be enough physical ram or virtual swap to allocate the memory.

Can this be set for a specific process (or process group/tree), or only globally?
Globally

For a process you can use setrlimit() (ulimit command)

I'd want the OOM killer to kill processes with more over-commit first.
Why? There are many innocent applications that consume lots of virtual memory with no intention of using it. Sometimes sparse algorithms call for that sort of thing. It can be a nice crutch when you're in a pinch for time. A good example of a tool that does this is TSAN. It gobbles up virtual address space, which is practically free, thanks to the magic of modern MMUs. That's too powerful and good a thing to need to give up completely.
Another example would be the JVM’s new low-latency GC, ZGC, that does some smart addressing tricks to store some information in pointer addresses themselves, effectively tripling the actual memory usage when naively looked at through virt memory.
Such applications could handle this explicitly, e.g. with mmap and mprotect. In principle there is no reason for such use-cases to rely on implicit overcommit behavior. Which also isn't present on Windows.
It might actually be using that, I’m not intimately familiar with the topic.
Yes, I would expect that as well.

Implicit overcommit is really only necessary for fork and general backwards compatibility.

That's because Windows forces you to use special userspace libraries that account for memory and let you create "reservations" for large regions, without actually populating entries in the MMU (i.e. PML4t). Linux doesn't have that. You create reservations for large memory regions by allocating virtual memory, which of course doesn't become real (resident) until you use it. That's overcommit. But since there's no userspace libraries like WIN32 forced upon you on Linux, there's really no way for the kernel to know your intent when it comes to memory, because the abstractions for memory on Linux are so simple. Windows has a ridiculous byzantine system, which some people think is better. But people are really missing the point when they try to make an apples-to-apples comparison between the two systems of memory management. It's not a question of (a) overcommit good, vs. (b) overcommit bad. Anyone who thinks either is a fool. Linux is simply a system that requires a smart system administrator who understands your use cases and can tune sysctl accordingly when you want to push beyond the normal boundaries.
It seems to me that the prot parameter and/or MAP_NORESERVE would indicate the intent.
BTW the newer Generational ZGC is not using multi-mapped memory any more.
I like to run my development VM with overcommit_memory == 2, but I recently learned that the way it actually fails is pretty confusing. It's not an orderly 'abort: malloc failed' or process termination like I would've expected. Instead, you get weird intermittent failures that don't look like OOMs and your system appears to have plenty of free memory at the time.

Still good for flushing out scenarios where i.e. your build process needs 64GB of memory for approximately 500ms and then drops back down to reasonable levels, due to parallelism

That sounds wild. How else can it possibly fail than by failing malloc? Where do the intermittent failures occur, if not in the return value? Could it be some part of your code isn't checking the return value for NULL?
IIUC, the way overcommit works on Linux is the memory returned by malloc() is not actually mapped to physical memory until it is accessed. So malloc may succeed and then at some point later you may get a segfault or similar when something actually tries to access that memory.
malloc() will not return unmapped memory for small allocations. Your process might get killed if malloc() fails to get more backing pages.
Do you mean that my process might get killed [by the OOM killer] if /another process/ fails to get more backing pages?

Or are you suggesting that there is some other failure path that results in my process getting killed if my own malloc() fails to get pages? (That seems wrong. Should simply return NULL in that case, no?)

> Do you mean that my process might get killed [by the OOM killer] if /another process/ fails to get more backing pages?

It can be your process or some other process, depending on the OOM killer weights and heuristics.

> Or are you suggesting that there is some other failure path that results in my process getting killed if my own malloc() fails to get pages? (That seems wrong. Should simply return NULL in that case, no?)

No, malloc() in Linux will never return NULL for small allocations. What happens is that glibc will try to get more pages for its heap by calling mmap(). The call to mmap() will result in the OOM killer waking up and freeing some memory.

It then can kill some other process, and your process mmap() will succeed, or it can kill your process, and in this case mmap() never returns.

> IIUC, the way overcommit works on Linux

Doesn't vm.overcommit_memory == 2 disable overcommit? That's the situation we're talking about...

You're correct; for some reason I didn't interpret the question in that context. I was thinking about when vm.overcommit_memmory is 0 or 1.
> That sounds wild. How else can it possibly fail than by failing malloc?

By killing your process and/or by failing fork()s.

In general, malloc() in Linux will never fail for small allocations.

With overcommit disabled, fork may fail, but your other claims are backwards: malloc can fail, and processes aren't killed.
I suggest trying that :) You'd be surprised.

glibc allocator, like pretty much any other allocator, maps pages in advance without touching them. It's quite likely that you'll get OOM killed when you first try to touch them, rather than during mmap() for new pages.

Assuming you've also set overcommit_ratio to a conservative value, this would mean that the kernel overcommit=2 setting is not working, since mmap-ing writable pages counts as allocation from overcommit POV. It's a kernel sysctl, so I wouldn't expect it to change glibc's behaviour. There don't seem to be any open bugs about this in the kernel bugzilla. But do tell if you have found a bug in this area.

(This is documented in https://www.kernel.org/doc/html/latest/mm/overcommit-account... )

> malloc() in Linux will never fail for small allocations

That's in kernel-space, not user-space processes. If you have overcommit disabled, it will be user-space processes making syscalls mmap() or sbrk() to get more heap memory (in which small allocations reside), that fails with an error return code, I assume. But I guess it could also fail to auto-grow the main thread stack, and presumably get a segfault. If the kernel needs to allocate more memory for a fork(), that syscall can fail. But if the kernel needs to allocate memory for some other internal purpose, probably back to the OOM killer, I guess ... hopefully this would be very rare?

> If you have overcommit disabled, it will be user-space processes making syscalls mmap() or sbrk() to get more heap memory (in which small allocations reside), that fails with an error return code, I assume.

Nope. No overcommit simply means that the OOM killer will be invoked earlier. The errors likely won't happen on the synchronous path due to the way glibc() maps the pages.

Long ago, I tried to write an OOM-resilient container library, and I wanted to test it. It worked as expected on Windows, but on Linux I was not able to get NULLs from malloc(). No amount of MLOCKALL and other tricks helped.

Wow, that's disappointing. It seems there's really a lot of reasons to just accept overcommit on linux. (Browers, js engines used in various components now, databases that fork, many existing libraries not expecting allocations to fail ... and I guess they really don't after all?!)
I think to fix this stuff you'd have to time travel back to 1980 or so. There's just so much software dependent on the broken behavior and a culture of not fixing things.
From what I saw when running into this regularly, it seemed to be code that wasn't checking malloc return values for NULL, or code that didn't have well-defined behavior when out of memory. So I'd get weird error messages from random libraries being consumed by the applications I was using during execution, or individual operations in a larger task would fail but it would keep on chugging along.
Do you run with swap in that case? I would suspect two possible reasons: firstly most software on linux is not run in that configuration and is thus unprepared for malloc failure: I would not be surprised if the return is not checked for many malloc() calls. Secondly, running without swap will cause bad performance (like, whole system freezing for a long time) failure modes as linux will evict cached code pages from RAM instead of rarely accessed data under memory pressure.
>Secondly, running without swap will cause bad performance (like, whole system freezing for a long time)

in my experience it is exactly the opposite. unless you swap onto ssds, swapping will grind the whole system to a halt, while without swap the oom-reaper will kill the culprit and you can immediately work on the system to check/fix things. result also depends on your applications of course.

The point is you need to have swap enabled but not use it. If you use swap it's bad and if you have swap disabled it's also bad. There "should" be no difference but that's not the case.
man 3 malloc:

       The malloc(), calloc(), realloc(), and reallocarray() functions
       return a pointer to the allocated memory, which is suitably
       aligned for any type that fits into the requested size or less.
       On error, these functions return NULL and set errno.
Problem is that thanks to overcommit malloc sort of always succeeds, and then many (most?) code doesn't check that malloc successfully returned and proceeds to march on towards a later obscure failure, hence below:

> It's not an orderly 'abort: malloc failed' or process termination like I would've expected. Instead, you get weird intermittent failures that don't look like OOMs

The truth is that if you're having issues with default overcommit configuration (namely overcommit_memory == 0 and overcommit_ratio == 50) your application probably sucks and you have to actually diagnose it and fix it.

"We run a pretty java-heavy environment, with multiple large JVMs configured per host. The problem is that the heap sizes have been getting larger, and we were running in an overcommitted situation and did not realize it. The JVMs would all start up and malloc() their large heaps, and then at some later time once enough of the heaps were actually used, the OOM killer would kick in and more or less randomly off one of our JVMs."

I know that 2007 may have been different times, but I'd argue that max heaps for all your JVMs running on a system probably shouldn't exceed around 88% of the total system memory. (percentage goes up as the total system memory goes up from 128GB -> 256GB -> 512GB)

HotSpot has a handy command line option for this issue: `-XX:+AlwaysPreTouch`, which forces the OS to actually assign memory and thus increases the likelihood to expose such issues.
> The truth is that if you're having issues with default overcommit configuration your application probably sucks and you have to actually diagnose it and fix it.

An alternative interpretation of the issue would be that if your OS allows a single process to overcommit more than the sum of total RAM+SWAP on the system, then maybe it is it who sucks.

It is a hen and egg situation. The OS has to do this because many applications have learned to rely on the behaviour and will pre-allocate huge amounts of memory that they never use. Linux practically forces applications into this because the only way to start a new process is to fork yourself which will instantly double virtual memory usage for no reason. There's also the problem that a lack of overcommit tends to lead to underutilization. I'm eagerly waiting for the day that I no longer have to size my workloads manually on kubernetes.
How is forking related to overcommit?
When you fork then a complete copy of the parent process is created, including all its memory. This memory is not actually copied until it is changed, but it still doubles the virtual memory usage. So without overcommit, even starting bash through a fork would immediately cause an OOM if your process uses more than 50% of physical memory.
That's an incorrect assumption though. Overcommit is for pages that have not been accessed yet, however when forking a valid use case is to share pre-loaded, mostly read-only memory between processes.
> the only way to start a new process is to fork yoursel

Not at all. Linux supports the clone system call which allows applications complete control over this. Linux user space chooses not to take advantage of it.

The problem with overcommitting beyond the sum of RAM and swap is that userspace processes can't protect themselves against the kernel's lies. You allocate memory, handle the result, access the address space and someone dies. The Linux default behaviour is an insane over-optimisation, but without it forking large processes becomes painfully expensive. The proper solution is to spawn directly through suitable system calls instead of the traditional fork() + execve() way.

Imo a good compromise to this day is to overcommit RAM with swap as fallback for correctness even if performance falls of a cliff if you use it. All of that is on top of resource limits and usage monitoring.

I have been a linux user/dev for a while now, and it seems I have the wrong idea of what is memory overcommit:

For me, for a modern large CPU implementation, it simplifies a lot the userland software stack to be able to book huge virtual address spaces in userland, which are populated on page faults. If really needed, I can "release" some memory ranges of this memory virtual address space (linux munmap has some options related to just that).

I do realize I am working more and more in a 'infinite memory model' (modulo some "released" ranges from time to time).

For instance, I would be more that happy to book a few 64GiB virtual address ranges on my small 8GiB workstation.

I am talking about anonymous memory, not file backed mmaping which is another story.