eve-kvm:core-isolation
Differences
This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| eve-kvm:core-isolation [2026/09/18 16:34] – created mc | eve-kvm:core-isolation [2026/09/19 17:35] (current) – mc | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | ====== EVE CPU Core Isolation — Step-by-Step Runbook ====== | ||
| + | |||
| + | Follow the steps in order. Every step has a command, the output you should see, and what to do if you do not see it. Do not skip **Step 5** (the BEFORE baseline) — it is what makes the AFTER checks meaningful. | ||
| + | |||
| + | **Scope.** The '' | ||
| + | |||
| + | **Time required.** About 20 minutes, including one reboot. | ||
| + | |||
| + | **Just here to run the demo?** Use the four-act script immediately below. The numbered steps in Parts 1-4 are the full procedure behind it. | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Quick demo script — 1, 2, 3, 4 ===== | ||
| + | |||
| + | Four commands-and-a-sentence. Run them in order, with the pinned VM **not yet deployed**. | ||
| + | |||
| + | Set this up first so every command is a single keystroke away: | ||
| + | |||
| + | <code bash> | ||
| + | alias iso=' | ||
| + | alias pools=' | ||
| + | alias plan=' | ||
| + | alias ticks=' | ||
| + | </ | ||
| + | |||
| + | ==== 1. BEFORE — a plain node: no isolation, no assignment ==== | ||
| + | |||
| + | <code bash> | ||
| + | iso | ||
| + | pools | ||
| + | ticks | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | isolated: [] | ||
| + | {" | ||
| + | {" | ||
| + | cpu0=73851 | ||
| + | </ | ||
| + | |||
| + | **Say:** "The kernel isolates nothing. There is one pool — all eight threads, shared. Every CPU is doing work. Any workload can be scheduled anywhere, and so can the kernel' | ||
| + | |||
| + | //This is the state before any configuration. If your node is already configured and you want to rehearse the whole arc, revert it with:// '' | ||
| + | |||
| + | ==== 2. AFTER the config — the isolated pool exists, and nobody may use it ==== | ||
| + | |||
| + | Enable per Part 2 (property, '' | ||
| + | |||
| + | <code bash> | ||
| + | iso | ||
| + | pools | ||
| + | ticks | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | isolated: [4-7] | ||
| + | {" | ||
| + | {" | ||
| + | {" | ||
| + | cpu0=9525 | ||
| + | </ | ||
| + | |||
| + | **Say:** "Now there are three pools. Four CPUs are isolated. Look at the housekeeping pool — of eight threads, six are gone: two reserved for EVE, four withheld for isolation. Only one whole core is left for ordinary work. And cores 4 through 7 have executed **zero** cycles since boot — not the scheduler, not a workload, nothing." | ||
| + | |||
| + | **Point at:** '' | ||
| + | |||
| + | ==== 3. DEPLOY — a pinned VM takes an isolated core ==== | ||
| + | |||
| + | Deploy a **2-vCPU** app with CPU pinning enabled, then: | ||
| + | |||
| + | <code bash> | ||
| + | plan | ||
| + | pools | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | { " | ||
| + | " | ||
| + | |||
| + | {" | ||
| + | {" | ||
| + | {" | ||
| + | </ | ||
| + | |||
| + | **Say:** "Two vCPUs, one whole physical core, taken from the isolated set — cores 4 and 5. The isolated pool drops from two free cores to one. The app asked for nothing but 'CPU pinning'; | ||
| + | |||
| + | ==== 4. PROVE IT — the kernel agrees, down to the thread ==== | ||
| + | |||
| + | <code bash> | ||
| + | PID=$(pgrep -f qemu-system | head -1) | ||
| + | for t in / | ||
| + | printf '%-18s %s | ||
| + | ' "$(cat $t/ | ||
| + | done | sort -u | ||
| + | cat / | ||
| + | ticks | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | qemu-system-x86 | ||
| + | qemu-system-x86 | ||
| + | qemu-system-x86 | ||
| + | vhost-7981 | ||
| + | |||
| + | 4-5 | ||
| + | |||
| + | cpu0=9525 | ||
| + | </ | ||
| + | |||
| + | **Say:** "One vCPU thread per hardware thread, pinned 1:1. Everything else the VM needs is confined to the same core. The cgroup agrees. And the second isolated core is **still at zero** — it is reserved and untouchable, | ||
| + | |||
| + | **The closing line:** cores 6 and 7 are idle and cannot be used by anything that did not ask for isolation. On this node, a workload that needs guaranteed CPU gets it — not by priority, not by best effort, but because nothing else is allowed to run there. | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Part 0 — Understand what you are doing ===== | ||
| + | |||
| + | There are **two independent switches**. Neither works without the other. | ||
| + | |||
| + | ^ # ^ Switch ^ Where it is set ^ What it does ^ Needs reboot? ^ | ||
| + | | 1 | '' | ||
| + | | 2 | '' | ||
| + | |||
| + | A third thing is often confused with these: | ||
| + | |||
| + | * **"CPU pinning" | ||
| + | |||
| + | Target layout on a 4-core / 8-thread node: | ||
| + | |||
| + | ^ Role ^ Physical core ^ CPUs ^ | ||
| + | | EVE housekeeping | core 0 | 0, 1 | | ||
| + | | Dedicated pool (ordinary pinned workloads) | core 1 | 2, 3 | | ||
| + | | **Isolated pool** | cores 2-3 | **4, 5, 6, 7** | | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Part 1 — BEFORE: prerequisites and baseline ===== | ||
| + | |||
| + | Run every command in this part **on the node**, over SSH or the console, before changing anything. | ||
| + | |||
| + | ==== Step 1 — Confirm the CPU can do this ==== | ||
| + | |||
| + | <code bash> | ||
| + | lscpu | grep -E 'Model name|Thread\(s\) per core|Core\(s\) per socket|NUMA node\(s\)' | ||
| + | </ | ||
| + | |||
| + | Expected: | ||
| + | |||
| + | < | ||
| + | Model name: 13th Gen Intel(R) Core(TM) i3-13100TE | ||
| + | Thread(s) per core: 2 | ||
| + | Core(s) per socket: | ||
| + | NUMA node(s): | ||
| + | </ | ||
| + | |||
| + | **PASS if '' | ||
| + | |||
| + | You also need **at least 3 physical cores**: one for EVE, one for the dedicated pool, one to isolate. | ||
| + | |||
| + | ^ CPU ^ Topology ^ Usable ^ | ||
| + | | Core i3-10100 / 10105 / 10300 | 4C/8T | yes | | ||
| + | | Core i3-12100 / 12300, i3-13100, i3-14100 | 4 P-cores, 0 E-cores, 8T | yes — ideal | | ||
| + | | Core i5-12400 | 6 P-cores, 0 E-cores, 12T | yes — more headroom | | ||
| + | | Core i3-8100 / 8300, i3-9100 / 9300 | 4C/4T, no HT | **no** | | ||
| + | | Core i3-N300 / i3-N305 (Alder Lake-N) | 8 E-cores, no HT | **no** | | ||
| + | | Core Ultra 200S / Meteor Lake / Lunar Lake | SMT removed | **no** | | ||
| + | |||
| + | Avoid Intel hybrid parts with E-cores (i5/i7 12th gen and up): the E-cores have no SMT and are silently skipped for pinning, so they add nothing and confuse the results. | ||
| + | |||
| + | ==== Step 2 — Find out which CPUs are SMT siblings ==== | ||
| + | |||
| + | This decides the numbers you will type in Step 8. **Do not assume them.** | ||
| + | |||
| + | <code bash> | ||
| cat / | cat / | ||
| + | </ | ||
| + | |||
| + | Expected on the reference node: | ||
| + | |||
| + | < | ||
| + | 0-1 | ||
| + | 2-3 | ||
| + | 4-5 | ||
| + | 6-7 | ||
| + | </ | ||
| + | |||
| + | ^ If you see ^ Enumeration ^ Use the values in ^ | ||
| + | | '' | ||
| + | | '' | ||
| + | |||
| + | ==== Step 3 — Confirm nothing is isolated yet ==== | ||
| + | |||
| + | <code bash> | ||
| + | cat / | ||
| + | </ | ||
| + | |||
| + | Expected: **empty output.** That is the starting state. If it already lists CPUs, someone has been here before — read the existing ''/ | ||
| + | |||
| + | ==== Step 4 — Confirm the property is currently off ==== | ||
| + | |||
| + | <code bash> | ||
| + | grep -o ' | ||
| + | </ | ||
| + | |||
| + | Expected: | ||
| + | |||
| + | < | ||
| + | cpu.pinning.use.isolated": | ||
| + | </ | ||
| + | |||
| + | '' | ||
| + | |||
| + | ==== Step 5 — Capture the BEFORE baseline ==== | ||
| + | |||
| + | Paste this whole block. Save the output somewhere; you will compare against it in Step 12. | ||
| + | |||
| + | <code bash> | ||
| + | echo "=== cmdline ===" | ||
| + | tr ' ' ' | ||
| + | echo "=== kernel isolated set ===" | ||
| + | echo " | ||
| + | echo "=== CPU pools ===" | ||
| + | cat / | ||
| + | echo "=== placement plan ===" | ||
| + | cat / | ||
| + | echo "=== eve cpuset ===" | ||
| + | cat / | ||
| + | echo "=== per-CPU busy ticks ===" | ||
| + | awk '/ | ||
| + | </ | ||
| + | |||
| + | Expected BEFORE (no isolation, nothing pinned): | ||
| + | |||
| + | < | ||
| + | === cmdline === | ||
| + | eve_max_vcpus=1 | ||
| + | === kernel isolated set === | ||
| + | [] | ||
| + | === CPU pools === | ||
| + | {" | ||
| + | | ||
| + | | ||
| + | | ||
| + | ]} | ||
| + | === eve cpuset === | ||
| + | 0 | ||
| + | </ | ||
| + | |||
| + | Pool '' | ||
| + | |||
| + | What this baseline says: all 8 threads are in housekeeping, | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Part 2 — ENABLE ===== | ||
| + | |||
| + | Do these three steps **in this order**. Doing the property last costs you a second reboot. | ||
| + | |||
| + | ==== Step 6 — Set the controller property ==== | ||
| + | |||
| + | **This property is not exposed in the ZedControl UI.** Do not go looking for it there — the UI only offers properties it knows about, and this one is new in the feature branch. It can only be set via **Terraform** or the **REST API**. | ||
| + | |||
| + | **Terraform** — a '' | ||
| + | |||
| + | < | ||
| + | resource " | ||
| + | # ... existing fields ... | ||
| + | |||
| + | config_item { | ||
| + | key = " | ||
| + | string_value = " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Then '' | ||
| + | |||
| + | **Use '' | ||
| + | |||
| + | ^ Hop ^ Schema ^ Fields ^ | ||
| + | | you → controller | '' | ||
| + | | controller → device | eve-api '' | ||
| + | |||
| + | The controller has to collapse the typed fields into a single string '' | ||
| + | |||
| + | **REST** — '' | ||
| + | |||
| + | <code javascript> | ||
| + | { " | ||
| + | </ | ||
| + | |||
| + | **This is a full-object PUT, not a patch.** The body carries the whole edge node — '' | ||
| + | |||
| + | So the REST procedure is read-modify-write: | ||
| + | |||
| + | - '' | ||
| + | - append '' | ||
| + | - '' | ||
| + | |||
| + | This is why Terraform is the easier route: the provider does that read-modify-write for you. Schema verified against '' | ||
| + | |||
| + | **Do not expect to find the key itself in the API documentation.** In the spec '' | ||
| + | |||
| + | Whichever route you use, **Step 7 is what confirms it** — do not assume it landed. | ||
| + | |||
| + | ==== Step 7 — Confirm the property reached the node ==== | ||
| + | |||
| + | Do not go further until this passes. Allow a minute for the config poll. | ||
| + | |||
| + | <code bash> | ||
| + | grep -o ' | ||
| + | logread | grep ' | ||
| + | </ | ||
| + | |||
| + | Expected: | ||
| + | |||
| + | < | ||
| + | " | ||
| + | domainmgr: CPU placement: cpu.pinning.use.isolated is now true; workloads placed from now on may use the kernel-isolated CPUs | ||
| + | </ | ||
| + | |||
| + | **If '' | ||
| + | |||
| + | ==== Step 8 — Write / | ||
| + | |||
| + | ''/ | ||
| + | |||
| + | === Step 8a — adjacent siblings (0-1, 2-3, 4-5, 6-7) === | ||
| + | |||
| + | <code bash> | ||
| + | eve config mount /tmp/cfg | ||
| + | cat > / | ||
| + | set_getty | ||
| + | set_global hv_eve_cpu_settings " | ||
| + | set_global dom0_extra_args " | ||
| + | EOF | ||
| + | cat / | ||
| + | sync | ||
| + | eve config unmount | ||
| + | </ | ||
| + | |||
| + | === Step 8b — split siblings (0,4 1,5 2,6 3,7) === | ||
| + | |||
| + | <code bash> | ||
| + | eve config mount /tmp/cfg | ||
| + | cat > / | ||
| + | set_getty | ||
| + | set_global dom0_extra_args " | ||
| + | EOF | ||
| + | cat / | ||
| + | sync | ||
| + | eve config unmount | ||
| + | </ | ||
| + | |||
| + | There is **no '' | ||
| + | |||
| + | **Check the '' | ||
| + | |||
| + | Rules that matter here: | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * Do **not** use GRUB's '' | ||
| + | |||
| + | ==== Step 9 — Reboot ==== | ||
| + | |||
| + | <code bash> | ||
| + | reboot | ||
| + | </ | ||
| + | |||
| + | Allow about 3 minutes. The reboot is required for two reasons: '' | ||
| + | |||
| + | Expect the SSH host key to change: EVE regenerates ''/ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Part 3 — AFTER: validate ===== | ||
| + | |||
| + | Run these in order. Stop at the first failure. | ||
| + | |||
| + | ==== Step 10 — The kernel accepted the arguments ==== | ||
| + | |||
| + | <code bash> | ||
| + | tr ' ' ' | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | eve_max_vcpus=2 | ||
| + | isolcpus=managed_irq, | ||
| + | rcu_nocbs=4, | ||
| + | nohz_full=4, | ||
| + | irqaffinity=0, | ||
| + | </ | ||
| + | |||
| + | **If nothing appears:** GRUB never read your file. Confirm it is named '' | ||
| + | |||
| + | ==== Step 11 — The kernel is actually isolating ==== | ||
| + | |||
| + | <code bash> | ||
| + | cat / | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | 4-7 | ||
| + | </ | ||
| + | |||
| + | **This is the make-or-break check.** An empty result means there is no isolated pool and nothing after this point can work. Do not continue — fix the command line first. | ||
| + | |||
| + | ==== Step 12 — domainmgr saw the topology and the isolated set ==== | ||
| + | |||
| + | <code bash> | ||
| + | logread | grep -E 'CPU topology|Kernel isolates' | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | msg":" | ||
| + | msg":" | ||
| + | </ | ||
| + | |||
| + | '' | ||
| + | |||
| + | Ignore the wording of the second line — it is printed whenever the isolated set is non-empty, whether or not the switch is on. Step 7 is what tells you the switch is on. | ||
| + | |||
| + | ==== Step 13 — The pools changed ==== | ||
| + | |||
| + | <code bash> | ||
| + | cat / | ||
| + | </ | ||
| + | |||
| + | <code javascript> | ||
| + | {" | ||
| + | | ||
| + | | ||
| + | | ||
| + | ]} | ||
| + | </ | ||
| + | |||
| + | ==== Step 14 — BEFORE vs AFTER at a glance ==== | ||
| + | |||
| + | ^ Check ^ BEFORE ^ AFTER ^ | ||
| + | | ''/ | ||
| + | | '' | ||
| + | | Isolated pool (Kind 3) | does not exist | '' | ||
| + | | Housekeeping free CPUs (Kind 1) | '' | ||
| + | | Housekeeping free whole cores | 3 | 1 | | ||
| + | |||
| + | The housekeeping line is the one to point at. Of 8 threads, 6 are now allocated: CPUs 0-1 reserved for EVE and CPUs 4-7 withheld for isolation. Only core 1 is left for an ordinary workload. That is the feature working: housekeeping freeness answers //" | ||
| + | |||
| + | ==== Step 15 — Nothing is running on the isolated cores ==== | ||
| + | |||
| + | <code bash> | ||
| + | awk '/ | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | cpu4 0 | ||
| + | cpu5 0 | ||
| + | cpu6 0 | ||
| + | cpu7 0 | ||
| + | </ | ||
| + | |||
| + | Zero busy ticks since boot. Keep this number — it is the strongest single line in the demo. | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Part 4 — Demo ===== | ||
| + | |||
| + | ==== Step 16 — A pinned workload lands on the isolated cores ==== | ||
| + | |||
| + | Deploy a **2-vCPU** app with CPU pinning enabled (even vCPU counts only — an odd count fails closed with '' | ||
| + | |||
| + | <code bash> | ||
| + | cat / | ||
| + | cat / | ||
| + | </ | ||
| + | |||
| + | <code javascript> | ||
| + | { " | ||
| + | " | ||
| + | |||
| + | {" | ||
| + | | ||
| + | | ||
| + | | ||
| + | ]} | ||
| + | </ | ||
| + | |||
| + | Read it as: the isolated pool went from 2 free whole cores to 1, the dedicated pool //is// '' | ||
| + | |||
| + | Worth saying out loud during a demo: the app asked only for '' | ||
| + | |||
| + | ==== Step 17 — Capacity fails closed instead of spilling ==== | ||
| + | |||
| + | //Not yet captured on hardware; this is the expected behaviour.// | ||
| + | |||
| + | With the switch **on**, a second 2-vCPU pinned app is promoted onto the remaining isolated core ('' | ||
| + | |||
| + | With the switch **off**, it is the //second// app that fails, for the same reason. | ||
| + | |||
| + | The error names how many cores were withheld for isolation, so the operator is not left comparing " | ||
| + | |||
| + | ==== Step 18 — Prove it at the thread level ==== | ||
| + | |||
| + | The pool report is EVE's own bookkeeping. This shows the kernel agrees. | ||
| + | |||
| + | <code bash> | ||
| + | ls / | ||
| + | PID=$(pgrep -f qemu-system | head -1) | ||
| + | for t in / | ||
| + | printf '%-18s %s\n' "$(cat $t/ | ||
| + | done | sort -u | ||
| + | find / | ||
| + | awk '/ | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | qemu-system-x86 | ||
| + | qemu-system-x86 | ||
| + | qemu-system-x86 | ||
| + | vhost-7981 | ||
| + | iou-wrk-7981 | ||
| + | |||
| + | / | ||
| + | |||
| + | cpu4 1990 cpu5 813 cpu6 0 cpu7 0 | ||
| + | </ | ||
| + | |||
| + | The two vCPU threads are pinned **1:1** to CPUs 4 and 5; everything else is confined to '' | ||
| + | |||
| + | ==== Step 19 — One-shot cross-check ==== | ||
| + | |||
| + | The branch ships a script that cross-checks the allocator' | ||
| + | |||
| + | <code bash> | ||
| + | GUEST_PASS=< | ||
| + | </ | ||
| + | |||
| + | Override its lab defaults ('' | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Part 5 — Known limitations ===== | ||
| + | |||
| + | ==== 5.1 nohz_full and rcu_nocbs do nothing on the shipped kernel ==== | ||
| + | |||
| + | <code bash> | ||
| + | dmesg | grep -iE ' | ||
| + | (zcat / | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | Housekeeping: | ||
| + | Unknown kernel command line parameters "... rcu_nocbs=4, | ||
| + | # CONFIG_NO_HZ_FULL is not set | ||
| + | CONFIG_CPU_ISOLATION=y | ||
| + | </ | ||
| + | |||
| + | ^ Argument ^ Effect ^ | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | |||
| + | So what you can demonstrate today is **scheduler isolation**: | ||
| + | |||
| + | ==== 5.2 EVE's own cpuset stays on CPU 0 ==== | ||
| + | |||
| + | ''/ | ||
| + | |||
| + | ==== 5.3 The vault reports a PCR mismatch on the first boot ==== | ||
| + | |||
| + | Changing the kernel command line changes PCRs 8, 9 and 14, so '' | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Part 6 — Troubleshooting ===== | ||
| + | |||
| + | ^ Symptom ^ Cause ^ Fix ^ | ||
| + | | Cannot find the property in the controller UI | it is not exposed there | set it via Terraform or REST (Step 6) | | ||
| + | | Property set in Terraform, node still shows '' | ||
| + | | Property set on the application instead of the node | '' | ||
| + | | ''/ | ||
| + | | '' | ||
| + | | **Pinned VM stays on the dedicated cores after enabling the switch** | a running workload holds its allocation; '' | ||
| + | | VM halted with '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | Placement refused citing a synthetic topology | sysfs topology discovery failed | check ''/ | ||
| + | | Isolated cores free but nothing can use them | expected with the switch off — they are withheld from every workload that did not ask for isolation | set the property (Step 6), or use '' | ||
| + | | SSH host key changed after reboot | EVE regenerates ''/ | ||
| + | | Boot loop, '' | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Part 7 — Reference ===== | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * Pool kinds and error codes: '' | ||
| + | * Placement logic: '' | ||
| + | * The allocation-reuse shortcut discussed in Troubleshooting: | ||
| + | |||
eve-kvm/core-isolation.1789749252.txt.gz · Last modified: by mc
