nestybox / nestybox/sysbox

OOM kills in sys containers are not reported as `OOMKilled`

Open
#1,030 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Shell
Stars
3.9k
Forks
230
Avg merge
7h 48m
Merged PRs (30d)
3

Description

On a Kubernetes/CRI-O node using cgroup v2 with the systemd cgroup manager, when a sysbox container exceeds its memory limit the kernel OOM-kills it correctly, but Kubernetes reports the container as terminated with reason: Error rather than reason: OOMKilled.

I traced this to conmon being unable to read memory.events for the container. conmon derives its OOM-watch path from /proc/<container-pid>/cgroup, which for sysbox containers points at the init.scope leaf cgroup that sysbox-runc creates. That leaf has no memory.* files, because the memory controller is not enabled in the parent scope's cgroup.subtree_control.

I may well be misunderstanding the intended design here or how cgroup controller delegation is meant to work for sys containers, so I'd appreciate a sanity check on whether this is expected behaviour, a configuration mistake on our side, or an actual bug.

Environment

Component Version
sysbox-runc 0.7.0 (CE), commit a4dd414f7b9b7455c0fbf0d5e5db7bcfe30645bc
CRI-O 1.34.5
conmon 2.1.13
cgroups v2 (cgroup2fs), systemd cgroup manager
Kubernetes managed cluster, containers created via runtimeClassNamesysbox-runc handler

CRI-O runtime configuration:

[crio.runtime]
  cgroup_manager = "systemd"
  conmon_cgroup = "system.slice"

  [crio.runtime.runtimes.sysbox-runc]
    allowed_annotations = ["io.kubernetes.cri-o.userns-mode"]
    monitor_path = "/usr/local/bin/crio-conmon"
    runtime_path = "/usr/bin/sysbox-runc"
    runtime_type = "oci"

What I observe

1. The kernel performs a real cgroup OOM kill
memory: usage 6291456kB, limit 6291456kB, failcnt 127750
<workload> invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=903
oom-kill:constraint=CONSTRAINT_MEMCG,...,oom_memcg=/kubepods.slice/kubepods-burstable.slice/
  kubepods-burstable-pod<uid>.slice/crio-<ctr>.scope
Tasks in .../crio-<ctr>.scope are going to be killed due to memory.oom.group set
Memory cgroup out of memory: Killed process <pid> (tini) ...

memory.oom.group=1, so all processes in the container cgroup were killed, including the container's PID 1. So far so good.

2. conmon cannot open memory.events
conmon <ctr> <ninfo>: container <pid> exited with status 137
conmon <ctr> <nwarn>: Failed to open cgroups file:
  /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<uid>.slice/
  crio-<ctr>.scope/init.scope/memory.events
3. Kubernetes therefore records a plain error exit
kubelet generic.go:356 "Generic (PLEG): container finished" containerID="<ctr>" exitCode=137

kubernetes.containers.state.terminated reports reason: error. No OOMKilled is ever surfaced, in the pod status or in metrics.

Why memory.events is missing

What I measured

The container scope has the memory controller available, but enables nothing for its children, and the leaf has no memory-controller files:

$ cat <scope>/cgroup.controllers
cpuset cpu io memory hugetlb pids misc

$ cat <scope>/cgroup.subtree_control
                                  # empty

$ ls <scope>/init.scope
cgroup.controllers  cgroup.events  cgroup.freeze  cgroup.kill  cgroup.max.depth
cgroup.max.descendants  cgroup.pressure  cgroup.procs  cgroup.stat
cgroup.subtree_control  cgroup.threads  cgroup.type  cpu.pressure  cpu.stat
cpu.stat.local  io.pressure  memory.pressure

$ cat <scope>/init.scope/memory.events
cat: .../init.scope/memory.events: No such file or directory

(memory.pressure, cpu.pressure, io.pressure and cpu.stat are present irrespective of controller enablement, so the leaf has zero memory-controller files.)

All container processes live in the leaf, none in the parent:

PIDs in <scope>          : 0
PIDs in <scope>/init.scope: 1
$ cat /proc/<container-pid>/cgroup
0::/kubepods.slice/.../crio-<ctr>.scope/init.scope

The memory limit and memory.oom.group are set on the parent scope, so enforcement still works:

$ cat <scope>/memory.max ; cat <scope>/memory.oom.group
134217728
1
Same measurement on a crun container (same node, same kernel, same CRI-O)
$ cat <crun-scope>/cgroup.subtree_control
cpuset cpu io memory hugetlb pids misc

$ ls <crun-scope>/container | grep ^memory
memory.current  memory.events  memory.events.local  memory.high  memory.low
memory.max  memory.min  memory.numa_stat  memory.oom.group  memory.peak
memory.pressure  memory.reclaim  memory.stat  memory.swap.current
memory.swap.events  memory.swap.high  memory.swap.max  memory.swap.peak
memory.zswap.current  memory.zswap.max  memory.zswap.writeback

$ cat <crun-scope>/container/memory.events
low 0 high 0 max 0 oom 0 oom_kill 0 oom_group_kill 0

That container is also a leaf-in-scope arrangement with 0 processes in the parent and 1 in the leaf. The only relevant difference I can find is cgroup.subtree_control.

Based on this, these are the conclusions I can draw:

A controller is only available in a child cgroup if the parent has enabled it in cgroup.subtree_control. Combined with the comparison above, I read the empty subtree_control as the reason the leaf has no memory.events.

Comparing the two runtimes, both runtimes put container processes in a leaf cgroup but one in one of them does the leaf receive controllers.

sysbox-runc crun
PID 1 cgroup <scope>/init.scope <scope>/container
PIDs in parent / leaf 0 / 1 0 / 1
cgroup.subtree_control on scope (empty) cpuset cpu io memory hugetlb pids misc
<leaf>/memory.events missing present
conmon OOM detection fails works

I read through parts of sysbox-runc to see if I could understand if this was intentional. This comment where the leaf is created and the container's init is placed into it explains a lot about keeping the container's cgroup root free of processes so that inner sub-cgroups don't become "domain invalid"

What I could not find is any point where +memory (or the other controllers) is written into the parent scope's cgroup.subtree_control so that the leaf actually receives them. The leaf-creation logic in the cgroupfs manager looks similar, so it's possible the same applies there, but I've only tested the systemd manager.

Questions

  1. Is the leaf cgroup expected to have the memory controller enabled? If so, should sysbox-runc write +memory … to the parent scope's cgroup.subtree_control after creating init.scope?
  2. Or is the expectation that something inside the sys container must enable the controllers on the now-chowned cgroup.subtree_control? Is a non-systemd sys container a supported configuration in this respect?
  3. Is there a CRI-O or sysbox setting we've missed that would make the leaf inherit controllers?
  4. More broadly, is OOM reporting to the container runtime a supported property for sys containers, or a known limitation of the leaf-cgroup design?

Let me know if there's any more information I can provide.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with libcontainer/cgroups/systemd/v2.go around the linked leaf-cgroup creation code, then compare the cgroupfs manager path. Reproduce the systemd cgroup-manager setup and inspect cgroup.subtree_control, the leaf's memory.events, and conmon's OOM-watch behavior. Done means the supported configuration consistently exposes OOM termination as OOMKilled without breaking the leaf-cgroup design.

Written by the indexing model from the issue text.

Assessment

Tech stack
kubernetes, linux
Domain
devops, infrastructure, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.