The Daily Newsstand · Free, Always
Friday, September 18, 2026

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

Translate

Всем привет! Нашел в блоге 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».

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

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.