Prometheus 服务发现:Kubernetes (上)

介绍

在上一篇讲解 Docker 接入的服务发现文章《Prometheus 服务发现:Docker》,我们介绍了 Prometheus 接入 Docker 的容器服务,过滤和发现了需要采集的容器,支持了 Docker Daemon、Docker Compose、Docker Swarm 的架构覆盖,也额外提到了当下云原生的容器监控的核心目标,还是容器编排王座上的 Kubernetes。也正因为 Kubernetes 的体量比较大,所以我们会分两个篇章来介绍,第一篇会详细讲解 Prometheus 对 Kubernetes 服务发现的基础用法,第二篇章会介绍常见的场景分享。这是第一篇,以 Docker 的服务发现作为引子,我们将会在这一篇文章中,一步步地讲解 Prometheus 对 Kubernetes 服务发现的原理,和经常遇到的监控问题,并给出一定的方案建议。

接触或使用过 Kubernetes (K8S) 的同学,一定对 kubeclt 这个官方出品的管理工具不会陌生,我们介绍 Prometheus 的服务发现时,也会结合这个工具,对比起来介绍,方便你能够从二者的关联性出发,更快地理解 Prometheus 的相关原理。

  • 对比 kubectl 的 Watch:思考 Prometheus 如何设计类似的服务发现。
  • 对比 kubectl 的 cli-runtime 库:研究 Prometheus 如何寻找近似的方式请求 Kubernetes。
  • 对比 kubectl 的认证方式:让 Prometheus 服用同样的模式完成认证
  • 对比 kubectl 的资源结果:理解 Prometheus 对各类资源的标签发现来源

话不多说,我们开始今天的内容。

Kubernetes

Kubernetes 的体系非常庞大,单独把监控拎出来,涉及的知识面和案例也都错综复杂,为了全面吃透 Prometheus + Kubernetes 的这套组合,我们会花一点功夫,首先来剖析 Kubernetes 是如何支持资源 (Resources) 发现的,认识到 Prometheus 利用相同的机制去监听这部分资源,然后转化为一些列的标签集合,传递到采集流程。我们依旧会跟之前的文章一样,通过一系列的实验,演示和分析具体的功能,为此,我们将使用 KinD (K8S in Docker) 部署一个精简的 Kuberntes 环境。

部署 KinD

我选用了一台 Ubuntu VM 作为测试主机,KinD 的模式是让 Kubernetes 运行在 Docker 内,为此,我们需要首先部署 Docker,在《Prometheus 服务发现:Docker》介绍了详细的部署过程。另外,我们要为 Docker 添加一些额外的设置,我使用的是多网卡的 VM,因此额外指定了容器网络使用的网卡 IP (192.168.1.21),并且选择使用 systemd 的 cGroup。

cat > /etc/docker/daemon.json << EOF
{
  "ip""192.168.1.21",
  "exec-opts": ["native.cgroupdriver=systemd"]
}
EOF

systemctl restart docker.service

接着使用 KinD 构建一个 1*master + 2*worker 的 Kubernetees 集群,版本为 v1.27.3。KinD 是 Kubernetes in Docker 的模式,因此我的们的本机并不是 Kubernetes 的 node,所以不能直接连通集群内的 Pod 网络,所以,在初始化集群时,通过 extraPortMappings 将控制面节点的 32000 暴露到本机的 3128,这是 docker-proxy 完成的转发。

Prometheus 服务发现:Kubernetes (上)

# 下载 kubectl
curl -L https://storage.googleapis.com/kubernetes-release/release/v1.27.3/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl
chmod +x /usr/local/bin/kubectl

# 下载 KinD
curl -L https://github.com/kubernetes-sigs/kind/releases/download/v0.20.0/kind-linux-amd64 -o /usr/local/bin/kind
chmod +x /usr/local/bin/kind

# 创建集群定义文件
cat > cls.yaml << EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  podSubnet: "172.0.0.0/16"
  serviceSubnet: "172.255.0.0/16"
  kubeProxyMode: ipvs
nodes:
  - role: control-plane
    image: kindest/node:v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72
    extraPortMappings:
      - containerPort: 32000
        hostPort: 3128
  - role: worker
    image: kindest/node:v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72
  - role: worker
    image: kindest/node:v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72
EOF

# 应用配置创建集群
kind create cluster --config cls.yaml --name cls

KinD 部署集群完成后,我们可以使用下载的 kubectl 工具来使用我们的精简 Kubernetes 集群。

# 切换 kubeconfig 的 context 到部署的集群
kubectl cluster-info --context kind-cls

# 查看节点
kubectl get nodes
# 查看所有容器
kubectl get pod -A

如果一切顺利,那么会查询到以下的结果。

Prometheus 服务发现:Kubernetes (上)

Kubernetes 的工作流程非常复杂,为了快速理解 Prometheus 如何应用其中的发现原理,我们将通过浅入深出的过程,渐渐掌握 Kubernetes 提供资源暴露和 Prometheus 接入发现的原理。我们先使用最常用的 kubectl 工具,分别在两个窗口执行如下的命令,观察在创建一个 Deployment 时,kubectl watch 了哪些事件 (Events) 和资源 (Resources)。

# 窗口1: 使用 kubectl 监听 default 命名空间下 Pod 资源的操作
kubectl get pods --watch --output-watch-events

