Zum Inhalt springen

Anomalieerkennung für Application Monitoring

Anomalieerkennung für Anwendungsmetriken: statistische Methoden, Z-Scores, gleitende Durchschnitte, saisonale Zerlegung und Alarme ohne Alert Fatigue.

5 Min. Lesezeit
Zeitreihendiagramm mit normalen Verkehrsmustern und einer Anomalie, die von einem Erkennungsalgorithmus hervorgehoben wird

Statische Schwellenwerte sind der Standardansatz für Monitoring-Alerts: "Alarm, wenn die Fehlerrate 5% überschreitet" oder "Alarm, wenn die p99-Latenz 500ms überschreitet". Sie funktionieren, bis sich deine Verkehrsmuster ändern: Feiertagsspitzen, Wochenendflauten, allmähliches Wachstum. Eine Fehlerrate von 3% kann während eines Verkehrsspitze normal sein, aber um 3 Uhr morgens alarmierend, wenn nur interne Dienste deine API aufrufen.

Anomalieerkennung ersetzt statische Schwellenwerte durch dynamische Baselines. Statt "liegt dieser Wert über X?" fragt sie "ist dieser Wert ungewöhnlich für diesen Zeitpunkt und Kontext?". Das reduziert Fehlalarme bei erwarteten Verkehrsänderungen und erkennt gleichzeitig echte Vorfälle, die statische Schwellenwerte übersehen.

Statistische Baselines mit Z-Scores

Ein Z-Score misst, wie viele Standardabweichungen ein Wert vom Mittelwert entfernt ist. Ein Z-Score über 3 bedeutet, dass der Wert mehr als 3 Standardabweichungen vom Normalen abweicht — ungefähr eine 0.3%-Chance, natürlich aufzutreten. Das ist der einfachste Anomaliedetektor.

pypython
import numpy as np
from dataclasses import dataclass
 
@dataclass
class AnomalyResult:
    value: float
    mean: float
    std: float
    z_score: float
    is_anomaly: bool
 
def detect_zscore_anomaly(
    values: list[float],
    current: float,
    threshold: float = 3.0
) -> AnomalyResult:
    """Detect anomalies using z-score on recent history."""
    arr = np.array(values)
    mean = float(np.mean(arr))
    std = float(np.std(arr))
 
    # Avoid division by zero for constant metrics
    if std == 0:
        z_score = 0.0 if current == mean else float('inf')
    else:
        z_score = abs(current - mean) / std
 
    return AnomalyResult(
        value=current,
        mean=mean,
        std=std,
        z_score=z_score,
        is_anomaly=z_score > threshold
    )
 
# Example: error rate over the last 24 hours (1-minute intervals)
error_rates = [0.02, 0.018, 0.022, 0.019, ...]  # 1440 data points
current_rate = 0.085
 
result = detect_zscore_anomaly(error_rates, current_rate)
# z_score: 8.2, is_anomaly: True
# Mean 2%, current 8.5% → clearly anomalous

Exponentiell gleitender Durchschnitt (EMA)

Z-Scores behandeln alle historischen Daten gleich. EMA gewichtet neuere Werte stärker und passt sich schneller an Trends an. Der Glättungsfaktor alpha steuert, wie schnell sich die Baseline anpasst — ein höheres alpha bedeutet schnellere Anpassung.

tstypescript
class EMADetector {
  private ema: number | null = null;
  private emaVariance: number = 0;
 
  constructor(
    private alpha: number = 0.1,
    private threshold: number = 3.0
  ) {}
 
  update(
    value: number
  ): { isAnomaly: boolean; expected: number; deviation: number } {
    if (this.ema === null) {
      this.ema = value;
      return { isAnomaly: false, expected: value, deviation: 0 };
    }
 
    // Update EMA (exponentially weighted moving average)
    const prevEma = this.ema;
    this.ema = this.alpha * value + (1 - this.alpha) * this.ema;
 
    // Update variance estimate
    const diff = value - prevEma;
    this.emaVariance =
      this.alpha * diff * diff +
      (1 - this.alpha) * this.emaVariance;
 
    const stdDev = Math.sqrt(this.emaVariance);
    const deviation =
      stdDev > 0 ? Math.abs(value - prevEma) / stdDev : 0;
 
    return {
      isAnomaly: deviation > this.threshold,
      expected: prevEma,
      deviation,
    };
  }
}
 
// Usage: detect latency anomalies
const latencyDetector = new EMADetector(0.05, 3.0);
 
// Feed in normal values to establish baseline
for (const latency of normalLatencies) {
  latencyDetector.update(latency);
}
 
// Then check new values
const result = latencyDetector.update(850);
// { isAnomaly: true, expected: 120, deviation: 4.2 }

Saisonale Zerlegung

Viele Metriken haben tägliche und wöchentliche Muster. Das Anfragevolumen erreicht mittags seinen Höhepunkt und fällt um Mitternacht ab. Ein 50%-iger Verkehrsanstieg mittags ist normal; um 3 Uhr morgens ist er verdächtig. Die saisonale Zerlegung trennt die Komponenten Trend, Saisonmuster und Residuum.

pypython
from dataclasses import dataclass
import numpy as np
 
@dataclass
class SeasonalBaseline:
    hourly_means: np.ndarray  # 24 values (one per hour)
    hourly_stds: np.ndarray   # 24 values
    day_of_week_factors: np.ndarray  # 7 values
 
def build_seasonal_baseline(
    timestamps: list[float],
    values: list[float],
    min_weeks: int = 2
) -> SeasonalBaseline:
    """Build hourly baselines from historical data."""
    from datetime import datetime
 
    # Group values by hour-of-day
    hourly_buckets: dict[int, list[float]] = {h: [] for h in range(24)}
    dow_buckets: dict[int, list[float]] = {d: [] for d in range(7)}
 
    for ts, val in zip(timestamps, values):
        dt = datetime.fromtimestamp(ts)
        hourly_buckets[dt.hour].append(val)
        dow_buckets[dt.weekday()].append(val)
 
    hourly_means = np.array([
        np.mean(hourly_buckets[h]) if hourly_buckets[h] else 0
        for h in range(24)
    ])
    hourly_stds = np.array([
        np.std(hourly_buckets[h]) if len(hourly_buckets[h]) > 1 else 1
        for h in range(24)
    ])
 
    global_mean = np.mean(values) if values else 1
    day_factors = np.array([
        np.mean(dow_buckets[d]) / global_mean if dow_buckets[d] else 1
        for d in range(7)
    ])
 
    return SeasonalBaseline(hourly_means, hourly_stds, day_factors)
 
def is_seasonal_anomaly(
    baseline: SeasonalBaseline,
    current_value: float,
    hour: int,
    day_of_week: int,
    threshold: float = 3.0
) -> bool:
    """Check if value is anomalous for this time and day."""
    expected = baseline.hourly_means[hour] * baseline.day_of_week_factors[day_of_week]
    std = baseline.hourly_stds[hour]
 
    if std == 0:
        return current_value != expected
 
    z = abs(current_value - expected) / std
    return z > threshold

Aufbau einer Alerting-Pipeline

Anomalieerkennung ist nur nützlich, wenn sie umsetzbare Alerts erzeugt, ohne den Bereitschaftsdienst in Lärm zu ertränken.

tstypescript
// ❌ Alerting on raw anomaly detection output
// Every z-score > 3 triggers a page
// Result: 15 alerts per day, most are benign blips
// On-call engineer ignores all alerts after 2 days
 
// ✅ Multi-stage alerting pipeline
interface AlertPipeline {
  stage1_detection: {
    description: "Raw anomaly detection on 1-minute data";
    output: "Boolean: is this data point anomalous?";
  };
  stage2_confirmation: {
    description: "Require N consecutive anomalous points";
    config: { consecutiveRequired: 3; windowMinutes: 5 };
    output: "Boolean: sustained anomaly confirmed?";
  };
  stage3_severity: {
    description: "Classify severity based on deviation magnitude";
    config: {
      warning: "z-score 3-5 OR error rate increase 2-5x";
      critical: "z-score > 5 OR error rate increase > 5x";
    };
  };
  stage4_dedup: {
    description: "Suppress duplicate alerts for same incident";
    config: { silenceMinutes: 30; groupBy: "metric_name + service" };
  };
}
tstypescript
class AlertManager {
  private activeAlerts = new Map<string, { since: Date; count: number }>();
 
