在Almalinux替换CentOS的过程中,我们通过kubectl top nodes命令观察到了两个相同规格的节点(只有cgroup版本不同)。在分别调度两个相同的Pod后,我们预期它们的内存使用量应该相近。然而,我们发现使用了cgroupv2的节点的内存使用量比使用了cgroupv1的节点多了约280Mi。
初步分析表明,可能是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
NOTE: Containerd:1.6.21,K8s:1.28, Kernel:5.15.0 (同步以前的文章)
技术背景 在Kubernetes中,Google的cAdvisor项目被用于节点上容器资源和性能指标的收集。在kubelet server中,cAdvisor被集成用于监控该节点上kubepods(默认cgroup名称,systemd模式下会加上.slice后缀) cgroup下的所有容器。从1.29.0-alpha.2版本中可以看到,kubelet目前还是提供了以下两种配置选项(但是现在useLegacyCadvisorStats为false):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 的统计信息):
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 32 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, runtimeService, 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)) }
可以通过下述两种方式获取节点的内存使用情况
1 2 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 统计信息:
1 2 3 4 5 6 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逻辑
1 2 3 4 5 6 7 8 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 的内存使用方面存在差异。
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的两个集群
1 2 3 4 5 ~ # 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内存使用如下:
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 32 33 34 35 36 ~ # 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
meminfo文件如下
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 ~ # 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 集群
1 2 3 4 5 ~ # 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文件如下:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 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
1 2 3 usage = total - free = 16374584 - 9505980 workingset = 总内存 - 空闲内存 - 非活动文件 = 16374584 - 9505980 - 4608000 = 2207 Mi(kubelet 报告的结果)
结论 如上所述,在Linux kernel及runc cgroupv1计算内存使用为
1 2 3 4 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调整计算内存使用量方式为
1 stats.MemoryStats.Usage.Usage = stats.MemoryStats.Stats["anon"] + stats.MemoryStats.Stats["file"]
当然,我们同时还需要处理cadvisor的woringset的处理逻辑
由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流,上述排障信息已修改为社区内容。
参考文献 1.https://github.com/torvalds/linux/blob/06c2afb862f9da8dc5efa4b6076a0e48c3fbaaa5/mm/memcontrol.c#L3673-L3680 2.https://github.com/kubernetes/kubernetes/issues/68522 3.https://kubernetes.io/docs/reference/instrumentation/cri-pod-container-metrics/