kubernetes内存使用分析

背景

在使用k8s应用(可理解为一个命名空间的所有pod)的过程中,从prometheus中看到这个应用占用的内存大小为24G,而概览中显示集群中使用的内存大小为15G。此时问题就出来了为什么一个命名空间下占用的内存大小会大于节点占用的内存占用?

分析

第一猜测可能就是统计的指标不一样。

概览中的内存使用统计

在集群概览中,显示的内存使用为宿主机的已使用内存大小,由 Prometheus 的以下指标计算得出:

1
2
3
// 已使用内存 = 内存总量-可用内存数
cluster_memory_usage_wo_cache =
sum(node:node_memory_bytes_total:sum) - sum(node:node_memory_bytes_available:sum)

其中:

  • node:node_memory_bytes_total:sum:每个节点的内存总量,其中node_memory_MemFree_bytes是从对应节点的/proc/meminfoMemTotal获取的。
  • node:node_memory_bytes_available:sum:每个节点的可用内存大小
    计算方法:
    available = free + cache + buffers + SReclaimble,这些值是由node-exporterproc/meminfo中读取的,然后将单位换成了bytes
    链接

这里并没有直接使用MemAvailable?,不清楚是基于什么原因考虑的。
使用free -h命令时有一列是available, 这个值跟node:node_memory_bytes_available:sum的计算还有点不太一样。
所以通过free -h查看内存的使用信息和直接通过dashboard查看内存使用信息也是会出现一不致的情况。

1
2
3
4
5
free = node_memory_MemFree_bytes{job="node-exporter"}              --> MemFree
cache = node_memory_Cached_bytes{job="node-exporter"} --> Cached
buffers = node_memory_Buffers_bytes{job="node-exporter"} --> Buffers
SReclaimble = node_memory_SReclaimable_bytes{job="node-exporter"} --> SReclaimable

proc meminfo

已使用内存

假设集群为一个节点的情况下

1
已使用内存 = MemTotal - (MemFree + Cached + Buffers + SReclaimble)

应用中的已使用内存大小

应用中的已使用内存大小,metric名称为namespace:container_memory_usage_bytes_wo_cache:sum
namespace:container_memory_usage_bytes_wo_cache:sum 也通过自定义的prometheusRule进行预计算的。通过与 kube_namespace_labels join 聚合得到每个namespace的working_set。

1
2
3
4
- expr: |
sum(container_memory_working_set_bytes{job="kubelet", image!=""} * on(namespace) group_left(workspace) kube_namespace_labels{job="kube-state-metrics"}) by (namespace, workspace)
or on(namespace, workspace) max by(namespace, workspace) (kube_namespace_labels * 0)
record: namespace:container_memory_usage_bytes_wo_cache:sum

接下来看下container_memory_working_set_bytes这个指标是怎么计算出来的。
1
2
3
container_memory_working_set_bytes = usage - inactive_file

container_memory_usage_bytes = usage // 包括所有使用的内存

其中usageinactive_file都是由cadvisor通过读取cgroup下的文件然后计算出来的。
cadvisor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// cadvisor/info/v1/container.go
// The amount of working set memory, this includes recently accessed memory,
// dirty memory, and kernel memory. Working set is <= "usage".
// Units: Bytes.
WorkingSet uint64 `json:"working_set"`


func setMemoryStats(s *cgroups.Stats, ret *info.ContainerStats) {
ret.Memory.Usage = s.MemoryStats.Usage.Usage

...

inactiveFileKeyName := "total_inactive_file"
if cgroups.IsCgroup2UnifiedMode() {
inactiveFileKeyName = "inactive_file"
}

workingSet := ret.Memory.Usage
// s.MemoryStats.Stats是读取的对应的memory.stat文件
if v, ok := s.MemoryStats.Stats[inactiveFileKeyName]; ok {
if workingSet < v {
workingSet = 0
} else {
workingSet -= v
}
}
ret.Memory.WorkingSet = workingSet
}

其中s.MemoryStats.Usage.Usage(usage)的值是直接读取/sys/fs/cgroup下对应的文件
memeory.usage_in_bytes(cgroupv1)/memory.current(cgroupv2)得到的。
s.MemoryStats.Stat读取的是对应的memory.stat文件。

memory controller

从下面的代码可以看出cadvisor
get memory.current value with cgroupv2

以cgroupV2为例
memory.current: Shows the total amount of memory currently being used by the cgroup and its descendants. It includes page cache, in-kernel data structures such as inodes, and network buffers.

memory.stat
anno: Amount of memory used in anonymous mappings such as
brk(), sbrk(), and mmap(MAP_ANONYMOUS)
file: Amount of memory used to cache filesystem data,
including tmpfs and shared memory. Include pageCache.

inactive_file: bytes of file-backed memory on inactive LRU list

可以看出container_memory_working_set_bytes是包括了活跃的pageCache

两个指标的差异

1
2
3
4
5
6
7
8
9
10
11
12
已使用内存 = MemTotal - (MemFree + Cached + Buffers + SReclaimble)

container_memory_working_set_bytes = usage - inactive_file

这两个指标最大的差异在于`已使用内存`没有将`PageCache`计入
而`container_memory_working_set_bytes`则计入了活跃的`pageCache`

活跃的`pageCache`越大,会导致这两个值的大小相距越大。

如果inactive_file == 0, 则`已使用内存`比`container_memory_working_set_bytes`相当于多减去一项
`pageCache`

回到最上面的问题,应用应该是读取或写入大量文件导致pageCache很大, container_memory_working_set_bytes 显示为24G,减去活跃的pageCahce的话内存占用为10G

·
node:node_memory_bytes_total:sumnode:node_memory_bytes_available:sum这两个指标是通过promethuesRule聚合计算而来,默认是一分钟计算一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- expr: |
sum by (node, host_ip, role) (
(node_memory_MemFree_bytes{job="node-exporter"} + node_memory_Cached_bytes{job="node-exporter"} + node_memory_Buffers_bytes{job="node-exporter"} + node_memory_SReclaimable_bytes{job="node-exporter"})
* on (namespace, pod) group_left(node, host_ip, role)
node_namespace_pod:kube_pod_info:
)
record: node:node_memory_bytes_available:sum
- expr: |
sum by (node, host_ip, role) (
node_memory_MemTotal_bytes{job="node-exporter"}
* on (namespace, pod) group_left(node, host_ip, role)
node_namespace_pod:kube_pod_info:
)
record: node:node_memory_bytes_total:sum

- expr: |
sum(container_memory_working_set_bytes{job="kubelet", image!=""} * on(namespace) group_left(workspace) kube_namespace_labels{job="kube-state-metrics"}) by (namespace, workspace)
or on(namespace, workspace) max by(namespace, workspace) (kube_namespace_labels * 0)
record: namespace:container_memory_usage_bytes_wo_cache:sum

linux系统/proc/meminfo available值的计算

available

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
for_each_zone(zone)
+ wmark_low += zone->watermark[WMARK_LOW];
+
+ /*
+ * Estimate the amount of memory available for userspace allocations,
+ * without causing swapping.
+ *
+ * Free memory cannot be taken below the low watermark, before the
+ * system starts swapping.
+ */
+ available = i.freeram - wmark_low;
+
+ /*
+ * Not all the page cache can be freed, otherwise the system will
+ * start swapping. Assume at least half of the page cache, or the
+ * low watermark worth of cache, needs to stay.
+ */
+ pagecache = pages[LRU_ACTIVE_FILE] + pages[LRU_INACTIVE_FILE];
// 直接假设一半pageCache是可用的
+ pagecache -= min(pagecache / 2, wmark_low);
+ available += pagecache;
+
+ /*
+ * Part of the reclaimable swap consists of items that are in use,
+ * and cannot be freed. Cap this estimate at the low watermark.
+ */
+ available += global_page_state(NR_SLAB_RECLAIMABLE) -
+ min(global_page_state(NR_SLAB_RECLAIMABLE) / 2, wmark_low);
+
+ if (available < 0)
+ available = 0;

Pod之间能否共享PageCache

PageCache由内核控制,当Pod读取宿主机上相同的文件,产生的PageCache是可以共享的。
先后启动两个pod(poda -> podb), 两个pod先后分别读取同一个文件。
查看poda, podb的监控信息.
发现poda的container_memory_cache大小约为1.2M, 而podb的container_memory_cache大小一直基本为0(实际监控数值为4096字节)。

pod驱逐

kubelet通过判断memory.avaible是否小于10% 来进行对pod的驱逐。
memory.avaible = 内存总量 - WorkingSet
计算规则参考如下shell script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# for cgroupV2
#!/bin/bash

# This script reproduces what the kubelet does
# to calculate memory.available relative to kubepods cgroup.

# current memory usage
memory_capacity_in_kb=$(cat /proc/meminfo | grep MemTotal | awk '{print $2}')
memory_capacity_in_bytes=$((memory_capacity_in_kb * 1024))
memory_usage_in_bytes=$(cat /sys/fs/cgroup/kubepods.slice/memory.current)
memory_total_inactive_file=$(cat /sys/fs/cgroup/kubepods.slice/memory.stat | grep inactive_file | awk '{print $2}')

memory_working_set=${memory_usage_in_bytes}
if [ "$memory_working_set" -lt "$memory_total_inactive_file" ];
then
memory_working_set=0
else
memory_working_set=$((memory_usage_in_bytes - memory_total_inactive_file))
fi

memory_available_in_bytes=$((memory_capacity_in_bytes - memory_working_set))
memory_available_in_kb=$((memory_available_in_bytes / 1024))
memory_available_in_mb=$((memory_available_in_kb / 1024))

https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#memory-signals

驱逐策略

https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/node-pressure-eviction/
如果 kubelet 回收节点级资源的尝试没有使驱逐信号低于条件, 则 kubelet 开始驱逐最终用户 Pod。

kubelet 使用以下参数来确定 Pod 驱逐顺序:

Pod 的资源使用是否超过其请求
Pod 优先级
Pod 相对于请求的资源使用情况

  • 首先根据pod是否超出request.memory进行排序,这一步会将Qos为BestEffort的pod排在前面,guaranteed的pod排在后面
  • 然后是根据优先级排序
  • 最后再根据workingSet-request.memory的值进行排序,值越大排序越靠前。

REF:
https://www.kernel.org/doc/Documentation/cgroup-v2.txt
https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt