写点什么

存储卷指标消失之谜 | K8S Internals 系列第二期

作者:BoCloud博云
  • 2022 年 5 月 11 日
  • 本文字数:17835 字

    阅读完需:约 59 分钟

存储卷指标消失之谜 | K8S Internals 系列第二期

K8S Internals 系列:第二期


容器编排之争在 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 的熟悉,很快梳理出一个检查清单,在该清单中笔者直接给出了检查结果,下一章节展示排查过程。


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 kubeletmonitoring-system   kubelet                   210d$ kubectl get svc -A -l k8s-app=kubeletNAMESPACE     NAME      TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                        AGEkube-system   kubelet   ClusterIP   None         <none>        10250/TCP,10255/TCP,4194/TCP   246d$ kubectl get ep kubelet -n kube-systemNAME      ENDPOINTS                                                        AGEkubelet   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 接口时通常会出现如下错误,这是因为 Kubelet 接口开启了安全认证。

$ curl https://192.168.56.121:10250/metricsunauthorized
复制代码


通常情况下有三种方式访问 Kubelet 认证接口,这里要注意访问响应 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/apiserver-kubelet-client.crt --key /etc/kubernetes/pki/apiserver-kubelet-client.key https://192.168.56.121:10250/metrics
复制代码
  • 第二种:使用 token 方式

# 使用token方式,先查看一下用于kubelet所有权限的角色$ kubectl describe clusterrole system:kubelet-api-adminName:         system:kubelet-api-adminLabels:       kubernetes.io/bootstrapping=rbac-defaultsAnnotations:  rbac.authorization.kubernetes.io/autoupdate: truePolicyRule:  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.crtauthorization:  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 指标源码


通过上边几步基本上确认了是由于 Kubelet metrics 接口未上报kubelet_volume_stats-*指标导致的问题,那就去 google 一下这个指标吧。

经过多番查找 Kubernetes 在 1.8 版本增加了暴露 volume 指标的接口,详细链接如下:

  • https://github.com/google/cadvisor/issues/1702#issuecomment-381189602

  • https://github.com/kubernetes/kubernetes/commit/dac2068bbd7b3365a879cbd0a5131a0955832264?branch=dac2068bbd7b3365a879cbd0a5131a0955832264&diff=split

所以我们把 Kubernetes 代码 checkout 到 v1.21.2 版本查看,关键的代码如下:

// 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)  } }}
复制代码

同样,在这个文件中我们还可以看到 Kubelet 提供了哪些有关 volume 的指标。

 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"
复制代码

关于如何进行指标注册等部分代码,简单展示一下,如果写过为 prometheus 暴露指标的应该很容易明白。

// 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 关于 volume metrics 部分源码,我们开头提出的问题已经有了大概的答案,Kubelet 的 collector.statsProvider.ListPodStats()方法很显然只会列出当前节点上的容器指标,而 PVC 创建之后并未挂载到容器中,所以在 Kubelet 指标中是无法观察到存储卷指标的。

只通过源码分析还不足够我们还需要拿出切实的证据,下面我们将对 Kubelet 进行本地调试来验证一下猜想。


Kubelet 本地调试


如何验证上述代码的是否在工作,笔者想到的方法是启动 Kubelet debug 一下这块运行逻辑,制定好方法就这么干。


一个小插曲:拉取 Kubernetes 代码后,里边经常有很多红色的方法,导致无法顺利跳转经过反反复复各种实验后,最终删除了 Kubernetes 的 vendor 目录,配置 go.mod 模式后一切就 OK 了。


如果要 Debug Kubelet 首先要启动 Kubelet 并把它加入一个 Kubernetes 集群。

第一步:创建一个K8s集群$ kubectl get nodes -o wideNAME             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 nodesNAME             STATUS   ROLES                  AGE   VERSION192.168.56.101   Ready    <none>                 11s   v1.21.3192.168.56.120   Ready    control-plane,master   63m   v1.20.4192.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 nodesNAME             STATUS     ROLES                  AGE   VERSION192.168.56.101   Ready      <none>                 12m   v0.0.0-master+$Format:%H$192.168.56.120   Ready      control-plane,master   75m   v1.20.4192.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 获取指标代码。

// 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()}  
复制代码

还记得我们要查找为啥 pvcRef 对象是 nil,就是因为这个缓存里没有!

可以脑补一下,更新这个缓存必然是需要一个 goroutine,下边实际是要看这个定时 goroutine 是如何实现的,多长时间更新一次、从哪里获取这些指标。