# 窗口2: 在 default 命名空间创建一个3副本的 Deployment,然后缩容到2副本
kubectl create deployment blackbox-exporter --image=quay.io/prometheus/blackbox-exporter:latest --port=9115 --replicas=3
kubectl scale --replicas=2 deployment blackbox-exporter

在创建完 Deployment 后,watch 的监听里输出了这些 Pod 副本的动作,而且当副本缩容后,其中一个 Pod 的删除也被记录其中。

Prometheus 服务发现:Kubernetes (上)

我们在窗口 1查看创建的 Pod,也能发现与窗口 2的信息一致,有三个 Pod 被成功创建,然后有一个 Pod 被删除。

Prometheus 服务发现:Kubernetes (上)

这个 watch 能够获取到监听资源的变化事件,如果 Prometheus 可以像 kubectl 这样接入到 Kubernetes,那么服务发现也就不成问题了。如果让我们来设计 Prometheus 对 Kubernetes 的服务发现,也许会选择如下的方案:

  1. 首次接入到集群,执行 kubectl get --watch --output-watch-events 类似的动作,获取全部的资源信息,开始采集。
  2. 监听资源的变化事件,实时将目标增量同步到采集流程。

为了实现这个需求,我们来参考一下 kubectl 是怎么实现的,这里简单分析 kubectl 的源码。

Prometheus 服务发现:Kubernetes (上)

watch 方法中构造了请求,获得资源信息 (resource.Info),这里的请求操作位于 k8s.io/cli-runtime,这是专门为 kubectl 构建的项目,我们简单地看一下实现细节。

Prometheus 服务发现:Kubernetes (上)

兴许我们可以使用同样的方式,对 Kubernetes 的 APIServer 构造类似的 Request,获取全量的资源信息后,持续的监听变化情况。那么,是否有与 k8s.io/cli-runtime 职能相同,但是更加通用的公共库来实现这种能力?答案是有的,Kubernetes 官方为需要接入 Kubernetes 的外部服务,提供了 k8s.io/client-go 库来支持协议的交互,高度的封装也让我们不需要像 kubeclt 那般处理太多内部逻辑。

Prometheus 内置了对 Kubernetes 的服务发现模块 kubernetes_sd_configs,我们来分析一下 Prometheus 怎么通过 client-go 来完成 kubeclt 的 --watch --output-watch-events 操作。我们单独以 Pod 资源的服务发现机制来说明,构造器的代码非常少,关键之处是使用 AddEventHandler 将 Pod 的 AddDeleteUpdate 的事件添加处理函数,其实内部的操作都是一致的,就是将新的对象传入队列内。

Prometheus 服务发现:Kubernetes (上)

在主方法 Run 中,主要通过 process 来监听这个队列的数据,如果不在全量存储 (store) 里,就将构建 Pod 同名标识 (Source) 的空标签集合,发送到采集流程,相关的 Pod 也会从采集中被剔除。后续的子流程 buildPod 也会将一些无效的 (如预备中、未分配 IP 和被驱逐等状态) Pod 拦截并清理,最终,把正常的更新和新增的 Pod,提取关联的标签信息,组成 targetgroup 同步至采集流程。

Prometheus 服务发现:Kubernetes (上)

除了 Pod 外,Prometheus 现有的 Kubernetes 服务发现的资源,还包括 EndpointSlice、Endpoint、Service、Ingress、Node,但都是采用了与 Pod 一致的逻辑,使用 client-go 的封装方法,把发现的元数据信息同步到采集,让采集资源池去请求对应的资源地址。所以,我们从 kubectl 出发,分析出其内部采用了 cli-runtime 实现资源获取和监听,再到 Prometheus 使用 client-go 完成资源对象的发现和同步,也大概地知道了 Prometheus 是如何凭借 Kubernetes 底座的能力,来发现所需采集的目标。接下来,我们就以实际场景,更加深入地认清这一块内容。

服务发现

Prometheus 对 Kubernetes 主要使用 kubernetes_sd_configs 模块来进行服务发现,我们会根据后续几个通用场景来介绍服务发现的配置和细节。

  • 初步接入: 完成基础的 Pod 监控发现。
  • 代理模式: 如何使用集群私有网络进行采集。
  • 范围覆盖: 通过过滤条件筛选其他发现目标。
  • 标签转换: 对发现的 Kubernetes 资源完成标签提取和转换。

初步接入

我们知道,kubectl 默认使用 kubeconfig 来接入到对应的 APIServer,完成 RESTful API 的认证,再调用相关的 API 来执行最终的实际操作。如果没有额外的定制,kubectl 使用的 kubeconfig 一般都在 ~/.kube/config,我们将其拷贝到 Prometheus 的软件目录,以待使用。

mkdir -p /srv/prometheus/sd/kubernetes
cp ~/.kube/config /srv/prometheus/sd/kubernetes/kubeconfig.yml
chown -R prometheus:prometheus /srv/prometheus/sd/kubernetes

注意:为了复用集群的私有网络,这里的 Prometheus 运行在 KinD 的同一台服务器上,后续我们会说明如何从外部对私有网络进行采集的方案。

Pod 是 Kubernetes 里最小粒度的服务单元,也是业务应用监控最关键的一环,接下来,我们为 Prometheus 制订一个简单发现 Pod 的配置。

# /srv/prometheus/prometheus.yml

scrape_configs:
  - job_name: kubernetes-cls
    # 使用 Kubernetes 的服务发现模块
    kubernetes_sd_configs:
      # 添加一个发现 Pod 资源的服务发现
      - role: pod
        # 注入 kubeconfig 文件路径
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml

使用 /targets API 能够看到发现了所有的 Pod 信息:

curl -ks 127.0.0.1:9090/api/v1/targets | jq

我们单独拿 Kubernetes 的 coredns 容器来看,这里通过 __meta_kubernetes_pod_container_name__meta_kubernetes_pod_container_port_name 标签过滤出两个标签集合:

Prometheus 服务发现:Kubernetes (上)

discoveredLabels 中所有 __meta_kubernetes_前缀的标签,都是发现模块从 Kubernetes 的 Pod 数据提取而来,我们来看一下这个 Pod 在 Kubernetes 的信息。

Prometheus 服务发现:Kubernetes (上)

可见,这些发现的键值对都是从 Pod 的自身信息转化而来,在 Prometheus 发现 Pod 的方式中采用如下的发现机制。

Pod 发现:发现 Pod 的 .status.podIP,并从 .spec.containers 内每个 .ports 提取 TCP 的端口号,一起组成一个 __address__ 作为 Target。如果没有额外声明容器端口,则使用空的默认 80 端口,在将必要的容器信息转换为标签发现。

Pod 标签转化的逻辑如下:

Prometheus Kubernetes Comment
__meta_kubernetes_namespace .metadata.namespace Pod 所属的 Namespace 名称
__meta_kubernetes_pod_name .metadata.name Pod 的名称
__meta_kubernetes_pod_ip .status.podIP Pod 分配的 IP 地址
__meta_kubernetes_pod_label_<labelname> .metadata.labels.<labelname> Pod 标签的键值对
__meta_kubernetes_pod_labelpresent_<labelname> .metadata.labels.<labelname> 如果标签名在 Pod 中存在,则值为 true
__meta_kubernetes_pod_annotation_<annotationname> .metadata.annotations.<annotationname> Pod 注解的键值对
__meta_kubernetes_pod_annotationpresent_<annotationname> .metadata.annotations.<annotationname> 如果注解名在 Pod 中存在,则值为 true
__meta_kubernetes_pod_container_init .status.initContainerStatuses 如果该容器是 Pod 的初始化容器,则值为 true
__meta_kubernetes_pod_container_name `.spec.containers .[].ports.[].name`
__meta_kubernetes_pod_container_id .status.containerStatuses.[].containerID 此 Pod 下该容器的 ID
__meta_kubernetes_pod_container_image .status.containerStatuses.[].image 此 Pod 下该容器的镜像
__meta_kubernetes_pod_container_port_name .spec.containers.[].ports.[].name 此 Pod 下该容器端口的名称
__meta_kubernetes_pod_container_port_number .spec.containers.[].ports.[].containerPort 此 Pod 下该容器端口的端口号
__meta_kubernetes_pod_container_port_protocol .spec.containers.[].ports.[].protocol 此 Pod 下该容器端口的协议
__meta_kubernetes_pod_ready .status.containerStatuses.[].ready Pod 是否准备完毕
__meta_kubernetes_pod_phase .status.phase Pod 的运行状态
__meta_kubernetes_pod_node_name .spec.nodeName 运行此 Pod 的 node 名称
__meta_kubernetes_pod_host_ip .status.hostIP 运行此 Pod 的 node 的主机 IP
__meta_kubernetes_pod_uid .metadata.uid 此 Pod 在内部的唯一标识 ID
__meta_kubernetes_pod_controller_kind .metadata.ownerReferences.[].kind 负责控制此 Pod 的调度类型
__meta_kubernetes_pod_controller_name .metadata.ownerReferences.[].name 负责控制此 Pod 的调度名称

代理模式

Kubernetes 的网络方案里,会为 Pod 分配私有的内部子网,一般而言,在集群外是没有路由能够访问到这个私有网络的。当然,如果使用 CNI 管理 SDN 路由,或者将集群内的路由反射出去,能够实现 Pod 容器可达 IP 的效果,不过,我们这里依旧还是讨论常规模式下的网络隔离问题。你也许注意到,即便在同一台主机,但是当前 Prometheus 仍无法对发现的 Pod 发起正常采集,因为这些 Pod 是在 KinD 拉起的 Docker 容器平面内。为了在这种情况下,可以将发现的 Pod 容器进行正常的采集,并考虑到采集使用了 HTTP 协议,我们可以在集群内部署一个正代服务,选择 Nginx、Squid、TinyProxy 等方案,既可以部署在 node 的 OS 上,也可以部署 Deployment 内。

Prometheus 服务发现:Kubernetes (上)

我们这里将 Squid 部署到 Kubernetes 的 Deployment,使用 nodePort 的方式把服务暴露出来,将采集请求建立成功。我们在 KinD 部署的主机上,准备以下的 manifest

# squid.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: squid
  namespace: squid
spec:
  replicas: 1
  selector:
    matchLabels:
      app: squid
  template:
    metadata:
      labels:
        app: squid
    spec:
      containers:
        - name: squid
          image: ubuntu/squid:latest
          ports:
            - containerPort: 3128
              name: squid
              protocol: TCP
          volumeMounts:
            - name: squid-config-volume
              mountPath: /etc/squid/squid.conf
              subPath: squid.conf
      volumes:
        - name: squid-config-volume
          configMap:
            name: squid-config
            items:
            - key: squid
              path: squid.conf

准备 Squid 的配置文件,注意,这里是放开所有的访问,如果你在生产环境采用这个模式,请配置严格的 ACL 策略。

acl all src 0.0.0.0/0.0.0.0
http_access allow all
http_access deny all
http_port 3128

然后,将其部署到 squid 的命名空间:

kubectl create namespace squid
kubectl -n squid create configmap squid-config --from-file=squid=squid.conf
kubectl -n squid apply -f squid.yaml
# 这里 nodePort 选择的 node 端口,正是我们 KinD 转发到外部的端口号
kubectl -n squid create service nodeport squid --tcp=3128:3128 --node-port=32000

# 测试 Squid 服务
curl -skIv 127.0.0.1:3128

我们把 Pod 的发现配置,添加使用正代的 proxy_url 的字段:

# /srv/prometheus/prometheus.yml

scrape_configs:
  - job_name: kubernetes-cls
    # 配置采集的 HTTP 请求使用的正代地址
    proxy_url: "http://127.0.0.1:3128"
    kubernetes_sd_configs:
      - role: pod
        # 如果连接 APIServer 的服务发现请求也需要使用正代,则在此配置
        # proxy_url: ""
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml

重新 reload 后,我们在 Graph 上能够看到能够正常响应采集的 Pod,up 指标都变为正常的 1 了。

Prometheus 服务发现:Kubernetes (上)

因为考虑到 KinD 的部署方便,所以接下来我们都会使用这种正代方式来采集容器内的目标,但它可能在其他环境里不是必须的。

范围覆盖

我们已经知道把 Pod 发现出来的方法了,但是 Kubernetes 还有多种的资源对象,不单单只有 Pod,下面列举目前 kubernetes_sd_configs 支持以下的 role

Prometheus role K8S Resource Comment
node nodes 集群的节点信息,一般用来获取 node 的 IP
service services Service 暴露的端口列表,一般做服务拨测监控
pod pods Pod 内的容器端口列表,一般做容器指标采集
endpoints endpoints Service 内的 Endpoints 列表,一般做服务指标采集
endpointslice endpointslices 一种新的 endpoints 信息,可以更高效地发现 Service 的关联端点组
ingress ingresses Ingress 地址,一般做服务拨测监控

我们按顺序来讲解除 Pod 外的其他 role,来分析它们的发现逻辑标签转化关系

node

node 发现:发现 Node 资源,从 .status.addresses 提取到 InternalIP 类型的 address 字段作为目标 IP,使用 kubelet 的端口信息 .status.daemonEndpoints.kubeletEndpoint.Port 作为目标的端口 Port,从而生产 IP:Port 作为目标地址 __address__

node 标签转化的逻辑如下:

Prometheus Kubernetes Comment
__meta_kubernetes_node_name .metadata.name Node 的名称
__meta_kubernetes_node_provider_id .spec.providerID 在供应方的 ID,一般在云服务场景使用
__meta_kubernetes_node_label_<labelname> .metadata.labels.<labelname> Node 标签的键值对
__meta_kubernetes_node_labelpresent_<labelname> .metadata.labels.<labelname> 如果标签名在 Node 中存在,则值为 true
__meta_kubernetes_node_annotation_<annotationname> .metadata.annotations Node 注解的键值对
__meta_kubernetes_node_annotationpresent_<annotationname> .metadata.annotations 如果注解名在 Node 中存在,则值为 true
__meta_kubernetes_node_address_<address_type> .status.addresses.[].type Node 地址信息,type<address_type>,值为对应的地址

配置 Prometheus 发现 node

scrape_configs:
  - job_name: kubernetes-cls
    # 直接采集 Node 在 KinD 内不需要 proxy
    kubernetes_sd_configs:
      - role: node
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml

可以看到,Prometheus 发现了集群内的所有 node,采集地址为 kubelet 的监听地址,但是所有的采集都是 down 状态,这是因为我们的 kubelet 关闭了匿名认证模式,对非认证请求做了拒绝。为了能够在这个条件下正常采集 kubelet,我们可以设置一个专门给 Prometheus 采集使用的 ServiceAccount 来通过认证。

# 创建监控专用的命名空间
kubectl create namespace monitoring
# 创建采集 kubelet 的 SA
kubectl -n monitoring create serviceaccounts prometheus
# 为该 SA 绑定 system:kubelet-api-admin 角色
kubectl create clusterrolebinding prometheus --user=prometheus --clusterrole=system:kubelet-api-admin --serviceaccount=monitoring:prometheus
# 获取该 SA 的 Token
TOKEN=$(kubectl -n monitoring create token prometheus)
# 打印获得的 token
echo $TOKEN

我们把这个 Token 加入到采集配置:

scrape_configs:
  - job_name: kubernetes-cls
    kubernetes_sd_configs:
      - role: node
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml
    # kubeclt API 使用 HTTPS 协议,因此要指定使用 https
    scheme: https
    tls_config:
      # 因为非权威自签 CA,这里选择忽略安全
      insecure_skip_verify: true
    # 将入采集时使用的 Token
    authorization:
      credentials: eyJhbGciOiJSUzI1NiIsImtpZCI6IlEwemZJbWI0WVVGQkp1LXQwZThsUU90R2RHLXFzSGdpXzBVZnBwTjZxY2sifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzAxMzQwMDUxLCJpYXQiOjE3MDEzMzY0NTEsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJtb25pdG9yaW5nIiwic2VydmljZWFjY291bnQiOnsibmFtZSI6InByb21ldGhldXMiLCJ1aWQiOiI1MTYwYjk2Yy0xYjhmLTRhOTQtYmJlNC1hNDIzY2M4MjZiNmMifX0sIm5iZiI6MTcwMTMzNjQ1MSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Om1vbml0b3Jpbmc6cHJvbWV0aGV1cyJ9.eNZO4EdTrXm2E-cbHNd25RNka09rWt3Zf9SznwnAFLZ1FgwvDwd7-_mWF0cgHQJkxrwtuq8fEJ5r-ePxg93pSNviMMfJflCtOPd4TfRcCIgjEk34lTFZzpG-zMSamg26icaAu6mzRhm26SwNh1YNbMnUWbt2SLsA143y9_x3BPcnzE_KxcxcfHs4F0OwngrserpEhyaoGKwf1Mwck7o-DMbqhpHoE-5mT10ApW88Z_YZuG9A1Z6mLKBktossijPBOwiIPS_QjHZ7dRdyvaia9mf-iA7pZkii5yS335mL9ktQ5_yv7A3G_cKGNgzHx3nGFPpE3gp_wsVPo5lqWyLVNA

查看采集结果,能看到 up 指标正常激活。

Prometheus 服务发现:Kubernetes (上)

这里接入 Node 时,我们又重新使用了在介绍采集配置《Prometheus 监控使用:采集详解》时提到的的几个老朋友字段(schemetls_configauthorization),希望你还能记得它们。另外,我们也分享了如何在 1.27 版本的 Kubernetse 里创建采集 kubelet 的 Token 的方式,注意,Kubernetes 在 1.24 版本之前,创建了 ServiceAccount 会默认分配关联的 <ServiceAccount>-token Secret,这里有一个差异需要提醒你知道。

service

service 发现:发现 Service 资源,从 .metadata.name 获取服务的名称 Name,从 .metadata.namespace 获取所在的命名空间 Namespace,遍历 .spec.ports 获取 TCP 的端口 Port,最终根据 Kubernetes 的 FQDN 格式拼接为 <Name>.<Namespace>.svc:<Port> 作为 __address__ 发现给采集流程。

service 标签转化的逻辑如下:

Prometheus Kubernetes Comment
__meta_kubernetes_namespace .metadata.namespace Service 所属的 Namespace 名称
__meta_kubernetes_service_annotation_<annotationname> .metadata.annotations.<annotationname> Service 注解的键值对
__meta_kubernetes_service_annotationpresent_<annotationname> .metadata.annotations.<annotationname> 如果注解名在 Service 中存在,则值为 true
__meta_kubernetes_service_cluster_ip .spec.clusterIP Service 的集群 IP 地址,可能是 serviceSubnet 分配或者外部 LB 地址
__meta_kubernetes_service_loadbalancer_ip .spec.loadBalancerIP Service 接入的 LB 地址
__meta_kubernetes_service_external_name .spec.externalName 接入的外部地址,可以供外部地址转发到服务
__meta_kubernetes_service_label_<labelname> .metadata.labels.<labelname> Service 标签的键值对
__meta_kubernetes_service_labelpresent_<labelname> .metadata.labels.<labelname> 如果标签名在 Service 中存在,则值为 true
__meta_kubernetes_service_name .metadata.name Service 的名称
__meta_kubernetes_service_port_name .spec.ports.[].name 端口列表里当前端口的名称
__meta_kubernetes_service_port_number .spec.ports.[].port 端口列表里当前端口的端口号
__meta_kubernetes_service_port_protocol .spec.ports.[].protocol 端口列表里当前端口的协议
__meta_kubernetes_service_type .spec.type Service 类型

配置 Prometheus 发现 node

scrape_configs:
  - job_name: kubernetes-cls
    proxy_url: "http://127.0.0.1:3128"
    kubernetes_sd_configs:
      - role: service
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml

此时,Prometheus 将所有的 Service 的 TCP 协议端口都发现了,但是我们很少直接去采集服务的指标,在 Endpoint 后端,它们可能转发了多个 Pod。所以,我们通常会使用拨测的形式,对发现的服务地址进行检测,保证对外服务的有效性,并不会直接去采集服务的指标,这一点,我们会在下面的场景分享中详细说道说道。

endpoints

在 Kubernetes 内,Endpoints 是负责将 Service 关联到具体 Pod 的资源对象,通过选择条件 (Selector) 来过滤需要关联的 Pod,承接对应 Service 的请求。因此,不同于上述的 Service,Endpoint 可以具体到服务提供的单元,因此更有监控的意义。在实际应用中,Endpoints 也是核心的监控对象,毕竟 Pod 可能有功能性的组件,如 SideCar 服务或者 Kubernetes 自身的 Job Container 等,但 Endpoints 关联的 Pod,一定是需要响应 Service 引入的服务请求,监控的覆盖价值更大。

Prometheus 服务发现:Kubernetes (上)

endpoints 发现:发现 Endpoints 资源,从 .subsets.addresses.[].targetRef 中拿到关联 Pod 的 Namespace 与 Name,然后在该命名空间内获取 Pod 资源,提取 Pod 的 .status.podIP 作为地址的 IP,再从 Pod 的容器端口列表遍历与 Endpoint 对应的端口号 Port,最终组成 <IP>:<Port> 作为发现目标的 __address__ 标签。注意,Endpoint 一端是 Service,所以自然会去获取对应 Service 的信息,也会同步到发现标签内,而另一端 Endpoint 的 targetRef,一般都是 Pod,所以会重新去获取这些关联 Pod 的信息,补充到发现标签内。

endpoints 标签转化的逻辑如下:

Prometheus Kubernetes Comment
__meta_kubernetes_pod_* - role: pod 一致
__meta_kubernetes_service_* - role: service 一致
__meta_kubernetes_namespace .metadata.namespace Endpoint 所属的 Namespace 名称
__meta_kubernetes_endpoints_name .metadata.name Endpoint 的名称
__meta_kubernetes_endpoints_label_<labelname> .metadata.labels.<labelname> Endpoint 标签的键值对
__meta_kubernetes_endpoints_labelpresent_<labelname> .metadata.labels.<labelname> 如果标签名在 Endpoint 中存在,则值为 true
__meta_kubernetes_endpoints_annotation_<annotationname> .metadata.annotations.<annotationname> Endpoint 注解的键值对
__meta_kubernetes_endpoints_annotationpresent_<annotationname> .metadata.annotations.<annotationname> 如果注解名在 Endpoint 中存在,则值为 true
__meta_kubernetes_endpoint_hostname .subsets.[].addresses.[].hostName Endpoint 的主机名
__meta_kubernetes_endpoint_node_name .subsets.[].addresses.[].nodeName 接收 Endpoint 地址的 node 名称
__meta_kubernetes_endpoint_ready `.subsets .[].notReadyAddresses`
__meta_kubernetes_endpoint_port_name - 从 Pod 的容器端口中获取名称
__meta_kubernetes_endpoint_port_protocol - 从 Pod 的容器端口中获取协议
__meta_kubernetes_endpoint_address_target_kind .subsets.[].addresses.[].targetRef.kind 关联后端的类型,大部分为 Pod
__meta_kubernetes_endpoint_address_target_name .subsets.[].addresses.[].targetRef.name 关联后端的名称

使用以下的配置进行采集:

scrape_configs:
  - job_name: kubernetes-cls
    proxy_url: "http://127.0.0.1:3128"
    kubernetes_sd_configs:
      # 使用 role 为 endpoints
      - role: endpoints
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml

加载后可以查询有 Pod 被采集,但是没有使用 role: pod 那么多,因为这里只有关联了 Service 的 Pod 被发现了,这些关系到服务的质量,是需要着重监控的单元。

endpointslice

看到 endpointslice,你是否困惑这长得像 endpoints 的家伙,两者之间有什么区别。我们简单的讲解一些 Kubernetes 的知识,以便理解这个 role 的使用场景。当我们使用 kubectl 列举 API 资源对象,能够看到两者的差异,其中 EndpointSlice 的 APIVERSION 是 discovery.k8s.io/v1,可以知道其主要是更新的一种注重服务发现能力的资源。

Prometheus 服务发现:Kubernetes (上)

我们查看两者的资源结果,能够看出,EndpointSlice 是封装了 Endpoints 之上的一层资源,正如名字所言是 Endpoint 的切片形式,为了减少集群在大规模时,太多 Endpoints 资源导致的臃肿问题,分拆出 Endpoints 单元,提高服务发现的性能和可伸缩性。所以,endpointslice 的服务发现结果其实与 endpoints 大同小异,如果你的 Kubernetes 版本支持,请尽可能地选择使用 endpointslice。

Prometheus 服务发现:Kubernetes (上)

endpointslice 发现:与 endpoints 类似,但没有再根据 Ready 状态来过滤后端目标,而是遍历 .endpoints.[].addresses 地址提取到目标 IP,然后直接在 .ports 获取到对应的端口 Port,最终组成了 <IP>:<Port> 作为 __address__ 传递给采集流程。至于关联的 Service 和 Pod,仍然会查询获取相关的信息,转化到发现标签内。

endpointslice 标签转化的逻辑如下:

Prometheus Kubernetes Comment
__meta_kubernetes_pod_* - role: pod 一致
__meta_kubernetes_service_* - role: service 一致
__meta_kubernetes_namespace .metadata.namespace EndpointSlice 所属的 Namespace 名称
__meta_kubernetes_endpointslice_name .metadata.name EndpointSlice 的名称
__meta_kubernetes_endpointslice_label_<labelname> .metadata.labels.<labelname> EndpointSlice 标签的键值对
__meta_kubernetes_endpointslice_labelpresent_<labelname> .metadata.labels.<labelname> 如果标签名在 EndpointSlice 中存在,则值为 true
__meta_kubernetes_endpointslice_annotation_<annotationname> .metadata.annotations.<annotationname> EndpointSlice 注解的键值对
__meta_kubernetes_endpointslice_annotationpresent_<annotationname> .metadata.annotations.<annotationname> 如果注解名在 EndpointSlice 中存在,则值为 true
__meta_kubernetes_endpointslice_address_target_kind .endpoints.[].targetRef.kind 后端目标的资源类型
__meta_kubernetes_endpointslice_address_target_name .endpoints.[].targetRef.name 后端目标的名称
__meta_kubernetes_endpointslice_address_type .addressType 协议簇类型
__meta_kubernetes_endpointslice_endpoint_conditions_ready .endpoints.[].conditions.ready 目标成员的准备状态
__meta_kubernetes_endpointslice_endpoint_conditions_serving .endpoints.[].conditions.serving 目标成员的服务状态
__meta_kubernetes_endpointslice_endpoint_conditions_terminating .endpoints.[].conditions.terminating 目标成员的退出状态
__meta_kubernetes_endpointslice_endpoint_topology_<topologyname> .endpoints.[].deprecatedTopology.<topologyname> Endpoint 拓扑内容的键值对
__meta_kubernetes_endpointslice_endpoint_topology_present_<topologyname> .endpoints.[].deprecatedTopology.<topologyname> 如果拓扑内容名在 Endpoint 中存在,则值为 true
__meta_kubernetes_endpointslice_port .ports.[].port 目标端口组的端口号
__meta_kubernetes_endpointslice_port_name .ports.[].name 目标端口组的名称
__meta_kubernetes_endpointslice_port_protocol .ports.[].protocol 目标端口组的协议

可见比 endpoints 多了更多的内部数据,并且在发现的性能和数据关联上,具有更好的表现,推荐使用这种方式来替换 endpoints。

scrape_configs:
  - job_name: kubernetes-cls
    proxy_url: "http://127.0.0.1:3128"
    kubernetes_sd_configs:
      # 使用 role 为 endpointslice
      - role: endpointslice
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml

ingress

ingress 发现:发现 Ingress 资源,提取每个路由规则的 Host 信息,作为 __address__ 传入采集流程。

ingress 标签转化的逻辑如下:

Prometheus Kubernetes Comment
__meta_kubernetes_namespace .metadata.namespace Ingress 所属的 Namespace 名称
__meta_kubernetes_ingress_name .metadata.name Ingress 的名称
__meta_kubernetes_ingress_label_<labelname> .metadata.labels.<labelname> Ingress 标签的键值对
__meta_kubernetes_ingress_labelpresent_<labelname> .metadata.labels.<labelname> 如果标签名在 Ingress 中存在,则值为 true
__meta_kubernetes_ingress_annotation_<annotationname> .metadata.annotations.<annotationname> Ingress 注解的键值对
__meta_kubernetes_ingress_annotationpresent_<annotationname> .metadata.annotations.<annotationname> 如果注解名在 Ingress 中存在,则值为 true
__meta_kubernetes_ingress_class_name .spec.ingressClassName Ingress 使用的控制器类型
__meta_kubernetes_ingress_scheme - 路由规则的协议
__meta_kubernetes_ingress_path .spec.rules.[].ingressRuleValue 路由路径

配置如下的采集:

scrape_configs:
  - job_name: kubernetes-cls
    proxy_url: "http://127.0.0.1:3128"
    kubernetes_sd_configs:
      # 使用 role 为 ingress
      - role: ingress
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml

Ingress 也是服务的入口方式,比 Service 有更高的服务提供高度,因此也往往不会直接监控采集,需要配合拨测来完成监控。

条件过滤

当你读到这里,已经表示你已经了解了 Prometheus 支持的所有 Kubernetse 服务发现资源,它们与我们使用 kubectl get -A 查询具体资源对象获取到的资源是一致的,但并不是所有场景都适合检索全部资源,对于那些超大规格的集群,一次性查询全部所有命名空间也会加大 APIServer 的性能损耗。所以,我们需要设置一些条件来限制检索范围和采集范围。

namespace 限制

按 Namespace 的维度,类似于 kubectl -n xxx get --watch 来建立 Watch,能够降低性能的消耗,加快资源检索的速率,例如我们需要查询 biz-1ops-1 两个 Namespace 的服务后端 Pod,可以如下设置服务发现配置:

scrape_configs:
  - job_name: kubernetes-cls
    kubernetes_sd_configs:
      - role: endpointslice
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml
        # 指定 Namespace
        namespaces:
          # 在此处添加需要监控的 Namespace 名称列表
          names:
            - 'biz-1'
            - 'ops-1'

如果我们的 Prometheus 是运行在 Kubernetes 上的,可以通过 namespaces.own_namespace 字段来自动获取 Prometheus 所在的 Namespace,其原理就是去读取本地文件 /var/run/secrets/kubernetes.io/serviceaccount/namespace,为此,我们可以如下设置额外采集我们 Prometheus 所在的 Namespace:

scrape_configs:
  - job_name: kubernetes-cls
    kubernetes_sd_configs:
      - role: endpointslice
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml
        namespaces:
          # 获取 Prometheus 自身所在的 Namespace,进行服务发现
          own_namespace: true
          names:
            - 'biz-1'
            - 'ops-1'

selectors 条件

Kubernets 库 client-goListWatch 内的事件处理函数,原生支持字段条件 (FieldSelector) 和标签条件 (LabelSelector),可以进一步减少 APIServer 的查询压力,Prometheus 利用配置中的 selectors 字段来注入条件规则。例如,对 biz-2 Namespace 下的 EndpointSlice 发现后端的 Pod 目标,这些目标的运行状态 (status.phase) 为 Running 且满足标签 appweb

scrape_configs:
  - job_name: kubernetes-cls
    kubernetes_sd_configs:
      - role: endpointslice
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml
        namespaces:
          names:
            - 'biz-2'
        selectors:
          - role: endpointslice
            # 标签条件
            label: 'app=web'
          - role: pod
            # 字段条件
            field: 'status.phase=Running'

selectorsclient-go 层面的 options 注入,需要理解一定的 kubernetes_sd_configs 代码逻辑,才能比较如预期地使用,如果你并不能从源码去分析 Prometheus 如何使用 client-go,那么不建议你贸然使用这个条件过滤。这里举一个例子,就拿我们部署的 Squid 为例,如果你使用如下的配置来采集,会产生让人困惑的结果:EndpointSlice 后端的 Pod 正常被发现,但是少了 Pod 的标签信息。这是 endpointslice 模式发现流程顺序的原因,先发现到对应的 EndpointSlice Address 后再查询 Pod 来补充标签信息,而 Pod 的 LabelSelector 不满足条件,因而缺失了这些标签,但依旧会使用 EndpointSlice Address 发现的标签来产生服务发现的结果。

scrape_configs:
  - job_name: kubernetes-cls
    kubernetes_sd_configs:
      - role: endpointslice
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml
        namespaces:
          names:
            - 'squid'
        selectors:
          - role: pod
            # 这里使用不存在的字段为条件
            label: 'app=not-exists'
            field: 'status.phase=Running'

但是,我们依旧会有类似的需求,那该如何实现呢?这个时候,希望你能想起《Prometheus 监控使用:Relabel》里介绍的 relabel 功能。

relabel 过滤

relabel_configs 可以控制 Targets 的保留 (Keep) 与丢弃 (Drop) 的行为,我们可以使用这些 action 来实现上面提到的,如何过滤 Squid EndpointSlice 的需求。

scrape_configs:
  - job_name: kubernetes-cls
    kubernetes_sd_configs:
      - role: endpointslice
        kubeconfig_file: /srv/prometheus/sd/kubernetes/kubeconfig.yml
        namespaces:
          names:
            - 'squid'
    relabel_configs:
      - action: keep
        source_labels: [__meta_kubernetes_pod_label_app]
        regex: 'not-exists'
      - action: keep
        source_labels: [__meta_kubernetes_pod_phase]
        regex: 'Running'

这样,结果就能满足我们的要求,并且不依赖 client-go 的代码实现,通过 relabel 简单易懂的链式动作,完成多种多样的组合条件。我们查看 /targets API 能够发现,不满足条件的 Pod 目标已经被丢弃:

{
  "status""success",
  "data": {
    "activeTargets": [],
    "droppedTargets": [
      {
        "discoveredLabels": {
          "__address__""172.0.1.2:3128",
          "__meta_kubernetes_endpointslice_address_target_kind""Pod",
          "__meta_kubernetes_endpointslice_address_target_name""squid-77df85f4b9-kfsgt",
          "__meta_kubernetes_endpointslice_address_type""IPv4",
          "__meta_kubernetes_endpointslice_annotation_endpoints_kubernetes_io_last_change_trigger_time""2023-11-30T09:42:15Z",
          "__meta_kubernetes_endpointslice_annotationpresent_endpoints_kubernetes_io_last_change_trigger_time""true",
          "__meta_kubernetes_endpointslice_endpoint_conditions_ready""true",
          "__meta_kubernetes_endpointslice_endpoint_conditions_serving""true",
          "__meta_kubernetes_endpointslice_endpoint_conditions_terminating""false",
          "__meta_kubernetes_endpointslice_label_app""squid",
          "__meta_kubernetes_endpointslice_label_endpointslice_kubernetes_io_managed_by""endpointslice-controller.k8s.io",
          "__meta_kubernetes_endpointslice_label_kubernetes_io_service_name""squid",
          "__meta_kubernetes_endpointslice_labelpresent_app""true",
          "__meta_kubernetes_endpointslice_labelpresent_endpointslice_kubernetes_io_managed_by""true",
          "__meta_kubernetes_endpointslice_labelpresent_kubernetes_io_service_name""true",
          "__meta_kubernetes_endpointslice_name""squid-6wndg",
          "__meta_kubernetes_endpointslice_port""3128",
          "__meta_kubernetes_endpointslice_port_name""3128-3128",
          "__meta_kubernetes_endpointslice_port_protocol""TCP",
          "__meta_kubernetes_namespace""squid",
          "__meta_kubernetes_service_label_app""squid",
          "__meta_kubernetes_service_labelpresent_app""true",
          "__meta_kubernetes_service_name""squid",
          "__metrics_path__""/metrics",
          "__scheme__""http",
          "__scrape_interval__""1m",
          "__scrape_timeout__""10s",
          "job""kubernetes-cls"
        }
      }
    ],
    "droppedTargetCounts": {
      "kubernetes-cls"1
    }
  }
}

总结

Kubernets 的服务发现基础使用的内容已经介绍完了,不管是任何 Kubernetes 架构,你都已经可以使用 Prometheus 快速地自动接入,让云原生监控全面护航容器服务。在下一篇文章里,我们会继续探讨 Kubernetes 的服务发现,在一些场景的使用案例,敬请期待。


原文始发于微信公众号(AcidPool):Prometheus 服务发现:Kubernetes (上)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/181747.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!