Skip to content

feat(metrics): add lifecycle-aware sandbox resource metrics - #1122

Open
zyl1121 wants to merge 3 commits into
TencentCloud:masterfrom
zyl1121:feat/sandbox-resource-metrics-cubelet
Open

feat(metrics): add lifecycle-aware sandbox resource metrics#1122
zyl1121 wants to merge 3 commits into
TencentCloud:masterfrom
zyl1121:feat/sandbox-resource-metrics-cubelet

Conversation

@zyl1121

@zyl1121 zyl1121 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Motivation

PR #1090 adds the standard guest Task.Stats transport, but raw cumulative counters are not sufficient as a stable sandbox metrics contract. Template restore and rollback can inherit or rewind guest counters, while reusable host cgroup slots can retain counters from a previous sandbox. Query-time collection would also make every Prometheus scrape fan out to active sandboxes.

This PR makes Cubelet the owner of lifecycle-aware CPU and memory semantics, bounded collection, latest-sample caching, and Prometheus export.

What Changed

  • Implement Cubelet sandbox-controller Metrics by querying the primary Cube task through standard containerd Task.Stats and converting transport failures to native containerd errors.
  • Add persisted guest metrics epochs and raw counter baselines for CPU, throttling, and memory failures.
  • Add host cgroup v1/v2 readers and persisted assignment baselines so reused pool slots do not expose a previous sandbox's counter history.
  • Expose guest_workload and host_sandbox as independent accounting scopes; all returns both without adding them together.
  • Add bounded guest and host samplers with independent concurrency limits, fixed per-cycle queues, request timeouts, staleness handling, and latest-sample caching.
  • Add the dedicated cache-only Prometheus endpoint GET /v1/metrics/resource.
  • Start only the samplers selected by export_scopes; default to host_sandbox, with guest_workload and all available through configuration.
  • Keep the existing generic containerd cgroups monitor available under /v1/metrics; the Cube-native resource endpoint is additive and independent.
  • Add aligned English and Chinese operator guides covering scopes, lifecycle semantics, configuration, and the Prometheus endpoint.

The PR is organized as three commits so lifecycle accounting, collection/export, and operator documentation can be reviewed in that order.

Metrics Semantics

CPU total/user/system, throttling, periods, and memory failures are cumulative counters. Memory current and limit are point-in-time gauges. Cubelet exports base values; CPU cores and percentage are derived with Prometheus rates rather than precomputed in the runtime.

Fresh create, clone from inherited state, rollback, and workload recreation start a new guest_workload epoch and baseline. Snapshot/commit, pause/resume, and Cubelet restart preserve the current epoch. Paused, deleted, unavailable, or stale views disappear instead of exporting zero.

host_sandbox uses a separate baseline for each sandbox/cgroup assignment. New sandboxes capture this baseline when the pool slot is assigned, before the sandbox process is attached. Existing sandboxes upgraded without a persisted baseline establish a compatibility baseline from their first successful sample. If assignment-time capture fails, sandbox creation continues but host metrics fail closed for that sandbox instead of exporting an incomplete counter window. Host accounting remains continuous across guest rollback because the host process and cgroup assignment are not rewound.

Prometheus reads only the latest typed cache. A scrape never initiates Task.Stats, guest RPCs, or host cgroup reads.

Exact epoch-scoped memory peak remains intentionally unexported because inherited maxima cannot be normalized by subtraction and sampled peak tracking can miss values between samples.

Review Focus

Rollback counter semantics

Rollback keeps the same sandbox_id but restores an earlier guest kernel and cgroup state. Cubelet therefore persists a prepared guest epoch before entering the shim restore, does not sample the prepared epoch, and activates a new pending baseline only after successful restore.

Task lookup and other local preflight checks complete before epoch preparation, so preflight failure preserves the current epoch. Preparation failure aborts before runtime mutation. A failed or uncertain restore leaves the new epoch prepared and guest_workload fail closed; a later successful rollback establishes a new epoch, while an unrecoverable sandbox must be deleted and recreated. host_sandbox counters remain continuous across rollback.

Relationship to #1022

This PR provides the Cubelet-owned collection/cache layer that can be reused by a later public sandbox metrics API. It does not add GET /sandboxes/{id}/metrics, query-time envd collection, range history, or E2B-compatible response routing.

Scope

This PR is limited to CPU and memory for the primary Cube workload container and the host sandbox cgroup. It does not add disk/network/filesystem metrics, OTLP, historical storage, Cube Guard, vsock metrics forwarding, eBPF collection, or a generic collector for non-Cube containerd runtimes.

Validation

  • Linux focused tests cover guest epoch persistence, rollback ordering, counter normalization, host cgroup v1/v2 reads, pool-slot reuse baselines, and Cubelet restart recovery.
  • The full resource-metrics package passes under the race detector, covering bounded scheduling, concurrency saturation, stale samples, deletion, cache isolation, scope selection, and Prometheus collection.
  • Generic cgroup monitor coexistence, OOM event handling, server registration, and the dedicated resource endpoint pass focused race tests.
  • The exact changed-file manifest matches the Linux validation tree, the bilingual documentation build passes, and Cubelet c0aa5f1 passes the full CPU/memory, snapshot/rollback, pause/resume, deletion, and cleanup lifecycle E2E through CubeAPI and CubeProxy.

Comment thread Cubelet/plugins/cube/internals/resourcemetrics/guest_workload_sampler.go Outdated
Comment thread Cubelet/services/cubebox/rollback.go
@cubesandboxbot

cubesandboxbot Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review: feat(metrics) — lifecycle-aware sandbox resource metrics (#1122)

AI-generated review. Reviewed against the base branch (master); PR head files were not checked out.

This is a large, carefully-engineered PR (~6.7k additions) that makes Cubelet the owner of lifecycle-aware CPU/memory accounting for sandboxes. The design is well thought out: guest metric epochs with persisted baselines, host cgroup assignment baselines that survive pool-slot reuse, bounded collection with per-scope concurrency limits, latest-sample caching so Prometheus scrapes never fan out to sandboxes, and a dedicated /v1/metrics/resource endpoint. The test coverage is extensive (epoch state machines, rollback ordering, counter regression, stale handling, race tests), and the documentation (EN + ZH) is thorough and mostly accurate.

I did not find a correctness bug that breaks the primary path. The findings below are robustness/completeness issues, plus a couple of cross-component contracts worth verifying before merge.

Findings

1. OOM counters are collected, persisted, and normalized but never exported (medium)
HostSandboxSnapshot.MemoryOOMTotal / MemoryOOMKillsTotal flow through the full pipeline — newHostSandboxSnapshot populates them, hostMetricsBaselineFromUsage persists them, and normalizeHostSandboxUsage baseline-subtracts them — but collectHostPrometheus only emits MemoryFailuresTotal. The entire OOM accounting path is dead state today. Additionally the sources are inconsistent: v1 (v1/usageSnapshotFromMetrics) populates only MemoryOOMKillsTotal (never MemoryOOMTotal, which stays 0) while v2 populates both. Either export them (and fix the v1 gap) or drop the fields. See inline comment on host_sandbox_sampler.go:406.

2. One-shot baseline failures permanently disable a scope (medium)
Two independent fail-closed decisions with no retry:

  • cgroup/local.go:352 — if the host cgroup baseline read fails during sandbox creation, HostMetricsBaselineMissingAtAssignment is set permanently and host_sandbox is disabled until the sandbox is deleted and recreated. A transient read failure (cgroup momentarily unreadable at create time) becomes a permanent outage, and the operator only sees a Warnf. Note the asymmetry: pre-upgrade sandboxes (nil baseline, flag false) recover via a first-successful-sample compatibility baseline, but post-upgrade sandboxes never do.
  • cube_container_create.go:134 — if l.cubeboxManger.Get fails after the sandbox is persisted, beginFreshGuestMetricsEpochBestEffort returns nil and the sandbox is left without a guest epoch. The guest sampler's epoch == nil branch then silently deletes the workload and never samples it again — guest_workload is silently absent, with no retry and no per-sandbox signal.

Both would be more robust if a transient failure at lifecycle time could fall back to the same lazy/recovery path that already exists for pre-upgrade sandboxes (establish baseline on the first successful sample).

3. Guest CPU unit / metrics-type contract with CubeShim (verify before merge)
DecodeGuestWorkloadMetric accepts only io.containerd.cgroups.v1.Metrics and treats CPUUsage.Total/User/Kernel as nanoseconds. The guest cube-agent reads cgroup-v2 cpu.stat (usage_usec/user_usec/system_usec), which are microseconds, and places them into CpuUsage unscaled (agent/rustjail/src/cgroups/fs/mod.rs get_cpuacct_stats). So the CubeShim conversion (from PR #1090) must scale µs→ns when building the v1 payload. If it doesn't, all exported cubesandbox_guest_workload_cpu_*_seconds_total counters are 1000× too low. Please confirm the shim scales and that it emits exactly io.containerd.cgroups.v1.Metrics (the docs' "guest requires a unified cgroup v2 hierarchy" would otherwise suggest a v2 metrics type). See inline comment on guest_workload.go:20.

Minor notes

  • PR description vs. diff mismatch: the body says "Remove the unused generic containerd cgroups monitor registration from Cubelet builtins", but Cubelet/cmd/cubelet/builtins.go still imports containerd/v2/core/metrics/cgroups and .../cgroups/v2; the diff only adds the resourcemetrics import. Either the removal is missing or the description is stale.
  • Rollback failure message (rollback.go): rsp.Ret.RetMsg now uses err.Error() from runRollbackWithPreparedGuestMetrics, which drops the previous stable prefix ("failed to update shim for rollback") in favor of raw wrapped internals ("preflight sandbox runtime rollback: …", "restore sandbox runtime: …"). Internal wrapping chains are now exposed verbatim to API callers.
  • Epoch-less sandboxes are silently skipped: the sampler's epoch == nil branch deletes the cache entry with no indicator (this is the pre-upgrade-sandbox path the docs describe, so it's intended, but it's worth knowing there is no error signal to distinguish "old agent" from "baseline never initialized").
  • Dispatch-interval lag: finishCollectionCycleLocked does not reset hasDispatchInterval, so the wait between collection cycles uses the previous cycle's interval until the next CollectOnce recomputes it. Harmless, but the code reads as if the interval is re-derived each cycle when it isn't.
  • The docs' claim that a fully-populated single-container sandbox with export_scopes = ["all"] exports at most 22 series checks out (12 guest + 10 host metric families).

Positive notes

  • The epoch state machine (prepared → pending → ready/degraded) is consistently enforced across rollback ordering, persistence failures, and sampler concurrency, with good fail-closed behavior and matching tests.
  • The sampler scheduler (batch dispatch, in-flight/token tracking, completed notification) is carefully implemented; the "one in-flight request per workload" and "saturation → wait for completion → resume" behaviors are well tested.
  • The handle.Interface addition is cleanly applied to both v1 and v2 handlers, and path validation in CgPlugin.UsageSnapshot correctly restricts reads to the CubeSandbox pools.

Overall: a high-quality PR. The items I'd want resolved before merge are the OOM dead-collection path (#1) and the two permanent-fail-closed behaviors (#2); #3 is a verification step rather than a code change.

@zyl1121
zyl1121 marked this pull request as ready for review July 23, 2026 12:26
@zyl1121
zyl1121 force-pushed the feat/sandbox-resource-metrics-cubelet branch 2 times, most recently from 3e2ffcc to b179ec5 Compare July 23, 2026 13:38

@fslongjin fslongjin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — overall direction looks good to me.

This PR turns Task.Stats into Cubelet-owned sandbox CPU/memory metrics with lifecycle handling (create, rollback, and reused cgroup paths). The layering is clear: raw stats → baseline / accounting window → bounded background sampling + cache → read-only export on /v1/metrics/resource. I like that scrapes do not fan out per sandbox, guest/host stay separate, rollback uses prepare→restore→activate, and host_sandbox is the default.

Before merge, a few points I want to confirm first (details in the inline comments):

ID Question
P1 Why remove the generic cgroups monitor in this PR?
P2-1 Docs say “from cgroup assignment”, implementation is “from first sample”
P2-2 After a failed restore, epoch stays Prepared — how do guest metrics recover?
P3-1 After a failed activate persist, in-memory and on-disk state can diverge (follow-up OK)
P3-2 guestWorkloadSnapshotFromTaskStats is unused on the production path (follow-up OK)

I'm marking Request changes, mainly for these three:

  1. P1: The new endpoint and /v1/metrics can coexist. I don't yet see why this PR must also remove the cgroups monitor — please explain the motivation.
  2. P2-1: Default host_sandbox docs and implementation disagree; please fix the implementation or the wording.
  3. P2-2: Preferring to export nothing on uncertain failure is fine, but please make recovery clear so metrics do not just disappear quietly.

Questions for the author:

  1. Why does this PR remove the generic cgroups monitor? How does that relate to the PR goal? Is anyone scraping container_* from /v1/metrics? If we keep the removal, how will the upgrade notes call it out?
  2. On the host side we can drop usage between sandbox start and the first sample (up to about one collection_interval). Is that acceptable? If not, can the baseline be established at cgroup assignment time?
  3. After a failed restore, guest metrics may stay unavailable for a long time. Is that the intended final behavior, or should we restore the previous accounting window when the runtime was not actually mutated?


import (
_ "github.com/containerd/containerd/v2/core/metrics/cgroups"
_ "github.com/containerd/containerd/v2/core/metrics/cgroups/v2"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Why remove the generic cgroups monitor here?

These two imports were deleted:

_ "github.com/containerd/containerd/v2/core/metrics/cgroups"
_ "github.com/containerd/containerd/v2/core/metrics/cgroups/v2"

This feels odd to me.

/v1/metrics/resource is a new endpoint, and the docs say it is independent from /v1/metrics. Those two can coexist. I don't see why “add sandbox resource metrics” requires removing the old cgroups TaskMonitor. After this change, container_* series that used to show up on /v1/metrics may simply disappear.

The PR calls it unused, but before merge I'd like clarity on:

  1. Why remove it in this PR, and how that relates to the PR goal?
  2. Is anyone scraping container_*? If we keep the removal, how do upgrade notes tell users?

My preference: if this is not required, restore these side-effect imports and keep this PR focused on sandbox resource metrics. If it must go, please explain the reason and dependency check in the review thread first, then update the changelog / upgrade docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Why remove the generic cgroups monitor here?

The original removal was based on our observation that the generic monitor does not currently produce usable container_* series for Cube tasks and cannot provide the lifecycle semantics required by this PR.

I agree that this does not justify removing an existing containerd capability as part of the sandbox resource metrics change. I restored both side-effect imports. /v1/metrics/resource is now strictly additive and remains independent from /v1/metrics.

Any future removal of the generic monitor should be handled separately with a usage and compatibility audit.

Comment thread Cubelet/cmd/cubelet/builtins_test.go Outdated
"github.com/containerd/plugin/registry"
)

func TestGenericCgroupsTaskMonitorIsNotRegistered(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 follow-up]

This test hard-locks “the cgroups monitor must never be registered again”.

That's fine only if we have really decided to remove it permanently. Please explain the motivation together with the builtins.go change: if the reason is weak, this test should be reverted with those imports; if we keep the removal, please document the reason and replacement in a comment or the upgrade guide — don't leave only an assertion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Since the generic cgroups monitor is no longer being removed, I deleted this test as well.

if baseline == nil || baseline.CGroupPath != cgroupPath {
previous := baseline
candidate := hostMetricsBaselineFromUsage(cgroupPath, usage)
cb.RestoreHostMetricsBaseline(&candidate)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This is a first-sample baseline, not a cgroup-assignment baseline

The implementation writes HostMetricsBaseline from the raw counters on the first successful UsageSnapshot.

But the help text / docs say since host cgroup assignment baseline — i.e. counting from cgroup assignment. With the current timing:

  1. After the cgroup is assigned and the process is attached, CPU / throttling / memory-failure counters are already increasing;
  2. The first sample can be up to one collection_interval later;
  3. Using that moment as the baseline permanently drops the earlier delta.

Avoiding inherited history when a later sandbox reuses a cgroup path from the pool looks fine (empty baseline, or a changed cgroup path, triggers a rebuild).

What I'm worried about is the other case: default host_sandbox silently drops CPU / throttling / memory-failure counts between sandbox start and the first sample.

Please pick one:

  1. Establish the baseline in the cgroup assignment path (e.g. after CleanForReuse, around process attach); the sampler should only read an existing baseline;
  2. Or, if we keep this implementation for now, change the help text and docs to “since first successful sample” and stop saying assignment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by implementing an assignment-time baseline.

Cubelet now reads the raw host counters immediately after the cgroup pool slot is assigned and before the sandbox process is attached. The baseline is carried through the cgroup create result into CubeBox and persisted with the sandbox metadata. The sampler uses this persisted baseline instead of dropping startup usage before its first collection.

For compatibility, a sandbox that already existed before this field was introduced may still establish a baseline from its first successful sample after upgrade.

If assignment-time capture fails for a newly created sandbox, sandbox creation continues, but the failure is persisted and host_sandbox fails closed for that sandbox instead of exporting an incomplete accounting window.

guestEpoch = prometheus.NewDesc("cubesandbox_guest_workload_metrics_epoch", "Current guest workload metrics epoch generation.", guestLabels, nil)
guestEpochStarted = prometheus.NewDesc("cubesandbox_guest_workload_metrics_epoch_start_time_seconds", "Start time of the current guest workload metrics epoch as Unix seconds.", guestLabels, nil)

hostCPUUsage = prometheus.NewDesc("cubesandbox_host_sandbox_cpu_usage_seconds_total", "CPU time consumed by the current sandbox since its host cgroup assignment baseline.", hostLabels, nil)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 follow-up]

For example:

CPU time consumed by the current sandbox since its host cgroup assignment baseline.

Compared with the sampler, this is closer to “since first successful sample” than “since cgroup assignment completed”.

Please keep this consistent with the host-sampler comment above: either implement a real assignment-time baseline, or change this (and the EN/ZH docs) to a first-sample baseline. Otherwise users will read “from assignment” and under-count startup usage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation now matches this wording: new sandboxes capture and persist the baseline at cgroup assignment time, before process attachment.

The first-successful-sample behavior is retained only as an upgrade compatibility path for existing sandboxes that do not have the new persisted assignment baseline. The English and Chinese documentation now describe both cases explicitly.

if err := restore(); err != nil {
return fmt.Errorf("restore sandbox runtime: %w", err)
}
return nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] After a failed restore, epoch stays Prepared — how do guest metrics recover?

Today prepare first persists a new accounting window as Prepared, then restore runs; on failure it stays Prepared and the sampler skips it. Tests lock this in. I read the intent as: when restore outcome is uncertain, prefer exporting nothing over exporting wrong numbers.

Being conservative when restore may have already mutated the runtime is fine. There is another case though:

  • prepare has already overwritten the previous usable baseline;
  • restore fails before the runtime is actually mutated, so the sandbox may still be on the old state;
  • guest_workload stays missing until a later successful create/rollback;
  • there is no clear signal that we are stuck in Prepared.

Please decide the final behavior and capture it in code or docs:

  1. If we can tell “runtime not mutated yet” from “uncertain”: restore the previous accounting window in the former case;
  2. If we keep “on failure, never export”: document how to recover, and add searchable logs or a diagnostic metric so metrics do not disappear quietly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I split the failure handling at the point where Cubelet can still determine whether runtime mutation has begun.

Task lookup and other local preflight checks now complete before the new epoch is prepared. A preflight failure therefore preserves the previous usable epoch and does not affect guest metrics.

Once Task.Update has been dispatched, Cubelet cannot reliably determine whether the restore partially mutated the runtime. In that case the new epoch intentionally remains Prepared, and guest_workload stays fail closed rather than exporting counters against a potentially invalid baseline.

Recovery is a later successful rollback, which establishes a new epoch, or deleting and recreating the sandbox if it cannot be recovered. I added a searchable error log with the sandbox context and epoch generation, and documented the recovery behavior in both operator guides.

setRuntimeRestoreBaseLabels(cb, snapshotID, activatedAt)
if err := syncer.SyncByID(ctx, cb.ID); err != nil {
return fmt.Errorf("persist activated guest metrics epoch for %s: %w", cb.ID, err)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3]

Here the in-memory state moves from Prepared to Pending, then SyncByID persists it. If persist fails:

  • memory is already Pending (the sampler can keep going)
  • disk may still be Prepared

If Cubelet restarts, it reloads Prepared from disk and guest metrics get stuck again. In the same file, prepare/ready failure paths restore in-memory state; activate does not. The caller mostly gets a warning and may still think rollback succeeded.

Not a merge blocker for me. Prefer rolling memory back or retrying persist on failure; at least mark it abnormal and emit a searchable error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Before activation, Cubelet now snapshots the previous Prepared epoch and the existing snapshot/restore binding labels.

If persistence fails after the in-memory transition to Pending, it conditionally restores the previous epoch and labels. The restoration checks the expected generation and state first, so it does not overwrite a concurrent successful transition.

}, nil
}

func guestWorkloadSnapshotFromTaskStats(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3]

guestWorkloadSnapshotFromTaskStats is mainly used by transport-layer tests; production collection goes through Metrics() and normalization in the sampler. That makes the lower-level sandbox controller depend upward on resourcemetrics.

Not a merge blocker. Please move it to a test helper or a separate adapter instead of leaving it on the controller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. guestWorkloadSnapshotFromTaskStats has been removed from the production sandbox controller and moved into resource_metrics_transport_test.go as a test-only helper.

The production controller now only exposes the standard Metrics() transport boundary; decoding and normalization remain owned by the resource metrics layer.

zyl1121 added 3 commits July 31, 2026 09:52
Query the primary Cube task through standard Task.Stats, define typed guest workload metrics, and persist guest counter baselines across create, restore, and rollback lifecycle events.

Read host sandbox cgroup usage with baselines captured when pool slots are assigned so reused cgroup paths do not inherit prior sandbox accounting. Fail host metrics closed without blocking sandbox creation when assignment-time capture is unavailable.

Preflight rollback task lookup before preparing a new guest epoch, and keep uncertain runtime restore failures fail closed with explicit recovery guidance.

Add focused store, lifecycle, cgroup reader, and normalization tests.

Signed-off-by: zhengyilei <zheng_yilei@qq.com>
Add bounded guest workload and host sandbox samplers with independent concurrency limits, fixed per-cycle queues, and latest-sample caching.

Expose selected scopes through a dedicated Prometheus endpoint while keeping the existing generic containerd cgroup monitor available under the generic metrics endpoint.

Add focused scheduling, cache, scope, Prometheus, configuration, and race tests.

Signed-off-by: zhengyilei <zheng_yilei@qq.com>
Document the host_sandbox and guest_workload scopes, lifecycle counter semantics, configuration, Prometheus endpoint, and operational guidance.

Add aligned English and Chinese guides and register them in the documentation navigation.

Signed-off-by: zhengyilei <zheng_yilei@qq.com>
@zyl1121
zyl1121 force-pushed the feat/sandbox-resource-metrics-cubelet branch from b179ec5 to c0aa5f1 Compare July 31, 2026 02:46
@zyl1121

zyl1121 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@fslongjin Thanks for the detailed review. I have updated the PR to address all five points.

  1. The generic containerd cgroups monitor is restored. /v1/metrics/resource is now strictly additive and independent from /v1/metrics.

  2. New sandboxes now capture the raw host_sandbox counter baseline immediately after the cgroup pool slot is assigned and before the sandbox process is attached. The baseline is propagated through the create workflow and persisted in CubeBox.

    Existing sandboxes upgraded without this field retain the first-successful-sample compatibility behavior. For a newly created sandbox, if the assignment-time read fails, sandbox creation continues but host_sandbox fails closed rather than exporting an incomplete accounting window.

  3. Rollback task lookup and local preflight now complete before the new guest epoch is prepared. A preflight failure therefore preserves the current epoch.

    Once Task.Update has been dispatched, Cubelet cannot safely determine whether the runtime was partially mutated. A restore failure therefore intentionally leaves the epoch Prepared and keeps guest_workload unavailable. Recovery is a later successful rollback, or delete/recreate if the sandbox cannot be recovered. The failure log now includes the sandbox context, epoch generation, failure stage, and recovery guidance.

  4. If activation persistence fails, Cubelet conditionally restores the previous Prepared epoch and the previous snapshot/restore labels, avoiding in-memory/on-disk divergence without overwriting a concurrent state transition.

  5. The transport-only normalization helper has been moved out of production controller code and into the transport test.

Validation completed on real deployment:

  • focused lifecycle, cgroup, sampler, transport, and server tests passed under -race;
  • bilingual VitePress documentation build passed;
  • the full CubeAPI/CubeProxy lifecycle E2E passed for fresh CPU/memory collection, snapshot/rollback, pause/resume, deletion, and cache cleanup;
  • rollback observed guest CPU 5.375190 -> 0, guest epoch 1 -> 2, while host CPU remained continuous at 7.391498 -> 8.836734.

}
}

sandBox, err := l.cubeboxManger.Get(ctx, opts.GetSandboxID())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best-effort epoch initialization silently swallows the store Get error. If l.cubeboxManger.Get fails here (e.g., a transient metadata-store read error right after createCubeboxContainer persisted the sandbox), the new sandbox is left with no GuestMetricsEpoch. The guest sampler's CollectOnce then hits the epoch == nil branch and permanently deletes the workload — guest_workload is never exported for this sandbox and nothing ever retries. The failure is only a Warnf in the Cubelet log. Consider surfacing this (e.g., a visible per-sandbox error) or letting the sampler lazily begin an epoch for started, epoch-less sandboxes so a transient failure doesn't permanently disable guest metrics.

if err != nil {
return ret.Errorf(errorcode.ErrorCode_CreateCgroupFailed, "%s", err)
}
cgroupPath := MakeCgroupPathByID(*fullCgID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A baseline-capture failure here is permanent: HostMetricsBaselineMissingAtAssignment is set once and never retried, so host_sandbox is disabled for the sandbox until it is deleted and recreated. If this read fails transiently at create time (cgroup momentarily unreadable, controller not yet enabled in the reused slot), a healthy sandbox loses host metrics permanently — the operator only sees a Warnf. Note the asymmetry: pre-upgrade sandboxes (nil baseline + flag false) get a compatibility baseline from the first successful sample in HostSandboxSampler.collect, but post-upgrade sandboxes with a one-shot read failure never do. Consider falling back to the same first-successful-sample baseline path when the assignment-time read fails, instead of a permanent fail-closed flag.

}
}

