目录

dcgm-exporter补充容器信息

概述

dcgm-exporter 默认不会输出太多容器相关的信息,但是容器的信息,比如环境变量等,通常会包括一些业务相关的信息,如果可以将环境变量相关的信息配置到指标的标签上,在后面配置 Grafana 的可视化面板的时候会更方便按不同的维度来配置。另外,虽然依赖 Prometheus 也可以通过其他指标将标签配置到指定的指标,但是 Prometheus 的配置门槛比较高,在不增加大量的标签的情况下,让 dcgm-exporter 直接带上业务相关的环境变量等标签会更加方便。

与Kubelet的交互获取Pod Name和Namespace

Kubelet 可以拿到的信息是比较有限的,这也是官方实现的 dcgm-exporter 获取的一些容器信息,这里主要就是 Pod 的信息,比如 Pod 的名称,Namespace 等。实现的方式是通过 Kubelet 的 Socket 来获取 Pod 的信息,这是通过配置参数 pod-resources-kubelet-socket 来指定 Kubelet 的 Socket 的路径,然后通过 grpc 接口将所有的 Pod 查询出来,然后构建一个 deviceToPodMap := make(map[string]PodInfo) 数据结构,方便在后面的指标处理的时候,通过设备 ID 来获取到 Pod 的信息,然后给相关的指标带上 Pod 的 Name,Namespace 等等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func (p *PodMapper) Process(metrics collector.MetricsByCounter, deviceInfo deviceinfo.Provider) error {
	socketPath := p.Config.PodResourcesKubeletSocket
	_, err := os.Stat(socketPath)
	if os.IsNotExist(err) {
		slog.Info("No Kubelet socket, ignoring")
		return nil
	}

	// TODO: This needs to be moved out of the critical path.
	c, cleanup, err := connectToServer(socketPath)
	if err != nil {
		return err
	}
	defer cleanup()

	pods, err := p.listPods(c)
	if err != nil {
		return err
	}

	slog.Debug(fmt.Sprintf("Podresources API response: %+v", pods))

	deviceToPod := p.toDeviceToPod(pods, deviceInfo)

	slog.Debug(fmt.Sprintf("Device to pod mapping: %+v", deviceToPod))

	// Note: for loop are copies the value, if we want to change the value
	// and not the copy, we need to use the indexes
	for counter := range metrics {
		for j, val := range metrics[counter] {
			deviceID, err := val.GetIDOfType(p.Config.KubernetesGPUIdType)
			if err != nil {
				return err
			}

			podInfo, exists := deviceToPod[deviceID]
			if exists {
				if !p.Config.UseOldNamespace {
					metrics[counter][j].Attributes[podAttribute] = podInfo.Name
					metrics[counter][j].Attributes[namespaceAttribute] = podInfo.Namespace
					metrics[counter][j].Attributes[containerAttribute] = podInfo.Container
				} else {
					metrics[counter][j].Attributes[oldPodAttribute] = podInfo.Name
					metrics[counter][j].Attributes[oldNamespaceAttribute] = podInfo.Namespace
					metrics[counter][j].Attributes[oldContainerAttribute] = podInfo.Container
					maps.Copy(metrics[counter][j].Labels, podInfo.Labels)
				}
			}
		}
	}

	return nil
}

但是,这个方式获取的信息是有限的,因为这是通过 kubelet 的 podresources 接口获取的,而这里的 PodResources 结构体也只有 NameNamespace,以及 ContainerDevices,寥寥几个字段,因此像 Pod 和容器的环境变量等信息是获取不到的,这个时候就需要通过其他的方式来获取容器的信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// PodResources contains information about the node resources assigned to a pod
type PodResources struct {
	Name                 string                `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
	Namespace            string                `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
	Containers           []*ContainerResources `protobuf:"bytes,3,rep,name=containers,proto3" json:"containers,omitempty"`
	XXX_NoUnkeyedLiteral struct{}              `json:"-"`
	XXX_sizecache        int32                 `json:"-"`
}

