为 Kubernetes 构建自定义指标导出器

Kubernetes 内置了对CPU和内存的监测能力,但实际扩缩容决策常常依赖这个范围之外的信号:队列中有多少待处理消息,上一次批处理任务花了多久,一个Pod维持着多少活跃WebSocket连接。当内置指标不够用时,指标导出器可以弥补这一缺口。

本文从零开始编写导出器,将它打包为容器并接入集群,让Prometheus以及最终的HorizontalPodAutoscaler能够消费这些指标。

指标导出器实际做什么

导出器是一个小型HTTP服务器,职责是通过/metrics端点以文本形式暴露应用状态。Prometheus定期抓取这个端点,存储时间序列数据,供查询、告警和自动扩缩容规则使用。

某些情况下可以直接给应用加入监测代码:在同一进程内嵌入Prometheus客户端库并提供/metrics,无需独立导出器。当数据源位于应用之外,或你无法控制应用代码时,独立导出器更合适。

Prometheus需要纯文本格式:每行一个指标,包括名称、可选标签和数值。客户端库处理序列化,因此通常只需决定测量什么,并在值变化时调用相应函数。

决定测量什么

编写代码之前,先确定信号的类型。本文介绍Prometheus数据模型中的三类主要指标:

  • **Counter(计数器)**只会增加,适合累计处理请求数、任务数和错误数。不能用它表示会下降的值。
  • **Gauge(仪表)**表示当前值的快照,可以自由增减。队列深度、活跃连接数和缓存大小都适用。
  • **Histogram(直方图)**记录观测值分布,例如请求延迟。除了平均值,还可以计算p99、p50等分位数。

选定类型后,采用snake_case并遵循<namespace>_<name>_<unit>命名惯例。任务处理器可暴露worker_jobs_processed_total(计数器)、worker_queue_depth(仪表)和worker_job_duration_seconds(直方图)。清晰名称能节省后续排障时间。

建立项目

Go的Prometheus客户端是Kubernetes生态中常见的导出器选择,主要因为大多数官方Kubernetes组件也使用它。先创建模块,添加依赖:

mkdir my-exporter && cd my-exporter
go mod init example.com/my-exporter
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttp

注册指标

创建main.go。首先声明指标,并向Prometheus默认注册表注册。注册告诉客户端库这些指标存在,使它们在尚未记录第一次观测之前就出现在输出中:

package main

import (
    "log"
    "net/http"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    jobsProcessed = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "worker_jobs_processed_total",
            Help: "Total number of jobs processed, partitioned by status.",
        },
        []string{"status"},
    )

    queueDepth = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "worker_queue_depth",
        Help: "Current number of jobs waiting in the queue.",
    })
    jobDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
        Name:    "worker_job_duration_seconds",
        Help:    "Time spent processing a single job.",
        Buckets: prometheus.DefBuckets,
    })
)

func init() {
    prometheus.MustRegister(jobsProcessed, queueDepth, jobDuration)
}

prometheus.MustRegister在重复注册时触发panic,让配置问题在启动时显露。若将导出器嵌入一个库,而其他包也会注册指标,优先使用prometheus.Register并自行处理错误。

采集真实数值

注册之后,需持续更新指标。可以随数据变化实时更新,也可以运行内部刷新循环。下面的轮询模式通过goroutine定期读取应用的数据源,并更新已注册指标。请把模拟数值替换为对数据库、内部API或消息代理的真实调用:

import (
    "math/rand"
    "time"
)
func collectMetrics() {
    for {
        // Replace these with real reads from your application.
        depth := float64(rand.Intn(50))
        queueDepth.Set(depth)

        start := time.Now()
        time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
        jobDuration.Observe(time.Since(start).Seconds())
        jobsProcessed.WithLabelValues("success").Inc()

        time.Sleep(5 * time.Second)
    }
}

轮询间隔(此处为五秒)应短于Prometheus抓取间隔,让每次抓取都读到新值。原文说明,多数集群部署的默认抓取间隔为十五秒,因此这里留有充足余量。

暴露端点

在main中接起采集循环与HTTP处理器。在/metrics旁提供/healthz,作为Kubernetes存活探针的目标,避免在健康检查路由暴露指标:

func main() {
    go collectMetrics()

    http.Handle("/metrics", promhttp.Handler())
    http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    log.Println("Listening on :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatalf("server error: %v", err)
    }
}

构建镜像前,先在本地检查输出:

go run .
curl http://localhost:8080/metrics | grep worker_

应当看到三个# HELP、# TYPE区块以及当前指标值。原文以这些输出作为导出器正常工作、可以容器化的检查依据。

构建容器镜像

多阶段构建缩小最终镜像,并避免把Go工具链带进生产环境。第一阶段编译静态链接二进制;第二阶段只把二进制复制到最小基础镜像。示例使用Docker,同样模式也适用于Buildah、Podman等兼容OCI的构建工具:

FROM golang:1.21-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /exporter .
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /exporter /exporter
EXPOSE 8080
ENTRYPOINT ["/exporter"]

distroless/static:nonroot没有shell和包管理器,默认以非root用户运行,通常无需额外配置即可符合多数集群安全策略。

将<registry>替换为自己的仓库地址,构建并推送镜像:

docker build -t <registry>/my-exporter:v1.0.0 .
docker push <registry>/my-exporter:v1.0.0

通常应由CI/CD流水线自动执行,而不是手工执行这些命令。

部署到集群

运行导出器只需两份清单:Deployment管理Pod生命周期,Service为Prometheus提供稳定抓取地址。若你的用例更适合抓取每一个Pod,也可以配置为逐Pod抓取。

下面使用monitoring命名空间,这是将Prometheus和相关组件放在一起运行的常见约定。按实际集群情况调整命名空间。

Deployment使用适合轻量级、类似sidecar进程的保守资源限制,并用/healthz作为存活探针:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-exporter
  namespace: monitoring
  labels:
    app.kubernetes.io/name: my-exporter
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: my-exporter
  template:
    metadata:
      labels:
        app.kubernetes.io/name: my-exporter
    spec:
      containers:
      - name: exporter
        image: <registry>/my-exporter:v1.0.0
        ports:
        - name: metrics
          containerPort: 8080
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          requests:
            cpu: 50m
            memory: 32Mi
          limits:
            cpu: 100m
            memory: 64Mi

Service把端口命名为metrics,后面的ServiceMonitor会引用该名称:

apiVersion: v1
kind: Service
metadata:
  name: my-exporter
  namespace: monitoring
  labels:
    app.kubernetes.io/name: my-exporter
spec:
  selector:
    app.kubernetes.io/name: my-exporter
  ports:
  - name: metrics
    port: 8080
    targetPort: metrics

应用两份清单:

kubectl apply -f deployment.yaml -f service.yaml

告诉Prometheus去哪里抓取

抓取配置取决于Prometheus的安装方式。

方案一:Prometheus Operator与ServiceMonitor

如果使用Prometheus Operator或kube-prometheus-stack Helm chart,创建ServiceMonitor前必须已有Operator在集群运行。release标签必须匹配Prometheus资源配置的标签选择器;原文标准Helm安装示例使用kube-prometheus-stack:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-exporter
  namespace: monitoring
  labels:
    release: kube-prometheus-stack
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: my-exporter
  endpoints:
  - port: metrics
    interval: 15s
    path: /metrics

方案二:基于注解的发现

如果Prometheus使用基于注解的Pod发现,需要在Prometheus配置里有匹配的scrape_config规则。向负责安装的人确认规则是否存在。

无论采用哪种方式,都可以在Pod模板添加以下三个注解。Prometheus Operator会忽略它们,而基于注解的配置会自动读取:

annotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "8080"     # omit if not using annotation-based discovery
  prometheus.io/path: "/metrics" # omit if not using annotation-based discovery

若不确定集群使用哪种配置,ServiceMonitor方式更明确,也更容易调试。

验证抓取

向Prometheus Service建立端口转发,打开targets页面,确认导出器已经被发现:

kubectl port-forward svc/prometheus-operated 9090 -n monitoring

访问http://localhost:9090/targets。my-exporter目标应处于UP状态。如果显示DOWN,检查ServiceMonitor的release标签是否匹配,并确认Pod正在运行:

kubectl get pods -n monitoring -l app.kubernetes.io/name=my-exporter
kubectl describe servicemonitor my-exporter -n monitoring

目标健康后,在表达式浏览器运行简单查询,确认数据正在流入:

rate(worker_jobs_processed_total{status="success"}[2m])

非零结果表示整条链路在工作:应用产生数据,Prometheus抓取数据,时间序列已存储且可查询。

下一步

可用的导出器只是基础。下一步是把这些指标提供给HorizontalPodAutoscaler,让工作负载根据实际驱动负载的信号扩缩容,而不局限于CPU。这需要指标适配器。常用的Prometheus Adapter会通过Kubernetes Custom Metrics API注册自定义指标。

注册之后,集群中的HorizontalPodAutoscaler可在metrics区块引用worker_queue_depth或worker_jobs_processed_total。

配置流程参阅多个指标与自定义指标的自动扩缩容。数据库、消息代理与云服务的现成导出器目录可从Prometheus导出器与集成开始。


作者:Victor David Effiok。原文:Building a Custom Metrics Exporter for Kubernetes,发表于2026-07-14,原页最后修改于2026-08-14。依据官方网站仓库许可证,采用CC BY 4.0。改动:完整中文翻译、链接和代码块排版调整;保留原文命令、代码注释及版本标签。

静态核对补充:示例使用golang:1.21-alpine,而依赖命令没有固定版本;不能保证今天下载的最新依赖与Go1.21兼容。应按目标环境固定并核对依赖版本。math/rand、time的导入代码展示的是增补片段,需合并到main.go已有import区块中,不能原样放在变量声明之后。原文未包含HPA适配器的完整部署配置,本文也不补造。本环境未编译、运行或部署这些示例。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容