Skip to content

OOMKill can take down entire worker #3092

Closed

Description

An OOM kill inside a worker pod can kill the whole worker and every actor on it. This happens two ways.

1) One actor going over its limit, on nodes with runc 1.5.2+ and systemd 253 through 258.

runc 1.5.2 sets OOMPolicy=kill on the container's systemd scope when kubelet asks for memory.oom.group=1, which kubelet does by default on cgroup v2 (you can disable this via a kubelet config option, but that's not very portable).

systemd 253-258 then stops the whole scope on any OOM kill under it, because it watches the scope's hierarchical oom_kill count. The kernel confined the kill to the actor's cgroup, systemd kills everything else.

https://github.com/opencontainers/cgroups/blob/19a241326c83a5100dff94732733791a0c9e4720/systemd/v2.go#L366-L384

systemd 259 fixed this by reading oom_group_kill from memory.events.local for these units:

systemd/systemd@e03e505

Current distros are in the affected systemd range:

Details
Distro systemd Leaf OOM kills worker with runc 1.5.2+?
Ubuntu 22.04 249 no
Ubuntu 24.04 LTS 255 yes
Ubuntu 25.10 257 yes
Ubuntu 26.04 259 no
Debian 12 252 no
Debian 13 257 yes
RHEL 9 family (CentOS Stream 9, Alma 9, Rocky 9) 252 no
RHEL 10 family (CentOS Stream 10) 257 yes
Fedora 42 257 yes
Fedora 43 258 yes
Fedora 44 259 no
openSUSE Leap 15.6 254 yes
openSUSE Tumbleweed 261 no
Arch 262 no
Amazon Linux 2 219 no
GKE COS 254 yes

If you are using runc 1.5.2+ (current stable) and have systemd cgroup driver enabled (default in e.g. kubeadm) then you're impacted by this. That includes GKE rapid, and it will include kind once runc is updated.

This hits gVisor, where the actor's cgroup has memory.max at its memory limit. Micro-VM guests OOM inside the guest.

spec.Linux.Resources.Memory.Limit = &limit

2) The worker pod hitting its own memory limit, on any node.

kubelet's memory.oom.group=1 makes the kernel kill the whole container when the container itself runs out, so this doesn't depend on runc or systemd versions. The main way we hit it is suspend: snapshots are written through the page cache, charged to the worker pod with no limit of their own. On GKE, 2 filled 256Mi gVisor actors suspending together in a 512Mi pod OOM'd the worker, as did 4 in 1088Mi. Micro-VM snapshot writes OOM tight pods the same way.

// During checkpoint, moving the sentry from <uid>-_sandbox into a sibling
// <uid>-_ckpt cgroup (memory.max = "max", cpu.max copied from <uid>-_sandbox)

if err := client.Snapshot(ctx, checkpointDir); err != nil {

The pod can't turn either off. OOMPolicy is fixed when runc creates the scope and is only reachable over the host's D-Bus, and node settings (kubelet singleProcessOOMKill, systemd 259+) aren't something we can require.

What I think we should do

  • for 1), never let the kernel OOM-kill inside the worker: gVisor actor cgroups get memory.high at the limit instead of memory.max, and the worker kills an actor that stays over it with cgroup.kill, which systemd doesn't count as an OOM. This could be dropped once nodes move to systemd 259+ ... but that might be a while.
  • for 2), cap snapshot page cache with memory.high while it's written, unless the pod has enough unbooked memory free to lend it, and have workers keep some of their memory limit out of the capacity they report, see Fix Actor Memory Overhead Model #3091

I have prototypes of both, tested on GKE. We should ship this alongside #3091 for the better memory reservation API because ateworker will start OOMKilling based on it.