// ContainerResources contains information about the resources assigned to a container
type ContainerResources struct {
    Name                 string              `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Devices              []*ContainerDevices `protobuf:"bytes,2,rep,name=devices,proto3" json:"devices,omitempty"`
    XXX_NoUnkeyedLiteral struct{}            `json:"-"`
    XXX_sizecache        int32               `json:"-"`
}

// ContainerDevices contains information about the devices assigned to a container
type ContainerDevices struct {
    ResourceName         string   `protobuf:"bytes,1,opt,name=resource_name,json=resourceName,proto3" json:"resource_name,omitempty"`
    DeviceIds            []string `protobuf:"bytes,2,rep,name=device_ids,json=deviceIds,proto3" json:"device_ids,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

与容器运行时的交互获取环境变量

容器运行时交互,理论上可以获取容器上大部分信息,主要是指环境变量等。

Containerd

从 Kubernetes 1.24 开始就已经不使用 Docker 作为容器运行时了,而 Containerd 则是大部分公司的首选,我们也不例外,因此我们计划参考通过 Kubelet 获取 Pod Name 等信息的方式,通过每个节点上的 Containerd 的 Socket 来获取容器的信息,然后将容器的环境变量等信息通过指标的标签带上,首先构建一个 ContainerInfo 的结构体,用于记录从 Conatinerd 获取到的容器信息。

1
2
3
4
5
6
7
type ContainerInfo struct {
	PodName      string
	PodNamespace string
	Labels       map[string]string
	Annotations  map[string]string
	Env          map[string]string
}

然后通过 getContainerdInfo() 方法,先创建一个 grpc 的客户端,连接 Containerd 的 Socket 文件,通过 io.cri-containerd.kind 容器标签,排除 pause 容器的信息采集(因为大部分业务信息都不会在pause容器上),之后通过 spec.Annotationsspec.Process.Env 两个容器字段获取信息,记录在 ContainerInfo 上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 获取 Containerd 中的容器信息
func (p *PodMapper) getContainerdInfo() (map[string]ContainerInfo, error) {
	client, err := containerd.New("/run/containerd/containerd.sock")
	if err != nil {
		return nil, fmt.Errorf("failed to connect to containerd: %w", err)
	}
	defer client.Close()

	ctx := namespaces.WithNamespace(context.Background(), "k8s.io")
	containers, err := client.Containers(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to list containers: %w", err)
	}

	containerInfo := make(map[string]ContainerInfo)
	for _, container := range containers {
		// 获取容器信息
		info, err := container.Info(ctx)
		if err != nil {
			continue
		}

		// 只保留 "io.cri-containerd.kind" 为 "container" 的容器
		if kind, ok := info.Labels["io.cri-containerd.kind"]; !ok || kind != "container" {
			continue
		}

		labels := info.Labels
		spec, err := container.Spec(ctx)
		if err != nil {
			continue
		}

		// 提取 Pod 名字和 namespace
		podName := labels["io.kubernetes.pod.name"]
		podNamespace := labels["io.kubernetes.pod.namespace"]
		if podName == "" || podNamespace == "" {
			continue
		}

		annotations := spec.Annotations
		env := make(map[string]string)
		for _, e := range spec.Process.Env {
			parts := strings.SplitN(e, "=", 2)
			if len(parts) == 2 {
				env[parts[0]] = parts[1]
			}
		}

		// 保存容器信息
		containerInfo[info.ID] = ContainerInfo{
			PodName:      podName,
			PodNamespace: podNamespace,
			Labels:       labels,
			Annotations:  annotations,
			Env:          env,
		}
	}

	return containerInfo, nil
}

最后在处理指标的时候,遍历 containerdInfo,通过 Pod 的 Name 和 Namespace 匹配 Containerd 的信息,然后将 Containerd 的标签、注解和环境变量追加到指标的标签上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 通过 Pod 名字和 namespace 匹配 Containerd 的信息
for _, container := range containerdInfo {
	if container.PodName == podInfo.Name && container.PodNamespace == podInfo.Namespace {
		// 追加来自 Containerd 的标签、注解和环境变量
		for k, v := range container.Labels {
			if slices.Contains(podLabelsAllowList, k) {
				normalizedKey := normalizeLabelName(k)
				metrics[counter][j].Attributes["label_"+normalizedKey] = v
			}
		}
		for k, v := range container.Annotations {
			if slices.Contains(podLabelsAllowList, k) {
				normalizedKey := normalizeLabelName(k)
				metrics[counter][j].Attributes["annotation_"+normalizedKey] = v
			}
		}
		for k, v := range container.Env {
			// 判断键是否在允许的列表中
			if slices.Contains(podLabelsAllowList, k) {
				normalizedKey := normalizeLabelName(k)
				metrics[counter][j].Attributes["env_"+normalizedKey] = v
			}
		}
	}
}

Docker

虽说 Kubernetes 1.24 开始已经不使用 Docker 作为容器运行时了,但是还是有一些公司在使用 Docker,因此我们也需要考虑到这部分用户,Docker 的信息获取方式跟 Containerd 类似,只是需要通过 Docker 的 Socket 文件来获取信息,然后将 Docker 的标签、注解和环境变量追加到指标的标签上,可以说方法上跟 Containerd 是一样的,因此代码就不细说了,有兴趣可以直接查看这个 diff 文件的内容。

添加Node Label

在我司有个比较 tricky 的 case,我们在 GPU 的应用上,会用一个业务标签来代替显卡型号进行调度和分配,这样有个问题就是 dcgm-exporter 虽然本身可以带上节点的 Hostname 以及 modelName 这些标签,但是无法直接加上所在节点的 Kubernetes 的 Node Label。dcgm-exporter 无法直接从 kubelet 获取节点的非初始化的标签和污点等,虽然可以考虑让 exporter 通过访问 kube-apiserver 获取指定节点的 Node Label,但是需要考虑到的是大量的 exporter 访问 kube-apiserver 来获取节点标签的话,会造成巨大的访问压力,所以不建议让 exporter 在任何场景下直接访问。社区上有开发者提供了一个实现,但个人觉得 exporter 对 kube-apiserver 直接访问是有点隐患的,所以不建议读者采用这个方案。

不过我们可以通过 kube-state-metrics 和 Prometheus 来进行指标的补充,这也是最终的方案了,尽管 kube-state-metrics 也是通过访问 kube-apiserver 来获取节点的标签等信息,但是 kube-state-metrics 会将这些信息缓存起来,所以不会对 kube-apiserver 造成太大的压力。

参考资料

  1. dcgm-exporter