Kubernetes容器监控原理和源码解析(一)——API和数据来源

前言

本系列主要基于 v1.24.0 版本的 Kubelet 部分源代码,进行 Kubernetes 中容器监控的底层原理介绍与代码分析。

Kubelet 中的监控 API

在 Kubelet Server 提供的监控 API 中,大致可以分为两类:stats(统计数据)和 metrics(指标数据)。从命名和实际作用来看,前者提供了粗粒度的基础监控能力,目前用于各种内置组件;而后者用于持久化地进行细粒度的容器监控,主要提供给 Prometheus 等。

统计类 API

在 v1.24.0 版本中,目前统计类接口仅包含/stats/summary,该接口提供了节点和 Pod 的统计信息。节点部分包括 CPU、内存、网络、文件系统、容器运行时、Rlimit 的统计。Pod 部分主要提供 Pod 相关的基础统计与卷、临时存储、进程统计外,主要还包括了各个容器的统计信息。容器部分值得关注的是,该接口中提供了用户自定义指标。方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (h *handler) handleSummary(request *restful.Request, response *restful.Response) {
onlyCPUAndMemory := false

...

if onlyCluAndMemoryParam, found := request.Request.Form["only_cpu_and_memory"]; found &&
len(onlyCluAndMemoryParam) == 1 && onlyCluAndMemoryParam[0] == "true" {
onlyCPUAndMemory = true
}
var summary *statsapi.Summary
if onlyCPUAndMemory {
summary, err = h.summaryProvider.GetCPUAndMemoryStats()
} else {
forceStatsUpdate := false
summary, err = h.summaryProvider.Get(forceStatsUpdate)
}

...
}

查看 SummaryProvider 的实现可以发现,该实际上就是对 stats.Provider 的封装。

1
2
3
4
5
6
7
8
9
10
11
type SummaryProvider interface {
Get(updateStats bool) (*statsapi.Summary, error)
GetCPUAndMemoryStats() (*statsapi.Summary, error)
}

type summaryProviderImpl struct {
kubeletCreationTime metav1.Time
systemBootTime metav1.Time

provider Provider
}

在 handler 的处理逻辑中,它提供了一个只返回 CPU 和 Memory 信息的选项 onlyCPUAndMemory,如果只关心 cpu 和内存信息,通过此选项可以去除多余的统计信息,metrics-server(_< 0.6.0 版本_)中就默认设置了该值。该部分需要注意的是,如果获取的是完整信息,那么监控信息是从缓存中获取的,这里特指 CPU 中的 NanoCoreUsage 不会更新,只有调用 onlyCPUAndMemory 才会将该值更新。如果基于接口做定制开发需要将 forceStatsUpdate 修改为 true 以保证 NanoCoreUsage 的准确性。
此外,在最新版本的 0.6.x 版本的 metrics-server 中已经不再依赖该接口,而是采用了 Kubelet 中的/metrics/resource 接口进行资源的监控,在老版本集群中部署 metrics-server 时需要注意不兼容问题。

指标类 API

Kubelet Server 提供的指标类 API 目前包括以下四个:

  • /metrics:提供 kubelet 自身相关的一些监控,包括:apiserver 请求、go gc/内存/线程相关、kubelet 子模块关键信息、client-go 等指标
  • /metrics/cadvisor:提供 Pod/容器监控信息
  • /metrics/probes:提供对容器 Liveness/Readiness/Startup 探针的指标数据
  • /metrics/resource:提供 Pod/容器的 CPU 用量、wss 内存、启动时间基础指标数据

上述四个接口返回的指标信息默认都是 Promtheus 格式。一般来说,指标想要转化为 Promtheus 格式需要实现 Prometheus client 的 Registerer 和 Gatherer 接口,而在 K8s 中对应的封装实现就是 KubeRegistry。这里我们先跳过 Prometheus client 的实现原理和其他非容器指标相关的实现,而是来看看容器指标数据是如何获取到并转化返回的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
r := compbasemetrics.NewKubeRegistry()

includedMetrics := cadvisormetrics.MetricSet{
// 指标白名单设置
...
}

cadvisorOpts := cadvisorv2.RequestOptions{
IdType: cadvisorv2.TypeName,
// 每次仅返回一条数据
Count: 1,
Recursive: true,
}

r.RawMustRegister(metrics.NewPrometheusCollector(prometheusHostAdapter{s.host}, containerPrometheusLabelsFunc(s.host), includedMetrics, clock.RealClock{}, cadvisorOpts))
r.RawMustRegister(metrics.NewPrometheusMachineCollector(prometheusHostAdapter{s.host}, includedMetrics))

s.restfulCont.Handle(cadvisorMetricsPath,
compbasemetrics.HandlerFor(r, compbasemetrics.HandlerOpts{ErrorHandling: compbasemetrics.ContinueOnError}),
)

在 Prometheus Client 的注册逻辑里,返回数据需要实现指标收集器(Collector)部分。这里可以看到,Kubelet 提供了两种数据收集器,一部分是 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
func NewPrometheusMachineCollector(i infoProvider, includedMetrics container.MetricSet) *PrometheusMachineCollector {
c := &PrometheusMachineCollector{

infoProvider: i,
errors: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "machine",
Name: "scrape_error",
Help: "1 if there was an error while getting machine metrics, 0 otherwise.",
}),
machineMetrics: []machineMetric{
{
name: "machine_cpu_physical_cores",
help: "Number of physical CPU cores.",
valueType: prometheus.GaugeValue,
getValues: func(machineInfo *info.MachineInfo) metricValues {
return metricValues{{value: float64(machineInfo.NumPhysicalCores), timestamp: machineInfo.Timestamp}}
},
},

// 其他指标的类似实现
...
}...)
}
return c
}

可以看到收集器实际上主要是通过 getValues 方法把 infoProvider 提供的数据结构从原始结构转成 Prometheus 中的指标,同时每个指标需要定义其名称、类型以及描述。节点和 Pod/容器对应的原始数据结构都定义在 cAdvisor 的 API Spec 中,即 MachineInfo 和 ContainerStats。

1
2
3
4
5
6
7
8
type infoProvider interface {
// GetRequestedContainersInfo gets info for all requested containers based on the request options.
GetRequestedContainersInfo(containerName string, options v2.RequestOptions) (map[string]*info.ContainerInfo, error)
// GetVersionInfo provides information about the version.
GetVersionInfo() (*info.VersionInfo, error)
// GetMachineInfo provides information about the machine.
GetMachineInfo() (*info.MachineInfo, error)
}

而从上面 KubeRegistry 的注册逻辑中可以看到,infoProvider 的实现又需要通过 prometheusHostAdapter 进行一次转换,其转换前的实现接口为 HostInterface。

1
2
3
4
5
6
7
8
9
10
11
12
13
type prometheusHostAdapter struct {
host HostInterface
}

func (a prometheusHostAdapter) GetRequestedContainersInfo(containerName string, options cadvisorv2.RequestOptions) (map[string]*cadvisorapi.ContainerInfo, error) {
return a.host.GetRequestedContainersInfo(containerName, options)
}
func (a prometheusHostAdapter) GetVersionInfo() (*cadvisorapi.VersionInfo, error) {
return a.host.GetVersionInfo()
}
func (a prometheusHostAdapter) GetMachineInfo() (*cadvisorapi.MachineInfo, error) {
return a.host.GetCachedMachineInfo()
}

在 HostInterface 的定义中,查看监控相关的部分接口,prometheusHostAdapter 需要的 VersionInfo 和 MachineInfo 是由 Kubelet 实现的,而 GetRequestedContainersInfo 即容器相关监控信息的接口则由前面提到的 stats.Provider 中实现。