  shouldAlert(
    metricKey: string,
    isAnomaly: boolean,
    severity: "warning" | "critical"
  ): { alert: boolean; action: "page" | "slack" | "none" } {
    const existing = this.activeAlerts.get(metricKey);
 
    if (!isAnomaly) {
      // Anomaly resolved
      if (existing) {
        this.activeAlerts.delete(metricKey);
        return { alert: true, action: "slack" }; // Resolution notification
      }
      return { alert: false, action: "none" };
    }
 
    if (existing) {
      existing.count++;
      // Already alerted, suppress until silence window expires
      return { alert: false, action: "none" };
    }
 
    // New anomaly
    this.activeAlerts.set(metricKey, { since: new Date(), count: 1 });
 
    return {
      alert: true,
      action: severity === "critical" ? "page" : "slack",
    };
  }
}

Mehrere Signale kombinieren

Anomalieerkennung mit nur einer Metrik erzeugt False Positives. Die Kombination korrelierter Metriken verbessert die Genauigkeit drastisch.

tstypescript
interface CorrelatedCheck {
  primary: { metric: string; isAnomaly: boolean };
  supporting: Array<{ metric: string; isAnomaly: boolean }>;
  confidence: number;
}
 
function correlatedAnomalyCheck(
  checks: CorrelatedCheck
): { isReal: boolean; confidence: number } {
  if (!checks.primary.isAnomaly) {
    return { isReal: false, confidence: 0 };
  }
 
  const supportingAnomaly = checks.supporting.filter(
    (s) => s.isAnomaly
  ).length;
  const totalSupporting = checks.supporting.length;
 
  // Higher confidence when multiple related metrics are anomalous
  const correlationScore =
    totalSupporting > 0 ? supportingAnomaly / totalSupporting : 0;
 
  const confidence = 0.5 + 0.5 * correlationScore;
 
  return {
    isReal: confidence > 0.6,
    confidence,
  };
}
 
// Example: high error rate is more likely real if:
// - Latency is also elevated (correlated)
// - Traffic volume is normal (not a traffic spike causing errors)
// - CPU usage is elevated (resource pressure)
const check: CorrelatedCheck = {
  primary: { metric: "error_rate", isAnomaly: true },
  supporting: [
    { metric: "p99_latency", isAnomaly: true },
    { metric: "request_rate", isAnomaly: false },
    { metric: "cpu_usage", isAnomaly: true },
  ],
  confidence: 0,
};
// 2/3 supporting anomalies → confidence: 0.83 → alert

Die wichtigsten Erkenntnisse

  1. Statische Schwellenwerte brechen, wenn sich Verkehrsmuster ändern — ersetze "Alarm über X" durch dynamische Baselines, die Tageszeit- und Wochentagsmuster berücksichtigen
  2. Z-Scores sind der einfachste Anomaliedetektor — liegt ein Wert mehr als 3 Standardabweichungen vom Mittelwert entfernt, ist er ungewöhnlich; das erkennt 99.7% der echten Anomalien bei minimalen False Positives
  3. Fordere anhaltende Anomalien, bevor du alarmierst — ein einzelner anomaler Datenpunkt kann Rauschen sein; verlange 3+ aufeinanderfolgende anomale Punkte, um einen echten Vorfall zu bestätigen
  4. Kombiniere korrelierte Metriken, um False Positives zu reduzieren — hohe Fehlerrate plus hohe Latenz plus normaler Verkehr ist eher ein echter Vorfall als eine hohe Fehlerrate allein
  5. Saisonale Baselines verhindern tageszeitabhängige Fehlalarme — baue separate Baselines für jede Stunde und jeden Wochentag, um nicht auf normale tägliche Verkehrsmuster zu alarmieren
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX