容器编排之争在 Kubernetes 一统天下局面形成后,K8S 成为了云原生时代的新一代操作系统。K8S 让一切变得简单了,但自身逐渐变得越来越复杂。【K8S Internals 系列专栏】围绕 K8S 生态的诸多方面,将由博云容器云研发团队定期分享有关调度、安全、网络、性能、存储、应用场景等热点话题。希望大家在享受 K8S 带来的高效便利的同时,又可以如庖丁解牛般领略其内核运行机制的魅力。
众所周知 Kubernetes 暴露了非常多的监控指标,而开源社区也提供了各种各样的 exporter 来进行指标采集,Prometheus 负责指标收集及存储,Grafana 负责指标展示。本文则是由 Grafana 不展示一个存储指标而引起,我们将在本篇文章中展示问题排查思路及手段,进而勾勒出 Kubelet 完整的指标管理流程。
一、问题
最近有小伙伴反映,使用存储驱动(比如:NFS-CSI、Ceph-CSI)创建的存储卷,在 Grafana 看不到其容量指标,并且 Prometheus 中也收集不到卷容量、使用量等指标。
据笔者所知 Grafana 内置了kubernetes/Persistent Volumes指标模板,并且其指标数据来源于 kubelet;在此之前笔者并未对 kubelet metrics 接口进行深入研究过,借此机会探究一下 kubelet 对于存储卷指标收集的实现。
二、分析 & 确认
1. 基本概念Prometheus 主动调用应用服务的 metrics 接口来获取应用指标,在 Kubernetes 中通过部署 CRD 资源 ServiceMonitor 来使 Prometheus operator 发现需要采集指标的服务,我们将创建一个检查清单来确认在这个链条中哪一个环节出了问题。kubelet 启动后默认监听 10250 端口,接收并执行 Master 发来的指令,管理 Pod 及 Pod 中的容器。
2. 检查清单我们首先要看一下出现问题的环境,以笔者对 Kubernetes 以及 Prometheus 的熟悉,很快梳理出一个检查清单,在该清单中笔者直接给出了检查结果,下一章节展示排查过程。
确认项 | 结果 |
---|---|
Kubernetes Version | v1.21.4 |
使用存储驱动(NFS-CSI、Ceph-CSI)创建了PVC并且已经绑定成功 | √ |
打开 Grafana 查看 kubernetes/Persistent Volumes 是否有指标 | × |
在 Grafana 查看其他的 kubernetes 内置指标 kubernetes/Compute Resources/Cluster,指标正常 | √ |
在 Grafana 查看 kubernetes/Persistent Volumes 指标计算公式,获取指标公式 | √ |
在 Prometheus 查看是否已经收集到对应的指标,发现未收集到 | × |
在 Prometheus 查看是否收集到 kubelet 其他指标,已经收集到 | √ |
在 K8s 集群中查看 kubelet 相关的 ServiceMonitor 资源,正常配置 | √ |
直接调用 https://kubelet:10250/metrics,接口正常但未能获取到 volume 相关指标 | × |
3. 排查过程
- 在 Grafana 处获取kubernetes/Persistent Volumes指标计算公式,通过名字就可以看出是 kubelet 暴露的指标
(sum without(instance, node) (kubelet_volume_stats_capacity_bytes{cluster="", job="kubelet", namespace="", persistentvolumeclaim=""}) sum without(instance, node) (kubelet_volume_stats_available_bytes{cluster="", job="kubelet", namespace="",persistentvolumeclaim=""}))
- 查看 ServiceMonitor 资源及相关资源
$ kubectl get serviceMonitor -A | grep kubelet monitoring-system kubelet 210d $ kubectl get svc -A -l k8s-app=kubelet NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kube-system kubelet ClusterIP None <none> 10250/TCP,10255/TCP,4194/TCP 246d $ kubectl get ep kubelet -n kube-system NAME ENDPOINTS AGE kubelet 10.20.9.66:10250,10.20.9.51:10250,10.20.9.67:10250 + 6 more... 246d
- 获取 Kubelet 指标
# 在第四章我们将解决如何访问需要认证的kubelet接口 $ curl -k -cert xxx https://kubelet:10250/metrics
4. 结论
通过上述检查清单我们基本可以确定问题范围:Kubelet metrics接口并未暴露volume卷相关的指标数据!下一步自然是查看一下为何 Kubelet metrics 接口并未暴露出 volume 相关指标数据。
三、Kubelet 指标接口
通常情况下有三种方式访问 Kubelet 认证接口,这里要注意访问响应 unauthorized 或者没有任何响应数据都表示访问失败。$ curl https://192.168.56.121:10250/metrics unauthorized
- 第一种:使用管理员证书
# 创建kubernetes需要创建kubectl使用的admin权限证书,使用此证书可以直接访问该接口 $ curl -s --cacert /etc/kubernetes/pki/ca.pem --cert /etc/kubernetes/pki/admin.pem --key /etc/kubernetes/pki/admin-key.pem https://192.168.56.121:10250/metrics|head # 如果你使用kubeadm部署的集群,可使用如下命令访问 $ curl -k --cacert /etc/kubernetes/pki/ca.crt --cert /etc/kubernetes/pki/API" target="_blank">apiserver-kubelet-client.crt --key /etc/kubernetes/pki/API" target="_blank">apiserver-kubelet-client.key https://192.168.56.121:10250/metrics
- 第二种:使用 token 方式
# 使用token方式,先查看一下用于kubelet所有权限的角色 $ kubectl describe clusterrole system:kubelet-API" target="_blank">api-admin Name: system:kubelet-api-admin Labels: kubernetes.io/bootstrapping=rbac-defaults Annotations: rbac.authorization.kubernetes.io/autoupdate: true PolicyRule: Resources Non-Resource URLs Resource Names Verbs --------- ----------------- -------------- ----- nodes/log [] [] [*] nodes/metrics [] [] [*] nodes/proxy [] [] [*] nodes/spec [] [] [*] nodes/stats [] [] [*] nodes [] [] [get list watch proxy] $ kubectl create sa kube-api-test $ kubectl create clusterrolebinding kubelet-api-test --clusterrole=system:kubelet-api-admin --serviceaccount=default:kubelet-api-test $ SECRET=$(kubectl get secrets | grep kubelet-api-test | awk '{print $1}') $ TOKEN=$(kubectl describe secret ${SECRET} | grep -E '^token' | awk '{print $2}') $ echo ${TOKEN} $ curl -s --cacert /etc/kubernetes/cert/ca.pem -H "Authorization: Bearer ${TOKEN}" https://192.168.56.121:10250/metrics|head # 如果是使用kubeadm部署的集群可使用如下命令访问 $ curl -k --cacert /etc/kubernetes/pki/ca.crt -H "Authorization: Bearer ${TOKEN}" https://192.168.56.121:10250/metrics | head
- 第三种:直接关闭 Kubelet 的证书认证
# 修改 /var/lib/kubelet/config.yaml,直接关闭kubelet证书认证,然后重启kubelet # 这样就能愉快的用浏览器访问https://192.168.56.121:10250/metrics接口了 ``` authentication: anonymous: enabled: true webhook: cacheTTL: 0s enabled: false x509: clientCAFile: /etc/kubernetes/pki/ca.crt authorization: mode: AlwaysAllow webhook: cacheAuthorizedTTL: 0s cacheUnauthorizedTTL: 0s ````
- 顺便一提,10250端口能访问很多资源,比如:
/pods、/runningpods /metrics、/metrics/cadvisor、/metrics/probes /spec /stats、/stats/container /logs /run/、/exec/, /attach/, /portForward/, /containerLogs/ 更多详情:https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/server/server.go#L434:3
四、Kubelet Volume 指标源码
经过多番查找 Kubernetes 在1.8版本增加了暴露 volume 指标的接口,详细链接如下:
- https://github.com/google/cadvisor/issues/1702#issuecomment-381189602
- https://github.com/kubernetes/kubernetes/commit/dac2068bbd7b3365a879cbd0a5131a0955832264?branch=dac2068bbd7b3365a879cbd0a5131a0955832264&diff=split
同样,在这个文件中我们还可以看到 Kubelet 提供了哪些有关 volume 的指标。// k8s.io/kubernetes/pkg/kubelet/metrics/collectors/volume_stats.go:91 // CollectWithStability implements the metrics.StableCollector interface. func (collector *volumeStatsCollector) CollectWithStability(ch chan<- metrics.Metric) { // 关键就这么一句,指标均来源于当前节点上的POD // 其实这也证明了kubelet只能获取当前在它节点上挂载中的volume podStats, err := collector.statsProvider.ListPodStats() if err != nil { return } ... allPVCs := sets.String{} for _, podStat := range podStats { if podStat.VolumeStats == nil { continue } for _, volumeStat := range podStat.VolumeStats { pvcRef := volumeStat.PVCRef if pvcRef == nil { // 之所以metrics指标接口未有kubelet_volume_stats-*是因为代码执行了这一句直接跳过了; // 怎么判断出代码走了这条路径,下边分解 // ignore if no PVC reference continue } pvcUniqStr := pvcRef.Namespace + "/" + pvcRef.Name if allPVCs.Has(pvcUniqStr) { // ignore if already collected continue } addGauge(volumeStatsCapacityBytesDesc, pvcRef, float64(*volumeStat.CapacityBytes)) addGauge(volumeStatsAvailableBytesDesc, pvcRef, float64(*volumeStat.AvailableBytes)) addGauge(volumeStatsUsedBytesDesc, pvcRef, float64(*volumeStat.UsedBytes)) addGauge(volumeStatsInodesDesc, pvcRef, float64(*volumeStat.Inodes)) addGauge(volumeStatsInodesFreeDesc, pvcRef, float64(*volumeStat.InodesFree)) addGauge(volumeStatsInodesUsedDesc, pvcRef, float64(*volumeStat.InodesUsed)) allPVCs.Insert(pvcUniqStr) } } }
关于如何进行指标注册等部分代码,简单展示一下,如果写过为 prometheus 暴露指标的应该很容易明白。VolumeStatsCapacityBytesKey = "volume_stats_capacity_bytes" VolumeStatsAvailableBytesKey = "volume_stats_available_bytes" VolumeStatsUsedBytesKey = "volume_stats_used_bytes" VolumeStatsInodesKey = "volume_stats_inodes" VolumeStatsInodesFreeKey = "volume_stats_inodes_free" VolumeStatsInodesUsedKey = "volume_stats_inodes_used"
经过查看 Kubelet 关于 volume metrics 部分源码,我们开头提出的问题已经有了大概的答案,Kubelet 的 collector.statsProvider.ListPodStats()方法很显然只会列出当前节点上的容器指标,而 PVC 创建之后并未挂载到容器中,所以在 Kubelet 指标中是无法观察到存储卷指标的。只通过源码分析还不足够我们还需要拿出切实的证据,下面我们将对 Kubelet 进行本地调试来验证一下猜想。// initializeModules will initialize internal modules that do not require the container runtime to be up. // Note that the modules here must not depend on modules that are not initialized here. // k8s.io/kubernetes/pkg/kubelet/kubelet.go:1323 // kubelet 初始化,里边包含了Prometheus metrics指标注册 func (kl *Kubelet) initializeModules() error { // Prometheus metrics. metrics.Register( // 通过传入参数的方式加入volume metrics collectors.NewVolumeStatsCollector(kl), collectors.NewLogMetricsCollector(kl.StatsProvider.ListPodStats), ) metrics.SetNodeName(kl.nodeName) servermetrics.Register() .... // Start resource analyzer kl.resourceAnalyzer.Start() return nil } // k8s.io/kubernetes/pkg/kubelet/metrics/metrics.go:439 // Register registers all metrics. func Register(collectors ...metrics.StableCollector) { // Register the metrics. registerMetrics.Do(func() { legacyregistry.MustRegister(NodeName) legacyregistry.MustRegister(PodWorkerDuration) legacyregistry.MustRegister(PodStartDuration) ... // 实际上在这里注册的volume metrics,这是prometheus metrics推荐的注册方式,注册一个实现指定接口的结构体 for _, collector := range collectors { legacyregistry.CustomMustRegister(collector) } }) }
五、Kubelet 本地调试
一个小插曲:拉取 Kubernetes 代码后,里边经常有很多红色的方法,导致无法顺利跳转经过反反复复各种实验后,最终删除了 Kubernetes 的 vendor 目录,配置 go.mod 模式后一切就OK了。
如果要 Debug Kubelet 首先要启动 Kubelet 并把它加入一个 Kubernetes 集群。第一步:创建一个K8s集群 $ kubectl get nodes -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP 192.168.56.120 Ready control-plane,master 52m v1.20.4 192.168.56.120 192.168.56.121 Ready <none> 49m v1.20.4 192.168.56.121 第二步:本地开发环境为ubuntu16,需要做一些配置 ① 关闭swapoff -a ② 开启ipv4转发 ③ 配置docker cgroupManager为systemd ④ 关闭防火墙 等等 ⑤ 设置hostname hostnamectl 192.168.56.101,之所以如此设置是为了让各个节点可以通过hostname直接访问 ⑥ 安装kubeadm yum install kubeadm kubelet 第三步:使用kubeadm join命令将开发环境加入集群 kubeadm join 192.168.56.120:6443 --token 4smpu7.uji2fimas85b5fwy --discovery-token-ca-cert-hash sha256:97de144cb013ba79ce7fc059f418e92a900944ebbba2b51c39c6ecbb08406bf2 $ kubectl get nodes NAME STATUS ROLES AGE VERSION 192.168.56.101 Ready <none> 11s v1.21.3 192.168.56.120 Ready control-plane,master 63m v1.20.4 192.168.56.121 Ready <none> 61m v1.20.4 备注①:如果你是重复join,需要先删除/etc/kubernete/* /var/lib/kubelet/* 下的所有文件 备注②:节点上docker的配置的cgroup使用systemd管理,在/var/lib/kubelet/config.yaml中要加入cgroupDriver: systemd配置 备注③:这一步的join操作主要是为了生成节点kubelet证书,笔者曾实验过将其他节点的证书挪到开发环境,发现证书和节点hostname绑定,故使用这种方法生成节点证书 备注④:如果能自定义生成节点证书可以不必这样 第四步:停止kubelet,启动我们Goland中的kubelet $ systemctl stop kubelet && systemctl disable kubelet Goland的中需要配置一下kubelet main目录,以及启动参数,可以参考下图 /data/gopath/src/k8s.io/kubernetes/cmd/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --network-plugin=cni --pod-infra-container-image=registry.cn-hangzhou.aliyuncs.com/antmoveh/pause:3.4.1 启动后,查看集群节点 $ kubectl get nodes NAME STATUS ROLES AGE VERSION 192.168.56.101 Ready <none> 12m v0.0.0-master+$Format:%H$ 192.168.56.120 Ready control-plane,master 75m v1.20.4 192.168.56.121 Ready <none> 73m v1.20.4 至此我们就能愉快的debug kubelet了 备注①:我给Goland分配了16G可用内存,编译起来尚好。配置差些编译时间略长
既然我们已经可以 Debug Kubelet 了,我们来解决上一个问题,如何确定方法 metrics 执行的if pvcRef == nil {continue}。访问节点所在的 Kubeletcurl -k --xxxx https://127.0.0.1:10250/metrics。一图胜千言,直观感受一下吧,如果细心观察所有的 pod 的 volumeStats PVCRef 其实都是 nil。
那问题就转到 pod 的 pvcRef 是如何填充的?
六、Kubelet 深入 Volume 指标源码
POD 指标的获取,跟踪collector.statsProvider.ListPodStats()一路点进去就点到 cadvisor 获取指标代码。
还记得我们要查找为啥 pvcRef 对象是 nil,就是因为这个缓存里没有!可以脑补一下,更新这个缓存必然是需要一个 goroutine,下边实际是要看这个定时 goroutine 是如何实现的,多长时间更新一次、从哪里获取这些指标。// k8s.io/kubernetes/pkg/kubeletstats/cadvisor_stats_provider.go:78 // ListPodStats returns the stats of all the pod-managed containers. func (p *cadvisorStatsProvider) ListPodStats() ([]statsapi.PodStats, error) { ... // 这里有很多指标收集的代码,把它们省略,我们只关注收集volumeStats的部分 // Add each PodStats to the result. result := make([]statsapi.PodStats, 0, len(podToStats)) for _, podStats := range podToStats { // Lookup the volume stats for each pod. podUID := types.UID(podStats.PodRef.UID) var ephemeralStats []statsapi.VolumeStats // 在这里获取的具体某个Pod的volumeStats // 这里的代码不用细究,只是路过下边的才会涉及到如何获取volume stats if vstats, found := p.resourceAnalyzer.GetPodVolumeStats(podUID); found { ephemeralStats = make([]statsapi.VolumeStats, len(vstats.EphemeralVolumes)) copy(ephemeralStats, vstats.EphemeralVolumes) podStats.VolumeStats = append(append([]statsapi.VolumeStats{}, vstats.EphemeralVolumes...), vstats.PersistentVolumes...) } return result, nil } // 继续点进去,就来到了 // k8s.io/kubernetes/pkg/kubelet/server/stats/fs_resource_analyzer.go:99 // 这段代码看起来异常简单,其实就是获取了一些缓存,那我们就忒观察一下这个缓存如何更新的了 // GetPodVolumeStats returns the PodVolumeStats for a given pod. Results are looked up from a cache that // is eagerly populated in the background, and never calculated on the fly. func (s *fsResourceAnalyzer) GetPodVolumeStats(uid types.UID) (PodVolumeStats, bool) { cache := s.cachedVolumeStats.Load().(statCache) statCalc, found := cache[uid] if !found { // TODO: Differentiate between stats being empty // See issue #20679 return PodVolumeStats{}, false } return statCalc.GetLatest() }
// 简单的start函数 // k8s.io/kubernetes/pkg/kubelet/server/stats/fs_resource_analyzer.go:61 // Start eager background caching of volume stats. func (s *fsResourceAnalyzer) Start() { s.startOnce.Do(func() { // 这里要注意一下,这个时间是通过/var/lib/kubelet/config.yaml中volumeStatsAggPeriod: 30s配置的 默认为为1m0s,注意将这个值设置小于零更新指标协程还是会正常启动 if s.calcPeriod <= 0 { klog.InfoS("Volume stats collection disabled") return } klog.InfoS("Starting FS ResourceAnalyzer") // 只有这个方法比较关键 go wait.Forever(func() { s.updateCachedPodVolumeStats() }, s.calcPeriod) }) } // 这两个方法靠着 // updateCachedPodVolumeStats calculates and caches the PodVolumeStats for every Pod known to the kubelet. func (s *fsResourceAnalyzer) updateCachedPodVolumeStats() { oldCache := s.cachedVolumeStats.Load().(statCache) newCache := make(statCache) // Copy existing entries to new map, creating/starting new entries for pods missing from the cache for _, pod := range s.statsProvider.GetPods() { if value, found := oldCache[pod.GetUID()]; !found { // 这个方法就是获取新的指标了,点击startOnce一路就会点到核心方法 newCache[pod.GetUID()] = newVolumeStatCalculator(s.statsProvider, s.calcPeriod, pod, s.eventRecorder).StartOnce() } else { newCache[pod.GetUID()] = value } } ... // Update the cache reference s.cachedVolumeStats.Store(newCache) } // 点下去会找到,这个方法,这算是触及到获取指标的核心了 // k8s.io/kubernetes/pkg/kubelet/server/stats/volume_stat_calculator.go:96 // calcAndStoreStats calculates PodVolumeStats for a given pod and writes the result to the s.latest cache. // If the pod references PVCs, the prometheus metrics for those are updated with the result. func (s *volumeStatCalculator) calcAndStoreStats() { // Find all Volumes for the Pod volumes, found := s.statsProvider.ListVolumesForPod(s.pod.UID) if !found { return } ... // Call GetMetrics on each Volume and copy the result to a new VolumeStats.FsStats var ephemeralStats []stats.VolumeStats var persistentStats []stats.VolumeStats for name, v := range volumes { // 这个就是获取真实的指标接口了,找了这么久终于找到了! // 等点击去一看 懵 ,看下图 metric, err := v.GetMetrics() if err != nil { // Expected for Volumes that don't support Metrics continue } ... // Store the new stats s.latest.Store(PodVolumeStats{EphemeralVolumes: ephemeralStats, PersistentVolumes: persistentStats}) }metrics 的实现有这么多,这获取指标到底走的哪个方法!
七、Kubelet 日志调试
笔者曾试图在 ubuntu 开发环境部署存储服务进行卷挂载,如果挂载成功通过 Debug 方式查看到底走的哪个方法,不过遗憾的是笔者的开发环境挂卷存在各种各样问题,这就不得不导致笔者换个思路来验证。
- 第一步:在 K8S 集群部署一个 CSI 存储服务,并部署 Pod 使用该存储卷,比如 NFS-CSI、Ceph-CSI 等。
- 第二步:部署一个使用 hostpath 卷的 Pod。
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: task-pvc spec: storageClassName: manual accessModes: - ReadWriteOnce resources: requests: storage: 10Gi --- apiVersion: v1 kind: PersistentVolume metadata: name: task-pv-volume labels: type: local spec: storageClassName: manual capacity: storage: 10Gi accessModes: - ReadWriteOnce hostPath: path: "/mnt/volume" --- apiVersion: v1 kind: Pod metadata: name: volume-test namespace: default spec: containers: - name: volume-test image: nginx imagePullPolicy: IfNotPresent volumeMounts: - name: hostpath mountPath: /data ports: - containerPort: 80 nodeName: 192.168.56.121 volumes: - name: hostpath persistentVolumeClaim: claimName: task-pvc
-
第三步,在所有的指标实现加上日志,比如:
// k8s.io/kubernetes/pkg/volume/metrics_nil.go:30 // 走这个方法表示不支持获取指标,中间我们加了一行日志 func (*MetricsNil) GetMetrics() (*Metrics, error) { fmt.Println("metrics not support") return &Metrics{}, NewNotSupportedError() }
-
第四步,进入k8s.io/kubernetes/cmd/kubelet目录下执行go build .。
-
第五步,将编译好的 Kubelet 放到192.168.56.121节点上替换它的/usr/bin/kubelet并重启 Kubelet。
-
第六步,很快就可以收集到 Kubelet 日志journalctl -u kubelet > /tmp/kubelet.log,分析查看一下我们添加的日志。
Jul 28 06:55:06 192.168.56.121 kubelet[1523]: metrics du /var/lib/kubelet/pods/89897880-6cc6-4e57-b22a-77fd647c8a22/etc-hosts Jul 28 06:55:06 192.168.56.121 kubelet[1523]: &{2021-07-28 06:55:06.023601432 +0000 UTC m=+567.083508594 4096 42954248Ki 37792860Ki 1 20984Ki 21396325 <nil> <nil>} Jul 28 06:55:07 192.168.56.121 kubelet[1523]: hostpath Jul 28 06:55:07 192.168.56.121 kubelet[1523]: /mnt/volume Jul 28 06:55:07 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:55:07 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:55:07 192.168.56.121 kubelet[1523]: default-token-4gbq6 Jul 28 06:55:07 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods/0a0a59a7-6422-483c-8ae8-3b7d3ad8795a/volumes/kubernetes.io~secret/default-token-4gbq6 Jul 28 06:55:07 192.168.56.121 kubelet[1523]: metrics_cached ? Jul 28 06:55:07 192.168.56.121 kubelet[1523]: &{2021-07-28 06:48:42.628758231 +0000 UTC m=+183.688665397 12288 940964Ki 940952Ki 9 235241 235232 <nil> <nil>} Jul 28 06:55:43 192.168.56.121 kubelet[1523]: csi-volume Jul 28 06:55:43 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods/f8d89fbd-c759-4062-bf1a-920b45296d9f/volumes/kubernetes.io~csi/pvc-8b5770ec-1b16-4c19-b97d-e7cfa8d0ceec/mount Jul 28 06:55:43 192.168.56.121 kubelet[1523]: metrics csi carina.storage.io Jul 28 06:55:43 192.168.56.121 kubelet[1523]: I0728 06:55:43.720937 1523 clientconn.go:106] parsed scheme: "" Jul 28 06:55:43 192.168.56.121 kubelet[1523]: I0728 06:55:43.720947 1523 clientconn.go:106] scheme "" not registered, fallback to default scheme Jul 28 06:55:43 192.168.56.121 kubelet[1523]: I0728 06:55:43.721108 1523 passthrough.go:48] ccResolverWrapper: sending update to cc: {[{/var/lib/kubelet/plugins/csi.carina.com/csi.sock <nil> 0 <nil>}] <nil> <nil>} Jul 28 06:55:43 192.168.56.121 kubelet[1523]: I0728 06:55:43.721126 1523 clientconn.go:948] ClientConn switching balancer to "pick_first" Jul 28 06:55:43 192.168.56.121 kubelet[1523]: &{2021-07-28 06:55:43.720886779 +0000 UTC m=+604.780793954 33184Ki 7158Mi 7296608Ki 3 3584Ki 3670013 <nil> <nil>} Jul 28 06:55:43 192.168.56.121 kubelet[1523]: default-token-vnnr4 Jul 28 06:55:43 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods/f8d89fbd-c759-4062-bf1a-920b45296d9f/volumes/kubernetes.io~secret/default-token-vnnr4 Jul 28 06:55:43 192.168.56.121 kubelet[1523]: metrics_cached ? Jul 28 06:55:43 192.168.56.121 kubelet[1523]: &{2021-07-28 06:51:42.633793572 +0000 UTC m=+363.693700755 12288 940964Ki 940952Ki 9 235241 235232 <nil> <nil>} Jul 28 06:55:45 192.168.56.121 kubelet[1523]: scheduler-config Jul 28 06:55:45 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods/89897880-6cc6-4e57-b22a-77fd647c8a22/volumes/kubernetes.io~configmap/scheduler-config Jul 28 06:55:45 192.168.56.121 kubelet[1523]: metrics_cached ? Jul 28 06:55:45 192.168.56.121 kubelet[1523]: &{2021-07-28 06:46:42.673965766 +0000 UTC m=+63.733872957 4096 42954248Ki 37793448Ki 5 20984Ki 21396589 <nil> <nil>} Jul 28 06:55:56 192.168.56.121 kubelet[1523]: xtables-lock Jul 28 06:55:56 192.168.56.121 kubelet[1523]: /run/xtables.lock Jul 28 06:55:56 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:55:56 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:55:56 192.168.56.121 kubelet[1523]: metrics du/var/lib/kubelet/pods/f8d89fbd-c759-4062-bf1a-920b45296d9f/etc-hosts Jul 28 06:55:56 192.168.56.121 kubelet[1523]: &{2021-07-28 06:55:56.285126727 +0000 UTC m=+617.345033890 4096 42954248Ki 37792528Ki 1 20984Ki 21396325 <nil> <nil>} Jul 28 06:56:14 192.168.56.121 kubelet[1523]: carina-csi-controller-token-dmhns Jul 28 06:56:14 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods/572f71e5-a97c-4720-974c-bbc09002fad3/volumes/kubernetes.io~secret/carina-csi-controller-token-dmhns Jul 28 06:56:14 192.168.56.121 kubelet[1523]: metrics_cached ? Jul 28 06:56:14 192.168.56.121 kubelet[1523]: &{2021-07-28 06:46:42.712326121 +0000 UTC m=+63.772233305 12288 940964Ki 940952Ki 9 235241 235232 <nil> <nil>} Jul 28 06:56:14 192.168.56.121 kubelet[1523]: socket-dir Jul 28 06:56:14 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods/572f71e5-a97c-4720-974c-bbc09002fad3/volumes/kubernetes.io~empty-dir/socket-dir Jul 28 06:56:14 192.168.56.121 kubelet[1523]: metrics du/var/lib/kubelet/pods/572f71e5-a97c-4720-974c-bbc09002fad3/volumes/kubernetes.io~empty-dir/socket-dir Jul 28 06:56:14 192.168.56.121 kubelet[1523]: &{2021-07-28 06:56:14.346621409 +0000 UTC m=+635.406528574 0 940964Ki 940964Ki 2 235241 235239 <nil> <nil>} Jul 28 06:56:14 192.168.56.121 kubelet[1523]: &{2021-07-28 06:56:14.346621409 +0000 UTC m=+635.406528574 0 940964Ki 940964Ki 2 235241 235239 <nil> <nil>} Jul 28 06:56:14 192.168.56.121 kubelet[1523]: certs Jul 28 06:56:14 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods/572f71e5-a97c-4720-974c-bbc09002fad3/volumes/kubernetes.io~secret/certs Jul 28 06:56:14 192.168.56.121 kubelet[1523]: metrics_cached ? Jul 28 06:56:14 192.168.56.121 kubelet[1523]: &{2021-07-28 06:46:42.652734769 +0000 UTC m=+63.712641961 8192 940964Ki 940956Ki 7 235241 235234 <nil> <nil>} Jul 28 06:56:16 192.168.56.121 kubelet[1523]: metrics du /var/lib/kubelet/pods/c8ab6fee-a91e-46fe-b468-71f104af1eac/etc-hosts Jul 28 06:56:16 192.168.56.121 kubelet[1523]: &{2021-07-28 06:56:16.39212959 +0000 UTC m=+637.452036755 4096 42954248Ki 37792548Ki 1 20984Ki 21396325 <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: host-dev Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /dev Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: log-dir Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/log/carina Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: mountpoint-dir Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: modules Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /lib/modules Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: host-mount Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /run/mount Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: socket-dir Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/lib/kubelet/plugins/csi.carina.com/ Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: host-sys Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /sys/fs/cgroup Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: plugin-dir Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/lib/kubelet/plugins Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: device-plugin Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/lib/kubelet/device-plugins Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>} Jul 28 06:56:20 192.168.56.121 kubelet[1523]: registration-dir Jul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/lib/kubelet/plugins_registry/ Jul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not support Jul 28 06:56:20 192.168.56.121 kubelet[1523]: &{0001-01-01 00:00:00 +0000 UTC <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>}
-
第七步,日志分析如下(因为日志较多合并的很多同类型的日志):
metricsNil | cacheMetrics | metricsDu | metricsStatFs | metricsCsi |
---|---|---|---|---|
/var/lib/kubelet/plugins_registry/ /var/lib/kubelet/device-plugins /var/lib/kubelet/plugins /sys/fs/cgroup /var/lib/kubelet/plugins/ /run/mount /lib/modules /var/lib/kubelet/pods /dev /run/xtables.lock /etc/cni/net.d /var/lib/calico /opt/cni/bin |
configmap token secret |
/var/lib/kubelet/pods/xxx/etc-hosts | 无 | csi volume |
hostpath |
|
|
|
|
八、CSI 指标源码
1. Kubelet 获取指标源码
- cacheMetrics
// k8s.io/kubernetes/pkg/volume/metrics_cached.go:45 // 这个不就细究了,就是获取一下缓存,通过上边的日志可以看到 token/configmap/secret资源走这个方法 func (md *cachedMetrics) GetMetrics() (*Metrics, error) { md.once.cache(func() error { md.resultMetrics, md.resultError = md.wrapped.GetMetrics() return md.resultError }) return md.resultMetrics, md.resultError }
-
metricsDu
// k8s.io/kubernetes/pkg/volume/metrics_du.go:44 // 这个通过日志观察到,获取所有pod的/var/lib/kubelet/pods/89897880-6cc6-4e57-b22a-77fd647c8a22/etc-hosts文件状态,实际上就是host文件 func (md *metricsDu) GetMetrics() (*Metrics, error) { metrics := &Metrics{Time: metav1.Now()} if md.path == "" { return metrics, NewNoPathDefinedError() } // 这个就是获取指标的方法,点下去会得到执行的 nice -n 19 du -x -s -B 1 /var/xxxx/etc-hosts这条命令 err := md.runDiskUsage(metrics) if err != nil { return metrics, err } err = md.runFind(metrics) if err != nil { return metrics, err } // 这里使用了 unix.statfs获取文件信息 err = md.getFsInfo(metrics) if err != nil { return metrics, err } return metrics, nil }
- metricsCSI
// k8s.io/kubernetes/pkg/volume/csi/csi_metrics.go:53 // 可以看到这个实际是调用了CSI node服务获取的文件指标信息 func (mc *metricsCsi) GetMetrics() (*volume.Metrics, error) { currentTime := metav1.Now() ctx, cancel := context.WithTimeout(context.Background(), csiTimeout) defer cancel() // Get CSI client csiClient, err := mc.csiClientGetter.Get() if err != nil { return nil, err } ... // Get Volumestatus // 就是调用的这个方法,等会在看一下各个CSI这个方法的实现 metrics, err := csiClient.NodeGetVolumeStats(ctx, mc.volumeID, mc.targetPath) if err != nil { return nil, err } ... //set recorded time metrics.Time = currentTime return metrics, nil }
- metricsStatFS
// k8s.io/kubernetes/pkg/volume/metrics_statfs.go:43 func (md *metricsStatFS) GetMetrics() (*Metrics, error) { metrics := &Metrics{Time: metav1.Now()} if md.path == "" { return metrics, NewNoPathDefinedError() } // 这里使用了 unix.statfs获取文件信息,和du那个实现是调用的一个方法 err := md.getFsInfo(metrics) if err != nil { return metrics, err } return metrics, nil }
至此各个获取文件容量指标的方法就告一段落了,最后去各个 CSI 实现里去确认一下NodeGetVolumeStats的实现。
- Ceph-CSI:https://github.com/ceph/ceph-csi.git
import "k8s.io/kubernetes/pkg/volume" // github.com/ceph/ceph-csi/internal/csi-common/nodeserver-default.go // NodeGetVolumeStats returns volume stats. func (ns *DefaultNodeServer) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { var err error targetPath := req.GetVolumePath() ... isMnt, err := util.IsMountPoint(targetPath) // 看这里 NewMetricsStatFs,在看看上边的import,这就是kubelet获取文件指标的实现,ceph-csi实现引用了它 cephMetricsProvider := volume.NewMetricsStatFS(targetPath) volMetrics, volMetErr := cephMetricsProvider.GetMetrics() ... return &csi.NodeGetVolumeStatsResponse{ Usage: []*csi.VolumeUsage{ { Available: available, Total: capacity, Used: used, Unit: csi.VolumeUsage_BYTES, }, { Available: inodesFree, Total: inodes, Used: inodesUsed, Unit: csi.VolumeUsage_INODES, }, }, }, nil }
3. NFS-CSI 提供指标方法
- NFS-CSI :https://github.com/kubernetes-csi/csi-driver-nfs.git
import "k8s.io/kubernetes/pkg/volume" // github.com/csi-driver-nfs/pkg/nfs/nodeserver.go:144 // NodeGetVolumeStats get volume stats func (ns *NodeServer) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { ... // 看这里 NewMetricsStatFs,在看看上边的import,这就是kubelet获取文件指标的实现,nfs-csi实现引用了它 // 和ceph-CSi获取指标的方法,一模一样;互相借鉴的吧! volumeMetrics, err := volume.NewMetricsStatFS(req.VolumePath).GetMetrics() if err != nil { return nil, status.Errorf(codes.Internal, "failed to get metrics: %v", err) } ... return &csi.NodeGetVolumeStatsResponse{ Usage: []*csi.VolumeUsage{ { Unit: csi.VolumeUsage_BYTES, Available: available, Total: capacity, Used: used, }, { Unit: csi.VolumeUsage_INODES, Available: inodesFree, Total: inodes, Used: inodesUsed, }, }, }, nil }
九、总结
- 我们从 Grafana 不显示 volume 容量指标开始,一路追查 Prometheus 数据源、通过 Kubelet 证书认证、跨过 Kubelet 本地调试的坎,然后根据源码层层追查,最终一窥 Kubelet 提供 volume 指标方法的全貌。
- 只有 Pod 使用中的卷 metrics 才会返回其指标,因为 Kubelet 首先获取当前节点上的所有 pod,然后再查询其 volumeStats。
- CSI 驱动提供的存储卷,获取指标实际上是由 Kubelet 调用 CSI 的NodeGetVolumeStats方法获取的。
- 一些内置资源类型,token/configmap/secret 通过 cachemetrics 方法获取指标。
- hostpath 等等一些主机目录,是不支持获取 volume 指标的。
- metrics du 指标只用于获取所有 pod 的 etc-hosts (/var/lib/kubelet/pod/xxxxx/etc-hosts) 文件的指标。
- metrics statFs 该方法在 Kubelet 中并没有只用,但是各个 CSI 均用该方法获取的 volume 指标,比如 NFS-CSI、Ceph-CSI。