[Перевод] Как создать собственный экспортер метрик для Kubernetes


Всем привет! Нашел в блоге Kubernetes интересную статью о том, как написать собственный экспортер метрик на Go и подключить его к Prometheus. Автор разбирает весь путь: от выбора показателей и первых строк кода до развертывания в кластере и проверки сбора данных. Перевел материал и делюсь.
Автор оригинала — Victor David Effiok. А сам оригинал лежит тут.
Kubernetes из коробки умеет отслеживать загрузку CPU и потребление памяти. Но на практике решения о масштабировании чаще зависят от показателей, которые выходят за эти узкие рамки: сколько сообщений ждет в очереди, сколько времени заняла последняя пакетная задача, сколько активных WebSocket-соединений поддерживает под. Когда встроенных метрик недостаточно, этот пробел помогает восполнить экспортер метрик.
В этой статье мы напишем экспортер с нуля, упакуем его в контейнер и подключим к кластеру так, чтобы его данные мог использовать 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 (гистограмму). Понятные имена в дальнейшем сэкономят время на отладке всей команде.
Подготавливаем проект
Для экспортеров в экосистеме Kubernetes чаще всего выбирают клиентскую библиотеку Prometheus для Go — во многом потому, что она же используется в большинстве официальных компонентов Kubernetes. Но, кстати, в сообществе набирает популярность OpenTelemetry SDK.
Для начала создадим модуль и добавим зависимость:
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 приложение паникует. Благодаря этому ошибки конфигурации обнаруживаются сразу при запуске, а не остаются незамеченными во время работы. Если вы встраиваете экспортер в библиотеку, в которой другие пакеты тоже будут добавлять сбор метрик, лучше использовать prometheus.Register и обрабатывать ошибку самостоятельно.
Собираем реальные значения
После регистрации нужно поддерживать значения метрик в актуальном состоянии. Можно обновлять их по мере изменения данных или запустить собственный внутренний цикл обновления.
Ниже показал цикл опроса: горутина периодически читает данные из источника, с которым работает приложение, и обновляет зарегистрированные метрики.
Замените имитацию данных реальными обращениями к базе данных, внутреннему API или брокеру сообщений:
import (
"math/rand"
"time"
)
func collectMetrics() {
for {
// Замените на свои
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, чтобы при каждом обращении тот получал свежее значение. В большинстве установок в кластерах интервал сбора по умолчанию составляет 15 секунд, так что запас приличный.
Настраиваем HTTP-эндпоинт
В функции main объединим цикл сбора данных и HTTP-обработчик. Помимо /metrics добавим путь /healthz: Kubernetes сможет использовать его для проверки жизнеспособности (liveness probe), а данные метрик не будут попадать в ответ проверки:
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 нет ни командной оболочки, ни пакетного менеджера, а процесс по умолчанию запускается от непривилегированного пользователя. Это позволяет выполнить требования большинства политик безопасности в кластерах без дополнительной настройки.
Соберите образ и отправьте его в реестр, заменив <registry> адресом своего реестра:
docker build -t <registry>/my-exporter:v1.0.0 .
docker push <registry>/my-exporter:v1.0.0Примечание: как правило, лучше автоматизировать эти действия в CI/CD-пайплайне, чем выполнять команды вручную.
Развертываем экспортер в кластере
Для запуска экспортера достаточно двух манифестов: Deployment управляет жизненным циклом пода, а Service предоставляет Prometheus стабильный адрес для сбора метрик. Возможно, вам удобнее собирать метрики с каждого пода напрямую. Если это соответствует вашей задаче, то можно настроить и такой вариант.
В примерах ниже используется пространство имен 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.
Вариант 1. Prometheus Operator (ServiceMonitor)
Если вы установили Prometheus с помощью Prometheus Operator или Helm-чарта kube-prometheus-stack, перед созданием ServiceMonitor оператор уже должен работать в кластере. Метка 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 обнаруживает поды по аннотациям, в его конфигурации должно быть соответствующее правило scrape_config. Уточните у тех, кто администрирует вашу установку Prometheus, настроено оно или нет.
Следующие три аннотации можно добавить в шаблон пода независимо от выбранного способа сбора метрик. 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 и откройте страницу целей мониторинга (targets), чтобы убедиться, что экспортер обнаружен:
kubectl port-forward svc/prometheus-operated 9090 -n monitoringПерейдите по адресу http://localhost:9090/targets. Таргет my-exporter должен отображаться в состоянии UP. Если вы видите DOWN, проверьте соответствие метки release у ServiceMonitor и убедитесь, что под работает:
kubectl get pods -n monitoring -l app.kubernetes.io/name=my-exporter
kubectl describe servicemonitor my-exporter -n monitoringКогда цель перейдет в рабочее состояние, выполните простой запрос в интерфейсе выражений Prometheus, чтобы убедиться, что данные поступают:
rate(worker_jobs_processed_total{status="success"}[2m])Ненулевой результат означает, что вся цепочка работает корректно: приложение формирует данные, Prometheus собирает их, а временные ряды сохраняются и доступны для запросов.
Что дальше
Работающий экспортер — это основа для дальнейших действий. Следующий логичный шаг — передать эти метрики в HorizontalPodAutoscaler, чтобы приложение масштабировалось по показателям, которые действительно определяют нагрузку, а не только по CPU. Для этого нужен адаптер метрик. Самый распространенный вариант — Prometheus Adapter: он регистрирует ваши пользовательские метрики в Kubernetes Custom Metrics API.
После регистрации любой HorizontalPodAutoscaler в кластере сможет напрямую ссылаться на worker_queue_depth или worker_jobs_processed_total в своем блоке metrics.
Пошаговая настройка описана в разделе «Автомасштабирование по нескольким и пользовательским метрикам». Если нужны готовые экспортеры для баз данных, брокеров сообщений или облачных сервисов, начните со страницы «Экспортеры и интеграции Prometheus».
Но если не хотите заморачиваться с самостоятельной настройкой, у нас есть инструкция, как завести автоскейлер. И еще одна — для тех, кому нужно масштабироваться по произвольным событиям.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.