Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Latency Pods

Five things a normal Kubernetes pod cannot do, and what to use instead. They live in k8s/nri-latency-pods (node setup and the admission policy) and k8s/nri-shared-cpus (the NRI plugin). This page is the contract.

In short: five opt-in grants: a sized hugetlbfs for Aeron’s directory, an isolated cpuset partition, an io_uring seccomp profile, an admission policy against exec probes and sidecars, and device grants (the NIC’s PHC). Each is a mechanism; none has a measured latency effect. SCHED_FIFO is built in the shared-cpus plugin and off by default (evidence: mixed).

Status: the grants ship in the public image ghcr.io/mimran1980/ergon-shared-cpus:0.1.0. Install it with the chart oci://ghcr.io/mimran1980/charts/ergon-shared-cpus version 0.1.0 (Shared CPUs). This page’s admission YAML and node units are not an image. Chart 0.1.0 leaves seccomp.ioUring and ptp off, because its ConfigMap template does not write those keys. k8s/nri-shared-cpus/examples/try installs that chart and shows two things on kind: the shared-CPU grant, and this page’s admission policy denying an exec probe. It does not exercise hugepages, the cpuset partition, io_uring seccomp, or the PHC. The five-grant kind e2e (test/e2e.sh) builds the plugin from source. Not verified on EKS. No latency figure is measured.

The five

Every grant is opt-in by pod annotation. A grant that cannot be given is refused: the kubelet shows the text in the pod’s events as CreateContainerError, and the text names the fix. A pod author names kinds and modes only; values come from the plugin config or the node’s facts.

#extensionmechanismpod opt-inenv the app sees
1sized hugetlbfs for AERON_DIRNRI AddMount type: hugetlbfs with pagesize=2M,size=N, plus AddLinuxHugepageLimit; N is the container’s hugepages-2Mi limithugepages.ergon.io/<ctr>: "2M" and a hugepages-2Mi resource (requests = limits)ERGON_HUGEPAGES_DIR, ERGON_HUGEPAGE_SIZE
2cpuset.cpus.partition=isolated on the exclusive CPUsa createRuntime hook makes a threaded child cgroup holding only those CPUs, unions them into the ancestors’ cpuset.cpus.exclusive, then writes cpuset.cpus.partition=isolated, and reads every write back. Takes a process, then single threads (see below)cpu-partition.ergon.io/<ctr>: "isolated"ERGON_ISOLATED_CGROUP, ERGON_ISOLATED_CPUS, ERGON_SHARED_CPUS
3io_uring seccomp profile(a) NRI SetLinuxSeccompPolicy: the request’s profile plus the io_uring_* allow rules; (b) fallback, pod field seccompProfile: Localhostseccomp.ergon.io/<ctr>: "io-uring" (or the Localhost fallback)ERGON_SECCOMP
4no exec probes, no always-restart sidecarsValidatingAdmissionPolicy (CEL, GA 1.30)pod label ergon.io/latency: "hot"—
5device grantsNRI AddDevice, plus a read-only bind of the node facts fileptp.ergon.io/<ctr>: "auto" (the NIC PHC); cpuqos.ergon.io/<ctr> (/dev/cpu_dma_latency, x86 only)ERGON_PTP_DEVICE, ERGON_NODE_FACTS, ERGON_CPU_DMA_LATENCY

Ride-along: rlimits.ergon.io/<ctr> grants MEMLOCK, NOFILE and RTPRIO (a limit value only — see below). The values are ceilings the pod may ask for, set in the plugin config.

Also numa.ergon.io/<ctr>: "0", "0,1" or "" binds the container’s memory to those NUMA nodes (cpuset.mems and MPOL_BIND). "" means the data NIC’s node, which needs that node recorded; on a one-node machine the only node is the answer. A request the node cannot answer is refused and says why — an unread topology, a node the machine does not have, or "" with several nodes and no NIC node. A binding is never dropped in silence.

The five grants on one pod. Everything stays inside the pod’s own cpuset: exclusive CPUs from the kubelet’s static CPU manager, shared CPUs from ergon.io/shared-millicpus. Helpers go on the shared CPUs; SQPOLL takes an exclusive CPU of the pod’s own (SMT sibling).

flowchart TD
    POD["latency pod: exclusive CPU(s) and shared CPU(s) in its cpuset"]
    POD --> A["1 hugepages.ergon.io"]
    POD --> B["2 cpu-partition.ergon.io"]
    POD --> C["3 seccomp.ergon.io/io-uring"]
    POD --> D["4 ergon.io/latency hot"]
    POD --> E["5 ptp. and cpuqos.ergon.io"]
    A --> A1["sized hugetlbfs + hugepage limit"]
    B --> B1["child cgroup, isolated partition"]
    C --> C1["seccomp + io_uring_* rules"]
    D --> D1["no exec probes, no Always sidecars"]
    E --> E1["PHC, cpu_dma_latency, node facts"]
    SQ["helpers on shared CPUs"] -.-> POD

1. The app owns AERON_DIR

The plugin mounts a sized hugetlbfs at /dev/hugepages and exports ERGON_HUGEPAGES_DIR and ERGON_HUGEPAGE_SIZE. It does not set AERON_DIR. The app creates $ERGON_HUGEPAGES_DIR/aeron, points AERON_DIR there, and, if it runs the media driver, sets AERON_FILE_PAGE_SIZE=$ERGON_HUGEPAGE_SIZE.

Why: an unsized hugetlbfs is what an emptyDir medium: HugePages gives you, and Aeron refuses it. A 4 KiB AERON_DIR takes a page fault on each first touch of a page. Latency effect not measured here.

2. Isolated partition

