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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: ## 前言本系列主要基于v1.24.0版本的Kubelet部分源代码,进行Kubernetes中容器监控的底层原理介绍与代码分析。## Kubelet中的监控API在Kubelet Server提供的监控API中,大致可以分为两类:stats(统计数据)和metrics(指标数据)。从命名和实际作用来看,前者提供了粗粒度的基础监控能力,目前用于各种内置组件;而后者用于持久化地进行细粒度的容器

前言

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

Kubelet中的监控API

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

统计类API

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

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的封装。

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的实现原理和其他非容器指标相关的实现,而是来看看容器指标数据是如何获取到并转化返回的。

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/容器的指标收集器,另一部分是节点指标收集器。

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。

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。

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中实现。

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部分是如何定义的:

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限流次数和限流总时长等:

Cgroup是以树形结构来划分和组织系统内的各个进程的,Kubelet在运行时会创建一个kubepods的叶节点。而根据Pod的QoS等级划分,不同Pod被放在不同的QoS叶节点下。对应的,容器本身作为Pod管理的最小单元,其被划分在Pod的叶节点以下(_需开启containerd中runc的SystemdCgroup选项,否则container由containerd cgroup单独管理_)。

大致了解了Cgroup之后,细心的你就能想明白在Provider中不太容易理的GetRawContainerInfo和GetRequestedContainersInfo接口的用法,即参数中的Container指的不仅是被Pod管理的Container,其实指的也是Cgroup的叶节点。它可以是Container/Pod/QoS/Kubepods,甚至是节点本身(即"/")。这也能解释为什么会有subcontainers这个参数,毕竟Container作为最小运行单元肯定是不存在子容器这个说法的。subcontainers可以直接理解为是否包含子节点的信息,GetRawContainerInfo接口实际上是提供了当前节点与子节点的统计信息。

Provider初始化

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

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接口提供容器监控数据。

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
2月前
|
移动开发 前端开发 HTML5
Twaver-HTML5基础学习(20)数据容器(3)_数据的批量加载(节省性能方法)
本文介绍了Twaver HTML5中数据的批量加载方法,通过使用`box.startBatch()`可以在大量数据加载时提高性能。文章通过示例代码展示了如何在React组件中使用批量加载功能,以减少界面重绘次数并提升效率。
55 1
Twaver-HTML5基础学习(20)数据容器(3)_数据的批量加载(节省性能方法)
|
2月前
|
XML 存储 JSON
Twaver-HTML5基础学习(19)数据容器(2)_数据序列化_XML、Json
本文介绍了Twaver HTML5中的数据序列化,包括XML和JSON格式的序列化与反序列化方法。文章通过示例代码展示了如何将DataBox中的数据序列化为XML和JSON字符串,以及如何从这些字符串中反序列化数据,重建DataBox中的对象。此外,还提到了用户自定义属性的序列化注册方法。
45 1
|
1月前
|
Kubernetes 安全 Cloud Native
云上攻防-云原生篇&K8s安全-Kubelet未授权访问、API Server未授权访问
本文介绍了云原生环境下Kubernetes集群的安全问题及攻击方法。首先概述了云环境下的新型攻击路径,如通过虚拟机攻击云管理平台、容器逃逸控制宿主机等。接着详细解释了Kubernetes集群架构,并列举了常见组件的默认端口及其安全隐患。文章通过具体案例演示了API Server 8080和6443端口未授权访问的攻击过程,以及Kubelet 10250端口未授权访问的利用方法,展示了如何通过这些漏洞实现权限提升和横向渗透。
145 0
云上攻防-云原生篇&K8s安全-Kubelet未授权访问、API Server未授权访问
|
2月前
|
XML 移动开发 JSON
Twaver-HTML5基础学习(18)数据容器(1)_增删查改、遍历数据容器、包含网元判断
本文介绍了Twaver HTML5中的数据容器(DataBox),包括如何进行增删查改操作、遍历数据容器以及判断网元是否存在于数据容器中。DataBox用于管理所有的网元对象,如ElementBox、LayerBox、AlarmBox等,并通过示例代码展示了其常用方法的使用。
46 1
Twaver-HTML5基础学习(18)数据容器(1)_增删查改、遍历数据容器、包含网元判断
|
1月前
|
存储 监控 Shell
docker的底层原理二:容器运行时环境
本文深入探讨了Docker容器运行时环境的关键技术,包括命名空间、控制组、联合文件系统、容器运行时以及分离的进程树,这些技术共同确保了容器的隔离性、资源控制和可移植性。
38 5
|
2月前
|
存储 索引 Python
python中的数据容器
python中的数据容器
|
2月前
|
Kubernetes Linux 虚拟化
一文详解容器技术简介和基本原理
本文全面阐述了容器技术的发展历程、关键技术、架构和当前的行业生态,特别是容器技术在云环境中的应用和演进。
|
3月前
|
安全 网络安全 数据安全/隐私保护
云原生技术探索:容器化与微服务架构的实践之路网络安全与信息安全:保护数据的关键策略
【8月更文挑战第28天】本文将深入探讨云原生技术的核心概念,包括容器化和微服务架构。我们将通过实际案例和代码示例,展示如何在云平台上实现高效的应用部署和管理。文章不仅提供理论知识,还包含实操指南,帮助开发者理解并应用这些前沿技术。 【8月更文挑战第28天】在数字化时代,网络安全和信息安全是保护个人和企业数据的前线防御。本文将探讨网络安全漏洞的成因、加密技术的应用以及提升安全意识的重要性。文章旨在通过分析网络安全的薄弱环节,介绍如何利用加密技术和提高用户警觉性来构建更为坚固的数据保护屏障。
|
3月前
|
存储 Kubernetes 安全
在K8S中,你用的flannel是哪个工作模式及fannel的底层原理如何实现数据报文转发的?
在K8S中,你用的flannel是哪个工作模式及fannel的底层原理如何实现数据报文转发的?
|
2月前
|
运维 Kubernetes 监控
Loki+Promtail+Grafana监控K8s日志
综上,Loki+Promtail+Grafana 监控组合对于在 K8s 环境中优化日志管理至关重要,它不仅提供了强大且易于扩展的日志收集与汇总工具,还有可视化这些日志的能力。通过有效地使用这套工具,可以显著地提高对应用的运维监控能力和故障诊断效率。
288 0

相关产品

  • 容器服务Kubernetes版