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 行为配置。