Resctrl-Mon NRI Plugin
The resctrl-mon NRI plugin creates per-pod resctrl monitoring groups
(mon_groups) to support Kepler’s
passive mode for Application Energy Telemetry (AET).
When a container is created, the plugin assigns its init process to a
mon_group before the process starts executing. The Linux kernel then
propagates the RMID (Resource Monitoring ID) to all child processes
automatically, eliminating the fork race that affects userspace-based
approaches.
How It Works
The container runtime creates a container process (paused).
The NRI
PostCreateContainerhook fires.The plugin creates a
mon_groupnamed with the pod’s UUID under the appropriate resctrl control group.The NRI
StartContainerhook fires with the container’s init PID.The plugin writes the init PID to the
mon_group’stasksfile. (If the PID is not yet available,PostStartContainerretries.)The runtime starts the container. All child processes inherit the RMID.
Kepler scans the resctrl filesystem and reads monitoring data.
The
mon_grouplives for the lifetime of the pod sandbox, not the individual container. It is not removed when a container stops or restarts, so the pod keeps a stable RMID across container restarts. (Releasing and re-allocating an RMID would hand the replacement container a recycled RMID whose counters still carry the previous tenant’s residual, producing a false energy/occupancy spike.)When the pod’s last sandbox is removed, the plugin removes the
mon_groupin the NRIRemovePodSandboxhook. Removing an older sandbox of a live pod (kubelet garbage collection after a sandbox is recreated) keeps it. If the teardown is missed (for example while the plugin is disconnected), the next NRISynchronizereaps the stalemon_groupusing the runtime’s authoritative pod list. A background reconciler additionally retries removals that failed transiently and clears untracked directories left behind by an earlier plugin process.
The plugin DaemonSet runs with hostPID: true so that it can write
host-namespace PIDs to the resctrl tasks file. Without hostPID,
the kernel rejects the write with ESRCH because the PID does not
exist in the plugin’s PID namespace.
Mon_Group Naming
Mon_groups are named with the Kubernetes pod UID:
/sys/fs/resctrl/[<rdt-class>/]mon_groups/<pod-uid>/
This enables Kepler to correlate monitoring data with Kubernetes metadata by querying the K8s API using the pod UID extracted from the directory name.
Plugin Configuration
Configuration is loaded from a YAML file specified with the -config flag
or pushed by the container runtime via NRI. It is read once at startup; to
apply a change, restart the plugin (for example
kubectl rollout restart daemonset/nri-resctrl-mon -n <namespace>).
# Path to the resctrl filesystem. Override for testing.
resctrlPath: /sys/fs/resctrl
# Namespace filter: only create mon_groups for pods in these namespaces.
# Empty list = all namespaces.
namespaces: []
# Pod label selector: only create mon_groups for pods matching these labels.
# Empty = all pods.
labelSelector: {}
# Embedded OpenTelemetry exporter. Exposes a Prometheus /metrics endpoint
# and/or pushes metrics via OTLP.
telemetry:
prometheus:
enabled: true
listenAddress: ":9100"
# Metric-name prefix. Leave empty: a non-empty value renames every series
# and breaks the sample Grafana dashboards (which query l3_*/perf_*).
namespace: ""
otlp:
enabled: false
endpoint: "" # e.g. "otel-collector-resctrl.monitoring.svc:4317"
protocol: grpc # grpc | http
interval: 15s # must be a positive duration
insecure: false # true sends OTLP in plaintext (no TLS)
perfCounters:
enabled: false # gate rdt=perf counters (c1_res, stalls_*, etc.)
include: [] # path.Match patterns for counters to include
exclude: [] # path.Match patterns for counters to exclude
resourceAttributes: {} # static OTel resource attributes on all metrics
If telemetry cannot start (for example, its port is in use), the plugin logs
an error and keeps managing mon_groups without it. When the plugin runs in
the host network namespace (for example, launched by the runtime), the default
:9100 collides with node_exporter; set telemetry.prometheus.listenAddress
or disable Prometheus.
Metrics
The Prometheus endpoint exports these metrics per pod mon_group and domain:
| Prometheus name | Type | Unit | Source file |
|---|---|---|---|
l3_llc_occupancy_bytes |
gauge | bytes | mon_L3_*/llc_occupancy |
l3_mbm_local_bytes_total |
counter | bytes | mon_L3_*/mbm_local_bytes |
l3_mbm_bytes_total |
counter | bytes | mon_L3_*/mbm_total_bytes |
perf_core_energy_joules_total |
counter | joules | mon_PERF_PKG_*/core_energy |
perf_activity_farads_total |
counter | farads | mon_PERF_PKG_*/activity |
Other mon_PERF_PKG_* counters (c1_res, stalls_*, …) are exported only
when telemetry.perfCounters.enabled is true. Names follow goresctrl’s
naming rules
under the UnderscoreEscapingWithSuffixes translation strategy, which the
plugin pins.
Labels (OTLP uses the dotted attribute names, e.g. k8s.pod.uid):
domain_id: domain instance, e.g.00.domain_name: domain directory, e.g.mon_L3_00.k8s_pod_uid: the pod UID (themon_groupname).resctrl_control_group: the parent control group (RDT class); empty for the root group.resctrl_group_source: alwayspod.k8s_node_name: the node name, from theNODE_NAMEenvironment variable.Each key in
telemetry.resourceAttributes. A key must not map to one of the labels above (for examplek8s.pod.uidordomain_id), to another key’s label, or to a label that OTLP-to-Prometheus conversion derives:joborservice_name(setservice.nameitself to override it), andinstancewhenservice.instance.idis set. Names starting with__are reserved by Prometheus. Such a configuration is rejected.
Samples
deployment/samples/resctrl-mon
has files that the Helm chart does not deploy:
grafana-resctrl-pod-energy.json,grafana-resctrl-perf-counters.json: Grafana dashboards for the metrics above. They need kube-state-metrics (forkube_pod_info) and an emptytelemetry.prometheus.namespace.otel-collector-rbac.yaml,otel-collector-agent.yaml: an OTel Collector agent DaemonSet for the OTLP push path (telemetry.otlp.enabled=true). It adds pod and namespace labels with thek8sattributesprocessor and exposes the metrics to Prometheus on port 8889. Add exporters (for exampleotlphttp) to send them elsewhere.
Before applying the collector manifests:
Set the two namespace placeholders in the NetworkPolicy (the plugin’s and Prometheus’s).
Copy any
tolerationsornodeSelectorset for the plugin to the collector DaemonSet.Configure TLS on the collector’s OTLP receivers, or set
telemetry.otlp.insecure=true: the receivers do not use TLS.Set
telemetry.prometheus.enabled=falseif Prometheus scrapes the collector; otherwise it ingests every series twice.
kubectl apply -f deployment/samples/resctrl-mon/otel-collector-rbac.yaml
kubectl apply -f deployment/samples/resctrl-mon/otel-collector-agent.yaml
Coexistence with Allocation Plugins
If an NRI resource allocation plugin (balloons, topology-aware) is running,
it assigns containers to RDT classes via SetLinuxRDTClass. The resctrl-mon
plugin reads the effective RDT class from the NRI container spec and creates
mon_groups under the corresponding control group:
/sys/fs/resctrl/<rdt-class>/mon_groups/<pod-uid>/
The container keeps its CLOSID (allocation) and gets a distinct RMID
(monitoring). Pods without an RDT class, from an allocation plugin or from the
runtime’s RDT annotations, get mon_groups under the root resctrl directory.
RMID Management
RMID allocation is delegated entirely to the Linux kernel:
Allocation:
mkdiron amon_groupdirectory assigns an RMID. If none are available, the kernel returnsENOSPCand the plugin logs a warning and skips the pod.Deallocation:
rmdirreleases the RMID. The kernel handles the hardware recycling window.
Limitations
PID assignment moves one task. Writing a PID to a
tasksfile moves only that thread. Threads and children that already exist stay where they are. Containers that started while the plugin was down (adopted inSynchronize), or whose PID is assigned only inPostStartContainer, are partly attributed until they restart.One control group per pod. A pod’s
mon_grouplives under one RDT class. A container in a different class (for example, a sidecar) is not monitored, because assigning it would overwrite its allocation. The plugin reads a container’s class from its OCIlinux.intelRdt.closID(NRILinux.Rdt.ClosId), which containerd and CRI-O set from the pod’s RDT annotations. The group’s class is chosen as follows:If the pod has the annotation
rdt.resources.beta.kubernetes.io/pod, the group is under that class. A container that its own annotation (rdt.resources.beta.kubernetes.io/container.<name>) puts in another class is not monitored, whichever container is created first.Otherwise, or when the pod-level class was not applied (for example, an NRI plugin set another class), the first container to be created fixes the class.
The runtime’s RDT setup removes groups. Each time containerd or CRI-O starts with an RDT configuration (
rdt_config_file), it removes every control group that the configuration does not define and everymon_groupthat has no tasks. Pods with no running task (Completed, in back-off, or killed) lose theirmon_group, with its RMID and counters, and the plugin does not re-create it. Do not keep hand-made control groups on such a host.Uninstall leaves groups behind. Removing the plugin leaves the pods’
mon_groups(and their RMIDs) in place. To release them, run this as root on each node. It removes every UUID-namedmon_group, including any that another tool owns:find /sys/fs/resctrl -mindepth 2 -maxdepth 3 -type d -regextype posix-extended \ -regex '.*/mon_groups/[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}' -exec rmdir {} +
RMID exhaustion. When no RMID is free, the plugin logs a warning for each container and the pod is not monitored. Use
namespacesorlabelSelectorto limit monitoring to the pods that matter.Ownership. The plugin reconciles every UUID-named
mon_group: it keeps the groups of all live pods (including filtered ones) and removes the rest. Other tools must not create UUID-named groups for anything else.
Developer’s Guide
Prerequisites
Containerd v1.7+ or CRI-O v1.36+
Enable NRI in the container runtime:
containerd — in
/etc/containerd/config.toml:[plugins."io.containerd.nri.v1.nri"] disable = false disable_connections = false plugin_config_path = "/etc/nri/conf.d" plugin_path = "/opt/nri/plugins" plugin_registration_timeout = "5s" plugin_request_timeout = "2s" socket_path = "/var/run/nri/nri.sock"
CRI-O — in
/etc/crio/crio.conf.d/10-nri.conf(or equivalent):[crio.nri] enable_nri = true
See the CRI-O NRI documentation for additional options.
Intel CPU with RDT monitoring support
resctrl filesystem mounted at
/sys/fs/resctrl
Build
make PLUGINS=nri-resctrl-mon build-plugins
Run
./build/bin/nri-resctrl-mon -config sample-configs/nri-resctrl-mon.yaml -idx 90 -vv
Manual Test
Verify that mon_groups are created when pods start:
# Start a test pod
kubectl run test-pod --image=busybox -- sleep 3600
# Check that a mon_group was created with the pod UID
POD_UID=$(kubectl get pod test-pod -o jsonpath='{.metadata.uid}')
# Without an RDT allocation plugin, mon_groups are under the root class:
MON_GROUP_BASE=/sys/fs/resctrl/mon_groups
# With an allocation plugin that assigns an RDT class (e.g. BestEffort):
# MON_GROUP_BASE=/sys/fs/resctrl/BestEffort/mon_groups
ls "$MON_GROUP_BASE/$POD_UID/"
# Verify monitoring data is available
cat "$MON_GROUP_BASE/$POD_UID/mon_data/mon_L3_00/llc_occupancy"
Debug
go install github.com/go-delve/delve/cmd/dlv@latest
dlv exec build/bin/nri-resctrl-mon -- -config sample-configs/nri-resctrl-mon.yaml -idx 90
(dlv) break plugin.PostCreateContainer
(dlv) continue
Deploy
Build an image, import it on the node, and deploy the plugin by
running the following in nri-plugins:
rm -rf build
make clean
make PLUGINS=nri-resctrl-mon IMAGE_VERSION=devel images
ctr -n k8s.io images import build/images/nri-resctrl-mon-image-*.tar
kubectl create -f build/images/nri-resctrl-mon-deployment.yaml