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. 排查过程
(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=""}))
复制代码
$ 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接口
$ 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方式,先查看一下用于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
复制代码
# 修改 /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
````
复制代码
/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 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 方式查看到底走的哪个方法,不过遗憾的是笔者的开发环境挂卷存在各种各样问题,这就不得不导致笔者换个思路来验证。
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>}
复制代码
经过分析日志,我们基本确认了哪些目录会采用哪个获取指标的方法,并且观察到 CSI 创建的 volume 是通过metricsCsi
获取的指标信息。
下边分析查看一个各个指标的实现代码,实际上都挺简单。
CSI 指标源码
1. Kubelet 获取指标源码
// 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
}
复制代码
// 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
}
复制代码
// 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
}
复制代码
// 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 提供指标方法
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 提供指标方法
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。
评论