func newHostSandboxSnapshot(sandboxID, cgroupPath string, timestamp time.Time, usage handle.UsageSnapshot) *HostSandboxSnapshot {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MemoryOOMTotal/MemoryOOMKillsTotal are collected here, persisted in the host baseline (hostMetricsBaselineFromUsage), and subtracted in normalizeHostSandboxUsage — but the Prometheus collector only emits MemoryFailuresTotal. These OOM counters are never exported, so the whole OOM accounting path is currently dead state. Also note the sources are inconsistent: the v1 usageSnapshotFromMetrics populates only MemoryOOMKillsTotal (never MemoryOOMTotal, which stays 0), while v2 populates both. Either export them (and fix the v1 gap) or drop the fields to avoid maintaining unused, cross-version-inconsistent state.

type WorkloadRef struct {
SandboxID string
ContainerID string
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DecodeGuestWorkloadMetric hard-requires the cgroup-v1 metrics type URL and treats Usage.Total/User/Kernel as nanoseconds. The guest cube-agent (agent/rustjail/src/cgroups/fs/mod.rs get_cpuacct_stats) reads cgroup-v2 cpu.stat usage_usec/user_usec/system_usec, which are microseconds, and puts them into CpuUsage.total_usage/usage_in_usermode/usage_in_kernelmode without scaling. So the CubeShim conversion (PR #1090) must multiply by 1000 when building this v1 payload — if it doesn't, exported cubesandbox_guest_workload_cpu_*_seconds_total counters are under-reported by 1000×. Worth explicitly verifying the shim's conversion is ns-scaled and that io.containerd.cgroups.v1.Metrics is exactly the type URL CubeShim emits (the docs say the guest requires a unified cgroup v2 hierarchy, which would otherwise suggest a v2 metrics type).

@fslongjin fslongjin self-assigned this Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants