背景
在使用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/meminfo
的MemTotal
获取的。node:node_memory_bytes_available:sum
:每个节点的可用内存大小
计算方法:
available = free + cache + buffers + SReclaimble
,这些值是由node-exporter
从proc/meminfo
中读取的,然后将单位换成了bytes
。
链接
这里并没有直接使用MemAvailable?,不清楚是基于什么原因考虑的。
使用free -h命令时有一列是available, 这个值跟node:node_memory_bytes_available:sum的计算还有点不太一样。
所以通过free -h查看内存的使用信息和直接通过dashboard查看内存使用信息也是会出现一不致的情况。
1 | free = node_memory_MemFree_bytes{job="node-exporter"} --> MemFree |
已使用内存
假设集群为一个节点的情况下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
3container_memory_working_set_bytes = usage - inactive_file
container_memory_usage_bytes = usage // 包括所有使用的内存
其中usage
和inactive_file
都是由cadvisor
通过读取cgroup下的文件然后计算出来的。
cadvisor1
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文件。
从下面的代码可以看出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 | 已使用内存 = MemTotal - (MemFree + Cached + Buffers + SReclaimble) |
回到最上面的问题,应用应该是读取或写入大量文件导致pageCache很大, container_memory_working_set_bytes 显示为24G,减去活跃的pageCahce的话内存占用为10G
·node:node_memory_bytes_total:sum
和node:node_memory_bytes_available:sum
这两个指标是通过promethuesRule
聚合计算而来,默认是一分钟计算一次。
1 | - expr: | |
linux系统/proc/meminfo available值的计算
1 | for_each_zone(zone) |
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 script1
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