共计 9235 个字符,预计需要花费 24 分钟才能阅读完成。
1 问题现象
我们有一些机器,需要统计基础监控的相关信息,数据是从Prometheus拿的,但是发现部分节点metrics的某些label是空的。
下面是正常节点的metrics
container_memory_working_set_bytes{container="",id="/kubepods/burstable/pod193b61ed-efd0-4784-b72a-4b585a058c55",image="nginx",name="nginx",namespace="test",pod="test-748f796c6c-qm6zr"} 2.57388544e+08 1653559998508
下面是异常节点的metrics
container_memory_working_set_bytes{container="",id="/kubepods/burstable/pod193b61ed-efd0-4784-b72a-4b585a058c55",image="",name="",namespace="test",pod="test-748f796c6c-qm6zr"} 2.57388544e+08 1653559998508
可以看到,Prometheus取出来的数据,label中的 container_name、name、namespace、pod_name 都是空的。
container_memory_working_set_bytes
这个指标是kubelet提供的(kubelet是Prometheus的一个target),可以通过https://${nodeip}:10250/metrics
查看metrics。
使用下面的命令查看 metrics,对比从正常、异常节点的 kubelet 取出来的数据有什么不同。
curl -i -k -H "Authorization: Bearer ${token}" https://${nodeip}:10250/metrics/cadvisor |grep container_memory_working_set_bytes
正常节点:
container_memory_working_set_bytes{container="",id="/kubepods/burstable/pod193b61ed-efd0-4784-b72a-4b585a058c55",image="nginx",name="nginx",namespace="test",pod="test-748f796c6c-qm6zr"} 2.57388544e+08 1653559998508
异常节点:
container_memory_working_set_bytes{container="",id="/kubepods/burstable/pod193b61ed-efd0-4784-b72a-4b585a058c55",image="",name="",namespace="test",pod="test-748f796c6c-qm6zr"} 2.57388544e+08 1653559998508
也就是说,从 kubelet 取出来的数据就已经有某些 Label 为空的问题了,问题跟 Prometheus无关,接下来看看为什么 kubelet 会有问题。
2 kubelet与cadvisor
我们知道,kubelet很多指标,是通过cadvisor获取的,而 cadvisor 就直接运行在 kubelet 进程中,节点上并没有一个单独的 cadvisor 进程。不过 cadvisor 代码仍然是使用的是github.com/google/cadvisor
,换句话说就是,cadvisor已经变成了一个SDK,供kubelet调用。
kubelet在启动的时候,会初始化一个CAdvisorInterface。
func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan struct{}) (err error) { | |
... | |
if kubeDeps.CAdvisorInterface == nil { | |
imageFsInfoProvider := cadvisor.NewImageFsInfoProvider(s.ContainerRuntime, s.RemoteRuntimeEndpoint) | |
kubeDeps.CAdvisorInterface, err = cadvisor.New(s.Address, uint(s.CAdvisorPort), imageFsInfoProvider, s.RootDirectory, cadvisor.UsingLegacyCadvisorStats(s.ContainerRuntime, s.RemoteRuntimeEndpoint)) | |
if err != nil { | |
return err | |
} | |
} |
初始化过程主要完成的就是启动一个cadvisor http server。
// New creates a cAdvisor and exports its API on the specified port if port > 0. | |
func New(address string, port uint, imageFsInfoProvider ImageFsInfoProvider, rootPath string, usingLegacyStats bool) (Interface, error) { | |
... | |
cadvisorClient := &cadvisorClient{ | |
imageFsInfoProvider: imageFsInfoProvider, | |
rootPath: rootPath, | |
Manager: m, | |
} | |
err = cadvisorClient.exportHTTP(address, port) | |
if err != nil { | |
return nil, err | |
} | |
return cadvisorClient, nil |
在exportHTTP
函数中启动http server,并且向cadvisor http server注册了一个kubelet的回调函数containerLabels
,kubelet在这个回调函数里,将自己需要的label加到cadvisor的metrics中去。
func (cc *cadvisorClient) exportHTTP(address string, port uint) error { | |
// Register the handlers regardless as this registers the prometheus | |
// collector properly. | |
mux := http.NewServeMux() | |
... | |
cadvisorhttp.RegisterPrometheusHandler(mux, cc, "/metrics", containerLabels) |
先看看containerLabels
做了什么。
func containerLabels(c *cadvisorapi.ContainerInfo) map[string]string { | |
// Prometheus requires that all metrics in the same family have the same labels, | |
// so we arrange to supply blank strings for missing labels | |
var name, image, podName, namespace, containerName string | |
if len(c.Aliases) > 0 { | |
name = c.Aliases[0] | |
} | |
image = c.Spec.Image | |
if v, ok := c.Spec.Labels[types.KubernetesPodNameLabel]; ok { | |
podName = v | |
} | |
if v, ok := c.Spec.Labels[types.KubernetesPodNamespaceLabel]; ok { | |
namespace = v | |
} | |
if v, ok := c.Spec.Labels[types.KubernetesContainerNameLabel]; ok { | |
containerName = v | |
} | |
set := map[string]string{ | |
metrics.LabelID: c.Name, | |
metrics.LabelName: name, | |
metrics.LabelImage: image, | |
"pod_name": podName, | |
"namespace": namespace, | |
"container_name": containerName, | |
} | |
return set | |
} |
我们看到了什么?pod_name
、namespace
、container_name
,这些正好对应前面缺失的label:
那么,cadvisor是什么时候调用containerPrometheusLabelsFunc
的呢?继续看RegisterPrometheusHandler
。
func RegisterPrometheusHandler(mux httpmux.Mux, containerManager manager.Manager, prometheusEndpoint string, f metrics.ContainerLabelsFunc) { | |
r := prometheus.NewRegistry() | |
r.MustRegister( | |
metrics.NewPrometheusCollector(containerManager, f), | |
... | |
} | |
func NewPrometheusCollector(i infoProvider, f ContainerLabelsFunc) *PrometheusCollector { | |
if f == nil { | |
f = DefaultContainerLabels | |
} | |
c := &PrometheusCollector{ | |
infoProvider: i, | |
containerLabelsFunc: f, //here 1 | |
... | |
containerMetrics: []containerMetric{ | |
}, { | |
name: "container_memory_working_set_bytes", | |
help: "Current working set in bytes.", | |
valueType: prometheus.GaugeValue, | |
getValues: func(s *info.ContainerStats) metricValues { | |
return metricValues{{value: float64(s.Memory.WorkingSet), timestamp: s.Timestamp}} | |
}, | |
}, | |
}, { |
在NewPrometheusCollector
中,我们看到了前面采集的container_memory_working_set_bytes
,并且还将containerLabels
挂到containerLabelsFunc
上了。
这样,在用户API请求metrics的时候,cadvisor就可以通过调用kubelet的回调函数containerLabels
,按kubelet的要求,将他需要的labels/values添加到metrics的labels中去了。
func (c *PrometheusCollector) collectContainersInfo(ch chan<- prometheus.Metric) { | |
.. | |
for _, container := range containers { | |
values := make([]string, 0, len(rawLabels)) | |
labels := make([]string, 0, len(rawLabels)) | |
containerLabels := c.containerLabelsFunc(container) | |
for l := range rawLabels { | |
labels = append(labels, sanitizeLabelName(l)) | |
values = append(values, containerLabels[l]) | |
} |
3 cadvisor与runtime
从前面的分析可以看到,丢失的labels,是kubelet“委托”cadvisor帮忙处理的,但是显然cadvisor处理出了点问题。是在哪里呢?
回到“委托函数”里,从下面的代码可以看到,containerLabels
实际就是从ContainerInfo
中读取Labels这个map的值,如果这个map没有相应的key,那么自然取到的就是空的了(初始值为空字符串)。
func containerLabels(c *cadvisorapi.ContainerInfo) map[string]string { | |
var name, image, podName, namespace, containerName string | |
... | |
if v, ok := c.Spec.Labels[types.KubernetesPodNameLabel]; ok { | |
podName = v | |
} | |
if v, ok := c.Spec.Labels[types.KubernetesPodNamespaceLabel]; ok { | |
namespace = v | |
} | |
if v, ok := c.Spec.Labels[types.KubernetesContainerNameLabel]; ok { | |
containerName = v | |
} | |
const ( | |
KubernetesPodNameLabel = "io.kubernetes.pod.name" | |
KubernetesPodNamespaceLabel = "io.kubernetes.pod.namespace" | |
KubernetesPodUIDLabel = "io.kubernetes.pod.uid" | |
KubernetesContainerNameLabel = "io.kubernetes.container.name" | |
KubernetesContainerTypeLabel = "io.kubernetes.container.type" | |
) |
所以问题简化为,c.Spec.Labels
是什么时候处理的?
4 cadvisor与docker
还是以定位问题的思路来看。
因为在此之前我没看过cadvisor的代码,也不了解cadvisor的架构,所以直接去看cadvisor代码里是怎么获取到Labels
的。
可以看到,有docker、containerd、rkt、crio等等相关的实现,因为在我们集群上使用的是docker,所以,我们先来看下docker这里是怎么处理的。
func (self *dockerContainerHandler) GetSpec() (info.ContainerSpec, error) { | |
hasFilesystem := !self.ignoreMetrics.Has(container.DiskUsageMetrics) | |
spec, err := common.GetSpec(self.cgroupPaths, self.machineInfoFactory, self.needNet(), hasFilesystem) | |
spec.Labels = self.labels | |
spec.Envs = self.envs | |
spec.Image = self.image | |
spec.CreationTime = self.creationTime | |
return spec, err | |
} |
显然,ContainerSpec
就是containerLabels
里的c.Spec.
,spec.Labels = self.labels
这行告诉我们,cadvisor是从dockerContainerHandler
来获取Labels的。
那 dockerContainerHandler 又是从哪里获取Labels的呢?继续查找代码。
func newDockerContainerHandler( | |
client *docker.Client, | |
name string, ...) | |
... | |
// We assume that if Inspect fails then the container is not known to docker. | |
ctnr, err := client.ContainerInspect(context.Background(), id) | |
if err != nil { | |
return nil, fmt.Errorf("failed to inspect container %q: %v", id, err) | |
} | |
handler := &dockerContainerHandler{ | |
... | |
labels: ctnr.Config.Labels, | |
ignoreMetrics: ignoreMetrics, | |
zfsParent: zfsParent, | |
} |
ok,找到数据的真正来源了,其实就是 docker inspect 了容器,获取容器的ctnr.Config.Labels
,然后一层层传出来,所以问题的原因,很可能是ctnr.Config.Labels
并没有我们想要的pod_name
、namespace
、container_name
,也就是types.KubernetesPodNameLabel
等Label。
到异常所在GPU节点上,实际看看docker的信息。
# docker inspect b8f6b265835e | |
[ | |
{ | |
"Id": "b8f6b265835ea112000aec5fd426be66f922bb241f3e65c45997e7bc63bad003", | |
"Config": { | |
"Labels": { | |
"io.kubernetes.container.name": "xxx", | |
"io.kubernetes.docker.type": "container", | |
"io.kubernetes.pod.name": "xxx-67d8c96565-nmcwg", | |
"io.kubernetes.pod.namespace": "test" |
很遗憾,docker inspect有相应的Label。说不通哟,不应该获取不到的。
5 真正的原因
那么有没有可能,cadvisor跟docker的通路不畅,导致直接就没从docker取到任何信息?
我们理一下cadvisor启动到建立到docker通路的流程。
kubelet在创建cadvisorClient的时候,创建了container manager,之后会调用container manager的Start
函数,在Start
函数里,会注册docker、rkt、containerd、crio等等运行时。
所以,cadvisor实际上是Labels的搬运工,其数据也是调用docker、containerd等等的API来获取的。
// Start the container manager. | |
func (self *manager) Start() error { | |
err := docker.Register(self, self.fsInfo, self.ignoreMetrics) | |
if err != nil { | |
glog.V(5).Infof("Registration of the Docker container factory failed: %v.", err) | |
} | |
err = rkt.Register(self, self.fsInfo, self.ignoreMetrics) | |
... | |
err = containerd.Register(self, self.fsInfo, self.ignoreMetrics) | |
if err != nil { | |
glog.V(5).Infof("Registration of the containerd container factory failed: %v", err) | |
} | |
err = crio.Register(self, self.fsInfo, self.ignoreMetrics) | |
if err != nil { | |
glog.V(5).Infof("Registration of the crio container factory failed: %v", err) | |
} |
大胆假设下
情况1
如果docker.Register
注册失败,那么就别想从docker inspect到任何信息了。
查一下kubelet日志
[root@10-0-0-2 /var/log]# grep "Registration of the Docker container factory" messages* | |
messages-20191013:Oct 8 16:51:45 10-0-0-2 kubelet: I1008 16:51:45.784752 3217 manager.go:297] Registration of the Docker container factory failed: failed to validate Docker info: version string "dev" doesn't match expected regular expression: "(\d+)\.(\d+)\.(\d+)". | |
messages-20191013:Oct 9 10:10:11 10-0-0-2 kubelet: I1009 10:10:11.221103 751886 manager.go:297] Registration of the Docker container factory failed: failed to validate Docker info: version string "dev" doesn't match expected regular expression: "(\d+)\.(\d+)\.(\d+)". |
果然,docker注册失败了。注册失败的原因很简单,这个docker 的版本是我们做过修改的,docker server的version是“dev”,不满足正则表达式"(\d+)\.(\d+)\.(\d+)"
的要求,所以注册失败了,所以,也就inspect不到想要的label了。
情况2
如果kubelet 先于 docker 启动,kubelet中的Cadvisor无法连接到docker守护程序,当docker和kubelet一起启动并且docker守护程序尚未完全初始化时,Cadvisor无法从docker守护程序获取docker根目录,并将从kubelet的–docker根标志获取此值,其默认值为/var/lib/docker
,但当用户的docker目录不是标准目录时,这将导致缺乏监控。
参考https://github.com/google/cadvisor/pull/2359
6 解决办法
情况1
很简单,把docker server
的version
按正则表达式生成就行了,例如“18.06.1”
情况2
重启kubelet