1
2
3
4
5
6
7
8
9
type HostInterface interface {
stats.Provider
GetVersionInfo() (*cadvisorapi.VersionInfo, error)
GetCachedMachineInfo() (*cadvisorapi.MachineInfo, error)
GetRunningPods() ([]*v1.Pod, error)
GetHostname() string

// 省略监控无关接口
}

监控数据提供方 Stats Provider

Provider 定义

基于上一节分析,可以了解到 Kubelet Server 中返回的监控信息,无论是统计类信息还是指标类信息,容器相关的部分都是通过 stats.Provider 获取的。那么我们看看 stats.Provider 部分是如何定义的:

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
type Provider interface {

// ListPodStats 返回Pod管理的容器统计信息
ListPodStats() ([]statsapi.PodStats, error)
// ListPodCPUAndMemoryStats 返回Pod管理的容器统计信息(CPU/内存部分)
ListPodCPUAndMemoryStats() ([]statsapi.PodStats, error)
// ListPodStatsAndUpdateCPUNanoCoreUsage 返回Pod管理的容器统计信息,这个方法会强制更新cpu的NanoCoreUsage信息,主要用于部分未内部集成cAdvisor的CRI Runtime实现。详见:https://github.com/kubernetes/kubernetes/issues/72788
ListPodStatsAndUpdateCPUNanoCoreUsage() ([]statsapi.PodStats, error)
// ImageFsStats 返回镜像文件系统的统计信息
ImageFsStats() (*statsapi.FsStats, error)

// GetCgroupStats 通过指定的cgroup名称返回统计信息及网络用量
GetCgroupStats(cgroupName string, updateStats bool) (*statsapi.ContainerStats, *statsapi.NetworkStats, error)
// GetCgroupCPUAndMemoryStats 通过指定的cgroupName返回CPU和内存统计信息
GetCgroupCPUAndMemoryStats(cgroupName string, updateStats bool) (*statsapi.ContainerStats, error)

// RootFsStats 返回节点根分区的统计信息
RootFsStats() (*statsapi.FsStats, error)

// GetContainerInfo 通过Pod Uid返回该Pod管理的容器的指标信息
GetContainerInfo(podFullName string, uid types.UID, containerName string, req *cadvisorapi.ContainerInfoRequest) (*cadvisorapi.ContainerInfo, error)
// GetRawContainerInfo 通过容器名返回容器的指标信息,如果开启了subcontainers选项,则该方法会返回所有子容器的指标信息
GetRawContainerInfo(containerName string, req *cadvisorapi.ContainerInfoRequest, subcontainers bool) (map[string]*cadvisorapi.ContainerInfo, error)
// GetRequestedContainersInfo 通过容器名返回容器的指标信息,同事提供了一些cAdvisor特定的可选参数
GetRequestedContainersInfo(containerName string, options cadvisorv2.RequestOptions) (map[string]*cadvisorapi.ContainerInfo, error)

// GetPodByName 通过Pod的命名空间和名称返回具体的Pod信息
GetPodByName(namespace, name string) (*v1.Pod, bool)
// GetNode 返回节点规格信息
GetNode() (*v1.Node, error)
// GetNodeConfig 返回节点配置信息
GetNodeConfig() cm.NodeConfig
// ListVolumesForPod 通过Pod Uid返回对应Pod使用的卷统计信息
ListVolumesForPod(podUID types.UID) (map[string]volume.Volume, bool)
// ListBlockVolumesForPod 通过Pod Uid返回对应Pod使用的块设备卷统计信息
ListBlockVolumesForPod(podUID types.UID) (map[string]volume.BlockVolume, bool)
// GetPods 返回节点上运行的所有Pod的信息
GetPods() []*v1.Pod

// RlimitStats 返回系统的rlimit统计
RlimitStats() (*statsapi.RlimitStats, error)

// GetPodCgroupRoot 返回管理所有Pod的根Cgroup节点路径
GetPodCgroupRoot() string
// GetPodByCgroupfs 通过cgroup路径名查找并返回Pod信息
GetPodByCgroupfs(cgroupfs string) (*v1.Pod, bool)
}

