Kubernetes容器监控原理和源码解析(二)——StatsProvider的差异化实现

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

前言

前面讲到,在Kubelet的代码中,关于数据来源StatsProvider的实现有两种:CriStatsProvider(下称”CRI实现”)和CadvisorStatsProvider(下称”cAdvisor实现”)。从命名上大致可以猜测,前者是为了降低对cAdvisor的直接依赖而将数据的交互限定在CRI API层面,后者则是对cAdvisor进行直接调用的,接下来本文就对这个想法进行验证分析。

从数据结构与接口抽象上看差异

查看StatsProvider的构造器方法参数,对比前文列出的handler中的Provider接口定义,可以看出Provider接口方法除了kubelet本身实现的部分,监控相关的接口在Stats包中的实现被拆分为了几大部分:

  1. Pod相关元信息查询(podManager)
  2. CRI或cAdvisor提供的统计信息(containerStatsProvider)
  3. 仅cadvisor提供的监控信息(cadvisor)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func newStatsProvider(
    cadvisor cadvisor.Interface,
    podManager kubepod.Manager,
    runtimeCache kubecontainer.RuntimeCache,
    containerStatsProvider containerStatsProvider,
    ) *Provider {
    return &Provider{
    cadvisor: cadvisor,
    podManager: podManager,
    runtimeCache: runtimeCache,
    containerStatsProvider: containerStatsProvider,
    }
    }
    展开containerStatsProvider的定义,可以看出其仅包括了Pod以及镜像文件系统的统计信息。
    1
    2
    3
    4
    5
    6
    7
    type containerStatsProvider interface {
    ListPodStats() ([]statsapi.PodStats, error)
    ListPodStatsAndUpdateCPUNanoCoreUsage() ([]statsapi.PodStats, error)
    ListPodCPUAndMemoryStats() ([]statsapi.PodStats, error)
    ImageFsStats() (*statsapi.FsStats, error)
    ImageFsDevice() (string, error)
    }
    在前一篇中,我们知道统计信息仅返回给metrics-server基础监控组件使用。那么看到这里我们就知道了,无论是那种实现,其差异点仅在于对Pod基础监控统计信息获取的方法上,这部分被抽象为containerStatsProvider接口。而细粒度的监控指标信息,还是需要由cAdvisor完整提供。
    为了验证这一点,我们再看下用于提供给上层Proimtheus Collector组件提供具体指标信息的方法GetRequestedContainersInfo的具体实现:
    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)
    }
    可以看到,Collector需要的指标信息最终由Kubelet对内置的cAdvisor API直接调用获取。
    整体上看,从数据结构和接口抽象上的差异如下图所示:
    kubelet-API.png

    从源码实现上具体看差异

    上面说到,两种StatsProvider实现的差异点仅在于Pod基础监控统计信息获取的方法这种差异被抽象出的containerStatsProvider接口本身方法不多。由于cAdvisor实现上从整体来看仅仅是将cAdvisor API返回的数据进行了组装,并不具备其他依赖,因此我们着重分析CRI实现。下面对该实现接口中的方法逐一进行比较。

    ListPodStats和ListPodStatsAndUpdateCPUNanoCoreUsage

    该组方法完整需要返回Pod和Container的完整统计信息,区别主要在于是否更新CPU用量信息的缓存。
    在kubelet中,kubelet启动时设置了PodAndContainerStatsFromCRI特性门控。当该特性门控开启时,CriStatsProvider会首先尝试从CRI完全获取Pod统计信息(严格模式),如果失败就降级到CRI部分获取模式。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    containerMap, 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获取完整Pod和Container统计信息,然后过滤出PodSandbox和Container,把从API获取回来的CPU、内存、网络进程统计数据进行再组装。唯一例外的是,Pod统计信息中的存储部分的原始信息由cAdvisor返回的Rootfs统计得来。
    而在CRI部分获取模式下,会首先从RuntimeService的API获取Container统计信息,然后会从cAdvisor也获取一份完整信息。最终返回的Pod完整统计信息中,Container级别的信息由RuntimeService API先组装,如果cAdvisor中返回则使用cAdvisor返回的统计信息进行覆盖。类似的,Pod级别的信息则优先从cAdvisor返回中获取,如果获取失败,则CPU和内存使用Container级别数据累加得到,网络和进程统计则无法返回。而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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    func (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
    }
    两种模式的服务API调用如下所示:
    kubelet-Cri Mode.png

    ListPodCPUAndMemoryStats

    与上面完整Pod统计信息获取类似,该方法也分为两个模式。由于去除了网络、存储、进程等统计信息的返回,因此CRI严格模式下完全去除了对cAdvisor的依赖,只需要把从RuntimeService的API获取到的信息重新组装,而部分获取模式下的Container与Pod统计数据组装逻辑同上一个方法一致。整体上看,该方法是上一个方法的精简化实现。

    ImageFsStats和ImageFsDevice

    ImageFS相关的接口返回了镜像存储的相关统计信息,类似的,cAdvisor实现中,其统计信息的返回完全由cAdvisor负责。而CRI实现中, 其依然没有完全摆脱对cAdvisor的依赖,部分数据使用CRI-API定义的ImageService接口中获取。其他数据需要使用ImageService返回的FS Id找到关联的挂载点,从而通过cAdvisor查询到ImageFS对应挂载点的分区统计信息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    func (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
    }
    类似的,ImageFS设备信息也需要通过cAdvisor获取,ImageService只提供FS Id。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func (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的容器监控运行原理和各类别数据采集逻辑。