Repository navigation
feat(runtime): opt-in per-job memory budget; stop leaking dind parent cgroups - #219
Merged
Merged
Conversation
… cgroups Per-job memory budget ([runtime] memory_limit_mb, memory_high_percent): sharks (40 GB, 5 slots) had host-wide OOMs on 6 of 14 days; on 2026-10-23 03:00Z one killed processes in three concurrent ephpm jobs at once. A job cgroup memory.max confines an OOM to the job that caused it. The budget is per JOB, not per container: the heavy work of a container: job runs in a dind sibling, so a per-container limit would allow N x the limit. Siblings already share a parent cgroup (/ephemerd-dind-<id>, which containerd derives from the job's dind namespace); with a budget set the runner container moves under it too (/ephemerd-dind-<id>/runner) and the parent carries memory.max plus memory.high (default 90%), so page cache is reclaimed and the job throttled before anything is killed. Nesting the siblings under the runner's own cgroup is not possible: cgroup v2 forbids a cgroup with processes from having controller-enabled children. Off by default (0 = no limit, runner keeps /ephemerd/<id>). Fail-open: if the parent cannot be created or limited, the job runs unlimited as before. The parent's limits are rewritten before every container of the job, so they survive the sweep below removing an empty parent mid-job. Leaked cgroups: runc creates the job parent with MkdirAll and removes only container leaves, so every finished job left an empty /sys/fs/cgroup/ephemerd-dind-<id> behind (477 on ducks, 541 on sharks). destroySteps now removes it after the task delete and Dind.Stop, and the node disk sweeper (startup and every tick) removes any that hold no processes and no child cgroups and have been idle for 10 minutes. New package pkg/jobcgroup owns the layout; a dind test pins that its prefix equals DindNamespacePrefix.
The leaked-cgroup sweep removed any empty ephemerd-dind-* parent whose mtime was older than the grace. cgroupfs bumps a cgroup dir's mtime only when a child cgroup is created or removed, so a job without a memory limit that went more than 10 minutes between two dind siblings had an empty, "stale" parent while still running. An rmdir landing between runc's mkdir of the parent and mkdir of the leaf fails that sibling's start; under a memory limit whose runner-side setup failed, it can also drop the limits between Ensure and runc. The sweep now takes the live job set (runner containers in the runtime namespace plus the provisioning set, via Runtime.LiveJobIDs) and skips those jobs; if the set cannot be read the pass is skipped. Also moves the memoryLimit helper off runNodeDiskSweeper's doc comment and gofmts the dind Server literal.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
ld. A per-jobmemory.maxconfines an OOM to the job that caused it./sys/fs/cgroup/ephemerd-dind-<id>behind: 477 on ducks and 541 on sharks today.Memory budget (opt-in, off by default)
It's a per-JOB budget, not per container. The heavy work in a
container:job runs in a dind sibling, so a per-container limit would let a job use N × the limit./ephemerd-dind-<id>, which containerd derives from the job's dind namespace. With a budget set, the runner container moves under that same parent (/ephemerd-dind-<id>/runner). The parent carriesmemory.maxplusmemory.high, so page cache is reclaimed and the job throttled before anything is killed.memory.oom.groupis deliberately NOT set: killing the runner too would turn a failed step into a "runner lost" job.docker buildRUN steps run in BuildKit's own cgroup and aren't covered.Leaked cgroups
runc creates the job parent with
MkdirAlland only ever removes container leaves.destroyStepsnow removes the parent after the task delete andDind.Stop.ephemerd-dind-*cgroups that have no processes and no child cgroups and have been idle for 10 min. It never touches anything else.New package
pkg/jobcgroupowns the layout. A dind test pinsjobcgroup.Prefix == DindNamespacePrefix.Verification
go build ./....go vetpasses on Windows and withGOOS=linux.go testpasses on Windows for config, dind and runtime.pkg/jobcgroup: budget written and resized, no budget creates nothing, the memory controller enabled, Remove drops empty but keeps busy, and the sweep keeps cgroups with procs or children, others' cgroups, and ones inside the grace period;Rlimit|Pids|JobCgroup, including fail-open;pkg/dindsuite, plusTestJobMemoryOpts.Not verified on metal yet: writing
memory.maxon a live node's cgroupfs, and runc accepting the pre-created parent. The suggested rollout is one node first, e.g. sharks withmemory_limit_mb = 12288and 4 slots, after a few days of per-job anon-memory sampling.