通过上面 Provider 的接口划分,我们大致了解了 Kubelet 在监控方面提供的能力,主要包括容器、Pod、节点、文件系统的统计信息或指标信息等。其中不少接口都用到了 cgroup。
cgroup(control group)是 Linux 内核中用于限制、记录和隔离一组进程的资源使用(CPU、内存、磁盘 I/O、网络等)的模块,它与 namespace 一起作为基石构成了容器基础技术的实现。绝大部分场景下,大家对他比较熟悉的点在于其发挥的资源划分和限制能力,例如 K8S 中 Pod 与 Container 的 CPU/内存资源 Limit。除了资源限制能力外,实际上 cgroup 每个子系统还具有对应的统计能力。以 CPU 子系统为例,它可以统计 CPU 在用户空间和内核空间的运行时长分配、设置 Limit 后的 CPU 限流次数和限流总时长等:
image.png
Cgroup 是以树形结构来划分和组织系统内的各个进程的,Kubelet 在运行时会创建一个 kubepods 的叶节点。而根据 Pod 的 QoS 等级划分,不同 Pod 被放在不同的 QoS 叶节点下。对应的,容器本身作为 Pod 管理的最小单元,其被划分在 Pod 的叶节点以下(_需开启 containerd 中 runc 的 SystemdCgroup 选项,否则 container 由 containerd cgroup 单独管理_)。
image.png
大致了解了 Cgroup 之后,细心的你就能想明白在 Provider 中不太容易理的 GetRawContainerInfo 和 GetRequestedContainersInfo 接口的用法,即参数中的 Container 指的不仅是被 Pod 管理的 Container,其实指的也是 Cgroup 的叶节点。它可以是 Container/Pod/QoS/Kubepods,甚至是节点本身(即”/“)。这也能解释为什么会有 subcontainers 这个参数,毕竟 Container 作为最小运行单元肯定是不存在子容器这个说法的。subcontainers 可以直接理解为是否包含子节点的信息,GetRawContainerInfo 接口实际上是提供了当前节点与子节点的统计信息。

Provider 初始化

provider 初始化包含在 Kubelet 的初始化流程中,通过 useLegacyCadvisorStats 开关,Kubelet 会进行会在 CadvisorStatsProvider 和 CRIStatsProvider 中选择一种作为实际实现。

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
func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, kubeDeps *Dependencies, ... ){

// 其他初始化流程
...

hostStatsProvider := stats.NewHostStatsProvider(kubecontainer.RealOS{}, func(podUID types.UID) string {
return getEtcHostsPath(klet.getPodDir(podUID))
})
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.DisableAcceleratorUsageMetrics) utilfeature.DefaultFeatureGate.Enabled(features.PodAndContainerStatsFromCRI))
}

}

对比两者的初始化方法可以分析得出其主要的数据来源都是 cadvisor 和 resourceAnalyzer,其他参数是主要作为一些辅助选项。resourceAnalyzer 主要提供了节点资源消耗的统计,除了文件系统相关的统计,其主要实现还是我们之前提到的 SummaryProvider。
那么基本可以得出一个结论,无论是哪种实现,容器相关的监控都主要来自于 cAdvisor。

总结

本篇文章中,我们主要了解了 Kubelet Server 在对外提供的监控 API 中统计类和指标类的划分,并了解到容器监控的部分主要由 Stats Provider 定义。同时,我们也知道了目前 Kubelet 中对于 Stats Provider 的实现主要有两种——CadvisorStatsProvider 和 CRIStatsProvider。接下来的一篇文章,我们会对这两种实现进行深入的对比分析,并搞清楚两种 Provider 的实现是如何使用 cAdvisor 接口的。