You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #182. Under high density the VM population can occupy every host core; the CPU scope caps how much time VMs burn but not where — clone/wake execution, vk, and the OS still compete for CPU with the fleet on all cores. Field data (vk three-tier wake runs): CH clone median 6.98s but p95 24.9s / max 69.2s under load.
Neither backend can express this natively: CH --cpus affinity= pins only vCPU threads while virtio/io-wq workers (the 10-13% overhead measured in #182) roam every core; FC has nothing. The mechanism is cgroup v2 cpuset, and it lands on the parent/scope infrastructure #182 built.
Proposal
Two layers, both raw cpuset encodings, zero hot-path cost (one control-file write at parent setup / VM launch):
P0 — machine fence (config): cgroup_cpus config key (e.g. "0-14"; default empty = all cores = today's behavior). ensureParent enables +cpuset along the chain and writes cpuset.cpus on the parent once. Every VM scope inherits — VM threads physically cannot touch reserved cores regardless of per-VM quota planning. Overcommit then happens inside the fence; reserved cores stay available to the host, vk, and the clone path. Same shape as K8s reserved-cpus / systemd AllowedCPUs.
P1 — per-VM placement (flag): --cpuset-cpus on vm run/clone (range or specific cores) → the scope's own cpuset.cpus. Documented as non-work-conserving (#182 listed pinning as a non-goal for defaults — that reasoning stands; this is an explicit opt-in for NUMA affinity / isolation-sensitive workloads, never a default). Snapshot semantics follow #182's rule: the knob is host policy — a record, never applied from snapshots; clone takes it from flags alone, restore keeps the VM's.
Composition: fence (where) × cpu.weight (share) × cpu.max (ceiling) are orthogonal; all three combine.
Validation is cocoon's job — the kernel does not reject out-of-range requests
cgroup v2 cpuset.cpus is a requested set; cpuset.cpus.effective is what the parent actually grants, and the kernel contract allows the effective set to silently shrink — a request none of whose CPUs can be granted behaves like an empty request (inherit parent), not an error. So enforcement is explicit, all driven by one parsed set:
machine fence must be validated ⊆ the parent's ancestor cpuset.cpus.effective;
each VM placement must be validated ⊆ the parent's cpuset.cpus.effective;
shrinking the fence is refused while an existing VM's non-empty placement would be invalidated by it;
the same parsed set drives the queue_affinity clamp — no extra abstraction.
Convergence: clearing the fence must converge
The parent cgroup persists across runs and #182's cleanup removes only vm-*.scope children — so "empty config never writes" would leave a previously-set fence at 0-14 forever, breaking empty = all cores. Parent reconciliation is read-gated, mirroring #182's subtree_control handling: fresh/steady empty stays zero-write, but a stale non-empty cpuset.cpus on the parent is cleared once when the config is empty. The controller itself is never disabled.
Interactions to handle
Disk queue_affinity: args.go pins virtio-blk queues to host CPUs 0..N-1 unconditionally. With a fence (or per-VM cpuset) excluding low cores, those targets fall outside the allowed set — queue affinity targets must clamp to the effective cpuset or be dropped when they don't intersect. The clamp lives on the shared storageConfigToDisk path so it covers initial launch, clone's post-restore disk addition, and runtime DiskAttach alike — not just first boot.
Hotplug ceiling: CH max=NumCPU boot arg is unrelated (guest topology); no change.
Acceptance
Fence 0-14 on a 16-core host + saturated VM fleet: mpstat shows core 15 idle; Cpus_allowed_list of every VMM thread (incl. io-wq) ⊆ 0-14.
Clone-latency A/B under the same saturation, fence off vs on: reserved-core headroom must show in the tail (this is the motivating number).
--cpuset-cpus placement visible in /proc/<pid>/status; out-of-fence placement → cocoon rejects with an actionable error (kernel would silently degrade); fence shrink under a live conflicting placement → refused.
Fence rollback: set "0-14", restart with empty config → parent cpuset.cpus cleared once, subsequent empty runs zero-write.
queue_affinity targets stay inside the effective cpuset on launch, clone, and DiskAttach.
Follow-up to #182. Under high density the VM population can occupy every host core; the CPU scope caps how much time VMs burn but not where — clone/wake execution, vk, and the OS still compete for CPU with the fleet on all cores. Field data (vk three-tier wake runs): CH clone median 6.98s but p95 24.9s / max 69.2s under load.
Neither backend can express this natively: CH
--cpus affinity=pins only vCPU threads while virtio/io-wq workers (the 10-13% overhead measured in #182) roam every core; FC has nothing. The mechanism is cgroup v2cpuset, and it lands on the parent/scope infrastructure #182 built.Proposal
Two layers, both raw cpuset encodings, zero hot-path cost (one control-file write at parent setup / VM launch):
P0 — machine fence (config):
cgroup_cpusconfig key (e.g."0-14"; default empty = all cores = today's behavior).ensureParentenables+cpusetalong the chain and writescpuset.cpuson the parent once. Every VM scope inherits — VM threads physically cannot touch reserved cores regardless of per-VM quota planning. Overcommit then happens inside the fence; reserved cores stay available to the host, vk, and the clone path. Same shape as K8sreserved-cpus/ systemdAllowedCPUs.P1 — per-VM placement (flag):
--cpuset-cpusonvm run/clone(range or specific cores) → the scope's owncpuset.cpus. Documented as non-work-conserving (#182 listed pinning as a non-goal for defaults — that reasoning stands; this is an explicit opt-in for NUMA affinity / isolation-sensitive workloads, never a default). Snapshot semantics follow #182's rule: the knob is host policy — a record, never applied from snapshots; clone takes it from flags alone, restore keeps the VM's.Composition: fence (where) ×
cpu.weight(share) ×cpu.max(ceiling) are orthogonal; all three combine.Validation is cocoon's job — the kernel does not reject out-of-range requests
cgroup v2
cpuset.cpusis a requested set;cpuset.cpus.effectiveis what the parent actually grants, and the kernel contract allows the effective set to silently shrink — a request none of whose CPUs can be granted behaves like an empty request (inherit parent), not an error. So enforcement is explicit, all driven by one parsed set:cpuset.cpus.effective;cpuset.cpus.effective;queue_affinityclamp — no extra abstraction.Convergence: clearing the fence must converge
The parent cgroup persists across runs and #182's cleanup removes only
vm-*.scopechildren — so "empty config never writes" would leave a previously-set fence at0-14forever, breaking empty = all cores. Parent reconciliation is read-gated, mirroring #182'ssubtree_controlhandling: fresh/steady empty stays zero-write, but a stale non-emptycpuset.cpuson the parent is cleared once when the config is empty. The controller itself is never disabled.Interactions to handle
queue_affinity:args.gopins virtio-blk queues to host CPUs 0..N-1 unconditionally. With a fence (or per-VM cpuset) excluding low cores, those targets fall outside the allowed set — queue affinity targets must clamp to the effective cpuset or be dropped when they don't intersect. The clamp lives on the sharedstorageConfigToDiskpath so it covers initial launch, clone's post-restore disk addition, and runtimeDiskAttachalike — not just first boot.cpuset(testbed: user@1000.service delegates cpu/memory/pids only). Non-root test environments need widened delegation or root; production (root) is unaffected. Preflight follows vm: per-VM cgroup v2 CPU scope (request / limit / burst) — vCPU count only bounds guest parallelism #182: a missingcpusetinterface file with the knob set is an actionable error naming the file.max=NumCPUboot arg is unrelated (guest topology); no change.Acceptance
0-14on a 16-core host + saturated VM fleet:mpstatshows core 15 idle;Cpus_allowed_listof every VMM thread (incl. io-wq) ⊆ 0-14.--cpuset-cpusplacement visible in/proc/<pid>/status; out-of-fence placement → cocoon rejects with an actionable error (kernel would silently degrade); fence shrink under a live conflicting placement → refused."0-14", restart with empty config → parentcpuset.cpuscleared once, subsequent empty runs zero-write.queue_affinitytargets stay inside the effective cpuset on launch, clone, andDiskAttach.cgroup_cpus+ no flag → behavior byte-for-byte identical to vm: per-VM cgroup v2 CPU scope (request / limit / burst) — vCPU count only bounds guest parallelism #182 as merged (no cpuset writes, no+cpusetenablement).