本系列主要基于v1.24.0版本的Kubelet部分源代码,进行Kubernetes中容器监控的底层原理介绍与代码分析。
前言 本文主要从Kubelet Server提供的监控API出发,介绍其分类和作用,然后对这些接口的数据来源进行简单的分析。
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(containerName string , options v2.RequestOptions) (map [string ]*info.ContainerInfo, error) GetVersionInfo() (*info.VersionInfo, error) 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() ([]statsapi.PodStats, error) ListPodCPUAndMemoryStats() ([]statsapi.PodStats, error) ListPodStatsAndUpdateCPUNanoCoreUsage() ([]statsapi.PodStats, error) ImageFsStats() (*statsapi.FsStats, error) GetCgroupStats(cgroupName string , updateStats bool ) (*statsapi.ContainerStats, *statsapi.NetworkStats, error) GetCgroupCPUAndMemoryStats(cgroupName string , updateStats bool ) (*statsapi.ContainerStats, error) RootFsStats() (*statsapi.FsStats, error) GetContainerInfo(podFullName string , uid types.UID, containerName string , req *cadvisorapi.ContainerInfoRequest) (*cadvisorapi.ContainerInfo, error) GetRawContainerInfo(containerName string , req *cadvisorapi.ContainerInfoRequest, subcontainers bool ) (map [string ]*cadvisorapi.ContainerInfo, error) GetRequestedContainersInfo(containerName string , options cadvisorv2.RequestOptions) (map [string ]*cadvisorapi.ContainerInfo, error) GetPodByName(namespace, name string ) (*v1.Pod, bool ) GetNode() (*v1.Node, error) GetNodeConfig() cm.NodeConfig ListVolumesForPod(podUID types.UID) (map [string ]volume.Volume, bool ) ListBlockVolumesForPod(podUID types.UID) (map [string ]volume.BlockVolume, bool ) GetPods() []*v1.Pod RlimitStats() (*statsapi.RlimitStats, error) GetPodCgroupRoot() string 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中选择一种作为实际实现。
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接口的。