Activity

  1. thockin commented on Oct 21, 2026

    @thockin
    Collaborator

    There's a lot of directions we can take this, and the behaviors and interactions can be very subtle. I agree that this is a bad situation. Modelling overhead seems correct to me and unlikely to explode things.

    These seem much more subtle. How bad would it be if we did this part in a 1.1 release O(weeks) later?

  2. EItanya commented on Oct 21, 2026

    @EItanya
    Collaborator

    Modelling overhead seems correct to me and unlikely to explode things

    Agreed.

    These seem much more subtle. How bad would it be if we did this part in a 1.1 release O(weeks) later?

    1 really scares me, and I don't have a great answer off the top of my head. Just to be clear this will affect gVisor and uVM? The distro versions seem right in the sweet spot of what folks are probably running.

    2 follows a bit of a pattern, there were also recently oomkills on the snapshot transfer component. I'm worried generally also about reserving too much memory on top of what the actor is requesting, with such a high density system it adds up quickly. Just spitballin' here, can we potentially dodge memory altogether and write directly to a file, or create a shared pipeline of snapshot operations for all actors on that worker. Obviously those have their own fairly large downsides, but it feels to me like the answer here may be a set of options which the user can decide on based upon the style of actors they're running. What do you think?

  3. BenTheElder commented on Oct 22, 2026

    @BenTheElder
    CollaboratorAuthor

    These seem much more subtle. How bad would it be if we did this part in a 1.1 release O(weeks) later?

    Welll GKE 1.37 can pretty easily have the whole worker go down with no other fix in site. I already have implementations, just didn't want to start code dumping without discussion.

    1 really scares me, and I don't have a great answer off the top of my head. Just to be clear this will affect gVisor and uVM? [...]

    Right now this only impacts gVisor, uVM the user workload is inside the guest where THAT oomkill applies distinctly.

    However this is because we don't constrain the cloud-hypervisor or virtiofsd ... the only reason those are impacted is we don't set a memory limit on those cgroups currently, which sills them to triggering case 2.

    So ... no, but yes.

    The distro versions seem right in the sweet spot of what folks are probably running.

    Yeah, basically if you're on a common distro AND you have runc up to date you are probably impacted. Plus at least GKE.

    If you have very recent or very old systemd, don't use the systemd cgroup driver, or you have outdated runc you won't hit the first case ... but it's a relatively common combination that probably isn't going anywhere fast.

    2 follows a bit of a pattern, there were also recently oomkills on the snapshot transfer component. I'm worried generally also about reserving too much memory on top of what the actor is requesting, with such a high density system it adds up quickly. Just spitballin' here, can we potentially dodge memory altogether and write directly to a file, or create a shared pipeline of snapshot operations for all actors on that worker. Obviously those have their own fairly large downsides, but it feels to me like the answer here may be a set of options which the user can decide on based upon the style of actors they're running. What do you think?

    That's basically it: when the worker is tight on space, force it to reclaim page cache from the process, otherwise we can be more relaxed, which will be down to how much memory we reserve in #3091 we can tune it.

  4. BenTheElder commented on Oct 22, 2026

    @BenTheElder
    CollaboratorAuthor

    Here is what mitigating 1) looks like, it's really not bad: #3106

  5. destijl commented on Oct 29, 2026

    @destijl
    Collaborator

    Resolved on main by two fixes:

    1. PR #3106 (f9aec710) placed each gVisor actor into a dedicated child cgroup with memory.high throttling and cgroup.kill teardown on memory.max OOM events instead of relying on systemd scope OOMPolicy=kill across the worker pod.
    2. PR #3353 (e8cdf13d) throttled snapshot dirty page writeback during SuspendActor (memory.high) and dropped page cache in 8 MiB chunks (POSIX_FADV_DONTNEED) so concurrent snapshot writes cannot exhaust worker pod memory and trigger a pod-level cgroup OOM kill.
  6. destijl commented on Oct 29, 2026

    @destijl
    Collaborator

    Oh my bad Benjamin Elder (@BenTheElder). I was doing an issue cleanup and the agent thought this one was done.

  7. destijl commented on Oct 29, 2026

    @destijl
    Collaborator

    Reopening: keep open while remaining M3 memory reservation and worker OOM tracking is in flight.

  8. BenTheElder commented on Oct 31, 2026

    @BenTheElder
    CollaboratorAuthor

    Technically this is fixed but we are doing some hardening, this was ... very complicated to ship at the last moment and entirely too agentic as a result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions