为 Kubernetes 构建自定义指标导出器
Kubernetes 内置了对 CPU 和内存的感知能力,但大多数实际的扩缩容决策所依赖的信号完全超出了这个范围: 队列中有多少消息在等待、上一个批处理作业花费了多长时间、Pod 持有多少个活跃的 WebSocket 连接。 当内置指标不够用时,指标导出器(metrics exporter) 可以填补这一空白。
本文将从头开始编写一个指标导出器,将其打包为容器,并将其接入集群,以便 Prometheus —— 最终是 HorizontalPodAutoscaler —— 能够消费这些指标。
指标导出器实际上做什么
导出器是一个小型 HTTP 服务器,只有一个职责:将应用状态以文本形式暴露在 /metrics 端点上。
Prometheus 定期抓取该端点,存储时间序列数据,并使其可用于查询、告警和自动扩缩容规则。
在某些情况下,你可以直接在应用中添加监控 —— 嵌入 Prometheus 客户端库并从同一进程中暴露
/metrics —— 而不是运行单独的导出器。
当数据源位于应用外部或你无法控制应用代码时,独立的导出器更有意义。
Prometheus 期望的格式是纯文本 —— 每行一个指标,包含名称、可选标签和数值。 客户端库会处理序列化,因此实际上你只需要决定要测量什么,并在值变化时调用正确的函数。
选择要暴露的内容
在编写任何代码之前,最好先确定你要处理的信号类型。Prometheus 数据模型有三种主要类型:
Counter(计数器)只会增加。 它们是统计总量的正确工具:服务的请求数、处理的作业数、遇到的错误数。永远不要用计数器来表示可能下降的值。
Gauge(仪表)代表一个可以自由上升和下降的值的当前快照。 队列深度、活跃连接数和缓存大小都是仪表。
Histogram(直方图)记录观察值的分布,如请求延迟。 它们允许你计算百分位数(p99、p50)而不仅仅是平均值。
一旦你知道哪种类型适合你的信号,选择一个遵循 <namespace>_<name>_<unit>
约定的 snake_case 名称。
一个作业处理器可能会暴露 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 处理器连接在一起。
/healthz 路径与 /metrics 一起提供,使 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,但相同的模式适用于任何 OCI 兼容的构建工具,如 Buildah 或 Podman:
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
部署到集群
运行导出器只需要两个清单:一个管理 Pod 生命周期的 Deployment, 以及一个为 Prometheus 提供稳定抓取地址的 Service。
下面的示例使用 monitoring 命名空间,这是一起运行 Prometheus
和相关组件时的常见约定。根据你自己的集群设置调整命名空间。
Deployment 设置了适合轻量级边车式进程的保守资源限制,并使用 /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 的安装方式。
选项 1:Prometheus Operator(ServiceMonitor)
如果你使用 Prometheus Operator
或 kube-prometheus-stack Helm Chart 安装了 Prometheus,
那么在创建 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
选项 2:基于注解的发现
如果你的 Prometheus 使用基于注解的 Pod 发现,那么你需要在 Prometheus
配置中添加匹配的 scrape_config 规则 —— 请与管理你 Prometheus 安装的人确认它已到位。
无论你使用哪种抓取方法,都可以向 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 服务并打开目标页面,确认导出器已被发现:
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 exporters and integrations 页面,这是一个很好的起点。