初步分析表明,可能是cAdvisor在统计cgroupv1和v2的内存使用量时存在逻辑上的不一致。
理论上,无论使用cgroupv1还是cgroupv2,两个相同配置的节点的内存使用量应该相近。实际上,在比较/proc/meminfo时,我们发现了总内存使用量近似的情况。那么问题出在哪里呢?
我们发现,这个问题只影响了节点级别的内存统计数据,而不影响Pod级别的统计数据。
问题的根本原因是cAdvisor调用了runc的接口,其计算root cgroup的内存数据方面存在差异。在cgroupv2中,root cgroup不存在memory.current这个文件,但在cgroupv1中root cgroup是存在memory.usage_in_bytes文件的。这导致了在统计cgroupv2内存使用量时出现了不一致的情况。
这个问题可能需要在cAdvisor或runc的逻辑中进行修复,以确保在cgroupv1和cgroupv2中的内存统计一致性。下面我们基于社区issue展开介绍。
v1.28.3 commit:a8a1abc25cad87333840cd7d54be2efaf31a3177
技术背景
在Kubernetes中,Google的cAdvisor项目被用于节点上容器资源和性能指标的收集。在kubelet server中,cAdvisor被集成用于监控该节点上kubepods(默认cgroup名称,systemd模式下会加上.slice后缀) cgroup下的所有容器。从1.29.0-alpha.2版本中可以看到,kubelet目前还是提供了以下两种配置选项(但是现在useLegacyCadvisorStats为false):
if kubeDeps.useLegacyCadvisorStats {
klet.StatsProvider = stats.NewCadvisorStatsProvider(
klet.cadvisor,
klet.resourceAnalyzer,
klet.podManager,
klet.runtimeCache,
klet.containerRuntime,
klet.statusManager,
hostStatsProvider)
} else {
klet.StatsProvider = stats.NewCRIStatsProvider(
klet.cadvisor,
klet.resourceAnalyzer,
klet.podManager,
klet.runtimeCache,
kubeDeps.RemoteRuntimeService,
kubeDeps.RemoteImageService,
hostStatsProvider,
utilfeature.DefaultFeatureGate.Enabled(features.PodAndContainerStatsFromCRI))
}
kubelet以Prometheus指标格式在/stats/暴露所有相关运行时指标,如下图所示,Kubelet内置了cadvisor服务
从 Kubernetes 1.12 版本开始,kubelet 直接从 cAdvisor 暴露了多个接口。包括以下接口:
cAdvisor 的 Prometheus 指标位于 /metrics/cadvisor。 cAdvisor v1 Json API 位于 /stats/、/stats/container、/stats/{podName}/{containerName} 和 /stats/{namespace}/{podName}/{uid}/{containerName}。 cAdvisor 的机器信息位于 /spec。
此外,kubelet还暴露了summary API,其中cAdvisor 是该接口指标来源之一。在社区的监控架构文档中描述了“核心”指标和“监控”指标的定义。这个文档中规定了一组核心指标及其用途,并且目标是通过拆分监控架构来实现以下两个目标:
减小核心指标的统计收集性能影响,允许更频繁地收集这些指标。
使监控方案可替代且可扩展。
因此移除cadvisor的接口,成了一项长期目标,目前进度如下(进度状态的标记略为滞后):
[1.13] 引入 Kubelet 的 pod-resources gRPC 端点;KEP: 支持设备监控社区#2454 [1.14] 引入 Kubelet 资源指标 API
[1.15] 通过添加和弃用 --enable-cadvisor-json-endpoints 标志,废弃“直接” cAdvisor API 端点
[1.18] 默认将 --enable-cadvisor-json-endpoints 标志设置为禁用
[1.21] 移除 --enable-cadvisor-json-endpoints 标志
[1.21] 将监控服务器过渡到 Kubelet 资源指标 API(需要3个版本的差异)
[TBD] 为 kubelet 监控端点提出外部替代方案
[TBD] 通过添加和废弃 --enable-container-monitoring-endpoints 标志,废弃摘要 API 和 cAdvisor Prometheus 端点
[TBD+2] 移除“直接”的 cAdvisor API 端点
[TBD+2] 默认将 --enable-container-monitoring-endpoints 标志设置为禁用
[TBD+4] 移除摘要 API、cAdvisor Prometheus 指标和移除 --enable-container-monitoring-endpoints 标志。
当前版本的cadvisor接口已经做了部分废弃,例如/spec
及
/stats/*等
寻根溯源
kubelet 使用 cadvisor 来获取节点级别的统计信息(无论是使用 cri 还是通过cadvisor 来统计提供程序来获取 pod 的统计信息):
kubernetes/pkg/kubelet/stats/provider.go
NewCRIStatsProvider returns a Provider that provides the node stats
from cAdvisor and the container stats from CRI.
func NewCRIStatsProvider(
cadvisor cadvisor.Interface,
resourceAnalyzer stats.ResourceAnalyzer,
podManager PodManager,
runtimeCache kubecontainer.RuntimeCache,
runtimeService internalapi.RuntimeService,
imageService internalapi.ImageManagerService,
hostStatsProvider HostStatsProvider,
podAndContainerStatsFromCRI bool,
*Provider {
return newStatsProvider(cadvisor, podManager, runtimeCache, newCRIStatsProvider(cadvisor, resourceAnalyzer,
imageService, hostStatsProvider, podAndContainerStatsFromCRI))
NewCadvisorStatsProvider returns a containerStatsProvider that provides both
the node and the container stats from cAdvisor.
func NewCadvisorStatsProvider(
cadvisor cadvisor.Interface,
resourceAnalyzer stats.ResourceAnalyzer,
podManager PodManager,
runtimeCache kubecontainer.RuntimeCache,
imageService kubecontainer.ImageService,
statusProvider status.PodStatusProvider,
hostStatsProvider HostStatsProvider,
*Provider {
return newStatsProvider(cadvisor, podManager, runtimeCache, newCadvisorStatsProvider(cadvisor, resourceAnalyzer, imageService, statusProvider, hostStatsProvider))
可以通过下述两种方式获取节点的内存使用情况
kubectl top node
kubectl get --raw /api/v1/nodes/foo/proxy/stats/summary | jq -C .node.memory
结果显示cgroupv2节点的内存使用量比相同节点配置但使用 cgroupv1的高一些。kubectl top node 获取节点信息的逻辑在:https://github.com/kubernetes-sigs/metrics-server/blob/master/pkg/storage/node.go#L40
kubelet使用 cadvisor 来获取 cgroup 统计信息:
kubernetes/pkg/kubelet/server/stats/summary.go
rootStats, err := sp.provider.GetCgroupCPUAndMemoryStats("/", false)
if err != nil {
return nil, fmt.Errorf("failed to get root cgroup stats: %v", err)
}
这里GetCgroupCPUAndMemoryStats调用以下cadvisor逻辑
kubernetes/pkg/kubelet/stats/helper.go
infoMap, err := cadvisor.ContainerInfoV2(containerName, cadvisorapiv2.RequestOptions{
IdType: cadvisorapiv2.TypeName,
Count: 2, // 2 samples are needed to compute "instantaneous" CPU
Recursive: false,
MaxAge: maxAge,
})
cadvisor 基于 cgroup v1/v2 获取不同 cgroup manager接口实现,然后调用GetStats()获取监控信息。
这些实现在计算root cgroup 的内存使用方面存在差异。
v1 使用来自 memory.usage_in_bytes 的内存使用情况:https://github.com/opencontainers/runc/blob/92c71e725fc6421b6375ff128936a23c340e2d16/libcontainer/cgroups/fs/memory.go#L204-L224
v2 使用 /proc/meminfo 并计算使用情况为总内存 - 空闲内存:https://github.com/opencontainers/runc/blob/92c71e725fc6421b6375ff128936a23c340e2d16/libcontainer/cgroups/fs2/memory.go#L217
usage_in_bytes 大致等于 RSS + Cache。workingset是 usage - 非活动文件。
在 cadvisor 中,在workingset中排除了非活动文件:https://github.com/google/cadvisor/blob/8164b38067246b36c773204f154604e2a1c962dc/container/libcontainer/handler.go#L835-L844"
因此可以判断在cgroupv2计算内存使用使用了total-free,这里面包含了inactive_anon,而内核以及cgroupv1计算内存使用量时不会计入 inactive_anon
:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/memcontrol.c#n3720
通过下面的测试中,inactive_anon 解释数据看到了差异。
下述分别为cgroupv1及cgroupv2的两个集群
~ # kubectl top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
node1 98m 2% 1512Mi 12%
node2 99m 2% 1454Mi 11%
node3 94m 2% 1448Mi 11%
其中cgroupv1节点的root cgroup内存使用如下:
# cat /sys/fs/cgroup/memory/memory.usage_in_bytes
6236864512
# cat /sys/fs/cgroup/memory/memory.stat
cache 44662784
rss 3260416
rss_huge 2097152
shmem 65536
mapped_file 11083776
dirty 135168
writeback 0
pgpgin 114774
pgpgout 103506
pgfault 165891
pgmajfault 99
inactive_anon 135168
active_anon 3645440
inactive_file 5406720
active_file 39333888
unevictable 0
hierarchical_memory_limit 9223372036854771712
total_cache 5471584256
total_rss 767148032
total_rss_huge 559939584
total_shmem 1921024
total_mapped_file 605687808
total_dirty 270336
total_writeback 0
total_pgpgin 51679194
total_pgpgout 50291069
total_pgfault 97383769
total_pgmajfault 5610
total_inactive_anon 1081344
total_active_anon 772235264
total_inactive_file 4648124416
total_active_file 820551680
total_unevictable 0
~ # cat /proc/meminfo
MemTotal: 16393244 kB
MemFree: 9744148 kB
MemAvailable: 15020900 kB
Buffers: 132344 kB
Cached: 5207356 kB
SwapCached: 0 kB
Active: 1557252 kB
Inactive: 4526668 kB
Active(anon): 745916 kB
Inactive(anon): 792 kB
Active(file): 811336 kB
Inactive(file): 4525876 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 636 kB
Writeback: 0 kB
AnonPages: 618992 kB
Mapped: 624384 kB
Shmem: 2496 kB
KReclaimable: 285824 kB
Slab: 423600 kB
SReclaimable: 285824 kB
SUnreclaim: 137776 kB
KernelStack: 8400 kB
PageTables: 9060 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 8196620 kB
Committed_AS: 2800016 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 40992 kB
VmallocChunk: 0 kB
Percpu: 4432 kB
HardwareCorrupted: 0 kB
AnonHugePages: 270336 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
CmaTotal: 0 kB
CmaFree: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
DirectMap4k: 302344 kB
DirectMap2M: 3891200 kB
DirectMap1G: 14680064 kB
当前的计算
memory.current - memory.stat.total_inactive_file = 6236864512 - 4648124416 = 1515 Mi -> kubelet 报告的结果
cgroupv2 集群
~ # kubectl top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
node1 113m 2% 2196Mi 17%
node2 112m 2% 2171Mi 17%
node3 113m 2% 2180Mi 17%
其中一节点的meminfo文件如下:
MemTotal: 16374584 kB
MemFree: 9505980 kB
MemAvailable: 14912544 kB
Buffers: 155164 kB
Cached: 5335576 kB
SwapCached: 0 kB
Active: 872420 kB
Inactive: 5399340 kB
Active(anon): 2568 kB
Inactive(anon): 791340 kB
Active(file): 869852 kB
Inactive(file): 4608000 kB
Unevictable: 30740 kB
Mlocked: 27668 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 148 kB
Writeback: 0 kB
AnonPages: 716552 kB
Mapped: 608424 kB
Shmem: 6320 kB
KReclaimable: 274360 kB
Slab: 355976 kB
SReclaimable: 274360 kB
SUnreclaim: 81616 kB
KernelStack: 8064 kB
PageTables: 7692 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 8187292 kB
Committed_AS: 2605012 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 48092 kB
VmallocChunk: 0 kB
Percpu: 3472 kB
HardwareCorrupted: 0 kB
AnonHugePages: 409600 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
DirectMap4k: 271624 kB
DirectMap2M: 8116224 kB
DirectMap1G: 10485760 kB
usage = total - free = 16374584 - 9505980
workingset = 总内存 - 空闲内存 - 非活动文件 = 16374584 - 9505980 - 4608000 = 2207 Mi(kubelet 报告的结果)
结论
如上所述,在Linux kernel及runc cgroupv1计算内存使用为
mem_cgroup_usage =NR_FILE_PAGES + NR_ANON_MAPPED + nr_swap_pages (如果swap启用的话)
// - rss (NR_ANON_MAPPED)
// - cache (NR_FILE_PAGES)
但是runc在cgroupv2计算使用了total-free,因此在相似负载下,同一台机器上v1和v2版本的节点级别报告确实会相差约250-750Mi,为了让cgroup v2的内存使用计算更接近 cgroupv1, cgroup v2调整计算内存使用量方式为
stats.MemoryStats.Usage.Usage = stats.MemoryStats.Stats["anon"] + stats.MemoryStats.Stats["file"]
当然,我们同时还需要处理cadvisor的woringset的处理逻辑
由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流,上述排障信息已修改为社区内容。
真诚推荐你关注