Kubernetes بهصورت پیشفرض از CPU و حافظه آگاه است، اما در عمل خیلی از تصمیمهای مقیاسدهی بستگی به سیگنالهایی دارد که بیرون از این حدهای سادهاند: طول صفها، مدت اجرای آخرین batch، تعداد اتصالهای WebSocket فعال در یک pod و غیره. وقتی معیارهای داخلی کافی نیستند، یک custom exporter شکاف را پر میکند و آن سیگنالها را به Prometheus میدهد تا برای alerts و HorizontalPodAutoscaler قابل استفاده شوند. 🚀
یک exporter ساده وظیفهٔ مشخصی دارد: متریکها را روی endpoint /metrics در فرمت سادهٔ Prometheus منتشر کند تا Prometheus بتواند آن را scrape کند، سری زمانیها را ذخیره کند و برای alerting و scaling استفاده کند. گاهی برنامه را میتوان مستقیم instrument کرد (کتابخانهٔ Prometheus را embed کنیم)، اما وقتی منبع داده بیرونی است یا کنترل کد برنامه را ندارید، یک exporter مجزا منطقیتر است.
فرمت متریکها ساده است: یک خط برای هر متریک با نام، لیبلهای اختیاری و مقدار عددی. کتابخانههای client سریالسازی را انجام میدهند؛ کار شما انتخاب اینکه چه چیزی را اندازهگیری کنید و چه زمانی مقدار را بهروز کنید است.
قبل از نوشتن کد باید تصمیم بگیرید که با چه نوع سیگنال سروکار دارید. مدل دادهٔ Prometheus سه نوع اصلی دارد:
• Counter: فقط افزایش مییابد؛ مناسب برای مجموعها مثل درخواستها، jobهای پردازششده یا خطاها. هرگز برای مقادیری که ممکن است کاهش یابند از counter استفاده نکنید. 🔺
• Gauge: مقدار لحظهای که میتواند بالا و پایین برود؛ مناسب برای عمق صف، تعداد اتصالات فعال، اندازهٔ کش و غیره. ⚖️
• Histogram: توزیع مشاهدات را ثبت میکند (مثلاً latency) و اجازه میدهد صدکها (p99، p50) را محاسبه کنید، نه فقط میانگین. 📊
نامگذاری متریکها را با قرارداد snake_case و با پسوندهای متعارف مثل _total برای counter و _seconds برای زمانها رعایت کنید. مثال: worker_jobs_processed_total (counter)، worker_queue_depth (gauge)، worker_job_duration_seconds (histogram). نامهای تمیز در آینده کار دیباگ و alerting را خیلی سادهتر میکنند.
برای اکوسیستم Kubernetes، رایجترین انتخاب برای نوشتن exporter زبان Go و کتابخانهٔ github.com/prometheus/client_golang است. مثال گامبهگام زیر نشان میدهد چطور پروژه راهاندازی، متریکها تعریف، جمعآوری و در نهایت سرو شود.
# راهاندازی پروژه و گرفتن وابستگیها 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 متریکها را تعریف و ثبت کنید، سپس یک گوروتین برای poll کردن منابع و بهروزرسانی متریکها اجرا کنید و /metrics و /healthz را سرو کنید.
package main
import (
"log"
"math/rand"
"net/http"
"time"
"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)
}
func collectMetrics() {
for {
// مثال: خواندن از یک منبع واقعی مثل DB یا message broker
depth := float64(rand.Intn(50))
queueDepth.Set(depth)
start := time.Now()
// شبیهسازی پردازش یک job
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
jobDuration.Observe(time.Since(start).Seconds())
jobsProcessed.WithLabelValues("success").Inc()
time.Sleep(5 * time.Second)
}
}
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)
}
}
فاصلهٔ polling در collectMetrics باید کوتاهتر از scrape interval پرومتئوس باشد تا هر scrape مقدار تازه ببیند. در بسیاری از استقرارها scrape interval پیشفرض 15s است؛ در مثال بالا از 5s استفاده شده تا بهخوبی پوشش بدهد.
قبل از کانتینری کردن، محلی تست کنید:
# اجرا محلی go run . # سپس در یک ترمینال دیگر: curl http://localhost:8080/metrics | grep worker_
اگر خطهای HELP و TYPE و متریکهای شما ظاهر شدند، exporter آمادهٔ کانتینری شدن است. برای تصویر multi-stage Docker تا image نهایی کوچک بماند از الگوی زیر استفاده کنید:
# Dockerfile 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 -ldflags="-s -w" -o /exporter . FROM gcr.io/distroless/static:nonroot COPY --from=builder /exporter /exporter EXPOSE 8080 ENTRYPOINT ["/exporter"]
# Build & push (نمونه) docker build -t my-registry/my-exporter:v1.0.0 . docker push my-registry/my-exporter:v1.0.0
برای اجرا در کلاستر دو مانیفست اصلی لازم دارید: یک Deployment و یک Service. مثال خلاصهشده (بهصورت قابل ویرایش):
# deployment.yaml (نمونه)
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
annotations:
prometheus.io/scrape: "true" # حذف کنید اگر از ServiceMonitor استفاده میکنید
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
containers:
- name: exporter
image: my-registry/my-exporter:v1.0.0
ports:
- name: metrics
containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "100m"
memory: "64Mi"
---
# service.yaml
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: 8080
اگر از Prometheus Operator یا kube-prometheus-stack استفاده میکنید، بهتر است یک ServiceMonitor بسازید تا اپراتور آن را برای scrape پیدا کند:
# servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: my-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app.kubernetes.io/name: my-exporter
endpoints:
- port: metrics
path: /metrics
interval: 15s
اگر Prometheus شما بر پایهٔ annotations کار میکند (scrape via annotations)، آن annotations را در Pod template قرار دهید (همانطور که در نمونه بالا هست). اگر مطمئن نیستید از کدام مدل استفاده میشود، ServiceMonitor روش شفافتر و قابل اشکالزداییتری است. 🛠️
برای بررسی اینکه exporter در Prometheus ظاهر شده است، از port-forward به سرویس Prometheus استفاده کنید و صفحهٔ Targets را بررسی کنید. اگر وضعیت UP باشد، همه چیز درست است و میتوانید از metricها در alertها و HPAها استفاده کنید.
نکات عملی کوتاه و مفید:
• metric names و label keys را ثابت نگه دارید؛ تغییر آنها پس از انتشار باعث دردسر در alerting و داشبوردها میشود. 🧭
• برای متریکهای پر فرکانس از cardinality زیاد پرهیز کنید (labelهای با تعداد مقادیر بالا). این مورد حافظهٔ Prometheus و سرعت query را تحت تأثیر قرار میدهد. ⚠️
• اگر exporter را داخل یک کتابخانه قرار میدهید که بستههای دیگر هم ممکن است آن را instrument کنند، از prometheus.Register به جای MustRegister استفاده کنید و خطاها را مدیریت کنید تا ثبت تکراری باعث panic نشود.
اگر خواستید، میتوانم فایلهای نمونهٔ کامل و آمادهٔ deploy (main.go، Dockerfile، deployment/service/ServiceMonitor) را برای شما آماده کنم تا مستقیماً در CI/CD قرار بگیرند. آیا میخواهید آنها را دریافت کنید؟ 🙂