Skip to content

atelet: poll ateom workload stats into template-level metrics - #961

Draft
Tim Bai (baizhenyu) wants to merge 1 commit into
agent-substrate:mainfrom
baizhenyu:atelet-stats-reader
Draft

atelet: poll ateom workload stats into template-level metrics#961
Tim Bai (baizhenyu) wants to merge 1 commit into
agent-substrate:mainfrom
baizhenyu:atelet-stats-reader

Conversation

@baizhenyu

@baizhenyu Tim Bai (baizhenyu) commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Second piece of #896 (Phase 1 of #550): atelet polls every local ateom's GetActiveWorkloadStats and turns the samples into template-level metrics — the TSDB half of #174's cardinality split. After this PR, "how much CPU and memory is this template using" is a Cloud Monitoring query.

The reader

A statsPoller in atelet, driven by --actor-stats-poll-interval (default 1m; 0 disables; nonzero values are clamped to a 50s floor, the worst-case duration of sampling one micro-VM ateom — 25 containers × 2s per guest-agent call).

  • Stateless discovery: each tick lists ateoms/* (entry names are worker pod UIDs — the same sockets the lifecycle RPCs dial) and probes each with the parameterless discovery read. No worker-to-actor mapping, no control-plane dependency, nothing to recover after an atelet restart. Attribution comes solely from the identity echoed in each sample, per the RPC's contract.
  • One tolerance rule: any dial or call failure means "not a target this tick" — covering stale directories of deleted workers, ateoms that have not started listening, and teardowns mid-poll. NO_WORKLOAD / NOT_MEASURABLE_YET are skips by the RPC's own contract.
  • Bounded concurrency: distinct ateoms are probed 8 at a time (one probe per guest, so nothing the interval floor defends against is reintroduced); a node of stuck-but-accepting sockets degrades to ceil(n/8) timeouts instead of n sequential ones.
  • WorkerPool enrichment: one field+label-selected pod LIST per tick maps pod UID → owning pool (ate.dev/worker-pool, the label the pool controller stamps); unresolved pods group without pool labels rather than vanish. Chosen over an informer deliberately — negligible apiserver cost at this cadence, no cache-sync ordering, and the resolver sits behind a function seam if that trade ever changes. Needs one new Downward API env (NODE_NAME); the pods RBAC already existed.

The metrics

Labels on every series: ate.template.namespace/name, ate.sandbox.class, ate.stats.source, ate.workerpool.namespace/name — all bounded sets; actor and atespace identity never reach a metric label (they belong to the events channel, the next PR).

  • ate.actor.stats.sampled_actors, …memory_current_bytes, …memory_working_set_bytesobservable gauges over the latest tick's snapshot: each collection observes exactly the groups that currently exist, so a template whose actors leave a node disappears from the export. (Synchronous gauges would re-export their last value until process exit — stale memory for actors long gone.)
  • ate.actor.stats.cpu_usageFloat64Counter in seconds (cAdvisor / OTel *.cpu.time convention; the wire stays µs). The raw cpu_usage_usec is cumulative per-epoch per actor, so the poller accumulates per-sweep increases against per-actor baselines: first sight establishes a baseline and charges nothing (atelet cannot tell a new actor from its own restart — re-charging epochs the previous process counted would spike rate()), a decrease is an epoch reset charged from the new value, and baselines are pruned to the actors seen. Bounded imprecision (≤1 interval per actor across restarts; the pre-checkpoint tail) is documented on the instrument; per-actor precision arrives with the lifecycle events.

Validated live on ate-dev

Both source families, simultaneously, with one actor per class:

image

The same counter application reads 5-6× larger through the cgroup source (whole sandbox: sentry heap, netstack, gofers) than through the guest agent (workload containers only) — the concrete case for the ate.stats.source label and its group-don't-sum rule. Also exercised live: pool labels resolved on every series, restart-without-spike on the CPU counter across a DaemonSet rollout, and OTLP delivery to the gke-managed-otel collector with zero export errors.

Out of scope

Part of #896, toward #550.

The reader half of agent-substrate#896: a poller that discovers the node's ateoms from
the filesystem and turns their GetActiveWorkloadStats samples into the
TSDB half of agent-substrate#174's split -- per-ActorTemplate gauges with the bounded
label set (template, sandbox class, stats source), actor and atespace
identity never reaching a metric label.

Discovery holds no state and never asks the control plane: every ateom
registers itself on disk by creating its socket directory at boot, so a
sweep is one readdir plus one probe per entry, and an atelet restart
loses nothing. One tolerance rule covers the scan's noise -- any dial or
call failure means "not a target this tick" -- which uniformly handles
stale directories, half-born ateoms, and teardowns mid-sweep. The
no-sample reasons are skips by the RPC's own contract. Attribution
comes solely from the echoed identity, per the same contract.

The interval is a flag (--actor-stats-poll-interval, default 1m, 0
disables) clamped to the worst-case micro-VM sweep so a low setting
cannot pile overlapping polls onto one guest agent; within a sweep,
distinct ateoms are probed concurrently (bounded), which stacks nothing
on any one guest and keeps a node of stuck sockets from serializing
into minutes. Three gauges: sampled actors, memory current, memory
working set -- observable rather than synchronous, so a template whose
actors leave the node disappears from the export instead of freezing at
its last value. CPU is a counter, not a gauge: the raw cpu_usage_usec
is cumulative per-epoch per actor, so the poller tracks each actor's
last seen value and adds only the per-sweep INCREASE (a decrease is an
epoch reset, charged from zero), which keeps rate() meaningful across
actors joining, leaving, and resetting. Undercounts across atelet
restarts and misses the tail before a checkpoint; the events channel
carries per-actor precision.

Samples are enriched with the owning WorkerPool
(ate.workerpool.namespace/name) by resolving the node's worker pods --
one field- and label-selected list per sweep, joined on the pod UID the
ateom directory is named for; an unresolved pod groups without pool
labels rather than vanishing.

Part of agent-substrate#896, toward agent-substrate#550.
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.

1 participant