test(e2e): measure tenant worker CPU as a rate, and the sandbox nodes under it - #3836
Conversation
… under it Two instruments for the node-join failure the kubernetes-* suites keep hitting. Neither changes what the suites assert. The worker CPU capture now reads container_cpu_usage_seconds_total beside the CFS counters, and reads them twice a fixed interval apart instead of once. The throttled counters say a container met its ceiling and cannot say how much CPU it got, which are the two candidate explanations for a stalled worker: a guest held at a one-core ceiling, and a guest whose vCPU thread is rarely scheduled at all. Both leave throttled periods behind. Every counter involved accumulates from container start, so a single reading is an average over the whole uptime and no rate at all, and the profile under investigation is a guest that freezes for tens of seconds and then runs flat out: the average comes out low, the instantaneous figure comes out at the cap, and only the second separates it from a guest that is simply capped. The sandbox nodes are a new subject. The worker captures describe a cgroup, so a guest that is runnable and never scheduled looks from there like a guest that asked for nothing. Whether the sandbox node itself was given its turn is recorded only in the steal column of its own /proc/stat: no kubelet endpoint carries it, and crust-gather cannot reach it because PodSecurity refuses its node-shell Pod here, so the read goes over the Talos API, which is already how the report reaches those nodes for dmesg. The rows are captured verbatim with their column legend beside them rather than reduced to a percentage, because /proc/stat has no header, steal and guest sit next to each other, and guest is large on a node running VMs and already counted inside user. Both subjects are read on either side of one shared wait, so the pair pays it once. The wait is not the interval either subject's counters span, though: each pass walks its own nodes and the other subject's pass falls between a subject's two readings, and on the run this exists for those passes are the slow part. Each capture therefore stamps both ends of its read, so a rate comes from subtracting stamps rather than from assuming the knob, and the width of those brackets is the uncertainty. That wait is a measurement interval rather than a stand-in for a condition nobody wrote a wait for, so the e2e conventions carve it out by name instead of marking it as a fixed timeout to be replaced. The pair is gated below the serial console rather than above it. Its ceiling is sixteen bounded reads plus the interval, which is most of the phase budget, and admission gates only when a collector may start, so above the console a run that hit those bounds would leave the console never started rather than merely cut short. The console is the only capture that survives a worker which never reached apid, which is the shape this failure usually takes. That overrun is inside the term the budget derivation already allows for one late admission, and a guard holds the pair's ceiling against it. No collector here may leave an empty directory, since an empty directory reads as a healthy node: a missing binary, a missing talosconfig, a listing that never answered, a node with no address and a read that was cut off each land as a named note or a COLLECTION-FAILED.txt. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThe diagnostic phase now captures paired worker CPU/throttling and sandbox-node CPU samples. It records timestamps, bounds reads, handles incomplete results, updates collector ordering and budgets, and adds extensive Bats coverage. ChangesSampled CPU diagnostic flow
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant DiagnosticPhase
participant WorkerCPUCollector
participant SandboxCPUCollector
participant cAdvisor
participant Talosctl
DiagnosticPhase->>WorkerCPUCollector: Capture sample 1
WorkerCPUCollector->>cAdvisor: Read worker CPU metrics
DiagnosticPhase->>SandboxCPUCollector: Capture sample 1
SandboxCPUCollector->>Talosctl: Read sandbox /proc/stat
DiagnosticPhase->>DiagnosticPhase: Wait for sampling interval
DiagnosticPhase->>WorkerCPUCollector: Capture sample 2
DiagnosticPhase->>SandboxCPUCollector: Capture sample 2
Possibly related PRs
Suggested labels: Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
## What this PR does One line in the kubernetes e2e suite values: tenant workers move from `u1.medium` to `u1.large`, one vCPU to two. The reason is measured, in two independent ways on #3513. A single-scrape reading on a quiet branch run and the two-scrape instrument from #3836 on a loaded run with six parallel e2e workflows agree: the worker's compute container sits at 93 to 98 percent of its one-core ceiling for the whole node-Ready budget, the sandbox nodes under it show ~0.05 percent steal, and raising the CPU request changes nothing because the workers never lose a scheduling fight. The binding constraint is capacity: one vCPU has to boot Talos and unpack the kubelet image, and its quota also covers the QEMU emulator and IO threads. This is an experiment as much as a fix: the suites run the same assertions, and the next waves of runs under parallel load measure whether the join flake documented in #3513 disappears with the ceiling doubled. The diagnostics landed in #3804 and #3836 stay aboard, so the same instrument reports the new profile. Memory math on the sandbox: two suites with two workers each move from 4Gi to 8Gi guests, and the virt-launcher pods spread across three 24Gi nodes, so the placement still fits. ### Screenshots Not applicable, no UI change. ### Downstream repositories The diff is one value inside `hack/e2e-chainsaw/_lib/run-kubernetes.sh`. Nothing under `hack/` is moved or renamed, no make target changes behaviour, no package, schema, default or CRD changes. Nothing in the trigger map is touched. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: ### Release note ```release-note test(e2e): tenant workers in the kubernetes e2e suites run with two vCPUs, since measurements showed the single-vCPU worker saturating its CPU ceiling for the entire node-join budget ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated the tenant worker node configuration to use a larger instance type, improving available capacity for end-to-end Kubernetes testing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
Two instruments for the tenant worker that misses the node-Ready deadline. They are instruments, not a fix: the failure they instrument is #3513, and this PR does not close it. Neither changes what the suites assert.
The worker CPU capture now reads
container_cpu_usage_seconds_totalbeside the CFS counters it already read, and it is read twice a fixed interval apart instead of once. The throttled counters say a container met its ceiling; they cannot say how much CPU it got, and the two failures behind a stalled worker are exactly those, a guest held at a one-core ceiling and a guest whose vCPU thread is rarely scheduled at all. Both leave throttled periods behind. Every counter involved is cumulative since the container started, so a single reading divides out to an average over the whole uptime and says nothing about the window the deadline covered, which matters here because the profile under investigation is a guest that freezes for tens of seconds and then runs flat out: the average comes out low, the instantaneous figure comes out at the cap, and only the second distinguishes it from a guest that is simply capped.The second instrument is a new subject. The worker captures describe a cgroup, so they are blind to the layer under the sandbox, and a guest that is runnable and never scheduled looks from the cgroup like a guest that asked for nothing. Whether the sandbox node itself was given its turn is recorded only in the steal column of its own
/proc/stat. No kubelet endpoint carries it, and crust-gather cannot reach it because PodSecurity refuses its node-shell Pod in this cluster, so the capture goes over the Talos API, which is already how the report reads those nodes for dmesg. It is captured verbatim with its column legend beside it rather than reduced to a percentage:/proc/stathas no header, steal and guest sit next to each other, and guest is large on a node running VMs and already counted inside user, so reading one as the other turns a fraction of a percent into twenty-odd.Both subjects are read on either side of one shared wait, so the pair pays it once. That wait is guarded like every other external in the block: it is the one call there outside a
|| true, and underset -eua missingsleepwould exit 127 and take the collectors after it, two of which the phase's own missing-timeoutwarning promises keep collecting. Without it the two readings are taken back to back and the block says so, which is honest here because the interval is read off the stamps rather than off the knob. The default is 12 seconds and that is the whole wall-clock cost this adds to a failing run. The wait is not the interval either subject's counters span, though: each pass walks its own nodes and the other subject's pass falls between a subject's two readings, and on the run these diagnostics exist for those passes are the slow part rather than the wait. Each capture therefore stamps both ends of its read, so the real interval is subtracted off the pair instead of assumed from the knob, and the width of those brackets is the uncertainty. This matters more on the sandbox side than on the worker side, since a cAdvisor row carries its own sample time and a/proc/statrow carries none. That wait is a measurement interval rather than a stand-in for a condition nobody wrote a wait for, sodocs/agents/e2e-testing.mdcarves it out by name instead of leaving a reader to decide whether theTODO(e2e-replace-fixed-timeouts):marker applies to a gap between two readings of a cumulative counter.The pair is gated below the serial console rather than above it, and its ceiling rather than its worth is the reason. Reading two collectors twice is sixteen bounded reads plus the interval, which at the read bound and the three-node cap is most of the phase budget, and admission gates only when a collector may start. Above the console, a run that hit those bounds would leave the console never started rather than merely cut short, and the console is the only capture that survives a worker which never reached apid, which is the shape this failure usually takes. That overrun is inside the term the budget derivation already allows for one collector admitted a moment before the deadline, and a guard in
hack/run-kubernetes-node-join_test.batsholds the pair's ceiling against it. Two more guards there hold what may sit ahead of the console: nothing read more than once, and nothing whose ceiling could spend the budget on its own.Failure marking follows the existing collectors rather than inventing a shape: no collector here may leave an empty directory, because an empty directory reads as a healthy node, so a missing binary, a missing talosconfig, a listing that never answered, a node with no address and a read that was cut off each land as a named note or a
COLLECTION-FAILED.txt.The phase budget is unchanged at 420s and the derivation guard still holds.
Covered by
hack/run-kubernetes-sandbox-cpu-time_test.bats(new) plus additions to the cpu-throttle, node-join and network-counters suites. Each collector was checked against a deliberate break of its own guard: dropping the usage family, ignoring the sample number, removing the interval, accepting a zero interval, swallowing the missing-talosconfig marker, reading the wrong/procfile, taking a dual-stack node's second address for a node, counting addresses instead of nodes in the truncation marker, stamping only one end of a read, letting the stamp claim counters were sampled on an arm that read nothing, telling a capture with no counters to subtract a sibling, leaving the sampling wait unguarded, and starving the budget guard of its input. Every one is caught by a named test.Screenshots
Not applicable, no UI change.
Downstream repositories
The diff touches
hack/e2e-chainsaw/_lib/run-kubernetes.sh, twochainsaw-test.yamlfiles, four BATS files and one agent doc. Walking the trigger map: nothing underhack/is moved or renamed,hack/package.mkandhack/common-envs.mkare untouched, no make target changes what it does, no package is added or removed, no schema, default, enum, CRD or namespace moves. The one entry that mentions this area is theccprule about moving or renaming things underhack/, and no file is moved or renamed here.Release note
Summary by CodeRabbit
New Features
Documentation