This is an English translation of an article I originally published in Japanese on Qiita.
TL;DR
Scope: Kubernetes v1.37, Linux 6.12, cgroup v2, containerd. Exact versions are listed in Components and versions.
"Memory usage" in Kubernetes is several different numbers, and OOM kills and evictions are each triggered by a different one of them. That is why a container can be OOMKilled while kubectl top shows headroom, or a Pod can be evicted while node utilization looks low.
Terms used throughout (details): memcg OOM is an OOM kill for exceeding a configured limit (container, Pod or /kubepods.slice), and global OOM is an OOM kill caused by memory exhaustion of the whole node. Eviction is a separate mechanism, done by the kubelet.
| Event | Triggering value | Condition |
|---|---|---|
| memcg OOM (OOM kill from exceeding a limit) | memory.current (container_memory_usage_bytes) of the container, Pod or /kubepods.slice | Reaches memory.max at the same level and reclaim cannot bring it back under (a container's memory.max is its memory limit) |
| global OOM (OOM kill from node memory exhaustion) | Whether the whole node's memory is exhausted (from the kernel's point of view) | There is no threshold. The victim is chosen by oom_score (QoS class and memory request) |
| Eviction trigger | The node's memory.available (= MemTotal − node working set) | Capacity falls below the evictionHard / evictionSoft thresholds (kubelet default: memory.available<100Mi) |
| Choosing which Pod to evict | Difference between a Pod's working set and its memory request | Pods whose actual usage exceeds their request are preferred |
Key points:
-
kubectl topandcontainer_memory_working_set_bytesshow the working set, not RSS. It includes active page cache,shmemand kernel memory. - memcg OOM compares
memory.currentagainst the limit, not the working set, and only kills when reclaim cannot bring usage back under the limit. - Eviction is decided from the node-wide working set, not
MemAvailable, so node-exporter utilization can look healthy while the kubelet is close to its threshold. -
OOMKilleddoes not tell you the path. ASystemOOMevent on the Node means global OOM, but its absence proves nothing (events are kept for 1 hour by default). - The most effective fix for global OOM and eviction is to set the memory request to match actual usage. It does not help with memcg OOM, which needs a limit review.
Where to go next:
- Why do the numbers disagree? Quick reference and Kubernetes memory usage and cgroup v2
- Something was just killed or evicted: Investigating OOM and eviction
- Prevent it: What to do about it, with PromQL for checking requests
- Specific questions: FAQ
Introduction
I get asked about Kubernetes memory usage again and again, and I kept giving the same explanation. The information you need is spread across the Kubernetes and Linux kernel documentation, and I could not find anything that connects the two, so I wrote it down myself.
There is no single definition of "memory usage"
The question "how many MiB of memory is this container using?" has no single correct answer. Unlike CPU utilization or disk usage, memory usage is not one value with one definition. The number changes depending on what you want to measure and where you measure it from.
The reason is that Linux cannot assign every byte of memory to exactly one owner.
- Who owns the page cache? When you read a file, the kernel keeps its contents in memory. The application did not request this with
malloc; the kernel allocated it on its own for performance. And when memory runs short, the kernel silently throws it away. Whether it counts as "memory the application is using" depends on your purpose. - Who owns shared memory? Shared library pages, tmpfs and shared memory are referenced by several processes through the same physical pages. If you simply add up per-process numbers, the same page is counted many times, and the total can even exceed physical memory.
- When does "freed" memory actually go down? Even if an application calls
free, the OS-visible usage does not drop unless the allocator or language runtime returns the memory to the OS. If it does return it, usage drops. The value depends on which layer you look at. - Some memory is reclaimable and some is not. Both count as "in use", but under memory pressure the kernel can take some of it back (page cache) and cannot take back the rest (anonymous pages). You cannot ignore this distinction when estimating free memory.
In other words, memory usage is a value that only becomes defined once you fix the measurement method and the purpose. "The pages a process holds in physical memory", "the memory only that process uses" and "the memory that cannot be freed under pressure" are all legitimate definitions, and each gives a different number.
Kubernetes runs on its own definitions
On top of that, Kubernetes components use their own definitions for management purposes. If you read a value assuming it is the same thing you normally call "memory usage", you will misinterpret it.
The container memory usage you see with kubectl top pod or container_memory_working_set_bytes is not the amount of memory the application has allocated (RSS). Kubernetes treats the working set as container usage, and it includes RSS plus part of the page cache and memory used by the kernel.
container_memory_working_set_bytes # the value kubectl top pod shows ≒ container_memory_rss # memory the application allocated (anonymous pages) + active page cache # only inactive pages are subtracted + tmpfs and shared memory (shmem) # included in full, active or inactive + kernel memory, etc. So the working set is larger than what the application actually consumes. The tricky part is that OOM kills and evictions happen because a value other than the one you are looking at reached its limit. For an OOM kill caused by exceeding a limit (called memcg OOM in this article), the kernel compares the container's memory.current against the limit, which is not the working set. And memory.current merely reaching the limit does not cause an OOM; the kill happens when reclaim fails to bring usage back under the limit. Eviction is triggered by free memory computed from the node-wide working set, so it looks at a different scope than the per-container values.
If you treat these as the same thing, you cannot explain situations that look contradictory:
- The working set has not reached the memory limit, yet the container was
OOMKilled - Node memory utilization computed from
node_memory_MemAvailable_bytesis low, yet a Pod was evicted - The application does not leak memory, yet the working set keeps growing
- The RSS the application reports does not match
container_memory_rss
None of these are metric bugs. They are the result of each component reporting a value with a definition that suits its own purpose. Conversely, once you know which value has which definition, the apparent contradictions can be explained.
This article covers three things in order:
- Kubernetes memory usage and cgroup v2: what each value that appears as "memory usage" measures, which cgroup v2 value it corresponds to, and which value triggers eviction
- How to investigate OOM kills and evictions: there are two OOM kill paths, and how to tell which one happened when you see
OOMKilledor an eviction - What to do about it: what to do once you know the cause. Right-sizing the memory request is the most effective way to reduce OOM kills and evictions together, so the article focuses on that, including PromQL for checking it
How this article names the two OOM kill paths
There are two paths by which a Pod's process gets killed for memory reasons, and they differ in both cause and blast radius. If you call both "OOM" you cannot tell them apart, so this article uses the following two names throughout.
| Name in this article | Purpose | Triggering cause | What gets killed |
|---|---|---|---|
| memcg OOM | Enforcing a configured limit | A container, Pod or /kubepods.slice exceeded its memory.max (= memory limit) | Only processes inside the cgroup that hit its limit |
| global OOM | Keeping the whole system alive (protecting the node) | Memory exhaustion of the whole node, regardless of any container's limit | Any process on the node |
Put simply, memcg OOM is caused by "your own limit", and global OOM is caused by "the node's free memory". Many cases of OOMKilled despite headroom under the limit are the latter.
The difference in purpose is written in the kernel documentation. Global OOM exists to keep the rest of the system alive; Concepts overview - OOM killer says "In order to save the rest of the system, it invokes the OOM killer". The description of memory.max, on the other hand, is "This is the main mechanism to limit memory usage of a cgroup" (Memory Interface Files), with no mention of protecting other cgroups. cgroup v2 treats limits (memory.max / memory.high) and protection (memory.min / memory.low) as separate models ("implements both limit and protection models"), and memcg OOM belongs to the former.
So memcg OOM can happen even when the node has plenty of free memory. It happens solely because a configured limit was exceeded, regardless of actual pressure. Global OOM, conversely, happens only when node memory is genuinely exhausted, regardless of any limit.
The one exception is the /kubepods.slice hierarchy. Its memory.max is set to Capacity - kube-reserved - system-reserved (see below), which effectively reserves memory for non-Pod processes such as the kubelet and containerd. That is not the purpose of the kernel's memcg OOM; it is Kubernetes using the mechanism that way.
Note: These two names are not official Kubernetes terms. Kubernetes only exposes the termination reason
OOMKilled, and it does not distinguish the paths (the reason is explained in Why OOMKilled cannot distinguish the paths).Both are terms the kernel itself uses, and this article adopts them as they are. The two are used side by side in a kernel comment (mm/oom_kill.c,
pagefault_out_of_memory()):/* * The pagefault handler calls here because some allocation has failed. We have * to take care of the memcg OOM here because this is the only safe context without * any locks held but let the oom killer triggered from the allocation context care * about the global OOM. */The actual distinction is whether the
memcgfield ofstruct oom_controlis NULL. If it holds a value, the victim is chosen from within that cgroup; if NULL, from the whole node (oom.h).is_memcg_oom()just checks this field./* Memory cgroup in which oom is invoked, or NULL for global oom */ struct mem_cgroup *memcg;
memcgstands for memory cgroup (memory control group), a short form ofstruct mem_cgroupin the code above. Variable and function names in the source (is_memcg_oom(),try_charge_memcg()) use the same spelling, and the kernel documentation also writes "Memory cgroup (memcg)" (Documentation/mm/hmm.rst).Note that the term
global OOMappears only in source comments and OOM logs; it is not in the kernel documentation.memcg OOMdoes appear as "memcg OOM killer" inDocumentation/admin-guide/cgroup-v1/memory.rst.This is how they appear in the kernel log:
Name in this article How it appears in the kernel log memcg OOM constraint=CONSTRAINT_MEMCG,oom_memcg=<cgroup path>,Memory cgroup out of memory: Killed process ...global OOM constraint=CONSTRAINT_NONE,,global_oom,Out of memory: Killed process ...The prefix of the
Killed processline directly indicates the path (oom_kill.c):oom_kill_process(oc, !is_memcg_oom(oc) ? "Out of memory" : "Memory cgroup out of memory");
Also note that eviction is a separate mechanism from an OOM kill. It is the kubelet, not the kernel, that removes the Pod, and it is observed as Evicted. It is covered below as well.
Quick reference: contradictions explained and a metric map
This section expands the TL;DR: it explains the apparent contradictions from the introduction and maps each metric to the cgroup v2 value behind it.
With that, the seemingly contradictory situations from the introduction can be explained as follows:
- The working set has not reached the limit, yet
OOMKilled: memcg OOM comparesmemory.current, not the working set, against the limit (and merely reaching the limit does not trigger it; the kill happens when reclaim fails). Also, even if your container is under its limit, it can be killed in a node-wide memory exhaustion (global OOM). See My working set is below the limit, but the container was OOMKilled. - Node utilization computed from
MemAvailableis low, yet a Pod was evicted:MemAvailablecounts part of the page cache as free, but the kubelet is working-set based and treats all active page cache as "in use". See Node usage computed from MemAvailable is low, but Pods are evicted. - The application does not leak memory, yet the working set keeps growing: the working set includes active page cache. Page cache is not reclaimed until memory gets tight, so workloads with heavy file I/O or logging look like they keep growing. See Memory working set keeps growing.
- The RSS the application reports does not match
container_memory_rss: the former is per-process RSS (including file mappings), and the latter is the anonymous pages charged to the container's cgroup. See The RSS my application reports differs from the RSS metric.
The mapping between these values and the metrics you see in kubectl top and dashboards is as follows. Even among "memory usage" metrics, each one measures something different and corresponds to a different cgroup v2 field.
| Scope | Metric | Emitted by | Backing value in cgroup v2 / /proc | Page cache handling | Main use |
|---|---|---|---|---|---|
| Container | container_memory_working_set_bytes | cAdvisor (embedded in kubelet) | memory.current − inactive_file | Active pages count as in use | kubectl top pod, usage visualization |
| Container | container_memory_rss | cAdvisor (embedded in kubelet) | anon in memory.stat | Not included (anonymous pages only) | Understanding what the app allocated |
| Container | container_memory_usage_bytes | cAdvisor (embedded in kubelet) | memory.current | Everything included | What memcg OOM is evaluated against |
| Node | container_memory_working_set_bytes{id="/"} | cAdvisor (embedded in kubelet) | anon + file − inactive_file in the root cgroup's memory.stat (see below) | Active pages count as in use | Eviction decisions, kubectl top node |
| Node | node_memory_MemAvailable_bytes | node-exporter | MemAvailable in /proc/meminfo | Reclaimable pages count as free | Visualizing node memory utilization |
| Node | kube_node_status_capacity{resource="memory"} | kube-state-metrics | (status.capacity of the Node object) | (capacity, not usage) | Denominator when converting to utilization |
Looking at the same node, utilization computed from node_memory_MemAvailable_bytes does not match the utilization the kubelet uses for eviction decisions. With workloads that do a lot of file I/O or logging, the difference can reach several GiB.
Different emitters also mean different labels on the metrics. cAdvisor metrics carry no label identifying the node, so when you compute per-node utilization you need to align labels with kubelet_node_name, as shown later.
Finally, the conclusions for diagnosis and remedy:
- To tell which OOM path occurred, check whether the Node has a
SystemOOMevent. If it does, it is global OOM, but its absence is not evidence of memcg OOM (neitherOOMKillednor metrics distinguish the paths). See Telling which path occurred. - The most effective remedy is to set the memory request to match actual usage. It affects placement, eviction candidate selection and
oom_score_adjat the same time. PromQL for checking is collected in What to do about it.
Kubernetes memory usage and cgroup v2
This section sorts out which cgroup v2 value each metric corresponds to, and which value triggers OOM kills and evictions.
Memory breakdown in cgroup v2
Container memory usage is managed in a per-container cgroup. In cgroup v2, memory.current is the current usage and memory.stat is its breakdown.
# Example values in a container's cgroup (checked on the node) $ cat /sys/fs/cgroup/<container cgroup>/memory.current 5368709120 $ cat /sys/fs/cgroup/<container cgroup>/memory.stat anon 2147483648 # anonymous pages (heap, stack: memory not backed by a file) file 3087007744 # page cache (including tmpfs and shared memory) shmem 268435456 # the part of file that is tmpfs or shared memory ... inactive_anon 134217728 # anon LRU: not accessed for a while active_anon 2013265920 # anon LRU: recently accessed inactive_file 1073741824 # file LRU: not accessed for a while active_file 1744830464 # file LRU: recently accessed unevictable 268435456 # pages that cannot be reclaimed (here, the tmpfs part) ... The following relationships hold in this example. Note that inactive_file + active_file does not equal file; it equals file - shmem. The next section explains why.
anon + file + kernel memory = 5368709120 (memory.current) inactive_file + active_file = 2818572288 = file - shmem inactive_anon + active_anon = 2147483648 = anon (shmem is not in here) unevictable = 268435456 = shmem (tmpfs without swap) The main fields of memory.stat are:
anon (anonymous pages)
- Memory not backed by a file. Regions the application allocated with
mallocand similar belong here. - Unlike page cache, the kernel cannot reclaim it independently of the process. With swap it can write pages out and free up space, but on nodes that do not use swap it is memory the kernel cannot reclaim under pressure. When swap is unavailable, the kernel sets
scan_balance = SCAN_FILEand excludes the anon LRU from scanning (get_scan_count()in mm/vmscan.c). - On the other hand, this value drops if the process itself returns memory to the OS with
munmapormadvise(MADV_DONTNEED). Note that callingfreedoes not reduce it unless the allocator or language runtime returns the memory to the OS.
file (page cache)
- Contents of files held in memory by the kernel during reads and writes. The application did not allocate it explicitly.
- When memory runs short the kernel discards (reclaims) it automatically, so it is basically reclaimable memory.
- Cache of files on disk is split into
active_file(recently accessed) andinactive_file(not accessed for a while). Reclaim takesinactive_filefirst. -
active_file + inactive_filedoes not equalfile. These are totals of the LRU lists, and as described belowshmemis on the anon LRU rather than the file LRU. Besidesshmem, pages that fall off the LRU, such asmlocked pages, also show up as a difference. - It includes not only the cache of files on disk but also
shmem(tmpfs and shared memory), because Linux treats these as page cache too.shmemis written out to swap rather than to a file, so on nodes without swap it cannot be reclaimed. The breakdown is visible asshmeminmemory.stat.
Warning:
shmemis counted infile, but it is not on the file LRU. The kernel classifies swap-backed pages as anon, and tmpfs pages are among them.folio_is_file_lru()in mm_inline.h is defined as "0 if@foliois a normal anonymous folio, a tmpfs folio or otherwise ram or swap backed folio".The counters they are charged to are different, though. A tmpfs page is added to both
NR_FILE_PAGES(=file) andNR_SHMEM(=shmem) (mm/shmem.c):__lruvec_stat_mod_folio(folio, NR_FILE_PAGES, nr); __lruvec_stat_mod_folio(folio, NR_SHMEM, nr);Furthermore, for a Kubernetes
emptyDirwithmedium: Memory, the kubelet mounts tmpfs with thenoswapoption (generateTmpfsMountOptions()in empty_dir.go). The kernel makes the inode of anoswaptmpfs unevictable (shmem_get_inode()in mm/shmem.c), so these pages sit on the unevictable LRU, not the anon LRU.if (sbinfo->noswap) mapping_set_unevictable(inode->i_mapping);So
shmemis accounted as follows:
mount LRU Where it shows up in memory.statemptyDirwithmedium: Memory(noswap)unevictable unevictableAny other tmpfs / shared memory anon LRU inactive_anon/active_anonIn neither case does it enter
inactive_file/active_file. That matters in two ways later:
inactive_file + active_fileequalsfile - shmem, notfile- Only
inactive_fileis subtracted from the working set, soshmemstays in the working set in full
With workloads that write a lot of logs or read and write large files, the page cache can grow to several GiB even though the application did not intend that.
Note:
memory.currentincludes memory the kernel itself uses (slab, socket buffers and so on) in addition toanonandfile. So addinganonandfilealone does not givememory.current.
How cgroup v2 memory controls map to Pod settings
cgroup v2 memory control interface
cgroup v2 offers four levels of control over memory usage. memory.min and memory.low are protections ("do not reclaim up to here"), and memory.high and memory.max are limits ("do not allow use beyond here").
| File | Type | Behavior |
|---|---|---|
memory.min | Protection (hard) | Memory up to this value is not reclaimed. If nothing else can be reclaimed and usage cannot fit, an OOM kill occurs |
memory.low | Protection (soft) | Best-effort avoidance of reclaim up to this value. It is reclaimed if nothing else can be |
memory.high | Limit (soft) | Exceeding it applies heavy reclaim pressure and throttles the cgroup's processes. No OOM kill |
memory.max | Limit (hard) | Exceeding it triggers reclaim, and if usage still does not fit, the cgroup's OOM killer runs |
Other files that report state:
-
memory.current: current usage (whatcontainer_memory_usage_bytesis backed by) -
memory.stat: usage breakdown -
memory.events: counts oflow,high,max,oom,oom_killandoom_group_kill -
memory.swap.max: swap limit -
memory.pressure: PSI for waiting on memory allocation
For exact definitions of each file see the kernel's Memory Interface Files, and for how Kubernetes treats cgroup v2 see About cgroup v2.
How Pod settings are reflected
Pod settings are reflected into cgroup v2 as follows.
| Pod setting | Target in cgroup v2 |
|---|---|
resources.limits.memory | memory.max |
resources.requests.memory | Not reflected (with the default kubelet configuration in v1.37; you can reflect it by changing settings → KEP-2570) |
resources.limits.cpu | cpu.max |
resources.requests.cpu | cpu.weight |
cgroup v2 is a directory tree. Its top is the root cgroup (/sys/fs/cgroup), where any process that is in no child cgroup belongs, so it contains not only Pods but every process on the node, such as the kubelet and containerd. Kubernetes creates the hierarchy below it: "all Pods on the node → Pod → container".
# With the systemd cgroup driver (the kubeadm default; names differ with the cgroupfs driver, see below) /sys/fs/cgroup ← root cgroup (id="/") │ contains every process on the node │ the value used for eviction decisions │ ├── system.slice/ kubelet, containerd, sshd, etc. │ ├── kubelet.service (processes that are not Pods) │ └── containerd.service │ └── kubepods.slice/ ← all Pods on the node │ (id="/kubepods.slice") ├── kubepods-pod<UID>.slice/ ← Pod (Guaranteed) │ ├── cri-containerd-<ID>.scope ← container │ └── cri-containerd-<pause ID>.scope ← pause container │ ├── kubepods-burstable.slice/ (intermediate slice per QoS class) │ └── kubepods-burstable-pod<UID>.slice/ ← Pod (Burstable) │ └── cri-containerd-<ID>.scope ← container │ └── kubepods-besteffort.slice/ └── kubepods-besteffort-pod<UID>.slice/ ← Pod (BestEffort) └── cri-containerd-<ID>.scope ← container When filtering metrics, the root cgroup is id="/", all Pods on the node are id="/kubepods.slice", a Pod is container="", image="", pod!="", and a container is container!="", image!="". What the kubelet uses for eviction decisions is the working set of the topmost root cgroup (see below). That is a different level from memcg OOM caused by a container or Pod exceeding its limit.
Memory limits are set not only on containers but also on higher levels.
| Level | cgroup | Memory limit |
|---|---|---|
| Whole node (root cgroup) | / | Not set (the root has no memory.max → see below) |
| All Pods on the node | /kubepods.slice | Capacity - kube-reserved - system-reserved |
| Pod | Differs by QoS class (see below) | The Pod's effective limit (see below). Set only when every container has a limit |
| Container | cri-containerd-<ID>.scope under the Pod's cgroup | That container's limits.memory |
A Pod's effective limit is not simply the sum of its regular containers' limits.memory. If there are init containers, their maximum is also considered, and spec.overhead is added if set. For the calculation see Resource sharing within containers and Pod Overhead.
So even if a container has not reached its own limit, an OOM kill can occur when the Pod as a whole or /kubepods.slice hits its limit.
Note: The paths above assume the kubelet uses the systemd cgroup driver. With the cgroupfs driver, paths look like
/kubepods/burstable/pod<UID>/<container ID>, with no.sliceor.scopeand nocri-containerd-prefix on the container.The kubelet's own default is
cgroupfs, but kubeadm setssystemdwhen the value is empty (defaults.go, kubelet.go). You can check which one is in use with:$ kubectl get --raw "/api/v1/nodes/<node name>/proxy/configz" | jq '.kubeletconfig.cgroupDriver' "systemd"The
idlabel of cAdvisor metrics also changes with the cgroup driver, so when filtering in PromQL, check the value in your own cluster with this query:group by (id) (container_memory_working_set_bytes{id=~"/kubepods|/kubepods.slice"})A Pod's cgroup path depends on its QoS class. As the tree above shows, Burstable and BestEffort go through an intermediate slice per QoS class.
In the
<UID>part, systemd escaping replaces the hyphens of the Pod UID with underscores (example:6983351e-4ede-...→pod6983351e_4ede_...). The UID of a static Pod (mirror Pod) is a hash with no hyphens, so it looks likepod1ff70b81885169da4e9e32c9530b77c9.slice.The QoS class and UID of a Pod can be read from the Pod resource, and once you have those two you can assemble the cgroup path.
$ kubectl get pod -n <namespace> <Pod name> -o jsonpath='{.status.qosClass}{"\n"}{.metadata.uid}{"\n"}' Burstable 6983351e-4ede-4611-8bab-b986614af18d # To list them $ kubectl get pods -n <namespace> -o custom-columns='NAME:.metadata.name,QOS:.status.qosClass'
Container metrics
The cAdvisor built into the kubelet exposes the cgroup values above as Prometheus metrics. They are served from the kubelet's /metrics/cadvisor endpoint, and you do not need to run a separate cAdvisor Pod or DaemonSet.
| Metric | Emitted by | Backing value in cgroup v2 | Description |
|---|---|---|---|
container_memory_usage_bytes | cAdvisor (embedded in kubelet) | memory.current | Usage including anonymous pages, page cache and kernel memory. The value memcg OOM is evaluated against |
container_memory_working_set_bytes | cAdvisor (embedded in kubelet) | memory.current - inactive_file | Memory considered "not immediately reclaimable". The value Kubernetes treats as usage |
container_memory_rss | cAdvisor (embedded in kubelet) | anon in memory.stat | Amount of anonymous pages. Unlike true RSS, it does not include mmaped file pages (see below) |
container_memory_cache | cAdvisor (embedded in kubelet) | file in memory.stat | Page cache size. Includes shmem (tmpfs and shared memory) |
container_memory_total_active_file_bytes | cAdvisor (embedded in kubelet) | active_file in memory.stat | Active page cache |
container_memory_total_inactive_file_bytes | cAdvisor (embedded in kubelet) | inactive_file in memory.stat | Inactive page cache |
container_memory_mapped_file | cAdvisor (embedded in kubelet) | file_mapped in memory.stat | Amount of mmaped files |
container_pressure_memory_waiting_seconds_total | cAdvisor (embedded in kubelet) | total of some in memory.pressure | Time some processes were waiting for memory (see below) |
container_pressure_memory_stalled_seconds_total | cAdvisor (embedded in kubelet) | total of full in memory.pressure | Time all processes were stalled waiting for memory (see below) |
container_oom_events_total | kubelet (v1.37; the name starts with container_ but it is not from cAdvisor) | (not a cgroup value) | Counted by parsing oom-kill: lines in the kernel log. When the container terminates its cgroup disappears and the series is lost (whether or not it is recreated) |
Note: The kubelet has endpoints such as
/metrics,/metrics/cadvisorand/metrics/resource(server.go). The last two relate to container and node usage./metrics/cadvisorexposes thecontainer_*metrics above, while/metrics/resourceexposes only CPU, memory and swap usage (resource_metrics.go). The latter hascontainer_memory_working_set_bytesand also per-Pod and per-node series (pod_memory_working_set_bytes,node_memory_working_set_bytes) (seepodMemoryUsageDescandnodeMemoryUsageDescin resource_metrics.go). When to use which for per-Pod usage is covered in Align memory request with actual usage.
The important thing here is the definition of the working set.
container_memory_working_set_bytes = container_memory_usage_bytes - container_memory_total_inactive_file_bytes In other words, only inactive page cache is subtracted from the working set, and active page cache stays in as "in use". For the implementation see cAdvisor's handler.go.
workingSet := ret.Memory.Usage if v, ok := s.MemoryStats.Stats[inactiveFileKeyName]; ok { ret.Memory.TotalInactiveFile = v if workingSet < v { workingSet = 0 } else { workingSet -= v } } ret.Memory.WorkingSet = workingSet As a breakdown, it is roughly the sum of the following. RSS (container_memory_rss) is exactly anon in memory.stat under cgroup v2, that is, the amount of anonymous pages, so it is included in the working set.
container_memory_working_set_bytes ≒ container_memory_rss (anonymous pages; shmem not included) + shmem (tmpfs and shared memory; stays in full) + container_memory_total_active_file_bytes (active page cache) + unreclaimable pages, kernel memory, etc. (unevictable, slab, socket buffers, ...) shmem is listed as a separate term because neither container_memory_rss (= anon = NR_ANON_MAPPED) nor active_file includes shmem. As described above, shmem is not on the file LRU, so it never appears in active_file / inactive_file, and since only inactive_file is subtracted from the working set, it remains in full. No container_* metric is exposed for shmem, so this term can only be checked from memory.stat on the node.
It is ≒ because, besides shmem, there are also no exposed metrics for the kernel memory and locked pages contained in memory.current. For an exact breakdown, read memory.stat on the node directly.
Caution: RSS in
container_memory_rssstands for Resident Set Size, but its scope differs from the real meaning. True RSS means "the pages a process holds in physical memory" and includesmmaped file pages.But
memory.statin cgroup v2 has norssfield, and cAdvisor exposesanonascontainer_memory_rss. The kernel documentation definesanonas "Amount of memory used in anonymous mappings such as brk(), sbrk(), and mmap(MAP_ANONYMOUS)", which does not include file mappings. So it does not match theRSScolumn ofpsorVmRSSin/proc/[pid]/status.Warning: A large
container_memory_working_set_bytesdoes not necessarily mean the application needs that much memory. It may just be accumulated page cache. To see how much memory the application has actually allocated, compare againstcontainer_memory_rss.
Node metrics
There are two families of metrics representing whole-node memory usage, with different emitters.
From cAdvisor (the kubelet's view)
cAdvisor exposes memory usage not only for containers but also for the root cgroup (id="/", the topmost cgroup that contains every process on the node, as described above) and for the cgroup of all Pods. The id of the all-Pods cgroup depends on the cgroup driver, so the queries below match both values with =~.
-
container_memory_working_set_bytes{id="/"}: working set of the whole node (Pods + other processes). But the root cgroup has nomemory.current, so the backing value differs from containers (see below) -
container_memory_working_set_bytes{id=~"/kubepods|/kubepods.slice"}: working set of all Pods on the node -
container_memory_total_active_file_bytes{id="/"}: active page cache of the whole node
Both the ...