Tricks zur Verarbeitung von Metriken in Kapacitor

Heute stellt sich wahrscheinlich niemand mehr die Frage, warum es notwendig ist, Servicemetriken zu erfassen. Der nächste logische Schritt besteht darin, Warnmeldungen für die erfassten Metriken einzurichten, die Sie über für Sie geeignete Kanäle (E-Mail, Slack, Telegram) über Abweichungen in den Daten informieren. Im Online-Hotelbuchungsservice Ostrovok.ru Alle Metriken unserer Dienste werden in InfluxDB gestreamt und in Grafana angezeigt, wo auch grundlegende Alarmierungen konfiguriert sind. Für Aufgaben wie „muss etwas berechnen und damit vergleichen“ verwenden wir Kapacitor.

Tricks zur Verarbeitung von Metriken in Kapacitor
Kapacitor ist Teil des TICK-Stacks, der Metriken von InfluxDB verarbeiten kann. Es kann mehrere Dimensionen miteinander verbinden (joinen), aus den empfangenen Daten etwas Nützliches berechnen, das Ergebnis zurück in InfluxDB schreiben und eine Warnung an Slack/Telegram/Mail senden.

Der gesamte Stapel hat eine coole und detaillierte Dokumentation, aber es gibt immer nützliche Dinge, die in den Handbüchern nicht explizit angegeben sind. In diesem Artikel habe ich beschlossen, eine Reihe solcher nützlichen, nicht offensichtlichen Tipps zu sammeln (die grundlegende Syntax von TICKscipt wird beschrieben hier) und zeigen Sie deren Anwendung am Beispiel der Lösung eines unserer Probleme.

Lassen Sie uns gehen!

Float & Int, Berechnungsfehler

Ein absolutes Standardproblem, gelöst durch Casting:

var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))

Verwenden von default()

Wenn das Tag/Feld nicht ausgefüllt wird, kommt es zu Berechnungsfehlern:

|default()
        .tag('status', 'empty')
        .field('value', 0)

Verbindung ausfüllen (innen vs. außen)

Standardmäßig verwirft Join Punkte, an denen keine Daten vorhanden sind (innere).
Bei fill('null') wird ein äußerer Join ausgeführt, danach müssen Sie ein default() ausführen und die leeren Werte eintragen:

var data = res1
    |join(res2)
        .as('res1', 'res2)
        .fill('null')
    |default()
        .field('res1.value', 0.0)
        .field('res2.value', 100.0)

Hier gibt es noch eine Nuance. Wenn im obigen Beispiel eine der Reihen (res1 oder res2) leer ist, ist auch die resultierende Reihe (Daten) leer. Zu diesem Thema gibt es mehrere Tickets auf GitHub (1633, 1871, 6967) – wir warten auf Korrekturen und leiden ein wenig.

Verwenden von Bedingungen in Berechnungen (falls in Lambda)

|eval(lambda: if("value" > 0, true, false)

Die letzten fünf Minuten aus der Pipeline für den Zeitraum

Beispielsweise müssen Sie die Werte der letzten fünf Minuten mit denen der Vorwoche vergleichen. Sie können zwei Datenstapel in zwei separaten Stapeln erfassen oder einen Teil der Daten aus einem größeren Zeitraum extrahieren:

 |where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)

Eine Alternative für die letzten fünf Minuten besteht darin, einen BarrierNode-Knoten zu verwenden, der die Daten vor der angegebenen Zeit abschneidet:

|barrier()
        .period(5m)

Beispiele für die Verwendung von Go-Vorlagen in Nachrichten

Die Vorlagen entsprechen dem Format aus dem Paket Textvorlage, unten sind einige häufig auftretende Probleme aufgeführt.

ansonsten

Lasst uns die Dinge in Ordnung bringen und die Leute nicht unnötig mit Text provozieren:

|alert()
    ...
    .message(
        '{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
    )

Zwei Ziffern nach dem Dezimalpunkt in der Nachricht

Verbesserung der Lesbarkeit der Nachricht:

|alert()
    ...
    .message(
        'now value is {{ index .Fields "value" | printf "%0.2f" }}'
    )

Entfalten von Variablen in der Nachricht

Wir zeigen in der Nachricht weitere Informationen an, um die Frage „Warum schreit es?“ zu beantworten.

var warnAlert = 10
  |alert()
    ...
    .message(
       'Today value less then '+string(warnAlert)+'%'
    )

Eindeutige Alarmkennung

Dies ist notwendig, wenn die Daten mehr als eine Gruppe enthalten, da sonst nur eine Warnung generiert wird:

|alert()
      ...
      .id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')

Benutzerdefinierte Handler

Die große Liste der Handler umfasst exec, mit dem Sie Ihr Skript mit den übergebenen Parametern (stdin) ausführen können – Kreativität und nichts weiter!

Eine unserer Gewohnheiten ist ein kleines Python-Skript zum Senden von Benachrichtigungen an Slack.
Zunächst wollten wir ein Bild aus Grafana in einer autorisierungsgeschützten Nachricht versenden. Schreiben Sie anschließend „OK“ in den Thread zur vorherigen Warnung aus derselben Gruppe und nicht in eine separate Nachricht. Fügen Sie der Nachricht etwas später den häufigsten Fehler der letzten X Minuten hinzu.

Ein separates Thema ist die Verbindung mit anderen Diensten und alle durch den Alarm ausgelösten Aktionen (nur wenn Ihr Monitoring gut genug funktioniert).
Ein Beispiel für eine Handlerbeschreibung, wobei slack_handler.py unser benutzerdefiniertes Skript ist:

topic: slack_graph
id: slack_graph.alert
match: level() != INFO AND changed() == TRUE
kind: exec
options:
  prog: /sbin/slack_handler.py
  args: ["-c", "CHANNELID", "--graph", "--search"]

Wie debuggen?

Option mit Ausgabe ins Protokoll

|log()
      .level("error")
      .prefix("something")

Beobachten (CLI): kapacitor -url Host oder IP:9092 Protokollebene=Fehler

Option mit httpOut

Zeigt Daten in der aktuellen Pipeline:

|httpOut('something')

Ansehen (bekommen): Host oder IP:9092/kapacitor/v1/tasks/task_name/etwas

Ausführungsschema

  • Jede Aufgabe gibt einen Ausführungsbaum mit nützlichen Zahlen im Format zurück graphviz.
  • Wir nehmen einen Block Punkt.
  • In den Viewer einfügen, Lass uns genießen.

Wo sonst kann man sich etwas gönnen?

Zeitstempel in InfluxDB beim Rückschreiben

Beispielsweise richten wir einen Alarm für die Summe der Anfragen pro Stunde ein (groupBy(1h)) und möchten den Alarm aufzeichnen, der in Influxdb aufgetreten ist (um das Problem im Diagramm in Grafana deutlich darzustellen).

influxDBOut() schreibt den Zeitwert aus der Warnung in den Zeitstempel, sodass der Punkt im Diagramm früher/später geschrieben wird, als die Warnung eintraf.

Wenn Präzision erforderlich ist: Wir umgehen dieses Problem, indem wir einen benutzerdefinierten Handler aufrufen, der Daten mit dem aktuellen Zeitstempel in Influxdb schreibt.

Docker, Erstellen und Bereitstellen

Beim Start kann Kapacitor Aufgaben, Vorlagen und Handler aus dem in der Konfiguration im Block [load] angegebenen Verzeichnis laden.

Um eine Aufgabe korrekt zu erstellen, benötigen Sie Folgendes:

  1. Dateiname – wird zu ID/Skriptname erweitert
  2. Typ – Stream/Batch
  3. dbrp – Schlüsselwort zum Angeben, in welcher Datenbank + Richtlinie das Skript funktioniert (dbrp "supplier"."autogen")

Wenn für eine Batch-Aufgabe keine Zeile mit dbrp vorhanden ist, wird der gesamte Dienst nicht gestartet und dies wird ehrlich im Protokoll vermerkt.

Im Chronografen hingegen sollte diese Linie nicht vorhanden sein; es wird von der Schnittstelle nicht akzeptiert und gibt einen Fehler zurück.

Hack für den Container-Build: Dockerfile wird mit -1 beendet, wenn Zeilen mit //.+dbrp vorhanden sind, wodurch Sie den Grund für den Build-Fehler sofort verstehen.

Verbinden Sie eins mit vielen

Beispielaufgabe: Sie müssen das 95. Perzentil der Betriebszeit des Dienstes für eine Woche nehmen und jede Minute der letzten 10 mit diesem Wert vergleichen.

Sie können keinen Eins-zu-viele-Join durchführen. Letzter/Mittelwert/Medianwert einer Gruppe von Punkten wandelt den Knoten in einen Stream um. Der Fehler „Kann keine untergeordneten nicht übereinstimmenden Kanten hinzufügen: Batch -> Stream“ wird zurückgegeben.

Das Ergebnis des Batches wird als Variable im Lambda-Ausdruck ebenfalls nicht ersetzt.

Es besteht die Möglichkeit, die benötigten Zahlen aus dem ersten Batch per UDF in einer Datei zu speichern und diese Datei per Sideload zu laden.

Was haben wir damit gelöst?

Wir haben etwa 100 Hotelanbieter, jeder von ihnen kann mehrere Verbindungen haben, nennen wir es einen Kanal. Es gibt ungefähr 300 dieser Kanäle und jeder einzelne Kanal kann ausfallen. Von allen aufgezeichneten Metriken überwachen wir die Fehlerrate (Anfragen und Fehler).

Warum nicht Grafana?

In Grafana konfigurierte Fehlerwarnungen haben mehrere Nachteile. Manche sind kritisch, andere können, je nach Situation, ignoriert werden.

Grafana kann keine interdimensionalen Berechnungen und Warnungen durchführen, aber wir benötigen eine Rate (Anfragen-Fehler)/Anfragen.

Die Fehler sehen übel aus:

Tricks zur Verarbeitung von Metriken in Kapacitor

Und weniger schlimm, wenn man sich erfolgreiche Abfragen ansieht:

Tricks zur Verarbeitung von Metriken in Kapacitor

Okay, wir können die Rate im Dienst vor Grafana vorberechnen, und in einigen Fällen wird dies funktionieren. Aber nicht bei uns, denn für jeden Kanal gilt sein eigenes Verhältnis als „normal“ und die Alarme funktionieren nach statischen Werten (wir schauen mit den Augen, wir ändern es, wenn es oft Alarm gibt).

Dies sind Beispiele für „normal“ für verschiedene Kanäle:

Tricks zur Verarbeitung von Metriken in Kapacitor

Tricks zur Verarbeitung von Metriken in Kapacitor

Lassen wir den vorherigen Punkt außer Acht und gehen wir davon aus, dass das „normale“ Bild bei allen Anbietern ähnlich ist. Jetzt ist alles in Ordnung und wir kommen mit Warnungen in Grafana aus?
Wir können, aber wir wollen eigentlich nicht, weil wir uns für eine der Optionen entscheiden müssen:
a) Erstellen Sie für jeden Kanal eine große Anzahl von Diagrammen separat (und pflegen Sie diese mühsam)
b) ein Diagramm mit allen Kanälen belassen (und sich in bunten Linien und benutzerdefinierten Warnungen verlieren)

Tricks zur Verarbeitung von Metriken in Kapacitor

Wie haben Sie das gemacht?

Auch hier gibt es ein gutes Beispiel für den Einstieg in die Dokumentation (Berechnen von Raten über verbundene Reihen hinweg), können Sie einen Blick darauf werfen oder es als Grundlage für ähnliche Probleme verwenden.

Was wir letztendlich gemacht haben:

  • Fügen Sie in wenigen Stunden zwei Episoden hinzu, gruppieren Sie sie nach Kanälen.
  • wir füllen die Reihe nach Gruppen aus, wenn keine Daten vorhanden waren;
  • Vergleichen Sie den Median der letzten 10 Minuten mit den vorherigen Daten.
  • schreien, wenn wir etwas finden;
  • wir schreiben die berechneten Raten und die aufgetretenen Warnungen in Influxdb;
  • Senden Sie eine nützliche Nachricht an Slack.

Meiner Meinung nach ist es uns gelungen, am Ende alles, was wir erreichen wollten, auf die schönste Art und Weise zu erreichen (und mit benutzerdefinierten Handlern sogar noch ein bisschen mehr).

Sie können einen Blick auf github.com werfen Codebeispiel и Minimalschema (Graphviz) das empfangene Skript.

Beispiel für den resultierenden Code:

dbrp "supplier"."autogen"
var name = 'requests.rate'
var grafana_dash = 'pczpmYZWU/mydashboard'
var grafana_panel = '26'
var period = 8h
var todayPeriod = 10m
var every = 1m
var warnAlert = 15
var warnReset = 5
var reqQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."requests"'
var errQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."errors"'

var prevErr = batch
    |query(errQuery)
        .period(period)
        .every(every)
        .groupBy(1m, 'channel', 'supplier')

var prevReq = batch
    |query(reqQuery)
        .period(period)
        .every(every)
        .groupBy(1m, 'channel', 'supplier')

var rates = prevReq
    |join(prevErr)
        .as('req', 'err')
        .tolerance(1m)
        .fill('null')
    // заполняем значения нулями, если их не было
    |default()
        .field('err.value', 0.0)
        .field('req.value', 0.0)
    // if в lambda: считаем рейт, только если ошибки были
    |eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
        .as('rate')

// записываем посчитанные значения в инфлюкс
rates
    |influxDBOut()
        .quiet()
        .create()
        .database('kapacitor')
        .retentionPolicy('autogen')
        .measurement('rates')

// выбираем данные за последние 10 минут, считаем медиану
var todayRate = rates
    |where(lambda: duration((unixNano(now()) - unixNano("time")) / 1000, 1u) < todayPeriod)
    |median('rate')
        .as('median')

var prevRate = rates
    |median('rate')
        .as('median')

var joined = todayRate
    |join(prevRate)
        .as('today', 'prev')
    |httpOut('join')

var trigger = joined
    |alert()
        .warn(lambda: ("prev.median" - "today.median") > warnAlert)
        .warnReset(lambda: ("prev.median" - "today.median") < warnReset)
        .flapping(0.25, 0.5)
        .stateChangesOnly()
        // собираем в message ссылку на график дашборда графаны
        .message(
            '{{ .Level }}: {{ index .Tags "channel" }} err/req ratio ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}It is ok now{{ else }}
'+string(todayPeriod)+' median is {{ index .Fields "today.median" | printf "%0.2f" }}%, by previous '+string(period)+' is {{ index .Fields "prev.median" | printf "%0.2f" }}%{{ end }}
http://grafana.ostrovok.in/d/'+string(grafana_dash)+
'?var-supplier={{ index .Tags "supplier" }}&var-channel={{ index .Tags "channel" }}&panelId='+string(grafana_panel)+'&fullscreen&tz=UTC%2B03%3A00'
        )
        .id('{{ index .Tags "name" }}/{{ index .Tags "channel" }}')
        .levelTag('level')
        .messageField('message')
        .durationField('duration')
        .topic('slack_graph')

// "today.median" дублируем как "value", также пишем в инфлюкс остальные филды алерта (keep)
trigger
    |eval(lambda: "today.median")
        .as('value')
        .keep()
    |influxDBOut()
        .quiet()
        .create()
        .database('kapacitor')
        .retentionPolicy('autogen')
        .measurement('alerts')
        .tag('alertName', name)

Was ist die Schlussfolgerung?

Kapacitor eignet sich hervorragend zum Überwachen und Warnen einer Reihe von Gruppen, zum Durchführen zusätzlicher Berechnungen auf der Grundlage bereits aufgezeichneter Metriken, zum Ausführen benutzerdefinierter Aktionen und zum Ausführen von Skripten (udf).

Die Einstiegshürde ist nicht sehr hoch – probieren Sie es aus, wenn Grafana oder andere Tools Ihre Wünsche nicht ganz erfüllen.

Source: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz und VPS-VDS-Servern 🔥 Kaufen Sie zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server | ProHoster