Höchstwahrscheinlich hat heute niemand eine Frage, warum Servicemetriken erfasst werden müssen. Der nächste logische Schritt ist die Konfiguration des Alarms für die erfassten Metriken, der Sie über Abweichungen in den Daten in den für Sie geeigneten Kanälen (Mail, Slack, Telegramm) informiert. Im Online-Reservierungsservice von Hotels in
Ostrovok.ru werden alle Metriken unserer Services in InfluxDB eingegeben und in Grafana angezeigt. Dort wird auch der Basisalarm eingestellt. Für Aufgaben wie "Sie müssen etwas berechnen und damit vergleichen" verwenden wir Kapacitor.
Kapacitor ist Teil des TICK-Stacks, der Metriken aus InfluxDB verarbeiten kann. Er kann mehrere Dimensionen miteinander verbinden (Join), aus den empfangenen Daten etwas Nützliches berechnen, das Ergebnis in InfluxDB zurückschreiben, einen Alarm an Slack / Telegram / Mail senden.
Der gesamte Stack verfügt über eine coole und detaillierte
Dokumentation , aber es gibt immer nützliche Dinge, die nicht explizit in den Handbüchern angegeben sind. In diesem Artikel habe ich beschlossen, eine Reihe solcher nützlichen, nicht offensichtlichen Tipps (die grundlegende TICKscipt-Syntax wird
hier beschrieben) zu sammeln und anhand eines Beispiels zur Lösung eines unserer Probleme zu zeigen, wie sie angewendet werden können.
Lass uns gehen!
float & int, Rechenfehler
Absolut Standardproblem, es wird durch Kasten gelöst:
var alert_float = 5.0 var alert_int = 10 data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))
Standard () verwenden
Wenn das Tag / Feld nicht ausgefüllt ist, treten Fehler bei den Berechnungen auf:
|default() .tag('status', 'empty') .field('value', 0)
Füllen Sie den Join aus (innerer vs. äußerer)
Standardmäßig löscht der Join Punkte, an denen keine Daten vorhanden sind (inner).
Mit fill ('null') wird der Outer Join ausgeführt. Danach müssen Sie default () ausführen und die leeren Werte eingeben:
var data = res1 |join(res2) .as('res1', 'res2) .fill('null') |default() .field('res1.value', 0.0) .field('res2.value', 100.0)
Es gibt noch eine Nuance. Wenn im obigen Beispiel eine der Reihen (res1 oder res2) leer ist, ist auch die letzte Reihe (Daten) leer. Es gibt mehrere Tickets zu diesem Thema auf dem Github (
1633 ,
1871 ,
6967 ) - wir warten auf Korrekturen und leiden ein wenig.
Bedingungen in Berechnungen verwenden (wenn in Lambda)
|eval(lambda: if("value" > 0, true, false)
Die letzten fünf Minuten von der Pipeline im Zeitraum
Beispielsweise müssen Sie die Werte der letzten fünf Minuten mit der vorherigen Woche vergleichen. Sie können zwei Datenpakete mit zwei getrennten batch'ami nehmen oder einen Teil der Daten aus einem größeren Zeitraum entnehmen:
|where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)
Eine Alternative für die letzten fünf Minuten könnte die Verwendung des BarrierNode-Knotens sein, der Daten vor der angegebenen Zeit abschneidet:
|barrier() .period(5m)
Beispiele für die Verwendung von Go'sh-Mustern in Nachrichten
Die Vorlagen entsprechen dem Format des Pakets
text.template. Nachfolgend sind einige allgemeine Aufgaben aufgeführt.
wenn-sonst
Wir ordnen die Dinge, wir lösen nicht noch einmal Menschen mit dem Text aus:
|alert() ... .message( '{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}' )
Zwei Dezimalstellen in der Nachricht
Verbesserung der Lesbarkeit der Nachricht:
|alert() ... .message( 'now value is {{ index .Fields "value" | printf "%0.2f" }}' )
Variablen in der Nachricht erweitern
In der Nachricht werden weitere Informationen angezeigt, um die Frage zu beantworten: "Warum schreit es?"
var warnAlert = 10 |alert() ... .message( 'Today value less then '+string(warnAlert)+'%' )
Unique Alert Identifier
Das Richtige, wenn mehr als eine Gruppe in den Daten enthalten ist, andernfalls wird nur eine Warnung generiert:
|alert() ... .id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')
Benutzerdefinierte Handler
Die große Liste der Handler verfügt über exec, mit dem Sie Ihr Skript mit den übergebenen Parametern (stdin) ausführen können - Kreativität und mehr!
Eines unserer benutzerdefinierten Tools ist ein kleines Python-Skript zum Senden von Benachrichtigungen an Slack.
Zuerst wollten wir ein Bild von einer Grafik senden, die durch die Autorisierung in der Nachricht geschützt ist. Danach - Schreiben Sie OK in den Thread für die vorherige Warnung aus derselben Gruppe und nicht als separate Nachricht. Wenig später - um den häufigsten Fehler in den letzten X Minuten an die Nachricht anzuhängen.
Ein separates Thema ist die Kommunikation mit anderen Diensten und alle Aktionen, die durch eine Warnung ausgelöst werden (nur wenn Ihre Überwachung gut genug funktioniert).
Ein Beispiel für eine Beschreibung eines Handlers, wobei slack_handler.py unser selbst geschriebenes 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 debütieren?
Option zur Protokollausgabe
|log() .level("error") .prefix("something")
Beobachten Sie (cli): kapacitor -url
host-or-ip : 9092 logs lvl = error
Variante mit httpOut
Zeigt Daten in der aktuellen Pipeline an:
|httpOut('something')
Watch (get):
host-or-ip : 9092 / kapacitor / v1 / tasks / aufgabenname / something
Ausführungsschema
Wo sonst kann ich einen Rechen bekommen
influxdb timestamp on writeback
Zum Beispiel haben wir eine Warnung für die Summe der Anforderungen pro Stunde eingerichtet (groupBy (1h)) und möchten die aufgetretene Warnung in influxdb aufzeichnen (um die Tatsache eines Problems in der Grafik in grafana schön darzustellen).
influxDBOut () schreibt den Zeitwert von der Warnung zum Zeitstempel bzw. der Punkt auf dem Diagramm wird früher / später als die Warnung aufgezeichnet.
Wenn Genauigkeit 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 Tasks, Templates und Handler aus dem in der Konfiguration im Block [load] angegebenen Verzeichnis laden.
Um eine Aufgabe korrekt zu erstellen, sind folgende Dinge erforderlich:
- Dateiname - wird zu ID / Skriptname erweitert
- Typ - Stream / Charge
- dbrp - Schlüsselwort, um anzugeben, in welcher Datenbank + Richtlinie das Skript funktioniert (dbrp "supplier". "autogen")
Wenn in einer Batch-Task keine Zeile mit dbrp vorhanden ist, wird der gesamte Dienst den Start verweigern und ehrlich darüber im Protokoll schreiben.
In chronograf hingegen sollte diese Zeile nicht sein, sie wird nicht über die Schnittstelle akzeptiert und wirft einen Fehler aus.
Hack beim Erstellen des Containers: Dockerfile wird mit -1 beendet, wenn es Zeilen mit //.+dbrp gibt, wodurch der Grund für die Datei beim Erstellen des Builds sofort erkannt wird.
Schließe dich einem zu vielen an
Beispielaufgabe: Sie müssen das 95. Perzentil der Betriebszeit des Dienstes pro Woche nehmen und jede Minute der letzten 10 mit diesem Wert vergleichen.
Sie können nicht eins zu viele verbinden, last / mean / median nach Punktegruppe, um den Knoten in einen Datenstrom zu verwandeln. Der Fehler "Nicht übereinstimmende untergeordnete Kanten können nicht hinzugefügt werden: Batch -> Datenstrom" wird zurückgegeben.
Das Ergebnis von Batch als Variable in einem Lambda-Ausdruck wird ebenfalls nicht ersetzt.
Es besteht die Möglichkeit, die erforderlichen Nummern aus dem ersten Stapel über udf in eine Datei zu speichern und diese Datei über Sideload zu laden.
Was haben wir entschieden?
Wir haben ungefähr 100 Hotelanbieter, jeder von ihnen kann mehrere Verbindungen haben, nennen wir es einen Kanal. Es gibt ungefähr 300 dieser Kanäle, wobei jeder der Kanäle abfallen kann. Von allen aufgezeichneten Metriken überwachen wir die Fehlerrate (Anforderungen und Fehler).
Warum nicht Grafana?
In grafan konfigurierte Fehlermeldungen haben mehrere Nachteile. Manche kritisch, manche können je nach Situation die Augen schließen.
Grafana weiß nicht, wie man zwischen Dimensionen + Alert berechnet, aber wir benötigen eine Rate (Anfragen-Fehler) / Anfragen.
Fehler sehen bösartig aus:

Und bei erfolgreichen Anfragen weniger bösartig:

Okay, wir können die Rate im Service vorab vorab berechnen, und in einigen Fällen ist dies auch der Fall. Aber nicht bei uns, weil Für jeden Kanal wird das Verhältnis als „normal“ betrachtet und die Warnungen werden anhand statischer Werte ausgegeben (wir schauen mit unseren Augen, ändern uns, wenn sie oft alarmieren).
Dies sind Beispiele für "normal" für verschiedene Kanäle:


Wir vernachlässigen den vorherigen Absatz und gehen davon aus, dass alle Lieferanten ein "normales" Bild haben. Jetzt ist alles in Ordnung, und können wir mit Warnungen in Grafana auskommen?
Wir können, wollen aber wirklich nicht, weil wir eine der Optionen wählen müssen:
a) viele Diagramme für jeden Kanal separat zu erstellen (und sie schmerzlich zu begleiten)
b) Lassen Sie eine Karte mit allen Kanälen (und verlieren Sie sich in bunten Linien und eingestellten Warnungen)

