feat(installer): reserve the kubelet's own footprint on every node, measured per platform - #1017
Open
LukasWodka wants to merge 7 commits into
Open
feat(installer): reserve the kubelet's own footprint on every node, measured per platform#1017LukasWodka wants to merge 7 commits into
LukasWodka wants to merge 7 commits into
Conversation
…easured per platform (backend#2460) On every node the installer provisions, allocatable == capacity: k3s sets no kube-reserved, no system-reserved and no memory eviction threshold. Both twins now write kubeReserved / systemReserved / evictionHard.memory.available into the kubelet drop-in from backend#2634, and the values are derived from per-platform measurement records, never typed: scripts/tests/measure-node-reservation.sh -> scripts/spec/node-reservation/*.json scripts/gen-node-reservation-embed.sh -> the GENERATED block in both twins kubelet-config-agreement.sh -> the two blocks agree (+ mutations) e2e-cluster.sh -> capacity - allocatable == the drop-in Measured (k3d v5.9.0 / k3s v1.36.3): k3s copies the --kubelet-arg=config= file into kubelet.conf.d/10-cli-config.conf and the kubelet merges drop-ins key by key, so the file writes only memory.available and k3s's disk keys survive; the wholesale replacement belongs to the CLI path, which stays refused. macOS/Docker Desktop record: node-minus-pods working set 885 MiB max under a training-shaped load (the kubelet's own kubelet/runtime rows report ~150 MiB -- the k3s server sits outside /k3s), so darwin reserves kubeReserved cpu=100m memory=1120Mi, systemReserved 1024Mi (the contract's vm_reserve), evictionHard memory.available 256Mi (5% of PF_MIN_MEM_GB). Live: capacity 6 / 12233300Ki -> allocatable 5900m / 9775700Ki, exactly. Linux is measured by the new label-gated node-reservation-measure job (three Ubuntu runners); Windows/WSL2 is unmeasured and stays unreserved, said aloud at create. An existing edge's mounted pre-reservation file gets the recreate advisory, never a rewrite. Chart 1.9.108 -> 1.9.109. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
bugbot run |
…e, then slice) The repo's pipefail early-close guard flagged three '| head' / 'grep -m1' pipelines in the harness; each is now a capture followed by an awk/parameter slice, so a SIGPIPE'd writer cannot fail the run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
bugbot run |
… parse) bash 3.2 does not skip comment text while scanning for the closing paren of $( ), so the backtick in the previous commit's inline comment broke the parse of the whole harness on macOS. The comment moved above the block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
bugbot run |
…cope Script Run as a script the generated block is in script scope; dot-sourced by Pester inside a BeforeAll it is not, and a -Scope Script read reports a healthy embed as broken. Same change in the two Pester suites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
LukasWodka
requested review from
saqlainsyed007
and removed request for
aptracebloc
September 9, 2026 13:03
…es survive a failed summary The first CI run measured 37 rounds on three Linux runners and then lost all of them: one --argjson input was not JSON and the summary aborted with an empty record. Every fragment is now validated (json_or_null, noted on stderr), and an EXIT trap writes a partial record carrying the raw samples when the summary step fails, so a re-run costs the summary, not the measurement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a87c55e. Configure here.
…y derive ubuntu-22.04 amd64, ubuntu-24.04 amd64 and ubuntu-24.04 arm64 (GitHub runners, 4 cpu / 16 GiB, k3d v5.9.0 / k3s v1.36.3, 180 s idle + 240 s load with the real 2 GB task image): node-minus-pods working set max 828 / 889 / 812 MiB, sustained daemon cpu p95 62 / 64 / 49 m, image-unpack burst ~2 cores. The generator takes the max across a platform's records and lands on the same kubeReserved cpu=100m memory=1120Mi as darwin -- the k3s-in-k3d footprint is a property of the node image, not of the host OS. systemReserved stays the contract's vm_reserve (1024Mi); eviction 256Mi. Both twins regenerated; kubelet-config-agreement + 27 mutations green; e2e-cluster on the Linux legs now exercises the live capacity-minus-allocatable assertion. Windows/WSL2 remains unmeasured and unreserved. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
bugbot run |
Both anchors pinned the darwin-only platforms list and went INERT the moment linux was measured; the guard itself was unchanged. The twin-divergence case now drops linux from one twin, the stale-listing case adds an unmeasured windows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
bugbot run |
saqlainsyed007
approved these changes
Sep 9, 2026
saqlainsyed007
left a comment
Contributor
There was a problem hiding this comment.
Approve. Reviewed at 362ac18.
Verified the load-bearing claims against the code rather than the prose:
- Twins are provably equal. The generator emits every value into both
cluster.sh(NAME=v) andinstall-k8s.ps1($NAME = v) from one computation, andkubelet-config-agreement.sh§7 derives the name set as the union of both twins (never a held list), comparing each value field-by-field and flagging divergence, absence, non-positive values, a platform-listed-without-values, and values-without-platform. Not a tautology. - Measured, not typed.
systemReserved=1024Mi= the contract'svm_reserve(1073741824 B),evictionHard=256Mi= 5% ×PF_MIN_MEM_GB=5 GiB, both derived by the generator; the required Source-of-truth drift job re-derives from the records and would redden on a hand-edit. An unmeasured platform (Windows/WSL2) writes no reservation block and the install says so — confirmed in both writers and the bats coverage. - Only
memory.availableunder evictionHard, so k3s'simagefs/nodefsdisk keys survive the key-by-key drop-in merge — asserted in the bats writer test and re-checked live against /configz bye2e_assert_node_reservation, which parses the expected reservation out of the file the installer wrote and assertscapacity − allocatableequals it exactly (cpu side correctly expects only kubeReserved.cpu). Fails closed on an unreadable file/node. - Fail-closed on a broken embed in both twins (bash EOF-via-newline +
[1-9][0-9]*guard; PowerShell$null→ throw), with the zero case refused too. - No hidden regression on stock nodes: ~2.34 GiB reserved on Linux is below the envelope's old blind 3 GiB memory guess; the 7.75 GiB macOS VM still leaves ~5.4 GiB allocatable, and preflight reads physical capacity so its 5 GiB floor is untouched.
- The formerly-vacuous "writer stops emitting kubeReserved" mutation is now scoped to the writer function body, so the existing-cluster advisory's whole-file grep no longer masks it — verified the extraction boundaries.
CI is green end to end (drift + mutation + agreement + Pester + the 3-platform measurement jobs), both Bugbot threads resolved. Nice work — the derivation chain and the mutation-proofing are exactly right.
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.

What
On every node the installer has ever provisioned,
allocatable == capacity: k3s sets no kube-reserved, no system-reserved and no memory eviction threshold (its stockevictionHardcarries only the two disk keys). So "what a pod may have" meant the whole machine, k3s server, containerd and the Docker VM's own daemons included, and every consumer of allocatable inherited that.Both installer twins now write
kubeReserved,systemReservedandevictionHard.memory.availableinto the kubelet drop-in they already mount (the image-GC file from #912), and the values are measured per platform, never typed:kubeReserved.memorypodssystem container), idle and under a training-shaped load, from the platform's measurement recordkubeReserved.cpusystemReserved.memoryvm_reserve— what lives in the VM/host beside the node containers, already measured under the topology work; the record's own outside-the-nodes snapshot is printed as a cross-checkevictionHard.memory.availablePF_MIN_MEM_GBfloor, the same fraction k3s applies to its disk thresholds. k3s ships no memory threshold at all; the kubelet default is 100MiA platform with no record gets no reservation — not one borrowed from another platform — and the install says so. Today that is Windows/WSL2.
The chain, every link a file in this repo
scripts/tests/measure-node-reservation.shbrings a cluster up through the installer's owncreate_cluster(), samples the kubelet's/stats/summaryidle and under a training-shaped load (the real 2 GB CPU task image pulled and run), and writes a JSON record.scripts/spec/node-reservation/<os>-<arch>.json— the records (raw samples + p50/p95/max, capacity/allocatable, live/configz, how k3s wires its kubelet).scripts/gen-node-reservation-embed.shderives the block between the GENERATED markers in both twins;--checkruns inmake drift(the required Source-of-truth drift job).scripts/tests/kubelet-config-agreement.shholds the two blocks equal, derives the name set from both files (union), refuses a platform listed as measured with no values behind it, a value for a platform not in the list, a blank/zero value, an eviction threshold looser than the kubelet's 100Mi default, and a writer body that stops emitting a map.kubelet-config-mutations.shproves each of those (27/27 caught; one case was vacuous before the emission check was scoped to the writer's body, and is recorded as such).e2e-cluster.shasserts on the live node thatcapacity − allocatableequals exactly what the drop-in the installer wrote declares, cpu and memory, and that the mergedevictionHardstill carries k3s's two disk keys. The expected values are parsed from the file, never from the test.installer_parity.jsongains akubelet_reservationsection: the shape both writers must show, driven through the real writer in each language (bats + Pester), once as a measured platform and once as an unmeasured probe.Measured before writing any of it
k3s wiring (k3d v5.9.0 /
rancher/k3s:v1.36.3-k3s1): k3s writes its kubelet defaults tokubelet.conf.d/00-k3s-defaults.confand copies the file named by--kubelet-arg=config=into the same directory as10-cli-config.conf. The kubelet merges config-dir drop-ins key by key, so the #912 file already is the drop-in this ticket asks for. A probe writingevictionHard: {memory.available: 500Mi, imagefs.available: 7%}read back as{imagefs 7%, memory.available 500Mi, nodefs.available 5%}— k3s's untouched key survived. Mounting straight intokubelet.conf.d/99-tracebloc.confbehaved identically. That corrects a claim in #912's comments (that the file's map replaced k3s's); the wholesale replacement is real but belongs to the--kubelet-arg=eviction-hard=CLI path, which stays refused. Because of the merge, the file writes onlymemory.availableand lets k3s's disk keys through — restating them would pin k3s's numbers in a second place.macOS / Docker Desktop, arm64 (6 cpu / 11.67 GiB VM, 1 server 0 agents — the installer's own topology here), 300 s idle + 300 s load:
kubelet/runtimerowsThe kubelet's
kubeletandruntimerows both map to the/k3scgroup and report ~1/5 of the real non-pod footprint — the k3s server process sits outside it. That is why the reservation is derived from node-minus-pods and never from the self-report.Result:
kubeReserved cpu=100m memory=1120Mi,systemReserved memory=1024Mi,evictionHard memory.available=256Mi. Live, on a fresh cluster through the real create path: capacity 6 / 12233300Ki → allocatable 5900m / 9775700Ki, exactly the declared 100m / 2457600Ki;/configzshows all three maps and the merged eviction set.Linux — measured, three records from the new
node-reservation-measurejob (ubuntu-22.04 amd64, 24.04 amd64, 24.04 arm64; 4 cpu / 16 GiB runners; label-gated one2e, weekly on the schedule): node − pods working set max 828 / 889 / 812 MiB, sustained daemon CPU p95 62 / 64 / 49 m, unpack burst ~2 cores. The generator takes the max across a platform's records and lands on the samekubeReserved cpu=100m memory=1120Mias darwin — four machines, two OSes, two architectures within ±40 MiB: the k3s-in-k3d footprint is a property of the node image, not the host OS. The LinuxE2E clusterlegs now exercise the livecapacity − allocatable == drop-inassertion.Windows/WSL2 is unmeasured and stays unreserved; the PowerShell twin writes the image-GC file it always wrote and warns once at create. No number is extrapolated for it.
Existing edges
The drop-in is a bind-mounted host file. Rewriting it would arm a smaller allocatable on the node's next restart under pods sized against the old one, so the reuse path advises, never rewrites: a mounted file with no
kubeReserved:gets the recreate hint, only on a platform that has a record, and silence when the file cannot be read.Chart
client/Chart.yaml1.9.108 → 1.9.109 (version and appVersion). No template changes; the bump is the version-bump gate's, and the sibling control-plane-trim PR will need to re-bump after this lands.Test plan
make drift— 47/47 green (with PyYAML available; the two new guards are enrolled)bats scripts/tests/cluster.bats(kubelet subset 32/32) andinstaller-parity.bats(7/7); fullmake batsrun before pushkubelet-config-mutations.sh27 caught / 0 vacuouscreate_cluster()+ the sharede2e_assert_node_reservation: PASS (numbers above)shellcheck -S warning -x+bash -non every touched script;gen-manifest.sh --check,check-facts.sh --check,check-style.shcleanunit-pesteron Linux and WindowsNote
High Risk
Changes kubelet reservations and allocatable math that training admission and envelope sizing depend on; mistakes can cause scheduling failures or node OOM, though values are measurement-derived and guarded by drift, parity, and e2e checks.
Overview
Replaces guessed control-plane overhead with measured
kubeReserved,systemReserved, andevictionHard.memory.availablein the kubelet drop-in both installers already mount, so node allocatable no longer equals full capacity on recorded platforms (darwin,linux).Measurement → embed chain: JSON records under
scripts/spec/node-reservation/(macOS arm64 plus Linux Ubuntu legs in this PR) feedscripts/gen-node-reservation-embed.sh, which writes the generated block intoscripts/lib/cluster.shandscripts/install-k8s.ps1.make driftruns--checkon that generator;gen-installer-parity.shnow emits parity constants for reservation writer shape.Installer behavior: Bash
_write_kubelet_configand PowerShellWrite-KubeletConfigadd reservation maps only when the platform is listed; unmeasured platforms (e.g. Windows/WSL2) get image-GC only plus a one-time warn. Existing clusters with a mount but nokubeReserved:get recreate hints—no in-place rewrite. Comments are corrected: k3s merges config-dir drop-ins key-by-key, so onlymemory.availableis written and disk eviction keys stay from k3s.CI: New label-gated
node-reservation-measurejob runsmeasure-node-reservation.shon three Ubuntu runners and uploads artifacts (non-gating evidence). Chart 1.9.109;manifest.sha256updated for touched bootstrap scripts.Reviewed by Cursor Bugbot for commit 362ac18. Bugbot is set up for automated code reviews on this repo. Configure here.