Оповещение об аномалиях в Prometheus

66
Оповещение об аномалиях в Prometheus
Оповещение об аномалиях в Prometheus

Изучаем, как настраивать оповещения об аномалиях для сезонных данных в Prometheus.

В Auto Trader мы стремимся создавать оповещения, которые большинство сможет использовать «из коробки», а не жестко прописывать их под конкретные сценарии. Мы хотим, чтобы вы могли развернуть сервис и сразу получить отдачу от платформы — особенно когда речь идет об отслеживании и оповещениях по вашим «Golden Signals» или управлении затратами.

Мы неплохо справились с очевидными оповещениями, основанными на порогах (например: CPU usage > CPU request в течение 15 минут), но поставили задачу улучшить оповещения об аномалиях, чтобы автоматически обнаруживать регрессии в производительности без жестко заданных порогов.

В этом посте я покажу, как использовать Prometheus вместе с Istio для обнаружения аномалий в ответах ваших сервисов. Реализация будет достаточно универсальной, чтобы применяться ко всем сервисам в вашей mesh-сети, а аналогичный подход можно использовать с любым rate-метриком.

Запись базовой линии

Мы будем работать с метрикой istio_request_duration_milliseconds_bucket (гистограмма), у которой много измерений и высокая кардинальность. Сначала сведём её к простой временной серии с помощью Recording Rule:

- record: "recorded:istio_request_duration_milliseconds_bucket:p95"
  expr: |
    histogram_quantile(
      0.95,
      sum by (destination_workload, le) (
        irate(istio_request_duration_milliseconds_bucket[1m])
      )
    )

Это даст нам линию P95 для каждого destination_workload. У вас может быть другая схема, тогда добавьте destination_workload_namespace или используйте destination_service для группировки по FQDN. В примере мы фокусируемся на 95-м процентиле ответа, но вы можете выбрать подходящий для вашей системы уровень.

Построение прогноза

Чтобы обнаружить аномалию, нужно спрогнозировать, где должна быть текущая величина. Самый простой прогноз — отложить временной ряд на период (например, на сутки или неделю назад). В Auto Trader мы взяли окно той же недели, но усреднили данные за последние три недели, чтобы единичные выбросы не искажали прогноз. Для этого нужно сначала накопить как минимум три недели данных по правилу recorded:istio_request_duration_milliseconds_bucket:p95 (либо использовать backfill).

Далее сгладим прогноз с помощью avg_over_time и сдвигаем его на половину окна:

avg_over_time(
  istio_request_duration_milliseconds_bucket:destination:rate1m[5m] offset 6d23h57m30s
)

И скорректируем с учётом роста/спада:

avg_over_time(
  istio_request_duration_milliseconds_bucket:destination:rate1m[5m] offset 6d23h57m30s
)
+
(
  avg_over_time(
    istio_request_duration_milliseconds_bucket:destination:rate1m[5m]
  )
  -
  avg_over_time(
    istio_request_duration_milliseconds_bucket:destination:rate1m[5m] offset 1w
  )
)

В итоге собираем всё в одну запись с медианой по трём недельным образцам:

- record: anomaly:istio_request_duration_milliseconds_bucket:p95:prediction
  expr: |
    quantile(0.5,
      label_replace(
        (
          avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[5m] offset 6d23h57m30s)
          +
          (avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[1w]) - avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[1w] offset 1w))
        ), "offset", "1w", "", ""
      )
    or
      label_replace(
        (
          avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[5m] offset 13d23h57m30s)
          +
          (avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[1w]) - avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[1w] offset 1w))
        ), "offset", "2w", "", ""
      )
    or
      label_replace(
        (
          avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[5m] offset 20d23h57m30s)
          +
          (avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[1w]) - avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[1w] offset 1w))
        ), "offset", "3w", "", ""
      )
    ) without(offset)

Вычисление Z-оценки

Теперь можно создать сигнал для оповещений. Z-оценка показывает, на сколько стандартных отклонений текущая величина удалена от среднего. Формула:

(current value - predicted value) / stddev_over_time(past week)

Запишем это в Recording Rule:

- record: anomaly:istio_request_duration_milliseconds_bucket:p95:zscore
  expr: |
    (
      (
        avg_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[5m])
        -
        avg_over_time(anomaly:istio_request_duration_milliseconds_bucket:p95:prediction[5m])
      )
      /
      stddev_over_time(recorded:istio_request_duration_milliseconds_bucket:p95[1w])
    )

Так мы получаем Z-оценку для каждого сервиса и легко замечаем как положительные, так и отрицательные выбросы.

Оповещения

Мы хотим оповещать только при ухудшении, то есть при Z-оценке > 3. Чтобы сгладить ложные срабатывания, добавим for: 5m:

- alert: AnomalyResponseTime
  expr: anomaly:istio_request_duration_milliseconds_bucket:p95:zscore > 3
  for: 5m
  labels:
    severity: warning
    source: '{{ $labels.destination_workload }}'
  annotations:
    summary: Anomaly - Response Time
    description: >
      Обнаружена потенциальная аномалия во времени ответа сервиса.
      Время ответа > 3σ от прогноза более 5 минут.

Backfilling

promtool умеет backfill’ить исторические данные по новым правилам:

kubectl exec -it prometheus-0 -- /bin/promtool tsdb create-blocks-from rules \
  --start $(($(date +%s)-86400)) \
  --end=$(date +%s) \
  --url http://127.0.9.1:9090 /etc/prometheus/alerting.rules/anomaly.yaml

После завершения ждём события компактации в логах Prometheus.

Заключение

Математика и статистика — не моя сильная сторона, но благодаря этой схеме можно настроить общее оповещение об аномалиях без жёстких порогов для каждого сервиса.