How the OOM handler works
The OOM handler is a controller running as part ofmachined, the Talos PID 1 process.
It is enabled by default and requires no configuration.
It is disabled when Talos itself runs in a container (talosctl cluster create with the Docker provisioner).
Every sampleInterval the controller runs a single cycle:
- Collect memory metrics: pressure stall information for the root cgroup and for each quality of service (QoS) class, plus memory usage of each class.
- Evaluate the
triggerExpressionagainst those metrics. If it evaluates tofalse, the cycle ends here and nothing is killed. - Score every candidate cgroup with the
cgroupRankingExpression. - Select a single victim cgroup out of the scored candidates.
- Kill the victim: every process in it receives
SIGKILL(via thecgroup.killcgroup control file), and the kernel is asked to reclaim their memory immediately withprocess_mrelease.
Cgroup classes
Talos assigns every cgroup it monitors to one of five QoS classes. The classes are listed here from the least important to the most important, which is the order in which they are considered for killing:
The kubelet does not create a dedicated cgroup for the Guaranteed QoS class: Guaranteed pods live directly under
kubepods, next to the besteffort and burstable cgroups.
The init cgroup (machined itself) is accounted as System when metrics are collected,
but it is never considered as a kill candidate.
Candidates are always the immediate children of the cgroups listed above.
For Kubernetes this means a whole pod with all of its containers, and for Talos a single service.
Trigger
ThetriggerExpression is a boolean condition used by the OOM controller to
decide whether it should act.
If the expression evaluates to true, the OOM controller will activate and attempt to kill processes
in order to free up memory.
Pressure Stall Information is the key parameter provided to the
expression, it should be the primary indication for determining whether or not OOM killing is required.
To find more information on the meaning of the PSI parameters, please read the linked page.
The avg10, avg60 and avg300 values are percentages of time stalled over the last 10, 60 and 300 seconds,
while the total values are the absolute stall time accumulated since boot, in microseconds.
These variables describe the root cgroup, that is the machine as a whole:
memory_some_avg10- double -somememory pressure value, averaged over 10 secondsmemory_some_avg60- double -somememory pressure value, averaged over 60 secondsmemory_some_avg300- double -somememory pressure value, averaged over 300 secondsmemory_some_total- double -somememory pressure value, absolute cumulative valuememory_full_avg10- double -fullmemory pressure value, averaged over 10 secondsmemory_full_avg60- double -fullmemory pressure value, averaged over 60 secondsmemory_full_avg300- double -fullmemory pressure value, averaged over 300 secondsmemory_full_total- double -fullmemory pressure value, absolute cumulative value
Besteffort, Burstable, Guaranteed, Podruntime, System):
qos_memory_some_avg10,qos_memory_some_avg60,qos_memory_some_avg300,qos_memory_some_totalqos_memory_full_avg10,qos_memory_full_avg60,qos_memory_full_avg300,qos_memory_full_total
qos_memory_current- current memory usageqos_memory_peak- peak registered memory usageqos_memory_max- configured memory limit
d_ prefixed variants of all of the aforementioned variables (such as d_memory_full_avg10 or
d_qos_memory_full_total) are also available – these represent the current derivative of that value,
in absolute units per second.
The derivative is normalized by the sampling interval, so changing sampleInterval does not change the scale
of these values.
Additionally, time_since_trigger variable is provided, representing the time past since the previous OOM trigger
as the CEL duration type.
You may use this variable to rate limit OOM triggers to ensure the monitored
parameters have time to reflect the updated system state before new trigger decision.
The multiply_qos_vectors(values, weights) function is provided to combine a per-class map into a single number:
it returns the sum of values[class] * weights[class] over the classes listed in weights.
Classes which are not listed in the weights map contribute nothing, which makes it a convenient way to both
select and weigh the classes of interest.
Default condition in detail
The default value fortriggerExpression is:
System and Podruntime classes — Talos services, the container runtime,
kubelet and etcd.
The goal is not to keep any particular pod alive, but to keep the node itself responsive and manageable:
the OOM handler steps in exactly when workload memory usage starts to starve the components which have to
keep running.
This expression checks if all these are true to trigger the OOM killer:
- The
SystemandPodruntimecgroups are accumulating memory stall time right now, weighted 8:4 in favour of the Talos system services- This term is built on the derivative of the cumulative stall time, measured in microseconds of stall per second, so it only holds while pressure is actively being accumulated
- Without it, the handler would keep firing on the decaying 10 second average after the pressure has already subsided
- The
fullmemory pressure of theSystemandPodruntimecgroups combined, averaged over 10 seconds, is over 5%- This confirms the pressure is sustained and not a short spike
- The last OOM kill happened no less than 5 seconds ago
- Prevent the OOM killer from being triggered repeatedly without waiting for it to have an effect on the metrics used
In Talos 1.13 and earlier the default expression had an additional clause which triggered on the memory pressure of
the machine as a whole (
memory_full_avg10 > 75.0).
It was removed in Talos 1.14, because a pod running with a memory limit and doing heavy I/O can drive up the overall
memory pressure of the machine (the limit leaves little room for the page cache) while plenty of RAM is free,
which resulted in false triggers.Selecting a victim
Once the trigger fires, the controller enumerates the candidate cgroups and computes an OOM score for each of them using the expression configured by thecgroupRankingExpression property.
These variables are supplied to the expression and can be used for computing OOM score:
memory_max- optional<uint> - if reported for the cgroup: max allowed memory usage, in bytesmemory_current- optional<uint> - if reported for the cgroup: current memory usage, in bytesmemory_peak- optional<uint> - if reported for the cgroup: peak registered memory usage, in bytespath- string - absolute path to the cgroup being evaluatedclass- int - one of the cgroup classes, should be matched against the class constants
Besteffort, Burstable, Guaranteed, Podruntime and System) can be used to index CEL maps
or in ternary operators used to apply different expressions for different cgroup classes.
A cgroup is a candidate for killing only if its score is strictly greater than zero.
A score of zero does not mean “killed last” — it means the cgroup is never killed at all.
If no cgroup scores above zero, the trigger is logged and nothing is killed.
Strict QoS class ordering
Among the eligible cgroups the victim is picked according to thestrictCgroupClassOrdering setting:
- when it is enabled, which is the default, the controller first picks the least important class which has any eligible cgroup in it, and then the highest-scoring cgroup within that class;
- when it is disabled, the controller picks the highest-scoring cgroup regardless of its class.
kube-apiserver could outscore a small
BestEffort pod and be killed first, as the default ranking expression only weighs BestEffort twice over Burstable.
Default formula in detail
- If the cgroup has a memory limit configured, return 0 — those are processes with well-defined resource demands, and the kernel already enforces their limit, so the Talos OOM handler leaves them alone
- Otherwise, score by current memory usage, weighted by the cgroup class
- A map is used here to look up a coefficient depending on the cgroup class
orValueis a method of theoptionaltype allowing to unwrap the option, choosing a default value in case the value is not available
Guaranteed,PodruntimeandSystemcgroups get a coefficient of 0, so they are never eligible
Which pods can be killed
Combining the default ranking expression with the default strict class ordering, this is how a pod is treated:
The kubelet sets a memory limit on the pod cgroup only when every container in the pod declares
resources.limits.memory — including init containers.
A single container without a memory limit is enough to leave the pod cgroup unlimited, and therefore to make the
whole pod a candidate for the Talos OOM handler.
This is easy to miss with an init container which has already terminated: the pod cgroup keeps no memory limit for
the whole lifetime of the pod.
Protecting a workload
If a workload must not be killed by the Talos OOM handler, there are two options. The first one is to setresources.limits.memory on every container of the pod, including init containers.
The pod cgroup then gets a memory limit, the default ranking expression scores it zero, and it is never selected
by the Talos OOM handler.
The trade-off is that the pod is now bounded by that limit and the kernel OOM killer enforces it,
so an overrunning container is killed by the kernel instead.
The second one is to customize the cgroupRankingExpression so that the workload scores zero, for example
by giving its QoS class a coefficient of zero.
Note that the cgroup path available to the expression is built from the pod UID
(such as /sys/fs/cgroup/kubepods/burstable/podf3a1b0c2-...), not from the pod name or namespace,
so matching on path is not a practical way to exempt a specific application.
Whichever option is used, keep in mind that if every cgroup is exempted, the userspace OOM handler has nothing to
kill and the machine is back to relying on the kernel OOM killer alone.
Observing OOM handler activity
The OOM controller logs each trigger to the controller runtime log:runtime.OOMController controller:
no eligible cgroup to kill.
The last 50 actions are also kept as resources, which record the score of the victim, the command lines of the
killed processes, and a JSON dump of the metrics which caused the trigger:
OOMActions is a sensitive resource (it contains process command lines), so reading it requires the os:admin
role, which the generated talosconfig has by default.
The records are kept in memory only and are lost on reboot.