概述
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
结构体也只有 Name
,Namespace
,以及 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.Annotations
和 spec.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 造成太大的压力。
参考资料
- dcgm-exporter