从队列深度到自动扩缩容:为 Kubernetes 编写自定义指标 Exporter

2026-07-15 42 预计阅读时间: 1 分钟
来源: kubernetes.io AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

Kubernetes 原生理解 CPU 和内存,但真实业务压力往往来自另一组信号:消息队列里积压了多少任务、最近一次批处理耗时多久、每个 Pod 维持了多少 WebSocket 连接。自定义指标 Exporter 的作用,就是把这些应用状态转换成 Prometheus 能抓取的时间序列,为查询、告警以及后续的 HPA 自动扩缩容提供数据基础。

Exporter 在链路中的位置

Exporter 本质上是一个职责单一的 HTTP 服务:从数据库、消息中间件或内部 API 读取状态,然后通过 /metrics 输出 Prometheus 文本格式。

完整链路通常是:

业务系统或外部数据源
        ↓
自定义 Exporter 的 /metrics
        ↓
Prometheus 定期抓取并存储
        ↓
Prometheus Adapter 暴露 Custom Metrics API
        ↓
HorizontalPodAutoscaler 使用自定义指标

如果能够修改业务代码,也可以直接嵌入 Prometheus 客户端并暴露 /metrics。独立 Exporter 更适合两类场景:数据来自应用进程之外,或者团队无法修改被监控程序。

需要特别注意:Exporter 只负责生产指标,Prometheus 负责抓取和存储;要让 HPA 使用这些指标,还需要 Prometheus Adapter 一类的指标适配器。部署 Exporter 并不会自动让指标出现在 Kubernetes Custom Metrics API 中。

先选对指标类型

指标类型选错后,PromQL 和扩缩容规则都会变得难以解释。

  • Counter 只能递增,适合累计处理任务数、请求数和错误数。重启后归零是允许的,查询时通常配合 rate() 使用。
  • Gauge 表示可升可降的当前值,适合队列深度、活跃连接数和缓存条目数。
  • Histogram 记录观测值分布,适合任务耗时和请求延迟,并可通过桶计算分位数。

命名建议采用 <namespace>_<name>_<unit> 的 snake_case 形式。例如:

worker_jobs_processed_total
worker_queue_depth
worker_job_duration_seconds

标签要保持低基数。status="success" 这类有限集合通常没有问题,但不要把任务 ID、用户 ID 或原始 URL 放进标签,否则时间序列数量会迅速膨胀。

可以这样实践:用 Go 实现最小 Exporter

下面示例可以直接运行。它使用模拟数据演示采集循环;接入生产环境时,应将 rand.Intn 替换为对消息队列、数据库或内部 API 的真实读取。

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

package main

import (
    "log"
    "math/rand"
    "net/http"
    "time"

    "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)
}

func collectMetrics() {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
        queueDepth.Set(float64(rand.Intn(50)))

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

func main() {
    go collectMetrics()

    mux := http.NewServeMux()
    mux.Handle("/metrics", promhttp.Handler())
    mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    server := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
    }

    log.Println("exporter listening on :8080")
    log.Fatal(server.ListenAndServe())
}

运行并检查输出:

go run .

在另一个终端执行:

curl -fsS http://localhost:8080/metrics | grep '^worker_'
curl -fsS http://localhost:8080/healthz

采集周期应短于 Prometheus 的抓取周期。例如 Prometheus 每 15 秒抓取一次时,Exporter 每 5 秒刷新一次可以让 Gauge 保持较新。不过不要为了追求实时性而频繁压测上游数据库;采集开销、数据新鲜度和上游限流需要一起评估。

容器化并接入 Prometheus Operator

使用多阶段构建可以避免把 Go 工具链带进生产镜像。创建 Dockerfile

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 -trimpath -ldflags="-s -w" -o /exporter .

FROM gcr.io/distroless/static:nonroot
COPY --from=builder /exporter /exporter
EXPOSE 8080
ENTRYPOINT ["/exporter"]

<registry> 替换为实际镜像仓库:

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

以下清单假设集群安装了 Prometheus Operator,并使用 monitoring 命名空间。release 标签必须与 Prometheus 资源的 serviceMonitorSelector 匹配,不能机械照搬。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-exporter
  namespace: monitoring
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: metrics
          readinessProbe:
            httpGet:
              path: /healthz
              port: metrics
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
---
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
---
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
      path: /metrics
      interval: 15s

应用并验证资源:

kubectl apply -f exporter.yaml
kubectl rollout status deployment/my-exporter -n monitoring
kubectl get pods,service,servicemonitor -n monitoring \
  -l app.kubernetes.io/name=my-exporter

还可以先绕过 Prometheus,直接检查集群内的 Exporter:

kubectl port-forward -n monitoring service/my-exporter 8080:8080
curl -fsS http://localhost:8080/metrics | grep '^worker_'

确认 Prometheus 已发现目标后,可以执行:

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

对于队列深度则直接查询:

worker_queue_depth

若目标显示为 DOWN,依次检查 Pod 是否就绪、Service selector 是否匹配、ServiceMonitor 引用的端口名称是否为 metrics,以及 release 标签是否被 Prometheus 的选择器接受。

上线前的边界与检查项

Exporter 进入生产环境前,至少要确认以下事项:

  • 指标类型与业务语义一致,没有把可下降的数据做成 Counter。
  • 标签值集合有限,不包含用户 ID、任务 ID等高基数字段。
  • 上游读取失败时保留旧值、置零还是不暴露指标,已经形成明确约定。
  • 采集请求设置了超时,避免上游故障拖死 /metrics
  • /metrics 不泄露敏感业务数据,网络策略只允许监控系统访问。
  • Prometheus 中能查到时间序列,并能解释重启、数据延迟和缺失值。
  • 使用 Prometheus Adapter 映射指标后,再配置 HPA;扩容阈值应结合队列消费能力和 Pod 启动时间压测确定。

一个可工作的 Exporter 只是数据链路的起点。真正可靠的自动扩缩容,还依赖稳定的指标语义、受控的标签基数、正确的 Adapter 规则,以及不会因瞬时尖峰反复扩缩的 HPA 行为配置。


相关推荐