مانیتورینگ سطح: متوسط

Alert درست با Prometheus و Alertmanager؛ خداحافظی با Alert Fatigue

چطور Alertهایی طراحی کنیم که تیم به آن‌ها اعتماد کند: Alert روی علامت به‌جای علت، Severity، Routing، Inhibition، اتصال به Mattermost و Runbook برای هر Alert.

۴ دقیقه مطالعه

یکی از تیم‌هایی که با آن کار کردم روزانه بیش از ۲۰۰ Alert در کانال Mattermost داشت. نتیجه قابل پیش‌بینی بود: هیچ‌کس دیگر کانال را نگاه نمی‌کرد و Alert واقعی بین سروصداها گم می‌شد. به این وضعیت Alert Fatigue می‌گویند و از نبودن مانیتورینگ هم خطرناک‌تر است، چون احساس امنیت کاذب می‌دهد.

بعد از بازطراحی Alertها و نوشتن Runbook برای هر کدام، میانگین زمان پاسخ به Incident حدود ۴۵٪ کم شد. در این مقاله همان روش را توضیح می‌دهم.

قانون اول: Alert روی علامت، نه علت

Alert بد (علت) Alert خوب (علامت)
CPU سرور بالای ۸۰٪ است زمان پاسخ p95 API بیشتر از ۱ ثانیه است
یک Pod ری‌استارت شد درصد خطای 5xx بیشتر از ۲٪ است
Memory بالای ۹۰٪ است صف پرداخت بیش از ۱۰ دقیقه پردازش نشده

CPU بالا ممکن است کاملاً طبیعی باشد؛ مثلاً زمان یک Batch Job شبانه. اما کاربری که ۵ ثانیه منتظر پاسخ می‌ماند، همیشه یک مشکل واقعی است. علت‌ها را در داشبورد ببینید و فقط برای علامت‌ها کسی را بیدار کنید.

نوشتن Rule درست

با Prometheus Operator، Ruleها را به‌صورت PrometheusRule تعریف می‌کنیم:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: api-alerts
  namespace: production
spec:
  groups:
    - name: api.rules
      rules:
        - alert: ApiHighErrorRate
          expr: |
            sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
              / sum by (service) (rate(http_requests_total[5m])) > 0.02
          for: 5m
          labels:
            severity: critical
            team: backend
          annotations:
            summary: "{{ $labels.service }}: error rate above 2%"
            description: "Current value: {{ $value | humanizePercentage }}"
            runbook_url: "https://wiki.example.ir/runbooks/api-high-error-rate"
            dashboard: "https://grafana.example.ir/d/api?var-service={{ $labels.service }}"

        - alert: PodCrashLooping
          expr: increase(kube_pod_container_status_restarts_total[15m]) > 3
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "{{ $labels.namespace }}/{{ $labels.pod }} is crash looping"
            runbook_url: "https://wiki.example.ir/runbooks/pod-crash-looping"

        - alert: DiskWillFillIn24h
          expr: predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[6h], 24*3600) < 0
          for: 30m
          labels:
            severity: warning
          annotations:
            summary: "Disk on {{ $labels.instance }} will be full within 24h"

نکته‌های کلیدی:

  • for: 5m یعنی شرط باید ۵ دقیقه‌ی پیوسته برقرار باشد. همین یک خط بیشتر Alertهای لحظه‌ای و بی‌اهمیت را حذف می‌کند.
  • predict_linear به‌جای «دیسک ۹۰٪ پر است» می‌پرسد «دیسک کی پر می‌شود؟». دیسک ۹۵٪ پری که ماه‌هاست ثابت مانده، Alert نمی‌خواهد.
  • runbook_url و dashboard در Annotationها یعنی کسی که Alert را می‌گیرد، با یک کلیک می‌داند کجا را نگاه کند و چه کند.

سطح‌بندی Severity

Severity معنی کانال
critical کاربران همین الان آسیب می‌بینند تماس یا پیامک به On-Call + کانال Incident
warning اگر رسیدگی نشود، طی ساعت‌ها مشکل می‌شود کانال تیم در ساعت کاری
info فقط برای اطلاع فقط داشبورد، بدون اعلان

Routing در Alertmanager

route:
  receiver: default
  group_by: [alertname, namespace, service]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers: [severity="critical"]
      receiver: oncall
      repeat_interval: 30m
    - matchers: [team="backend"]
      receiver: backend-team

receivers:
  - name: default
    slack_configs:
      - api_url: https://chat.example.ir/hooks/xxxxxxxx   # Mattermost incoming webhook
        channel: "#alerts"
        send_resolved: true
        title: '{{ .CommonLabels.alertname }} ({{ .Status }})'
        text: >-
          {{ range .Alerts }}• {{ .Annotations.summary }}
          📖 {{ .Annotations.runbook_url }}
          {{ end }}
  - name: oncall
    webhook_configs:
      - url: http://sms-gateway.monitoring.svc/alert   # your SMS/call bridge
  - name: backend-team
    slack_configs:
      - api_url: https://chat.example.ir/hooks/yyyyyyyy
        channel: "#backend-alerts"

inhibit_rules:
  # If a whole node is down, don't also page for every pod on it
  - source_matchers: [alertname="KubeNodeNotReady"]
    target_matchers: [severity=~"warning|critical"]
    equal: [node]

group_by باعث می‌شود اگر ۳۰ Pod یک سرویس هم‌زمان خطا بدهند، فقط یک پیام دریافت کنید. inhibit_rules هم وقتی یک Node کامل از دسترس خارج شده، جلوی ده‌ها Alert ثانویه را می‌گیرد.

Runbook: نیمه‌ی فراموش‌شده‌ی Alerting

Runbook یک صفحه‌ی ساده در Wiki یا Confluence است که برای هر Alert جواب این چهار سؤال را دارد:

  1. این Alert یعنی چه؟ چه چیزی از دید کاربر خراب است؟
  2. چطور بررسی کنم؟ کدام داشبورد، کدام کوئری و کدام دستور kubectl؟
  3. چطور موقتاً رفعش کنم؟ Rollback، Scale کردن یا ری‌استارت.
  4. چه کسی را خبر کنم؟ اگر حل نشد، مسئول بعدی کیست؟

قدم بعدی خودکار کردن همین Runbookهاست. مثلاً با n8n یا یک اسکریپت ساده، وقتی Alert «دیسک در حال پر شدن» می‌آید، لاگ‌های قدیمی خودکار پاک شوند و نتیجه در کانال گزارش شود.

چک‌لیست بازبینی Alertها

  • هر Alert با critical واقعاً به اقدام فوری نیاز دارد
  • همه‌ی Alertها for دارند
  • هر Alert یک runbook_url و یک لینک داشبورد دارد
  • Alertهایی که در ماه گذشته هیچ اقدامی در پی نداشتند حذف یا به info تبدیل شده‌اند
  • send_resolved: true فعال است تا تیم بداند مشکل برطرف شده

جمع‌بندی

هدف Alerting این نیست که همه‌چیز را گزارش کند؛ هدفش این است که فقط چیزهای مهم را، به آدم درست، همراه با راه‌حل برساند. با Alert روی علامت‌ها، سطح‌بندی، Routing درست و Runbook برای هر Alert، تیم دوباره به کانال Alertها اعتماد می‌کند.