Prometheus وGrafana

مشروع: مراقبة خدمة من البداية إلى النهاية

18 دقيقة الدرس 10 من 32

مشروع: مراقبة خدمة من البداية إلى النهاية

كل مفهوم تعلمناه في الدروس التسعة السابقة — نموذج بيانات Prometheus وأنواع المقاييس وPromQL والمُصدِّرات واكتشاف الخدمات وقواعد التسجيل والتنبيه وAlertmanager وGrafana — موجود للإجابة على سؤال تشغيلي واحد: هل خدمتي بصحة جيدة، وهل سأعرف ذلك قبل أن يعرفه المستخدمون؟ يرشدك هذا المشروع الختامي خطوة بخطوة لبناء منظومة مراقبة كاملة لخدمة HTTP حقيقية: أضف القياسات، اجعل Prometheus يجمعها، اكتب استعلامات PromQL على مستوى الإنتاج، وابنِ لوحة Grafana يستطيع المهندسون المناوبون التصرف بناءً عليها، ثم اربط التنبيهات لتطلق في الوقت المناسب وللسبب الصحيح.

نظرة عامة على البنية

المنظومة التي ستبنيها تتكون من خدمة HTTP مكتوبة بـ Go ومُجهَّزة بمكتبة prometheus/client_golang، وخادم Prometheus يجمع مقاييسها، وAlertmanager لتوجيه الإخطارات، وGrafana للوحات المعلومات. في بيئة Kubernetes الإنتاجية ستضيف ServiceMonitor وتعتمد على Prometheus Operator؛ الأنماط هنا تنطبق مباشرةً على ذلك الإعداد.

End-to-end monitoring architecture HTTP Service /metrics :8080 Prometheus Scrape :8080/metrics Evaluate rules TSDB :9090 Alertmanager Route & silence :9093 Grafana Dashboards :3000 Explore / Alerts PagerDuty / Slack scrape fire alerts PromQL notify
بنية المراقبة الشاملة: تعرض خدمة HTTP مقاييسها، يجمعها Prometheus ويُقيِّم القواعد، يوجِّه Alertmanager التنبيهات، وتعرض Grafana كل شيء بصرياً.

الخطوة الأولى: تجهيز الخدمة بالقياسات

أضف الإشارات الأربع الذهبية كمقاييس Prometheus عند حدود التطبيق. عرِّف مقاييسك مرةً واحدة على مستوى الحزمة باستخدام promauto لضمان التسجيل التلقائي الآمن مع الخيوط المتوازية.

// main.go — instrument an HTTP service with four golden signals package main import ( "net/http" "strconv" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total HTTP requests by method, path, and status.", }, []string{"method", "path", "status"}) httpRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "HTTP request latency.", Buckets: prometheus.DefBuckets, }, []string{"method", "path"}) httpRequestsInFlight = promauto.NewGauge(prometheus.GaugeOpts{ Name: "http_requests_in_flight", Help: "Current number of requests being processed.", }) ) func instrument(path string, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { httpRequestsInFlight.Inc() defer httpRequestsInFlight.Dec() rw := &responseWriter{ResponseWriter: w, status: 200} start := time.Now() next(rw, r) dur := time.Since(start).Seconds() httpRequestsTotal.WithLabelValues(r.Method, path, strconv.Itoa(rw.status)).Inc() httpRequestDuration.WithLabelValues(r.Method, path).Observe(dur) } } func main() { http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/api/orders", instrument("/api/orders", ordersHandler)) http.ListenAndServe(":8080", nil) }
تصميم الحاويات (Buckets) مهم جداً. الحاويات الافتراضية في Prometheus (DefBuckets) مُعايَرة لزمن استجابة الشبكات النموذجية. إذا كان SLO خدمتك الداخلية هو 10ms عند p99، استخدم حاويات مخصصة مثل []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1}. المدرَّجات التكرارية عديمة الفائدة لقياس SLO إذا لم تتوافق حدود SLO مع حدود الحاوية.

الخطوة الثانية: إعداد الجمع (Scrape)

أضف مهمةً لخدمتك في prometheus.yml. في Kubernetes ستستخدم ServiceMonitor؛ محلياً يكفي إعداد ثابت لهذا المشروع. أبقِ scrape_interval عند 15 ثانية — الاستطلاع بتكرار أعلى نادراً ما يضيف قيمة ويزيد ضغط الكتابة على TSDB.

# prometheus.yml global: scrape_interval: 15s evaluation_interval: 15s rule_files: - /etc/prometheus/rules/*.yml alerting: alertmanagers: - static_configs: - targets: ["alertmanager:9093"] scrape_configs: - job_name: orders-api static_configs: - targets: ["orders-api:8080"] relabel_configs: - source_labels: [__address__] target_label: instance regex: '([^:]+)(?::\d+)?' replacement: '$1'

الخطوة الثالثة: قواعد التسجيل واستعلامات PromQL

اكتب قواعد تسجيل لكل تعبير يُغذِّي تنبيهاً أو لوحة معلومات. هذا يُحسب النتائج مسبقاً كي تُحمَّل اللوحات فوراً وتكون تقييمات التنبيهات رخيصة حتى على نطاق واسع.

# /etc/prometheus/rules/orders-api.yml groups: - name: orders_api_records interval: 15s rules: - record: job:http_requests:rate5m expr: | sum by (job, status) ( rate(http_requests_total[5m]) ) - record: job:http_error_ratio:rate5m expr: | sum by (job) (rate(http_requests_total{status=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m])) - record: job:http_request_duration_p99:rate5m expr: | histogram_quantile(0.99, sum by (job, le) ( rate(http_request_duration_seconds_bucket[5m]) ) ) - name: orders_api_alerts rules: - alert: HighErrorRate expr: job:http_error_ratio:rate5m{job="orders-api"} > 0.01 for: 5m labels: severity: page team: backend annotations: summary: "Error rate {{ $value | humanizePercentage }} on {{ $labels.job }}" description: "More than 1% of requests are returning 5xx. Check logs and downstream dependencies." runbook_url: "https://runbooks.internal/orders-api/high-error-rate" - alert: HighLatency expr: job:http_request_duration_p99:rate5m{job="orders-api"} > 0.5 for: 10m labels: severity: warning team: backend annotations: summary: "p99 latency {{ $value | humanizeDuration }} on {{ $labels.job }}"
شرط for ليس اختيارياً. إطلاق تنبيه بدون فترة for يُفعِّله عند أول استطلاع يتجاوز العتبة — وهو ما يُحدث إنذارات كاذبة بسبب استطلاع واحد سيئ أو اضطراب شبكة أو نشر متدرج. خمس دقائق لتنبيهات page و15 دقيقة لتنبيهات warning نقطة بداية معقولة. الفترات الأقصر تزيد الضجيج، والأطول تؤخر الاكتشاف.

الخطوة الرابعة: بناء لوحة Grafana

لوحة المعلومات الإنتاجية مُصممة للـتشخيص السريع: صف الحالة مع الأرقام الرئيسية في الأعلى، صف التفاصيل مع السلاسل الزمنية بالأسفل، وصف الموارد في أسفل اللوحة. يجب أن يصل المهندسون الواصلون الساعة 3 صباحاً إلى فرضية خلال 60 ثانية.

Grafana dashboard panel layout for the orders-api service ROW: Status (stat panels — last value) Request Rate 1.2k rps Error Ratio 0.3 % p99 Latency 48 ms In-Flight Requests 32 ROW: Latency Distribution (time-series) Request Rate by Status (stacked) Latency Heatmap (histogram) histogram_quantile over bucket series ROW: Saturation (in-flight, CPU throttle, queue depth) http_requests_in_flight go_goroutines / process_open_fds
تصميم لوحة المعلومات: ثلاثة صفوف — إحصاءات الحالة في الأعلى، السلاسل الزمنية للتأخير في المنتصف، إشارات الإشباع في الأسفل — مُحسَّنة للتشخيص في 60 ثانية.

خزِّن اللوحة كملف JSON في git لضمان إمكانية إعادة إنشائها. الاستعلامات الرئيسية لكل لوحة:

-- Panel: Request Rate (stat, last value) sum(job:http_requests:rate5m{job="orders-api"}) -- Panel: Error Ratio (stat, thresholds green=0 yellow=0.005 red=0.01) job:http_error_ratio:rate5m{job="orders-api"} -- Panel: p99 Latency (stat, unit=seconds, thresholds 0.1/0.5) job:http_request_duration_p99:rate5m{job="orders-api"} -- Panel: Request Rate by Status (time-series, stacked) sum by (status) (job:http_requests:rate5m{job="orders-api"}) -- Panel: Latency Heatmap (heatmap — use the raw histogram) sum by (le) ( rate(http_request_duration_seconds_bucket{job="orders-api"}[$__rate_interval]) ) -- Panel: In-Flight Requests (time-series, alert annotation overlay) http_requests_in_flight{job="orders-api"}
استخدم دائماً $__rate_interval في لوحات Grafana بدلاً من نافذة ثابتة مثل [5m]. تضبط Grafana هذا المتغير على ما لا يقل عن أربعة أضعاف فترة الاستطلاع، مما يضمن أن rate() تحصل دائماً على نقاط بيانات كافية بصرف النظر عن كيفية تكبير المستخدم لنطاق الوقت. النوافذ الثابتة تُنتج فجوات عند التكبير إلى ما يتجاوز حجم النافذة.

الخطوة الخامسة: توجيه Alertmanager والكتيبات التشغيلية

يُطلق Prometheus التنبيه — ويقرر Alertmanager من يُبلَّغ ومتى. استخدم قواعد الكبت (inhibition) لقمع تنبيهات warning عندما يكون تنبيه بدرجة page نشطاً على نفس الخدمة، كي يتلقى المهندسون المناوبون إشعاراً واحداً قابلاً للتنفيذ بدلاً من عاصفة من الرسائل.

# alertmanager.yml global: resolve_timeout: 5m slack_api_url: https://hooks.slack.com/services/T.../B.../xxx route: receiver: default-receiver group_by: [alertname, job] group_wait: 30s group_interval: 5m repeat_interval: 4h routes: - match: severity: page team: backend receiver: backend-pagerduty - match: severity: warning receiver: backend-slack receivers: - name: default-receiver slack_configs: - channel: "#alerts-dev" title: '{{ .GroupLabels.alertname }}' text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}' - name: backend-pagerduty pagerduty_configs: - routing_key: "YOUR_PAGERDUTY_KEY" description: '{{ .GroupLabels.alertname }}: {{ .CommonAnnotations.summary }}' - name: backend-slack slack_configs: - channel: "#alerts-warning" text: '{{ range .Alerts }}{{ .Annotations.summary }}\nRunbook: {{ .Annotations.runbook_url }}{{ end }}' inhibit_rules: - source_match: severity: page target_match: severity: warning equal: [alertname, job]

التحقق من المنظومة الكاملة

قبل إعلان منظومة المراقبة جاهزة للإنتاج، تحقق من كل طبقة من البداية إلى النهاية. استخدم promtool check rules في CI للكشف عن أخطاء صياغة PromQL قبل أن تصل إلى الإنتاج. تحقق من توجيه Alertmanager بـamtool config routes test — لا تكتشف مسارات التوجيه المُهيَّأة خطأً أثناء حادثة.

أكثر حالات الفشل الإنتاجي شيوعاً هي فقدان البيانات الصامت. مهلة استطلاع انتهت، أو امتلأ قرص TSDB، أو كسر قاعدة إعادة التسمية — كل هذا سيوقف تدفق المقاييس، وتنبيهاتك ستتوقف ببساطة عن الإطلاق (لن تُطلق بسبب غياب البيانات). نفِّذ مفتاح الرجل الميت (dead-man\'s switch): تنبيه Watchdog يُطلق باستمرار وغيابه في Alertmanager يُفعِّل بلاغاً. هذا النمط يكشف الأعطال الكاملة في خط أنابيب المراقبة التي لا تستطيع التنبيهات على مستوى الهدف اكتشافها.

أجرِ اختبار الفوضى: اقطع اتصال قاعدة البيانات الأمامية وتأكد من إطلاق تنبيه نسبة الأخطاء خلال خمس دقائق، وتوجيه Alertmanager إلى PagerDuty، وإظهار لوحة Grafana لارتفاع نسبة الأخطاء مع تعليق التنبيه على محور الزمن، وحل التنبيه تلقائياً عند استعادة قاعدة البيانات. إذا اجتازت المنظومة الشروط الأربعة، فهي جاهزة للإنتاج.