// 简单的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: v1kind: PersistentVolumeClaimmetadata:  name: task-pvcspec:  storageClassName: manual  accessModes:    - ReadWriteOnce  resources:    requests:      storage: 10Gi
---apiVersion: v1kind: PersistentVolumemetadata:  name: task-pv-volume  labels:    type: localspec:  storageClassName: manual  capacity:    storage: 10Gi  accessModes:    - ReadWriteOnce  hostPath:    path: "/mnt/volume"---
apiVersion: v1kind: Podmetadata:  name: volume-test  namespace: defaultspec:  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-hostsJul 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]: hostpathJul 28 06:55:07 192.168.56.121 kubelet[1523]: /mnt/volumeJul 28 06:55:07 192.168.56.121 kubelet[1523]: metrics not supportJul 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-4gbq6Jul 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-4gbq6Jul 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-volumeJul 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/mountJul 28 06:55:43 192.168.56.121 kubelet[1523]: metrics csi carina.storage.ioJul 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 schemeJul 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-vnnr4Jul 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-vnnr4Jul 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-configJul 28 06:55:45 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods/89897880-6cc6-4e57-b22a-77fd647c8a22/volumes/kubernetes.io~configmap/scheduler-configJul 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-lockJul 28 06:55:56 192.168.56.121 kubelet[1523]: /run/xtables.lockJul 28 06:55:56 192.168.56.121 kubelet[1523]: metrics not supportJul 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-hostsJul 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-dmhnsJul 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-dmhnsJul 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-dirJul 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-dirJul 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-dirJul 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]: certsJul 28 06:56:14 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pods/572f71e5-a97c-4720-974c-bbc09002fad3/volumes/kubernetes.io~secret/certsJul 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-hostsJul 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-devJul 28 06:56:20 192.168.56.121 kubelet[1523]: /devJul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not supportJul 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-dirJul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/log/carinaJul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not supportJul 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-dirJul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/lib/kubelet/podsJul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not supportJul 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]: modulesJul 28 06:56:20 192.168.56.121 kubelet[1523]: /lib/modulesJul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not supportJul 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-mountJul 28 06:56:20 192.168.56.121 kubelet[1523]: /run/mountJul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not supportJul 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-dirJul 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 supportJul 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-sysJul 28 06:56:20 192.168.56.121 kubelet[1523]: /sys/fs/cgroupJul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not supportJul 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-dirJul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/lib/kubelet/pluginsJul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not supportJul 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-pluginJul 28 06:56:20 192.168.56.121 kubelet[1523]: /var/lib/kubelet/device-pluginsJul 28 06:56:20 192.168.56.121 kubelet[1523]: metrics not supportJul 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-dirJul 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 supportJul 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>}
复制代码
  • 第七步,日志分析如下(因为日志较多合并的很多同类型的日志):


经过分析日志,我们基本确认了哪些目录会采用哪个获取指标的方法,并且观察到 CSI 创建的 volume 是通过metricsCsi获取的指标信息。

下边分析查看一个各个指标的实现代码,实际上都挺简单。


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:43func (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的实现。


2. Ceph-CSI 提供指标方法

  • 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 statsfunc (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}
复制代码


总结

  1. 我们从 Grafana 不显示 volume 容量指标开始,一路追查 Prometheus 数据源、通过 Kubelet 证书认证、跨过 Kubelet 本地调试的坎,然后根据源码层层追查,最终一窥 Kubelet 提供 volume 指标方法的全貌。

  2. 只有 Pod 使用中的卷 metrics 才会返回其指标,因为 Kubelet 首先获取当前节点上的所有 pod,然后再查询其 volumeStats。

  3. CSI 驱动提供的存储卷,获取指标实际上是由 Kubelet 调用 CSI 的NodeGetVolumeStats方法获取的。

  4. 一些内置资源类型,token/configmap/secret 通过 cachemetrics 方法获取指标。

  5. hostpath 等等一些主机目录,是不支持获取 volume 指标的。

  6. metrics du 指标只用于获取所有 pod 的 etc-hosts (/var/lib/kubelet/pod/xxxxx/etc-hosts) 文件的指标。

  7. metrics statFs 该方法在 Kubelet 中并没有只用,但是各个 CSI 均用该方法获取的 volume 指标,比如 NFS-CSI、Ceph-CSI。

发布于: 刚刚阅读数: 3
用户头像

BoCloud博云

关注

微信ID:beyondcent 2019.04.09 加入

微信订阅号:beyondcent 微信服务号:bocloudresearch 企业级PaaS及多云管理服务商。

评论

发布
暂无评论
存储卷指标消失之谜 | K8S Internals 系列第二期_Kubernetes_BoCloud博云_InfoQ写作社区