cpuset.cpus.partition=isolated makes the kernel treat the set as isolated: no load balancing into it (cgroup-v2). The grant is applied to a child cgroup so the pod’s own cgroup keeps its shared CPUs. Latency effect not measured here.

The child is threaded, and a threaded child is a legal partition root (k8s/nri-shared-cpus/internal/cpuset/partition.go). So:

app shapehow the exclusive CPUs are used
one process whose threads all belong on the exclusive CPUswrite the pid to $ERGON_ISOLATED_CGROUP/cgroup.procs, then pin each thread to ERGON_ISOLATED_CPUS
hot threads in a multi-threaded processwrite the pid to $ERGON_ISOLATED_CGROUP/cgroup.procs first, then move each helper thread back to the container cgroup through its cgroup.threads. The kernel confines the hot threads to the exclusive CPUs and the helpers to the shared ones (kernel lane: test/cgroup/kernel_test.go, TestOneThreadCanLiveInThePartition)

A program that does not make these moves itself, such as Aeron’s C media driver, keeps the boot line’s isolation and sched_setaffinity: see the worked example.

cpuset.cpus.exclusive is a replace, not a union, so every directory the new partition shares with an already-running pod has to be updated with the union of both reservations. The hook does that under a lock and reads every write back; a write that does not stick fails the create.

3. io_uring

RuntimeDefault blocks io_uring (containerd issue 9320). Aeron’s C media driver does not use io_uring; the grant is for applications that do, such as ergon-io’s uring-spin and uring-sqpoll backends. It is a clone of the pod’s own seccomp profile with the io_uring_* calls allowed — never Unconfined, which the plugin refuses.

4. Admission

An exec probe starts a process inside the container on any CPU of its cpuset, the loop’s exclusive CPU included (k8s/nri-shared-cpus/README.md, Limits). A container with restartPolicy: Always is an injected sidecar: it shares the pod’s CPU budget and can run on the hot core.

The policy refuses both on pods labelled ergon.io/latency: "hot". It also requires a priorityClassName (eviction and preemption stop a hot pod mid-trade), the ergon.io/shared-millicpus resource, and the required-plugins.noderesource.dev annotation naming the plugin.

httpGet, tcpSocket and grpc readiness probes are fine: they run from outside the container.

5. Devices

The NIC’s PHC (/dev/ptp*) for clock work, and on x86 /dev/cpu_dma_latency so a pod can cap C-states. The cap’s measured effect is mixed (C-states), and a spinning core never idles, so treat the grant as access, not a latency win. On arm64 the plugin ignores the cpuqos annotation (k8s/nri-latency-pods/README.md, item 5).

Real-time scheduling: built, default off

SCHED_FIFO is not part of the five grants. It is in k8s/nri-shared-cpus, off unless a node turns it on:

pathwhat it doesdefault
ergon-cpus --fifo <prio>the exec wrapper takes SCHED_FIFO on the thread before it execs the commandoff (no flag)
sched-fifo.ergon.io/<container>: "thread"the NRI plugin grants RLIMIT_RTPRIO and ERGON_SCHED_FIFO; the app or prelock takes the policy on the hot thread onlyschedFifo: "off" — the node refuses the annotation

Why it is off: its one committed A/B is mixed, and under it the loop’s nohz_full CPU took timer interrupts again (SCHED_FIFO). RT throttling (kernel.sched_rt_runtime_us, 950,000 of 1,000,000 µs by default, sched-rt-group) can take the CPU away. ergon-tune T7 warns on a real-time policy.

Deliberately not built

itemwhy
SR-IOV VF per podNitro guests have one ENA per ENI; there are no in-guest VFs
a netns-sysctl writer pretending the knobs are per-netnsbusy_poll/busy_read are global; gro_flush_timeout and napi_defer_hard_irqs are per-netdev sysfs; rmem_max is not writable in a non-init netns. The node sets them (node-checks.md)
an IRQ DaemonSet that pins the latency pod’s NIC IRQs to the shared CPUsIRQs on a CPU that also runs other work are not measured here; IRQs on a core of their own are (interrupt placement). The kept-CPU default stays

RTPRIO is granted as a limit only on the latency-pods path. That plugin never changes a scheduler policy, and the e2e asserts PID 1 stays SCHED_OTHER and ERGON_SCHED_FIFO is empty.

Node setup

The full check list is k8s/nri-latency-pods/node-checks.md. The short version:

settingvalue
Kubernetes>= 1.33 (the labs ran 1.36)
kubeletcpuManagerPolicy: static; reservedSystemCPUs = the IRQ CPU(s) plus the shared CPUs; x86 full-pcpus-only
strict-cpu-reservationoff (on, it empties the shared pool)
kernel>= 6.7, cgroup v2 (cpuset.cpus.exclusive is what the partition writes, and it is 6.7; a vendor backport of that file also works — the plugin reads the file, not the version). Also nohz_full, rcu_nocbs, isolcpus=managed_irq on the exclusive CPUs, never the shared ones. 6.2 is enough for the shared CPUs only
transparent_hugepagenever
net.core.rmem_max / wmem_maxat least Aeron’s AERON_SOCKET_SO_RCVBUF / SNDBUF when raised (hft)
net.core.busy_poll / busy_readunset (global; not measured here). ergon-io busy-polls per socket
containerd>= 2.2, NRI on, the default validator on

Why not AKS: it has no supported way to change containerd (so an NRI plugin cannot be installed) and no reservedSystemCPUs list. Everything here needs one of those. The target is EKS on AL2023.

The two labels

They are different on purpose.

labelwherewhat reads it
pod ergon.io/latency: "hot"every latency podthe validating policy (extension 4)
namespace ergon.io/latency: "true"the latency namespacethe mutating policy (it reads namespaceObject)