Wie hast du das gemacht
Auch hier hat die Dokumentation ein gutes Startbeispiel (
Berechnen von Raten für verbundene Serien ), das Sie einsehen oder als Grundlage für ähnliche Aufgaben verwenden können.
Was haben Sie als Ergebnis gemacht:
- Verbinden Sie zwei Episoden in wenigen Stunden und gruppieren Sie sie nach Kanälen.
- Füllen Sie die Reihe nach Gruppen aus, wenn keine Daten vorhanden sind.
- Vergleichen Sie den Median der letzten 10 Minuten mit den vorherigen Daten.
- wir schreien, wenn wir etwas finden;
- berechnete Raten und aufgetretene Warnungen in influxdb schreiben;
- Sende eine nützliche Nachricht an Slack.
Meiner Meinung nach haben wir so gut wie möglich alles geschafft, was wir am Ausgang haben möchten (und noch ein bisschen mehr mit benutzerdefinierten Handlern).
Auf github.com sehen Sie den
Beispielcode und das
Minimaldiagramm (graphviz) des resultierenden Skripts.
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 ist sehr gut darin, Warnungen mit einer Reihe von Gruppen zu überwachen, zusätzliche Berechnungen für bereits aufgezeichnete Metriken durchzuführen, benutzerdefinierte Aktionen durchzuführen und Skripte (udf) auszuführen.
Die Einstiegsschwelle ist nicht sehr hoch - versuchen Sie es, wenn Grafana oder andere Tools Ihre Wunschliste nicht vollständig erfüllen.