本系列主要基于v1.24.0版本的Kubelet部分源代码,进行Kubernetes中容器监控的底层原理介绍与代码分析。
前言
前面讲到,在Kubelet的代码中,关于数据来源StatsProvider的实现有两种:CriStatsProvider(下称”CRI实现”)和CadvisorStatsProvider(下称”cAdvisor实现”)。从命名上大致可以猜测,前者是为了降低对cAdvisor的直接依赖而将数据的交互限定在CRI API层面,后者则是对cAdvisor进行直接调用的,接下来本文就对这个想法进行验证分析。
从数据结构与接口抽象上看差异
查看StatsProvider的构造器方法参数,对比前文列出的handler中的Provider接口定义,可以看出Provider接口方法除了kubelet本身实现的部分,监控相关的接口在Stats包中的实现被拆分为了几大部分:
- Pod相关元信息查询(podManager)
- CRI或cAdvisor提供的统计信息(containerStatsProvider)
- 仅cadvisor提供的监控信息(cadvisor)展开containerStatsProvider的定义,可以看出其仅包括了Pod以及镜像文件系统的统计信息。
1
2
3
4
5
6
7
8
9
10
11
12
13func newStatsProvider(
cadvisor cadvisor.Interface,
podManager kubepod.Manager,
runtimeCache kubecontainer.RuntimeCache,
containerStatsProvider containerStatsProvider,
) *Provider {
return &Provider{
cadvisor: cadvisor,
podManager: podManager,
runtimeCache: runtimeCache,
containerStatsProvider: containerStatsProvider,
}
}在前一篇中,我们知道统计信息仅返回给metrics-server基础监控组件使用。那么看到这里我们就知道了,无论是那种实现,其差异点仅在于对Pod基础监控统计信息获取的方法上,这部分被抽象为containerStatsProvider接口。而细粒度的监控指标信息,还是需要由cAdvisor完整提供。1
2
3
4
5
6
7type containerStatsProvider interface {
ListPodStats() ([]statsapi.PodStats, error)
ListPodStatsAndUpdateCPUNanoCoreUsage() ([]statsapi.PodStats, error)
ListPodCPUAndMemoryStats() ([]statsapi.PodStats, error)
ImageFsStats() (*statsapi.FsStats, error)
ImageFsDevice() (string, error)
}
为了验证这一点,我们再看下用于提供给上层Proimtheus Collector组件提供具体指标信息的方法GetRequestedContainersInfo的具体实现:可以看到,Collector需要的指标信息最终由Kubelet对内置的cAdvisor API直接调用获取。1
2
3
4
5
6
7
8
9// collector对prometheus adapter的调用
func (a prometheusHostAdapter) GetRequestedContainersInfo(containerName string, options cadvisorv2.RequestOptions) (map[string]*cadvisorapi.ContainerInfo, error) {
return a.host.GetRequestedContainersInfo(containerName, options)
}
// kubelet实现了hostInterface,hostInterface中的方法调用转化为对cadvisor api的调用
func (kl *Kubelet) GetRequestedContainersInfo(containerName string, options cadvisorv2.RequestOptions) (map[string]*cadvisorapiv1.ContainerInfo, error) {
return kl.cadvisor.GetRequestedContainersInfo(containerName, options)
}
整体上看,从数据结构和接口抽象上的差异如下图所示:从源码实现上具体看差异
上面说到,两种StatsProvider实现的差异点仅在于Pod基础监控统计信息获取的方法上,这种差异被抽象出的containerStatsProvider接口本身方法不多。由于cAdvisor实现上从整体来看仅仅是将cAdvisor API返回的数据进行了组装,并不具备其他依赖,因此我们着重分析CRI实现。下面对该实现接口中的方法逐一进行比较。ListPodStats和ListPodStatsAndUpdateCPUNanoCoreUsage
该组方法完整需要返回Pod和Container的完整统计信息,区别主要在于是否更新CPU用量信息的缓存。
在kubelet中,kubelet启动时设置了PodAndContainerStatsFromCRI特性门控。当该特性门控开启时,CriStatsProvider会首先尝试从CRI完全获取Pod统计信息(严格模式),如果失败就降级到CRI部分获取模式。在CRI严格模式下,会首先从RuntimeService的API获取完整Pod和Container统计信息,然后过滤出PodSandbox和Container,把从API获取回来的CPU、内存、网络进程统计数据进行再组装。唯一例外的是,Pod统计信息中的存储部分的原始信息由cAdvisor返回的Rootfs统计得来。1
2
3
4
5
6
7
8
9
10
11
12
13
14containerMap, podSandboxMap, err := p.getPodAndContainerMaps()
if err != nil {
return nil, fmt.Errorf("failed to get pod or container map: %v", err)
}
if p.podAndContainerStatsFromCRI {
_, err := p.listPodStatsStrictlyFromCRI(updateCPUNanoCoreUsage, containerMap, podSandboxMap, &rootFsInfo)
if err != nil {
s, ok := status.FromError(err)
if !ok || s.Code() != codes.Unimplemented {
return nil, err
}
}
}
return p.listPodStatsPartiallyFromCRI(updateCPUNanoCoreUsage, containerMap, podSandboxMap, &rootFsInfo)
而在CRI部分获取模式下,会首先从RuntimeService的API获取Container统计信息,然后会从cAdvisor也获取一份完整信息。最终返回的Pod完整统计信息中,Container级别的信息由RuntimeService API先组装,如果cAdvisor中返回则使用cAdvisor返回的统计信息进行覆盖。类似的,Pod级别的信息则优先从cAdvisor返回中获取,如果获取失败,则CPU和内存使用Container级别数据累加得到,网络和进程统计则无法返回。而Pod存储部分的统计信息获取方式则与严格模式相同。两种模式的服务API调用如下所示: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
53func (p *criStatsProvider) listPodStatsPartiallyFromCRI(updateCPUNanoCoreUsage bool, containerMap map[string]*runtimeapi.Container, podSandboxMap map[string]*runtimeapi.PodSandbox, rootFsInfo *cadvisorapiv2.FsInfo) ([]statsapi.PodStats, error) {
// (省略)数据结构初始化
...
// 从RuntimeService API获取Container级别统计信息
resp, err := p.runtimeService.ListContainerStats(&runtimeapi.ContainerStatsFilter{})
if err != nil {
return nil, fmt.Errorf("failed to list all container stats: %v", err)
}
// 从cAdvisor API获取完整信息
allInfos, err := getCadvisorContainerInfo(p.cadvisor)
if err != nil {
return nil, fmt.Errorf("failed to fetch cadvisor stats: %v", err)
}
caInfos, allInfos := getCRICadvisorStats(allInfos)
// windows系统实现,忽略
containerNetworkStats, err := p.listContainerNetworkStats()
if err != nil {
return nil, fmt.Errorf("failed to list container network stats: %v", err)
}
for _, stats := range resp {
// (省略)contianer和sandbox过滤逻辑
...
// Pod Stats初始化
ps, found := sandboxIDToPodStats[podSandboxID]
if !found {
ps = buildPodStats(podSandbox)
sandboxIDToPodStats[podSandboxID] = ps
}
cs := p.makeContainerStats(stats, container, rootFsInfo, fsIDtoInfo, podSandbox.GetMetadata(), updateCPUNanoCoreUsage)
p.addPodNetworkStats(ps, podSandboxID, caInfos, cs, containerNetworkStats[podSandboxID])
p.addPodCPUMemoryStats(ps, types.UID(podSandbox.Metadata.Uid), allInfos, cs)
p.addProcessStats(ps, types.UID(podSandbox.Metadata.Uid), allInfos, cs)
// 尝试从cAdvisor数据中获取Container统计信息,有则直接覆盖
caStats, caFound := caInfos[containerID]
if !caFound {
klog.V(5).InfoS("Unable to find cadvisor stats for container", "containerID", containerID)
} else {
p.addCadvisorContainerStats(cs, &caStats)
}
ps.Containers = append(ps.Containers, *cs)
}
// (省略)清理缓存并更新Storage统计
...
return result, nil
}ListPodCPUAndMemoryStats
与上面完整Pod统计信息获取类似,该方法也分为两个模式。由于去除了网络、存储、进程等统计信息的返回,因此CRI严格模式下完全去除了对cAdvisor的依赖,只需要把从RuntimeService的API获取到的信息重新组装,而部分获取模式下的Container与Pod统计数据组装逻辑同上一个方法一致。整体上看,该方法是上一个方法的精简化实现。ImageFsStats和ImageFsDevice
ImageFS相关的接口返回了镜像存储的相关统计信息,类似的,cAdvisor实现中,其统计信息的返回完全由cAdvisor负责。而CRI实现中, 其依然没有完全摆脱对cAdvisor的依赖,部分数据使用CRI-API定义的ImageService接口中获取。其他数据需要使用ImageService返回的FS Id找到关联的挂载点,从而通过cAdvisor查询到ImageFS对应挂载点的分区统计信息。类似的,ImageFS设备信息也需要通过cAdvisor获取,ImageService只提供FS Id。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21func (p *criStatsProvider) ImageFsStats() (*statsapi.FsStats, error) {
resp, err := p.imageService.ImageFsInfo()
if err != nil {
return nil, err
}
// CRI有可能会返回多个ImageFS信息,此时仅使用第一个(未来需要扩充为多个实现)
if len(resp) == 0 {
return nil, fmt.Errorf("imageFs information is unavailable")
}
fs := resp[0]
// (省略)初始化和UsedBytes/InodesUsed数据组装
...
// 使用ImageFS的id获取挂载点信息,然后从cAdvisor查询该挂载点的统计信息
imageFsInfo := p.getFsInfo(fs.GetFsId())
if imageFsInfo != nil {
// 其他数据组装
...
}
return s, nil
}1
2
3
4
5
6
7
8
9
10
11
12
13func (p *criStatsProvider) ImageFsDevice() (string, error) {
resp, err := p.imageService.ImageFsInfo()
if err != nil {
return "", err
}
for _, fs := range resp {
fsInfo := p.getFsInfo(fs.GetFsId())
if fsInfo != nil {
return fsInfo.Device, nil
}
}
return "", errors.New("imagefs device is not found")
}总结
总体上来看,CRI实现在逐步摆脱对cAdvisor的依赖,但从目前来看,部分数据还是需要通过cAdvisor获取。
但是从结果和演进方向来看,CRI API监控方面的定义正在逐渐扩充以满足CRI实现下的基础监控需要,Kubelet也开放了严格模式这种仅尽力(try-best)返回监控数据的特性开关用于验证是否满足基础监控的需求。究其原因,是由于由于底层多种容器运行时的差异化实现,cAdvisor已经逐渐不能满足需求(例如kata等安全容器架构下容器级别监控无法被cAdvisor通过文件获取到、具备多个独立于各SandboxVM中而非宿主机的ImageFS)。
个人推测,社区未来最终会将cAdvisor从kubelet中剥离出来,作为一种细粒度的监控组件独立部署运行在节点上,用于给Prometheus组件提供指标信息。下一篇文章,我们将会开始进入cAdisor的源码部分,剖析cAdvisor的容器监控运行原理和各类别数据采集逻辑。