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