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

来源: 云巴巴 2022-05-25 14:23:58

容器编排之争在 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 接口时通常会出现如下错误,这是因为 Kubelet 接口开启了安全认证。

$ curl https://192.168.56.121:10250/metrics
unauthorized

通常情况下有三种方式访问 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-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 指标源码

 

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

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

https://github.com/google/cadvisor/issues/1702#issuecomment-381189602https://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 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 获取指标代码。

// 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: 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 创建的 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: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的实现。

 

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 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
}

 

九、总结

 

  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。

更多产品了解

欢迎扫码加入云巴巴企业数字化交流服务群

产品交流、问题咨询、专业测评

都在这里!

 

评论列表

为你推荐

在Gazebo仿真中使用OpenCV获取ROS机器人的视觉图像

在Gazebo仿真中使用OpenCV获取ROS机器人的视觉图像

没有机器人,如何学习ROS 作为ROS机器人视觉编程的开篇实验,这次将带同学们完成一个基本功能:在ROS系统中获取机器人的视觉图像。在这个实验里,我们将了解图像数据是以什么形式存在于ROS系统中,以及如何转换成我们熟悉的OpenCV格式,为后续的视觉编程

2022-11-21 14:18:07

怎样做才能实现人工智能云市场

怎样做才能实现人工智能云市场

技术影响社会的方方面面,供应链也不例外。人工智能提供了一个高度创新的技术形式,这种形式使管理人员和工作场所的供应链发生了巨大变化。

2022-11-21 15:58:31

关于智能数据管理引发的新难题

关于智能数据管理引发的新难题

随着时代的进步、科技的发展,数据管理平台愈发强大,但随之而来的却是只能数据管理带来的新难题。     如果你现在去乘坐公交企业或者通过地铁的话,通常会看到这样一种情况,在公交车上或者地铁上不少的男男女女一上车时间之后我们就会掏出随身所携带的手机、

2020-04-29 16:59:24

企业如何利用DevOps提升研发效能?京东行云DevOps平台这些优势快看看

企业如何利用DevOps提升研发效能?京东行云DevOps平台这些优势快看看

为了提高软件开发质量,并缩短软件开发生命周期,企业运用 DevOps 已变成趋势。

2023-12-11 16:54:21

乱、多、难找,企业档案怎么管?晨科档案管理快速解决行政困扰

乱、多、难找,企业档案怎么管?晨科档案管理快速解决行政困扰

衡量一个企业业绩与管理水平的重要尺度就是如何科学规范的管理档案。

2023-03-03 17:15:17

《21CBR》专题报道|打工人讨厌的垫付报销,投资人下场解决,引领行业“干掉报销”

《21CBR》专题报道|打工人讨厌的垫付报销,投资人下场解决,引领行业“干掉报销”

兰希从投资公司跳出来创业,创办了分贝通,从企业高频的商旅场景切入费控SaaS赛道,瞄准企业因公消费的痛点。

2022-07-26 14:21:13

严选云产品

腾讯乐享企业文化平台 腾讯乐享企业文化平台,文化落地方式更多元化,活动线上报名与统计,操作更方便。线上宣传结合渠道,更快速、精准,定时消息通知,并增加互动,触达更有力。乐享提供个性化K吧自定义,支持PC端、企业微信端随时访问,让企业打造属于自己的K吧空间。
深信服统一端点安全管理系统aES 深信服统一端点安全管理系统aES,PC&服务器&信创端&容器统管,EPP、EDR、CWPP、HIPS、容器安全能力融合。深信服aES定位,我们不做全家桶、大杂烩,我们是专注于用户端点安全的精专派。杀毒只是我们最基础的能力,聚焦市场上层出不穷的新威胁(勒索、顽固挖矿为代表)、APT式高级威胁检测防护,并快速响应闭环。
深信服SDP零信任工作空间 零信任工作空间是aTrust产品中的一个增值模块,为windows、Mac、uOS、iOS、android、HarmonyOS系统平台提供安全沙箱能力。以数据保护为核心,覆盖全终端的零信任工作空间。通过程序管控功能设置只允许在工作空间内运行的程序列表,当终端用户运行列表外的程序时会被阻断并提示。
埃文科技IP地址数据生活服务APP应用方案 埃文科技IP地址数据生活服务APP应用方案,通过IP的地理位置信息与GPS信息交叉验证,识别判断用户本次操作行为的风险程度,以保证用户账号及交易安全。根据平台/APP用户登录的IP地址,进行归属地解析,分析平台用户在不同城市、不同行政区域的分布活跃情况,并以此制定相关的用户拉新或增长策略。
网宿科技 零信任安全接入SEA 遵循Zero Trust安全框架体系,整合安全接入网关、安全管控中心、全链路传输加速三大核心能力模块,提供以身份认证与动态授权为基础的企业办公网络安全接入服务,为企业打造随时随地、在任何终端或边缘进行安全连接与访问的办公安全体系。
T+订货商城 畅捷通T+订货商城,企业专属接单平台,人工接单变为系统接单 ,降低接单成本,提升配送效率

甄选10000+数字化产品 为您免费使用

申请试用