Как создать собственный экспортер метрик для 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-exportergo mod init example.com/my-exportergo get github.com/prometheus/client_golang/prometheusgo get github.com/prometheus/client_golang/prometheus/promhttp

Регистрируем метрики

Создайте файл main.go. Сначала нужно объявить метрики и зарегистрировать их в реестре Prometheus по умолчанию. Регистрация сообщает библиотеке об их существовании, чтобы метрики появились в выдаче еще до записи первого наблюдения:

package mainimport (    "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 builderWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o /exporter .FROM gcr.io/distroless/static:nonrootCOPY --from=builder /exporter /exporterEXPOSE 8080ENTRYPOINT ["/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/v1kind: Deploymentmetadata:  name: my-exporter  namespace: monitoring  labels:    app.kubernetes.io/name: my-exporterspec:  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: v1kind: Servicemetadata:  name: my-exporter  namespace: monitoring  labels:    app.kubernetes.io/name: my-exporterspec:  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/v1kind: ServiceMonitormetadata:  name: my-exporter  namespace: monitoring  labels:    release: kube-prometheus-stackspec:  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-exporterkubectl 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».

Но если не хотите заморачиваться с самостоятельной настройкой, у нас есть инструкция, как завести автоскейлер. И еще одна — для тех, кому нужно масштабироваться по произвольным событиям. 

ссылка на оригинал статьи https://habr.com/ru/articles/1083808/