Kapitel 4. Charting und Alarmierung
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Überwachung muss kein All-Inclusive-Vorschlag sein. Wenn du nur ein Maß für die Fehlerquote bei Endbenutzerinteraktionen hinzufügst, bei denen du keine Überwachung hast (oder nur eine Ressourcenüberwachung wie CPU-/Speicherauslastung), hast du bereits einen großen Schritt zum Verständnis deiner Software gemacht. Schließlich können CPU und Speicher gut aussehen, aber eine benutzerseitige API schlägt bei 5 % aller Anfragen fehl, und die Fehlerquote lässt sich viel leichter zwischen Entwicklungsorganisationen und ihren Geschäftspartnern kommunizieren.
Während in den Kapiteln 2 und 3 verschiedene Formen von Überwachungsinstrumenten behandelt wurden, stellen wir hier die Möglichkeiten vor, wie wir diese Daten effektiv nutzen können, um durch Alarmierung und Visualisierung Maßnahmen zu fördern. Dieses Kapitel behandelt drei Hauptthemen.
Zunächst sollten wir uns überlegen, was eine gute Visualisierung eines SLI ausmacht. Wir werden nur Diagramme aus dem weit verbreiteten Grafana Charting- und Alerting-Tool zeigen, weil es ein frei verfügbares Open-Source-Tool ist, das Datenquellen-Plug-ins für viele verschiedene Überwachungs-Tools hat (daher ist das Erlernen von Grafana eine weitgehend übertragbare Fähigkeit von einem Überwachungs-Tool zum anderen). Viele der Vorschläge gelten auch für Charting-Lösungen, die in Produkte von Anbietern integriert sind.
Als Nächstes gehen wir auf die Messungen ein, die den größten Nutzen bringen, und wie du sie visualisierst und darauf aufmerksam machst. Betrachte sie als eine Checkliste von SLIs, die du schrittweise einführen kannst. Inkrementelles Vorgehen kann sogar besser sein, als sie alle auf einmal einzuführen, denn wenn du einen Indikator nach dem anderen hinzufügst, kannst du wirklich studieren und verstehen, was er im Kontext deines Unternehmens bedeutet, und ihn so gestalten, dass er den größten Nutzen für dich bringt. Wenn ich das Network Operation Center eines Versicherungsunternehmens betreten würde, wäre ich viel erleichterter, wenn ich nur Indikatoren für die Fehlerquote bei der Bewertung von Policen und bei der Einreichung von Anträgen sehen würde, als wenn ich hundert Signale auf niedriger Ebene und kein Maß für die Unternehmensleistung sehen würde.
Ein schrittweiser Ansatz bei der Einführung von Warnmeldungen ist ebenfalls wichtig für die Vertrauensbildung. Wenn du zu schnell zu viele Warnmeldungen einführst, besteht die Gefahr, dass du die Techniker überforderst und zu einer "Warnmüdigkeit" führst. Du willst, dass die Ingenieure sich wohl fühlen, wenn sie mehr Alarme abonnieren, nicht dass sie stumm geschaltet werden! Das verschafft dir auch etwas Zeit, wenn du dich noch nicht an den Bereitschaftsdienst gewöhnt hast, und die Schulung der Techniker, wie sie auf einen Alarmzustand zu reagieren haben, hilft dem Team, ein Wissensreservoir über den Umgang mit Anomalien aufzubauen.
In diesem Kapitel geht es also darum, Ratschläge zu den SLIs zu geben, die so nah wie möglich an der Unternehmensleistung liegen (z. B. die API-Ausfallrate und die Antwortzeiten, die die Benutzer sehen), ohne an ein bestimmtes Unternehmen gebunden zu sein. Soweit wir uns mit Dingen wie der Heap-Auslastung oder den Dateideskriptoren befassen, handelt es sich dabei um eine ausgewählte Gruppe von Indikatoren, die höchstwahrscheinlich die direkte Ursache für eine Verschlechterung der Unternehmensleistung sind.
Die Nachbildung der NASA-Mission Control(Abbildung 4-1) sollte nicht das Endergebnis eines gut überwachten verteilten Systems sein. Es mag zwar optisch beeindruckend sein, Bildschirme an einer Wand aufzustellen und sie mit Dashboards zu füllen, aber Bildschirme sind keine Aktionen. Sie erfordern, dass jemand auf sie schaut, um auf einen visuellen Hinweis auf ein Problem zu reagieren. Ich denke, das macht Sinn, wenn du eine einzelne Instanz einer Rakete überwachst, bei der exorbitante Kosten und Menschenleben auf dem Spiel stehen. Deine API-Anfragen haben natürlich nicht die gleiche Bedeutung für ein einzelnes Ereignis.
Fast jeder Kennzahlensammler sammelt mehr Daten, als du zu einem bestimmten Zeitpunkt nützlich findest. Auch wenn jede Metrik unter bestimmten Umständen nützlich sein kann, ist es nicht hilfreich, jede einzelne aufzuzeichnen. Einige Indikatoren (z. B. maximale Latenz, Fehlerquote, Ressourcennutzung) sind jedoch starke Zuverlässigkeitssignale für praktisch jeden Java-Microservice (mit einigen Anpassungen der Warnschwellenwerte). Auf diese Indikatoren werden wir uns konzentrieren.
Und schließlich ist der Markt bestrebt, Methoden der künstlichen Intelligenz auf Überwachungsdaten anzuwenden, um automatisiert Einblicke in deine Systeme zu erhalten, ohne dass du dafür ein großes Verständnis der Warnkriterien und Leistungskennzahlen brauchst. In diesem Kapitel werden wir verschiedene traditionelle statistische Methoden und Methoden der künstlichen Intelligenz im Zusammenhang mit der Anwendungsüberwachung untersuchen. Du solltest ein solides Verständnis für die Stärken und Schwächen der einzelnen Methoden haben, damit du den Marketing-Hype durchschauen und die besten Methoden für deine Bedürfnisse anwenden kannst.
Bevor du weitergehst, solltest du dir überlegen, wie viele verschiedene Überwachungssysteme es auf dem Markt gibt und welche Auswirkungen das auf deine Entscheidungen hat, wie du den Code instrumentierst und Daten an diese Systeme übermittelst.
Unterschiede in den Überwachungssystemen
Der Grund, warum wir hier die Unterschiede in den Überwachungssystemen diskutieren, ist, dass wir gleich sehen werden, wie wir mit Prometheus Diagramme und Alarme erstellen können. Ein Produkt wie Datadog hat ein ganz anderes Abfragesystem als Prometheus. Beide sind nützlich. In Zukunft werden weitere Produkte auf den Markt kommen, deren Fähigkeiten wir uns noch gar nicht vorstellen können. Im Idealfall wollen wir, dass unsere Überwachungsinstrumente (die wir in unsere Anwendungen einbauen) auf diese Überwachungssysteme übertragbar sind, ohne dass der Anwendungscode geändert werden muss (abgesehen von einer neuen Binärabhängigkeit und einer registryweiten Konfiguration).
Die Art und Weise, wie Backend-Systeme zur verteilten Ablaufverfolgung Daten erhalten, ist in der Regel sehr viel einheitlicher als die Art und Weise, wie Metriksysteme Daten erhalten. Verteilte Tracing-Instrumentierungsbibliotheken können unterschiedliche Übertragungsformate haben, was eine gewisse Einheitlichkeit bei der Auswahl einer Instrumentierungsbibliothek im gesamten Stack erfordert, aber die Daten selbst sind von Backend zu Backend grundsätzlich ähnlich. Das macht intuitiv Sinn, weil es sich um Daten handelt: Verteiltes Tracing besteht im Grunde genommen aus Timing-Informationen pro Ereignis (kontextabhängig durch die Trace-ID zusammengefügt).
Kennzahlensysteme könnten nicht nur aggregierte Zeitinformationen darstellen, sondern auch Messgeräte, Zähler, Histogramme, Perzentile usw. Sie sind sich nicht einig darüber, wie diese Daten aggregiert werden sollen. Sie haben nicht die gleichen Möglichkeiten, um weitere Aggregationen oder Berechnungen zur Abfragezeit durchzuführen. Es besteht ein umgekehrtes Verhältnis zwischen der Anzahl der Zeitreihen, die eine Metrik-Instrumentierungsbibliothek veröffentlichen muss, und den Abfragefunktionen eines bestimmten Metrik-Backends, wie in Abbildung 4-2 dargestellt.
Als Dropwizard Metrics zum Beispiel ursprünglich entwickelt wurde, war das gängige Überwachungssystem Graphite, das keine Ratenberechnungsfunktionen hatte, wie sie in modernen Überwachungssystemen wie Prometheus verfügbar sind. Daher musste Dropwizard bei der Veröffentlichung eines Zählers die kumulierte Anzahl, die 1-Minuten-Rate, die 5-Minuten-Rate, die 15-Minuten-Rate usw. veröffentlichen. Und weil das ineffizient war, wenn man sich nie eine Rate ansehen musste, unterschied die Instrumentierungsbibliothek selbst zwischen @Counted
und @Metered
. Die Instrumentierungs-API wurde mit Blick auf die Möglichkeiten der heutigen Überwachungssysteme entwickelt.
Heutzutage muss eine Bibliothek für Metrik-Instrumente, die für mehrere Ziel-Metriksysteme veröffentlicht werden soll, diese Feinheiten berücksichtigen. Ein Micrometer Counter
wird Graphite in Form einer kumulativen Anzahl und verschiedener Bewegungsraten präsentiert, Prometheus jedoch nur als kumulative Anzahl, da diese Raten zur Abfragezeit mit einer PromQL rate
Funktion berechnet werden können.
Bei der Entwicklung der API einer Instrumentierungsbibliothek ist es wichtig, nicht einfach alle Konzepte früherer Implementierungen zu übernehmen, sondern den historischen Kontext zu berücksichtigen, warum diese Konstrukte damals existierten. Abbildung 4-3 zeigt, wo sich Micrometer mit den Vorgängern Dropwizard und Prometheus Simple Client überschneidet und wo es die Fähigkeiten seiner Vorgänger erweitert hat. Einige Konzepte wurden nicht übernommen, um der Entwicklung im Bereich der Überwachung Rechnung zu tragen. In einigen Fällen ist dieser Unterschied sehr subtil. Micrometer enthält Histogramme als Merkmal eines einfachen Timer
(oder DistributionSummary
). An dem Punkt, an dem die Instrumentierung tief in einer Bibliothek erfolgt, an dem ein Vorgang zeitlich erfasst wird, ist oft unklar, ob die Anwendung, die diese Funktion einbindet, diesen Vorgang als kritisch genug ansieht, um den zusätzlichen Aufwand für den Versand von Histogrammdaten zu rechtfertigen. (Die Entscheidung sollte also eher dem Autor der nachgelagerten Anwendung als dem Autor der Bibliothek überlassen werden).
In der Ära von Dropwizard Metrics gab es in den Überwachungssystemen keine Abfragefunktionen, die es ermöglichten, Rückschlüsse auf die Zeitdaten zu ziehen (keine Perzentilschätzungen, keine Latenz-Heatmaps usw.). Das Konzept "Man sollte nicht messen, was man zählen kann, und nicht zählen, was man zeitlich erfassen kann" war also noch nicht anwendbar. Es war nicht unüblich, @Counted
zu einer Methode hinzuzufügen, während @Counted
heute fast nie die richtige Wahl für eine Methode ist (die von Natur aus zählbar ist, und Timer werden immer auch mit einer Zählung veröffentlicht).
Zum Zeitpunkt des Verfassens dieses Artikels befindet sich die Metrik-API von OpenTelemetry zwar noch in der Beta-Phase, aber sie hat sich in den letzten Jahren nicht wesentlich verändert, und es scheint, dass die Primitive des Zählers nicht ausreichen, um brauchbare Abstraktionen für Zeitmessung und Zählung zu erstellen. Beispiel 4-1 zeigt ein Micrometer Timer
mit unterschiedlichen Tags, je nach Ergebnis einer Operation (so ausführlich kann ein Timer in Micrometer sein).
Beispiel 4-1. Eine Mikrometer-Zeitschaltuhr mit variablem Ergebnis-Tag
public
class
MyService
{
MeterRegistry
registry
;
public
void
call
()
{
try
(
Timer
.
ResourceSample
t
=
Timer
.
resource
(
registry
,
"calls"
)
.
description
(
"calls to something"
)
.
publishPercentileHistogram
()
.
serviceLevelObjectives
(
Duration
.
ofSeconds
(
1
))
.
tags
(
"service"
,
"hi"
))
{
try
{
// Do something
t
.
tag
(
"outcome"
,
"success"
);
}
catch
(
Exception
e
)
{
t
.
tags
(
"outcome"
,
"error"
,
"exception"
,
e
.
getClass
().
getName
());
}
}
}
}
Selbst der Versuch, dies mit der OpenTelemetry-Metriken-API zu erreichen, ist schwierig, wie Beispiel 4-2 zeigt. Es wurde kein Versuch unternommen, etwas Ähnliches wie Perzentil-Histogramme oder SLO-Grenzwerte aufzuzeichnen, wie im Micrometer-Pendant. Das würde natürlich den Umfang dieser Implementierung, die ohnehin schon langatmig ist, deutlich erhöhen.
Beispiel 4-2. OpenTelemetry-Zeitmessung mit variablen Ergebnis-Tags
public
class
MyService
{
Meter
meter
=
OpenTelemetry
.
getMeter
(
"registry"
);
Map
<
String
,
AtomicLong
>
callSum
=
Map
.
of
(
"success"
,
new
AtomicLong
(
0
),
"failure"
,
new
AtomicLong
(
0
)
);
public
MyService
()
{
registerCallSum
(
"success"
);
registerCallSum
(
"failure"
);
}
private
void
registerCallSum
(
String
outcome
)
{
meter
.
doubleSumObserverBuilder
(
"calls.sum"
)
.
setDescription
(
"calls to something"
)
.
setConstantLabels
(
Map
.
of
(
"service"
,
"hi"
))
.
build
()
.
setCallback
(
result
->
result
.
observe
(
(
double
)
callSum
.
get
(
outcome
).
get
()
/
1
e9
,
"outcome"
,
outcome
));
}
public
void
call
()
{
DoubleCounter
.
Builder
callCounter
=
meter
.
doubleCounterBuilder
(
"calls.count"
)
.
setDescription
(
"calls to something"
)
.
setConstantLabels
(
Map
.
of
(
"service"
,
"hi"
))
.
setUnit
(
"requests"
);
long
start
=
System
.
nanoTime
();
try
{
// Do something
callCounter
.
build
().
add
(
1
,
"outcome"
,
"success"
);
callSum
.
get
(
"success"
).
addAndGet
(
System
.
nanoTime
()
-
start
);
}
catch
(
Exception
e
)
{
callCounter
.
build
().
add
(
1
,
"outcome"
,
"failure"
,
"exception"
,
e
.
getClass
().
getName
());
callSum
.
get
(
"failure"
).
addAndGet
(
System
.
nanoTime
()
-
start
);
}
}
}
Ich glaube, das Problem bei OpenTelemetry ist die Betonung der Polyglott-Unterstützung, die natürlich Druck auf das Projekt ausübt, eine einheitliche Datenstruktur für Zählerprimitive wie den "Doppelsummenbeobachter" oder "Doppelzähler" zu definieren. Die Auswirkungen auf die resultierende API zwingen den Endbenutzer dazu, die Bestandteile einer übergeordneten Abstraktion wie einem Mikrometer Timer
aus Bausteinen auf niedrigerer Ebene zusammenzusetzen. Dies führt nicht nur zu einem äußerst ausführlichen Instrumentierungscode, sondern auch zu einer Instrumentierung, die für ein bestimmtes Überwachungssystem spezifisch ist. Wenn wir zum Beispiel versuchen, einen Zähler für ein älteres Überwachungssystem wie Graphite zu veröffentlichen, während wir schrittweise zu Prometheus migrieren, müssen wir explizit die Bewegungsraten pro Intervall berechnen und diese ebenfalls ausliefern. Die Datenstruktur "Doppelzähler" unterstützt das nicht. Das umgekehrte Problem besteht ebenfalls: Die Datenstruktur von OpenTelemetry muss die Vereinigung aller für einen "Doppelzähler" verwendbaren Statistiken enthalten, um möglichst viele Überwachungssysteme zufrieden zu stellen, auch wenn die Übermittlung dieser zusätzlichen Daten an ein modernes Metrik-Backend reine Verschwendung ist.
Wenn du dich mit der Erstellung von Charts und Alarmen beschäftigst, möchtest du vielleicht mit verschiedenen Backends experimentieren. Wenn du dich heute für ein System entscheidest, das du noch nicht kennst, wirst du in einem Jahr vielleicht schon mehr Erfahrung mit der Umstellung haben. Vergewissere dich, dass deine Metrik-Instrumentierung einen fließenden Wechsel zwischen den Überwachungssystemen ermöglicht (und dass du während der Umstellung sogar in beiden veröffentlichen kannst).
Bevor wir uns mit den einzelnen SLIs beschäftigen, wollen wir erst einmal klären, was eine effektive Karte ausmacht.
Effektive Visualisierungen von Service Level Indikatoren
Die hier gegebenen Empfehlungen sind natürlich subjektiv. Ich bevorzuge kühnere Linien und weniger "Tinte" im Diagramm, beides weicht von den Standardeinstellungen von Grafana ab. Um ehrlich zu sein, ist es mir ein bisschen peinlich, diese Vorschläge zu machen, denn ich möchte nicht davon ausgehen, dass mein ästhetisches Empfinden besser ist als das des ausgezeichneten Design-Teams von Grafana.
Die stilistische Sensibilität, die ich anbieten werde, leitet sich von zwei bedeutenden Einflüssen aus den letzten Jahren meiner Arbeit ab:
- Ingenieure dabei beobachten, wie sie auf Tabellen starren und schielen
-
Ich mache mir Sorgen, wenn ein Ingenieur auf ein Diagramm schaut und blinzelt. Ich mache mir vor allem Sorgen, dass die Lektion, die sie aus einer übermäßig komplexen Visualisierung ziehen, darin besteht, dass die Überwachung selbst komplex ist und vielleicht zu komplex für sie. Die meisten dieser Indikatoren sind wirklich einfach, wenn sie richtig dargestellt werden. So sollte es sich auch anfühlen.
- Die visuelle Darstellung von quantitativen Informationen
-
Eine Zeit lang habe ich jedem Mitglied der seltenen Population von User Experience Designern, die ich kennengelernt habe und die sich auf Operations Engineering und Developer Experience konzentrieren, dieselbe Frage gestellt: Welche(s) Buch(e) hat/haben sie am meisten beeinflusst? The Visual Display of Quantitative Information von Edward Tufte (Graphics Press) war immer unter ihren Antworten. Eine der wichtigsten Ideen für die Visualisierung von Zeitreihen, die aus diesem Buch stammt, ist das Verhältnis von "Daten und Tinte", das man so weit wie möglich erhöhen sollte. Wenn die "Tinte" (oder Pixel) in einem Diagramm keine Informationen vermittelt, dann vermittelt sie Komplexität. Komplexität führt zum Blinzeln. Blinzeln führt dazu, dass ich mir Sorgen mache.
Gehen wir also davon aus, dass das Verhältnis zwischen Daten und Tinte verbessert werden muss. Die folgenden spezifischen Empfehlungen ändern das Standard-Styling von Grafana, um dieses Verhältnis zu maximieren.
Stile für Linienbreite und Schattierung
Das Standarddiagramm von Grafana enthält eine durchgezogene Linie mit einer Breite von 1 px, eine 10%ige Transparenzfüllung unter der Linie und eine Interpolation zwischen den Zeitabschnitten. Für eine bessere Lesbarkeit kannst du die Breite der durchgezogenen Linie auf 2 px erhöhen und die Füllung entfernen. Die Füllung verringert das Verhältnis von Daten und Tinte im Diagramm und die sich überlappenden Farben der Füllungen werden bei mehr als ein paar Linien in einem Diagramm unübersichtlich. Die Interpolation ist ein wenig irreführend, da sie einem zufälligen Beobachter suggeriert, dass der Wert kurzzeitig an Zwischenpunkten entlang der Diagonale zwischen zwei Zeitscheiben existiert haben könnte. Das Gegenteil von Interpolation wird in den Optionen von Grafana "Schritt" genannt. Das obere Diagramm in Abbildung 4-4 verwendet die Standardoptionen, während das untere Diagramm mit diesen Empfehlungen angepasst wurde.
Ändere die Optionen auf der Registerkarte "Visualisierung" des Diagrammeditors, wie in Abbildung 4-5 gezeigt.
Irrtümer und Erfolge
Die gestapelte Darstellung von Ergebnissen (Erfolg, Fehler usw.) ist bei Timern sehr verbreitet, wie wir in "Fehler" sehen werden, und kommt auch in anderen Szenarien vor. Wenn wir uns Erfolge und Fehler als Farben vorstellen, denken viele von uns sofort an Grün und Rot: Ampelfarben. Leider hat ein großer Teil der Bevölkerung eine Farbsehschwäche, die die Fähigkeit beeinträchtigt, Farbunterschiede wahrzunehmen. Bei den häufigsten Beeinträchtigungen, der Deuteranopie und der Protanopie, ist der Unterschied zwischen Grün und Rot schwer oder gar nicht zu erkennen! Diejenigen, die von Monochromie betroffen sind, können überhaupt keine Farben unterscheiden, sondern nur Helligkeit. Da dieses Buch monochromatisch gedruckt ist, können wir das alle kurz anhand der gestapelten Tabelle der Fehler und Erfolge in Abbildung 4-6 erleben.
Wir brauchen einen visuellen Indikator für Fehler und Erfolge, der nicht nur eine Farbe hat. In diesem Fall haben wir uns entschieden, "erfolgreiche" Ergebnisse als gestapelte Linien und die Fehler über diesen Ergebnissen als dicke Punkte darzustellen, um sie hervorzuheben.
Außerdem bietet Grafana keine Möglichkeit, die Reihenfolge der Zeitreihen in einer gestapelten Darstellung festzulegen (d.h. "Erfolg" am unteren oder oberen Ende des Stapels), auch nicht für eine begrenzte Anzahl möglicher Werte. Wir können eine Reihenfolge erzwingen, indem wir jeden Wert in einer separaten Abfrage auswählen und die Abfragen selbst ordnen, wie in Abbildung 4-7 gezeigt.
Schließlich können wir das Styling jeder einzelnen Abfrage überschreiben, wie in Abbildung 4-8 gezeigt.
"Top k" Visualisierungen
In vielen Fällen möchten wir einen Indikator für die "schlechtesten" Leistungen in einer bestimmten Kategorie anzeigen. Viele Überwachungssysteme bieten eine Art Abfragefunktion, mit der du die "Top k"-Zeitreihen nach bestimmten Kriterien auswählen kannst. Wenn du die "Top 3" der schlechtesten Performer auswählst, bedeutet das jedoch nicht, dass es maximal drei Linien in der Grafik gibt, denn dieser Wettlauf nach unten ist ein ständiger, und die schlechtesten Performer können sich im Laufe des Zeitintervalls, das die Grafik anzeigt, ändern. Schlimmstenfalls zeigst du N Datenpunkte in einer bestimmten Visualisierung an, und es werden 3*N verschiedene Zeitreihen angezeigt! Wenn du eine vertikale Linie durch einen beliebigen Teil von Abbildung 4-9 ziehst und die Anzahl der eindeutigen Farben zählst, die sie schneidet, wird sie immer kleiner oder gleich drei sein, weil dieses Diagramm mit einer "Top 3"-Abfrage erstellt wurde. In der Legende sind jedoch sechs Elemente enthalten.
Es kann sehr schnell sehr viel arbeitsreicher werden. In Abbildung 4-10 siehst du die fünf längsten Gradle-Build-Tasks über einen bestimmten Zeitraum. Da sich die Menge der laufenden Build-Tasks in den in diesem Diagramm dargestellten Zeitabschnitten schnell ändert, füllt sich die Legende mit viel mehr Werten als nur fünf.
In solchen Fällen wird die Legende mit Beschriftungen überfrachtet, so dass sie nicht mehr lesbar ist. Verwende die Grafana-Optionen, um die Legende in eine Tabelle auf der rechten Seite zu verschieben, und füge eine zusammenfassende Statistik wie "Maximum" hinzu, wie in Abbildung 4-11 gezeigt. Du kannst dann auf die zusammenfassende Statistik in der Tabelle klicken, um die Legende-als-Tabelle nach dieser Statistik zu sortieren. Wenn wir uns nun die Tabelle ansehen, können wir schnell erkennen, welche Leistungsträger in dem von uns betrachteten Zeitraum am schlechtesten sind.
Prometheus Rate Intervall Auswahl
In diesem Kapitel werden wir Prometheus-Abfragen sehen, die Bereichsvektoren verwenden. Ich empfehle dringend, Bereichsvektoren zu verwenden, die mindestens doppelt so lang sind wie das Abfrageintervall (standardmäßig eine Minute). Andernfalls besteht die Gefahr, dass du Datenpunkte verpasst, weil benachbarte Datenpunkte etwas mehr als das Scrape-Intervall voneinander entfernt sind. Wenn ein Dienst neu gestartet wird und ein Datenpunkt fehlt, kann die Ratenfunktion erst dann eine Rate für die Lücke oder den nächsten Datenpunkt ermitteln, wenn das Intervall mindestens zwei Punkte enthält. Die Verwendung eines größeren Intervalls für die Rate vermeidet diese Probleme. Da der Start der Anwendung je nach Anwendung länger sein kann als das Scrape-Intervall, kannst du einen Bereichsvektor wählen, der länger ist als das doppelte Scrape-Intervall (also näher an dem, was der Start der Anwendung plus zwei Intervalle wäre), wenn es dir wichtig ist, Lücken vollständig zu vermeiden.
Bereichsvektoren sind ein einzigartiges Konzept in Prometheus, aber das gleiche Prinzip gilt auch in anderen Zusammenhängen in anderen Überwachungssystemen. Du könntest zum Beispiel eine Abfrage vom Typ "Min über Intervall" erstellen, um mögliche Lücken während des Neustarts der Anwendung auszugleichen, wenn du einen Mindestschwellenwert für einen Alert festlegst.
Gauges
Eine Zeitreihendarstellung eines Messgeräts stellt mehr Informationen in etwa so kompakt dar wie ein momentanes Messgerät. Es ist genauso offensichtlich, wenn eine Linie eine Alarmschwelle überschreitet, und die historischen Informationen über die früheren Werte des Messwerts liefern einen nützlichen Kontext. Daher ist das untere Diagramm in Abbildung 4-12 vorzuziehen.
Lehren haben die Tendenz, stachelig zu sein. Thread-Pools können vorübergehend fast erschöpft erscheinen und sich dann wieder erholen. Warteschlangen werden voll und dann wieder leer. Die Arbeitsspeicherauslastung in Java ist besonders schwierig zu überwachen, da kurzfristige Zuweisungen schnell einen großen Teil des zugewiesenen Speicherplatzes zu füllen scheinen, bis die Speicherbereinigung einen Großteil des Verbrauchs beseitigt hat.
Eine der effektivsten Methoden, um die Schwatzhaftigkeit von Alarmen zu begrenzen, ist die Verwendung einer rollierenden Zählfunktion, deren Ergebnisse in Abbildung 4-13 dargestellt sind. Auf diese Weise können wir einen Alarm definieren, der nur dann ausgelöst wird, wenn ein Schwellenwert in den letzten fünf Intervallen mehr als dreimal überschritten wurde, oder eine andere Kombination aus Häufigkeit und Anzahl der Rückblickintervalle. Je länger der Rückblick ist, desto mehr Zeit vergeht, bevor der Alarm zum ersten Mal ausgelöst wird. Achte also darauf, dass du bei kritischen Indikatoren nicht zu weit zurückschaust.
Da es sich bei den Messgeräten um Momentanwerte handelt, werden sie im Grunde genommen einfach so dargestellt, wie sie auf jedem Überwachungssystem sind. Zähler sind ein wenig differenzierter.
Zähler
Zähler werden oft gegen einen maximalen (oder seltener gegen einen minimalen) Schwellenwert getestet. Die Notwendigkeit, gegen einen Schwellenwert zu testen, unterstreicht die Idee, dass Zähler als Raten und nicht als kumulative Statistik beobachtet werden sollten, unabhängig davon, wie die Statistik im Überwachungssystem gespeichert wird.
Abbildung 4-14 zeigt den Anfragedurchsatz eines HTTP-Endpunkts als Rate (gelbe durchgezogene Linie) und auch die kumulative Anzahl (grüne Punkte) aller Anfragen an diesen Endpunkt seit Beginn des Anwendungsprozesses. Außerdem zeigt das Diagramm einen festen Mindestschwellenwert (rote Linie und Fläche) von 1.000 Anfragen/Sekunde, der für den Durchsatz dieses Endpunkts festgelegt wurde. Dieser Schwellenwert ist in Bezug auf den als Rate dargestellten Durchsatz sinnvoll (der in diesem Fenster zwischen 1.500 und 2.000 Anfragen/Sekunde schwankt). In Bezug auf die kumulative Zählung ist er jedoch wenig sinnvoll, da die kumulative Zählung sowohl die Durchsatzrate als auch die Langlebigkeit des Prozesses misst. Die Langlebigkeit des Prozesses ist für diesen Alarm irrelevant.
Manchmal ist ein fester Schwellenwert von vornherein schwer zu bestimmen. Außerdem kann die Rate, mit der ein Ereignis auftritt, in regelmäßigen Abständen schwanken, z. B. zu den Haupt- und Nebengeschäftszeiten. Das ist vor allem bei einem Durchsatzwert wie Anfragen/Sekunde der Fall, wie in Abbildung 4-15 zu sehen ist. Wenn wir für diesen Dienst einen festen Schwellenwert festlegen würden, um zu erkennen, wann der Datenverkehr plötzlich nicht mehr den Dienst erreicht (einen Mindestschwellenwert), müssten wir ihn auf einen Wert unter 40 RPS festlegen, dem Mindestdurchsatz für diesen Dienst. Nehmen wir an, der Mindestschwellenwert ist auf 30 RPS festgelegt. Dieser Alarm wird ausgelöst, wenn der Datenverkehr in den Nebenverkehrszeiten unter 75 % des erwarteten Wertes fällt, aber nur, wenn der Datenverkehr in den Hauptverkehrszeiten unter 10 % des erwarteten Wertes fällt! Der Alarmschwellenwert ist nicht in allen Zeiten gleich wertvoll.
In diesen Fällen solltest du eine Warnung so formulieren, dass du einen starken Anstieg oder Rückgang der Rate feststellst. Ein guter allgemeiner Ansatz, der in Abbildung 4-16 zu sehen ist, besteht darin, den Zählerkurs zu nehmen, eine Glättungsfunktion darauf anzuwenden und die Glättungsfunktion mit einem Faktor zu multiplizieren (im Beispiel 85 %). Da die Glättungsfunktion natürlich eine gewisse Zeit braucht, um auf eine plötzliche Änderung des Kurses zu reagieren, kann ein Test, der sicherstellt, dass der Zählerkurs nicht unter die geglättete Linie fällt, plötzliche Änderungen erkennen, ohne dass der erwartete Kurs überhaupt bekannt sein muss. Eine ausführlichere Erläuterung der statistischen Methoden zur Glättung für die dynamische Alarmierung findest du in "Erstellen von Alarmen mithilfe von Prognosemethoden".
Es liegt in der Verantwortung von Micrometer, die Daten so an das Überwachungssystem deiner Wahl zu übermitteln, dass du eine Raten-Darstellung eines Zählers in deinem Diagramm zeichnen kannst. Im Fall von Atlas werden die Zähler bereits ratennormiert ausgeliefert, sodass eine Abfrage nach einem Zähler bereits einen Ratenwert liefert, der direkt gezeichnet werden kann, wie in Beispiel 4-3 gezeigt.
Beispiel 4-3. Die Atlas-Zähler sind bereits ein Tarif, also werden sie als Tarif ausgewählt
name,cache.gets,:eq,
Andere Überwachungssysteme erwarten, dass kumulierte Werte an das Überwachungssystem gesendet werden, und enthalten eine Art Ratenfunktion zur Verwendung bei der Abfrage. Beispiel 4-4 würde in etwa die gleiche Rate anzeigen wie das Atlas-Äquivalent, je nachdem, was du als Bereichsvektor auswählst (den Zeitraum in []
).
Beispiel 4-4. Prometheus-Zähler sind kumulativ, daher müssen wir sie explizit in eine Rate umwandeln
rate(cache_gets[2m])
Es gibt ein Problem mit der Prometheus-Rate-Funktion: Wenn innerhalb des Zeitbereichs eines Diagramms schnell neue Tag-Werte hinzugefügt werden, kann die Prometheus-Rate-Funktion einen NaN-Wert anstelle von Null erzeugen. In Abbildung 4-17 wird der Durchsatz von Gradle-Build-Tasks im Zeitverlauf dargestellt. Da Build-Aufgaben in diesem Fenster eindeutig durch Projekt- und Aufgabennamen beschrieben werden und eine abgeschlossene Aufgabe nicht mehr erhöht wird, entstehen innerhalb des Zeitbereichs, den wir für das Diagramm ausgewählt haben, mehrere neue Zeitreihen.
Die Abfrage in Beispiel 4-5 zeigt die Methode, mit der wir die Lücken füllen können.
Beispiel 4-5. Die Abfrage zum Null-Füllen der Prometheus-Zählraten
sum(gradle_task_seconds_count) by (gradle_root_project_name) - ( sum(gradle_task_seconds_count offset 10s) by (gradle_root_project_name) > 0 or ( (sum(gradle_task_seconds_count) by (gradle_root_project_name)) * 0 ) )
Wie man Zähler aufzeichnet, ist von Überwachungssystem zu Überwachungssystem unterschiedlich. Manchmal müssen wir explizit Raten erstellen, und manchmal werden die Zähler von vornherein als Raten gespeichert. Bei Timern gibt es noch mehr Möglichkeiten.
Timer
Ein Timer
Mikrometerzähler erzeugt mit einer Operation eine Vielzahl verschiedener Zeitreihen. Das Umhüllen eines Codeblocks mit einem Timer (timer.record(() -> { ... })
) reicht aus, um Daten über den Durchsatz durch diesen Block, die maximale Latenz (die mit der Zeit abnimmt), die Gesamtsumme der Latenz und optional andere Verteilungsstatistiken wie Histogramme, Perzentile und SLO-Grenzen zu sammeln.
Auf Dashboards ist die Latenzzeit am wichtigsten, weil sie am direktesten mit der Nutzererfahrung verbunden ist. Schließlich interessieren sich die Nutzer vor allem für die Leistung ihrer einzelnen Anfragen. Der Gesamtdurchsatz des Systems interessiert sie wenig bis gar nicht, es sei denn, er wirkt sich indirekt auf ihre Reaktionszeit aus.
In zweiter Linie kann der Durchsatz einbezogen werden, wenn eine bestimmte Form des Datenverkehrs erwartet wird (die auf der Grundlage von Geschäftszeiten, Zeitzonen der Kunden usw. periodisch sein kann). So kann zum Beispiel ein starker Rückgang des Durchsatzes während einer erwarteten Spitzenzeit ein deutlicher Hinweis auf ein systemisches Problem sein, bei dem der Verkehr, der das System erreichen sollte, nicht ankommt.
In vielen Fällen ist es am besten, Alarme auf die maximale Latenz zu setzen (in diesem Fall ist damit die maximale beobachtete Latenz für jedes Intervall gemeint) und für die vergleichende Analyse hochprozentige Näherungswerte wie das 99.
Timer-Warnungen bei maximaler Latenz einstellen
Bei Java-Anwendungen kommt es häufig vor, dass die maximale Latenzzeit um eine Größenordnung schlechter ist als das 99ste Perzentil. Am besten ist es, wenn du deine Alerts auf die maximale Latenzzeit einstellst.
Wie wichtig es ist, die maximale Latenzzeit zu messen, habe ich erst entdeckt, nachdem ich Netflix verlassen hatte und von Gil Tene ein überzeugendes Argument für eine Alarmierung bei maximaler Latenzzeit gehört habe. Er zieht eine Analogie zur Leistung eines Herzschrittmachers und betont, dass "dein Herz in 99,9 % der Fälle weiterschlagen wird", was nicht gerade beruhigend ist. Da ich schon immer eine Schwäche für gut begründete Argumente hatte, fügte ich pünktlich zur SpringOne-Konferenz 2017 die maximale Latenz als eine der wichtigsten Statistiken hinzu, die von Micrometer Timer
und DistributionSummary
geliefert werden. Dort traf ich einen ehemaligen Kollegen von Netflix und schlug diese neue Idee verlegen vor, wohl wissend, dass Netflix die maximale Latenz nicht wirklich überwacht. Er lachte sofort über die Idee und ging zu einem Vortrag, so dass ich ein wenig enttäuscht war. Kurze Zeit später erhielt ich eine Nachricht von ihm mit dem in Abbildung 4-18 gezeigten Diagramm, das zeigte, dass die maximale Latenz bei einem wichtigen internen Netflix-Dienst um eine Größenordnung schlechter war als P99 (er hatte die maximale Latenz als schnelles Experiment hinzugefügt, um diese Hypothese zu testen).
Noch erstaunlicher ist, dass Netflix vor kurzem eine architektonische Änderung vorgenommen hatte, die P99 ein wenig besser, Max aber deutlich schlechter machte! Man kann leicht argumentieren, dass Netflix durch die Änderung sogar schlechter dastand. Ich erinnere mich gerne an dieses Gespräch, weil es zeigt, dass jedes Unternehmen etwas von einem anderen lernen kann: In diesem Fall lernte die hochentwickelte Überwachungskultur bei Netflix einen Trick von Domo, das ihn wiederum von Azul Systems lernte.
In Abbildung 4-19 sehen wir den Größenunterschied zwischen dem maximalen und dem 99-ten Perzentil. Die Antwortlatenz liegt eng um das 99. Perzentil herum, mit mindestens einer separaten Gruppe in der Nähe des Maximums, die Speicherbereinigung, VM-Pausen usw. widerspiegelt.
In Abbildung 4-20 zeigt ein realer Dienst die Eigenschaft, dass der Durchschnitt über dem 99. Perzentil schwebt, weil die Anfragen so dicht um das 99.
So unbedeutend dieses oberste 1 % auch erscheinen mag, echte Nutzerinnen und Nutzer sind von diesen Latenzen betroffen, daher ist es wichtig zu erkennen, wo diese Grenze liegt und sie bei Bedarf auszugleichen. Ein anerkannter Ansatz, um die Auswirkungen der oberen 1 % zu begrenzen, ist eine clientseitige Lastausgleichsstrategie namens Hedge-Requests (siehe "Hedge-Requests").
Das Setzen eines Alarms für die maximale Latenz ist wichtig (wir werden im Abschnitt "Latenz" mehr dazu sagen ). Aber wenn ein Techniker auf ein Problem aufmerksam gemacht wurde, muss das Dashboard, das er verwendet, um das Problem zu verstehen, nicht unbedingt diesen Indikator enthalten. Es wäre viel nützlicher, die Verteilung der Latenzen als Heatmap zu sehen (wie in Abbildung 4-21), die einen Bereich ungleich Null enthält, in dem der Höchstwert liegt, der die Warnung ausgelöst hat, um zu sehen, wie groß das Problem im Vergleich zu den normalen Anfragen ist, die zu diesem Zeitpunkt durch das System laufen. In einer Heatmap-Darstellung stellt jede vertikale Spalte ein Histogramm (siehe "Histogramme" für eine Definition) für eine bestimmte Zeitspanne dar. Die farbigen Kästchen stellen die Häufigkeit der Latenzen dar, die in einem auf der y-Achse definierten Zeitbereich liegen. Die normative Latenz, die ein Endnutzer erfährt, sollte also "heiß" aussehen und Ausreißer sehen kühler aus.
Schlagen die meisten Anfragen in der Nähe des Maximalwerts fehl oder gibt es nur einen oder ein paar Ausreißer? Die Antwort auf diese Frage hat wahrscheinlich Einfluss darauf, wie schnell ein alarmierter Techniker das Problem eskaliert und andere zur Hilfe holt. Es ist nicht nötig, sowohl den Maximalwert als auch die Heatmap auf einem Diagnose-Dashboard darzustellen, wie in Abbildung 4-22 gezeigt. Nimm einfach die Heatmap auf.
Die Erstellung der Latenz-Heatmap ist außerdem sehr aufwändig, da für jede Zeitscheibe im Diagramm Dutzende oder Hunderte von Buckets (das sind einzelne Zeitreihen im Überwachungssystem) abgerufen werden müssen, so dass sich die Gesamtzahl der Zeitreihen oft auf Tausende beläuft. Das unterstreicht den Gedanken, dass es keinen Grund gibt, dieses Diagramm auf einem gut sichtbaren Display an der Wand automatisch zu aktualisieren. Lass das Warnsystem seine Arbeit machen und schau dir das Dashboard nur bei Bedarf an, um die Belastung des Überwachungssystems zu begrenzen.
Der Werkzeugkasten an nützlichen Darstellungen ist inzwischen so groß, dass ein Wort der Vorsicht angebracht ist.
Wann man aufhören sollte, Dashboards zu erstellen
Ich habe 2019 einen ehemaligen Kollegen von mir besucht, der jetzt VP of Operations bei Datadog ist. Er beklagte, dass ironischerweise ein Mangel an gesunder Mäßigung bei den von Kunden erstellten Dashboards eines der Hauptkapazitätsprobleme von Datadog ist. Stell dir Legionen von Computer- und Fernsehbildschirmen vor, die auf der ganzen Welt verteilt sind und in bestimmten Abständen automatisch eine Reihe von schön aussehenden Diagrammen aktualisieren. Ich fand das ein faszinierendes Geschäftsproblem, denn viele Fernsehbildschirme mit dem Datadog-Branding erhöhen die Sichtbarkeit und den Bekanntheitsgrad des Produkts, während sie gleichzeitig einen betrieblichen Albtraum für ein SaaS darstellen.
Ich fand die "Mission Control" Dashboard-Ansicht schon immer etwas seltsam. Was ist denn an einem Diagramm, das mich visuell auf ein Problem hinweist? Wenn es sich um eine starke Spitze, ein tiefes Tal oder einfach um einen Wert handelt, der über alle vernünftigen Erwartungen hinausgeht, kann ein Alarmschwellenwert festgelegt werden, um zu definieren, wo dieser Punkt der Unannehmlichkeit liegt, und die Kennzahl kann automatisch (und rund um die Uhr) überwacht werden.
Als Techniker im Bereitschaftsdienst ist es schön, eine Meldung mit einer sofortigen Visualisierung des Indikators (oder einem Link dazu) zu erhalten. Wenn wir eine Warnmeldung öffnen, wollen wir nach Informationen suchen, um die Ursache zu finden (oder um festzustellen, dass es sich nicht lohnt, der Meldung Aufmerksamkeit zu schenken). Wenn die Meldung auf ein Dashboard verlinkt, ist dieses Dashboard idealerweise so konfiguriert, dass es eine sofortige dimensionale Explosion oder Erkundung ermöglicht. Mit anderen Worten: Das TV-Dashboard behandelt den Menschen als eine Art notorisch unzuverlässiges Warnsystem mit geringer Aufmerksamkeitsspanne.
Die Visualisierungen, die für die Alarmierung nützlich sind, müssen nicht unbedingt in einem Dashboard enthalten sein, und nicht alle Diagramme auf einem Dashboard können für die Erstellung von Alarmen verwendet werden. Abbildung 4-22 zeigt zum Beispiel zwei Darstellungen desselben Timers: ein abklingendes Maximum und eine Heatmap. Das Warnsystem wird den Maximalwert beobachten, aber wenn ein Techniker auf eine Anomalie aufmerksam gemacht wird, ist es viel nützlicher, die Verteilung der Latenzen um diesen Zeitpunkt herum zu sehen, um zu wissen, wie schwerwiegend die Auswirkungen waren (und der Maximalwert sollte in einem Latenzbereich erfasst werden, der auf der Heatmap sichtbar ist).
Sei jedoch vorsichtig, wie du diese Abfragen erstellst! Wenn du genau hinsiehst, wirst du sehen, dass es auf der Heatmap keine Latenz um 15 ms gibt. Der Prometheus-Bereichsvektor lag in diesem Fall zu nah am Scrape-Intervall, und die daraus resultierende kurzzeitige Lücke im Diagramm, die nicht sichtbar ist, verdeckt die 15 ms Latenz! Da Micrometer maximal abklingt, sehen wir sie trotzdem auf der Max-Karte.
Die Darstellung von Heatmaps ist außerdem viel rechenintensiver als eine einfache Max-Linie. Für ein einzelnes Diagramm ist das in Ordnung, aber wenn du diese Kosten für viele einzelne Anzeigen in den verschiedenen Geschäftsbereichen eines großen Unternehmens zusammenrechnest, kann das für das Überwachungssystem selbst sehr anstrengend sein.
Diagramme sind kein Ersatz für Warnungen. Konzentriere dich zunächst darauf, die richtigen Personen zu alarmieren, wenn die Werte von den zulässigen Werten abweichen, anstatt übereilt Überwachungssysteme einzurichten.
Tipp
Ein Mensch, der ständig einen Monitor beobachtet, ist nur ein teures Warnsystem, das visuell nach unzulässigen Werten fragt.
Die Warnmeldungen sollten dem Bereitschaftspersonal so übermittelt werden, dass es schnell auf ein Dashboard springen und die fehlgeschlagene Kennzahl dimensionell aufschlüsseln kann, um herauszufinden, wo das Problem liegt.
Nicht jeder Alarm oder Verstoß gegen einen SLO muss als "Stop-the-World" -Notfall behandelt werden .
Service Level Indicators für jeden Java Microservice
Nachdem wir nun wissen, wie man SLIs in Diagrammen visuell darstellen kann, wenden wir uns den Indikatoren zu, die du hinzufügen kannst. Sie werden ungefähr in der Reihenfolge ihrer Wichtigkeit dargestellt. Wenn du also den inkrementellen Ansatz für das Hinzufügen von Diagrammen und Warnungen verfolgst, solltest du sie der Reihe nach implementieren.
Fehler
Beim Timing eines Codeblocks ist es aus zwei Gründen sinnvoll, zwischen erfolgreichen und nicht erfolgreichen Operationen zu unterscheiden.
Erstens können wir das Verhältnis der erfolglosen zu den gesamten Timings direkt als Maß für die Häufigkeit der Fehler im System verwenden.
Außerdem können erfolgreiche und erfolglose Ergebnisse je nach Fehlermodus völlig unterschiedliche Antwortzeiten haben. Zum Beispiel kann eine NullPointerException
aufgrund einer falschen Annahme über das Vorhandensein bestimmter Daten in der Anfrageeingabe schon früh in einem Request Handler fehlschlagen. Er kommt dann nicht weit genug, um andere nachgelagerte Dienste aufzurufen, mit der Datenbank zu interagieren usw., wo der Großteil der Zeit verbracht wird, wenn eine Anfrage erfolgreich ist. In diesem Fall verfälschen erfolglose Anfragen, die auf diese Weise fehlschlagen, unsere Sichtweise auf die Latenz des Systems. Die Latenz wird besser erscheinen, als sie tatsächlich ist! Andererseits kann ein Request Handler, der eine blockierende Downstream-Anfrage an einen anderen Microservice stellt, der unter Druck steht und bei dem die Antwort schließlich ausbleibt, eine viel höhere Latenz aufweisen als normal (etwa so viel wie die Zeitüberschreitung des aufrufenden HTTP-Clients). Indem wir Fehler nicht trennen, geben wir eine zu pessimistische Sicht auf die Latenz unseres Systems.
Status-Tags (siehe "Naming Metrics") sollten in den meisten Fällen auf zwei Ebenen zu den Zeitmessgeräten hinzugefügt werden.
- Status
-
Ein Tag, das einen detaillierten Fehlercode, einen Ausnahmenamen oder einen anderen spezifischen Indikator für den Fehlermodus enthält
- Ergebnis
-
Ein Tag, das eine detailliertere Fehlerkategorie bietet, die zwischen Erfolg, vom Benutzer verursachten Fehlern und vom Dienst verursachten Fehlern unterscheidet
Beim Verfassen von Warnmeldungen ist es besser, eine exakte Übereinstimmung mit dem Ergebnis-Tag (outcome="SERVER_ERROR"
) zu erzielen, als zu versuchen, ein Tag anhand eines Statuscode-Musters auszuwählen (z. B. mit dem Prometheus Tag-Selektor für status !~"2.."
). Durch die Auswahl von "not 2xx" gruppieren wir Serverfehler, wie den üblichen HTTP 500 Internal Server Error, mit Fehlern, die vom Nutzer verursacht werden, wie HTTP 400 Bad Request oder HTTP 403 Forbidden. Eine hohe Anzahl von HTTP 400-Fehlern kann darauf hindeuten, dass du kürzlich Code veröffentlicht hast, der versehentlich eine Rückwärtskompatibilität in einer API enthielt, oder dass ein neuer Endnutzer (z. B. ein anderer vorgelagerter Microservice) versucht, deinen Dienst zu nutzen, und die Nutzdaten noch nicht richtig verstanden hat.
Panera Faced Chatty Alerts unterscheidet nicht zwischen Client- und Server-Fehlern
Panera Bread, Inc. sah sich mit einer übermäßig geschwätzigen Warnung eines Anomaliedetektors konfrontiert, der von seinem Überwachungssystemanbieter für HTTP-Fehler implementiert wurde. Er löste an einem Tag mehrere E-Mail-Warnungen aus, weil ein einziger Benutzer fünfmal das falsche Passwort eingegeben hatte. Die Ingenieure entdeckten, dass der Anomalie-Detektor nicht zwischen Client- und Server-Fehlerquote unterschied! Warnungen über die Client-Fehlerquote könnten für die Erkennung von Eindringlingen nützlich sein, aber der Schwellenwert wäre viel höher als die Server-Fehlerquote (und sicherlich höher als fünf Fehler in einem kurzen Zeitraum).
Ein HTTP 500 ist im Grunde immer ein Fehler deines Dienstes und erfordert Aufmerksamkeit. Im besten Fall wirft ein HTTP 500 ein Schlaglicht darauf, wo eine bessere Überprüfung im Vorfeld zu einem nützlichen HTTP 400 für den Endnutzer hätte führen können. Ich denke, "HTTP 500 - Interner Serverfehler" ist zu passiv. Etwas wie "HTTP 500-Sorry, It's My Fault" ist besser.
Wenn du deine eigenen Timer schreibst, ist es üblich, ein Timer
Sample zu verwenden und die Bestimmung der Tags zu verschieben, bis bekannt ist, ob die Anfrage erfolgreich ist oder fehlschlägt, wie in Beispiel 4-6. Das Beispiel hält den Status der Zeit fest, zu der der Vorgang für dich begonnen hat.
Beispiel 4-6. Dynamische Bestimmung eines Fehler- und Ergebnis-Tags auf der Grundlage des Ergebnisses eines Vorgangs
Timer
.
Sample
sample
=
Timer
.
start
(
)
;
try
{
// Some operation that might fail...
sample
.
stop
(
registry
.
timer
(
"my.operation"
,
Tags
.
of
(
"exception"
,
"none"
,
"outcome"
,
"success"
)
)
)
;
}
catch
(
Exception
e
)
{
sample
.
stop
(
registry
.
timer
(
"my.operation"
,
Tags
.
of
(
"exception"
,
e
.
getClass
(
)
.
getName
(
)
,
"outcome"
,
"failure"
)
)
)
;
}
Einige Überwachungssysteme wie Prometheus erwarten, dass ein einheitlicher Satz von Tag-Schlüsseln für Metriken mit demselben Namen erscheint. Auch wenn es hier keine Ausnahme gibt, sollten wir sie mit einem Platzhalterwert wie "none" kennzeichnen, um zu zeigen, welche Tags auch in den Fehlerfällen vorhanden sind.
Vielleicht hast du eine Möglichkeit, die Fehlerbedingungen besser zu katalogisieren und kannst hier einen beschreibenden Tag-Wert angeben, aber auch das Hinzufügen des Ausnahmeklassennamens kann viel dazu beitragen, zu verstehen, welche Arten von Fehlern es gibt.
NullPointerException
ist eine ganz andere Art von Ausnahme als ein schlecht behandelter Verbindungs-Timeout bei einem Aufruf eines nachgeschalteten Dienstes. Wenn die Fehlerquote in die Höhe schnellt, ist es nützlich, den Namen der Ausnahme zu kennen, um einen kurzen Einblick in die Art des Fehlers zu bekommen. Von diesem Ausnahmenamen aus kannst du zu deinen Debugging-Beobachtungstools wie z.B. Logs wechseln und nach dem Auftreten des Ausnahmenamens zum Zeitpunkt des Alerts suchen.
Sei vorsichtig mit Class.getSimpleName() usw. als Tag-Wert
Sei dir bewusst, dass Class.getSimpleName()
und Class.getCanonicalName()
null oder leere Werte zurückgeben können, z.B. im Fall von anonymen Klasseninstanzen. Wenn du einen von ihnen als Tag-Wert verwendest, prüfe zumindest den Wert auf null/leer und greife auf Class.getName()
zurück.
Bei HTTP-Anfragemetriken beispielsweise versieht Spring Boot http.server.requests
automatisch mit einem status
-Tag, das den HTTP-Statuscode angibt, und einem outcome
-Tag, das eines von SUCCESS
, CLIENT_ERROR
oder SERVER_ERROR
ist.
Anhand dieser Markierung kann die Fehlerrate pro Intervall dargestellt werden. Es ist schwierig, einen Schwellenwert für die Fehlerrate festzulegen, da sie unter denselben Fehlerbedingungen stark schwanken kann, je nachdem, wie viel Verkehr durch das System fließt.
Für Atlas kannst du den :and
Operator verwenden, um nur die SERVER_ERROR
Ergebnisse auszuwählen, wie in Beispiel 4-7 gezeigt.
Beispiel 4-7. Fehlerquote von HTTP-Server-Anfragen in Atlas
# don't do this because it fluctuates with throughput! name,http.server.requests,:eq, outcome,SERVER_ERROR,:eq, :and, uri,$ENDPOINT,:eq,:cq
Für Prometheus verwendest du einen Tag-Selektor, wie in Beispiel 4-8 gezeigt.
Beispiel 4-8. Fehlerrate von HTTP-Server-Anfragen in Prometheus
# don't do this because it fluctuates with throughput! sum( rate( http_server_requests_seconds_count{outcome="SERVER_ERROR", uri="$ENDPOINT"}[2m] ) )
Wenn jede 10. Anfrage fehlschlägt und 100 Anfragen/Sekunde durch das System laufen, liegt die Fehlerrate bei 10 Fehlschlägen/Sekunde. Wenn 1.000 Anfragen/Sekunde durch das System laufen, steigt die Fehlerquote auf 100 Ausfälle/Sekunde! In beiden Fällen beträgt die Fehlerquote im Verhältnis zum Durchsatz 10%. Diese Fehlerquote normalisiert die Rate und es ist einfach, einen festen Schwellenwert festzulegen. In Abbildung 4-23 bewegt sich die Fehlerquote um die 10-15%, obwohl der Durchsatz und damit die Fehlerquote in die Höhe schießt.
Das grobkörnige Ergebnis-Tag wird verwendet, um Abfragen zu konstruieren, die die Fehlerquote des zeitlich begrenzten Vorgangs darstellen. Im Fall von http.server.requests
ist dies das Verhältnis von SERVER_ERROR
zur Gesamtzahl der Anfragen.
Für Atlas kannst du die Funktion :div
verwenden, um die Ergebnisse von SERVER_ERROR
durch die Gesamtzahl aller Anfragen zu teilen, wie in Beispiel 4-9 gezeigt.
Beispiel 4-9. Fehlerquote von HTTP-Server-Anfragen in Atlas
name,http.server.requests,:eq, :dup, outcome,SERVER_ERROR,:eq, :div, uri,$ENDPOINT,:eq,:cq
Für Prometheus verwendest du den /
Operator auf ähnliche Weise, wie in Beispiel 4-10.
Beispiel 4-10. Fehlerquote von HTTP-Server-Anfragen in Prometheus
sum( rate( http_server_requests_seconds_count{outcome="SERVER_ERROR", uri="$ENDPOINT"}[2m] ) ) / sum( rate( http_server_requests_seconds_count{uri="$ENDPOINT"}[2m] ) )
Bei Diensten mit niedrigem Durchsatz ist die Fehlerrate besser als die Fehlerquote
Im Allgemeinen solltest du die Fehlerquote der Fehlerrate vorziehen, es sei denn, der Endpunkt hat einen sehr niedrigen Durchsatz. In diesem Fall kann schon ein kleiner Unterschied bei den Fehlern zu starken Verschiebungen in der Fehlerquote führen. In solchen Situationen ist es sinnvoller, einen festen Schwellenwert für die Fehlerquote zu wählen.
Fehlerquote und -verhältnis sind nur eine Sichtweise auf einen Timer. Die Latenzzeit ist die andere wichtige Sichtweise.
Latenz
Alert auf die maximale Latenz (in diesem Fall das Maximum, das für jedes Intervall beobachtet wird), und verwende für die vergleichende Analyse Näherungswerte für hohe Perzentile wie das 99 . Beliebte Java-Webframeworks bieten als Teil ihrer "White Box" (siehe "Black Box Versus White Box Monitoring") eine automatische Konfiguration von Metriken und eine Instrumentierung von ein- und ausgehenden Anfragen mit Rich Tags. Ich werde die automatische Instrumentierung von Anfragen in Spring Boot im Detail vorstellen, aber die meisten anderen beliebten Java-Web-Frameworks haben mit Micrometer etwas sehr Ähnliches realisiert.
Server (eingehende) Anfragen
Spring Boot konfiguriert automatisch eine Timer-Metrik namens http.server.requests
für blockierende und reaktive REST-Endpunkte. Wenn die Latenz eines bestimmten Endpunkts ein Schlüsselindikator für die Leistung einer Anwendung ist und auch für vergleichende Analysen verwendet werden soll, dann füge die Eigenschaft management.metrics.distribution.percentiles-histogram.http.server.requests=true
zu deiner application.properties
hinzu, um Perzentil-Histogramme aus deiner Anwendung zu exportieren. Um Perzentil-Histogramme für eine bestimmte Gruppe von API-Endpunkten zu aktivieren, kannst du in Spring Boot die Annotation @Timed
hinzufügen, wie in Beispiel 4-11.
Beispiel 4-11. @Timed verwenden, um Histogramme zu einem einzelnen Endpunkt hinzuzufügen
@Timed
(
histogram
=
true
)
@GetMapping
(
"/api/something"
)
Something
getSomething
()
{
...
}
Alternativ kannst du auch eine MeterFilter
hinzufügen, die auf ein Tag reagiert, wie in Beispiel 4-12 gezeigt.
Beispiel 4-12. Ein MeterFilter, der Perzentil-Histogramme für bestimmte Endpunkte hinzufügt
@Bean
MeterFilter
histogramsForSomethingEndpoints
()
{
return
new
MeterFilter
()
{
@Override
public
DistributionStatisticConfig
configure
(
Meter
.
Id
id
,
DistributionStatisticConfig
config
)
{
if
(
id
.
getName
().
equals
(
"http.server.requests"
)
&&
id
.
getTag
(
"uri"
).
startsWith
(
"/api/something"
))
{
return
DistributionStatisticConfig
.
builder
()
.
percentilesHistogram
(
true
)
.
build
()
.
merge
(
config
);
}
return
config
;
}
};
}
Beispiel 4-13 für Atlas zeigt, wie du die maximale Latenz mit einem bestimmten Schwellenwert vergleichen kannst.
Beispiel 4-13. Atlas maximale API-Latenzzeit
name,http.server.requests,:eq, statistic,max,:eq, :and, $THRESHOLD, :gt
Für Prometheus ist das Beispiel 4-14 ein einfacher Vergleich.
Beispiel 4-14. Prometheus maximale API-Latenzzeit
http_server_requests_seconds_max > $THRESHOLD
Die Tags, die zu http.server.requests
hinzugefügt werden, sind anpassbar. Für das blockierende Spring WebMVC-Modell verwendest du ein WebMvcTagsProvider
. Wir könnten zum Beispiel Informationen über den Browser und seine Version aus dem "User-Agent"-Request-Header extrahieren, wie in Beispiel 4-15 gezeigt. Dieses Beispiel verwendet die MIT-lizenzierte Browscap-Bibliothek, um Browserinformationen aus dem User-Agent-Header zu extrahieren.
Beispiel 4-15. Hinzufügen von Browser-Tags zu Spring WebMVC Metriken
@Configuration
public
class
MetricsConfiguration
{
@Bean
WebMvcTagsProvider
customizeRestMetrics
()
throws
IOException
,
ParseException
{
UserAgentParser
userAgentParser
=
new
UserAgentService
().
loadParser
();
return
new
DefaultWebMvcTagsProvider
()
{
@Override
public
Iterable
<
Tag
>
getTags
(
HttpServletRequest
request
,
HttpServletResponse
response
,
Object
handler
,
Throwable
exception
)
{
Capabilities
capabilities
=
userAgentParser
.
parse
(
request
.
getHeader
(
"User-Agent"
));
return
Tags
.
concat
(
super
.
getTags
(
request
,
response
,
handler
,
exception
),
"browser"
,
capabilities
.
getBrowser
(),
"browser.version"
,
capabilities
.
getBrowserMajorVersion
()
);
}
};
}
}
Für Spring WebFlux (das nicht blockierende reaktive Modell) konfigurierst du eine WebFluxTagsProvider
ähnlich, wie in Beispiel 4-16.
Beachte, dass der http.server.requests
Timer erst mit der Zeitmessung einer Anfrage beginnt, wenn diese vom Dienst bearbeitet wird. Wenn der Thread-Pool für Anfragen routinemäßig ausgelastet ist, warten die Anfragen der Benutzer im Thread-Pool auf ihre Bearbeitung, und diese Zeitspanne ist für den Benutzer, der auf eine Antwort wartet, sehr real. Die fehlenden Informationen auf http.server.requests
sind ein Beispiel für ein größeres Problem, das Gil Tene als koordinierte Unterlassung bezeichnet hat (siehe "Koordinierte Unterlassung") und das in verschiedenen anderen Formen auftritt.
Es ist auch sinnvoll, die Latenz aus der Perspektive des Aufrufers (Kunden) zu überwachen. In diesem Fall meine ich mit Client in der Regel die Service-to-Service-Aufrufer und nicht die menschlichen Nutzer deines API-Gateways oder der ersten Service-Interaktion. Die Sichtweise eines Dienstes auf seine eigene Latenz berücksichtigt nicht die Auswirkungen von Netzwerkverzögerungen oder Thread-Pool-Konkurrenz (z. B. den Request-Thread-Pool von Tomcat oder den Thread-Pool eines Proxys wie Nginx).
Kundenanfragen (ausgehend)
Spring Boot konfiguriert außerdem automatisch eine Timer-Metrik namens http.client.requests
für blockierende und reaktive ausgehende Aufrufe. Auf diese Weise kannst du stattdessen (oder auch) die Latenz eines Dienstes aus der Perspektive aller Aufrufer überwachen, vorausgesetzt, sie ziehen alle die gleiche Schlussfolgerung, wie der Name des aufgerufenen Dienstes lautet. Abbildung 4-24 zeigt drei Dienstinstanzen, die denselben Dienst aufrufen.
Wir können die Leistung eines bestimmten Endpunkts für den aufgerufenen Dienst ermitteln, indem wir die Tags uri
und serviceName
auswählen. Wenn wir alle anderen Tags aggregieren, sehen wir die Leistung des Endpunkts für alle Anrufer. Eine Aufschlüsselung nach Dimensionen anhand des Tags clientName
würde die Leistung des Dienstes nur aus der Perspektive dieses Kunden zeigen. Selbst wenn der aufgerufene Dienst jede Anfrage in der gleichen Zeit bearbeitet, kann die Perspektive des Kunden unterschiedlich sein (z. B. wenn ein Kunde in einer anderen Zone oder Region eingesetzt wird). Wenn diese Abweichungen zwischen den Clients möglich sind, kannst du eine Abfrage wie die topk
von Prometheus verwenden, um sie mit einem Schwellenwert zu vergleichen, damit die Gesamtheit der Leistung eines Endpunkts für alle Clients nicht den Ausreißer für einen bestimmten Client verwischt, wie in Beispiel 4-17 gezeigt.
Beispiel 4-17. Maximale Latenzzeit für ausgehende Anfragen nach Client-Name
topk( 1, sum( rate( http_client_requests_seconds_max{serviceName="CALLED", uri="/api/..."}[2m] ) ) by (clientName) ) > $THRESHOLD
Um die HTTP-Client-Instrumentierung für die Spring-Schnittstellen RestTemplate
(blockierend) und WebClient
(nicht blockierend) automatisch zu konfigurieren, musst du Pfadvariablen und Anfrageparameter auf eine bestimmte Weise behandeln. Insbesondere musst du die Implementierungen die Pfadvariablen und Request-Parameter für dich ersetzen lassen, anstatt eine String-Verkettung oder eine ähnliche Technik zu verwenden, um einen Pfad zu konstruieren, wie in Beispiel 4-18 gezeigt.
Beispiel 4-18. Erlauben, dass RestTemplate die Pfadvariablensubstitution handhabt
@RestController
public
class
CustomerController
{
private
final
RestTemplate
client
;
public
CustomerController
(
RestTemplate
client
)
{
this
.
client
=
client
;
}
@GetMapping
(
"/customers"
)
public
Customer
findCustomer
(
@RequestParam
String
q
)
{
String
customerId
;
// ... Look up customer ID according to 'q'
return
client
.
getForEntity
(
"http://customerService/customer/{id}?detail={detail}"
,
Customer
.
class
,
customerId
,
"no-address"
)
;
}
}
.
.
.
@Configuration
public
class
RestTemplateConfiguration
{
@Bean
RestTemplateBuilder
restTemplateBuilder
(
)
{
return
new
RestTemplateBuilder
(
)
.
addAdditionalInterceptors
(
.
.
)
.
build
(
)
;
}
}
Klingt ruchlos?
Um die Vorteile der Autokonfiguration von Spring Boot für die Metriken von
RestTemplate
zu nutzen, musst du sicherstellen, dass du alle benutzerdefinierten Bean-Verkabelungen fürRestTemplateBuilder
und nicht fürRestTemplate
erstellst (und beachte, dass Spring dir auch eineRestTemplateBuilder
automatisch mit den Standardwerten über die Autokonfiguration zur Verfügung stellt). Spring Boot fügt allen solchen Beans, die es findet, einen zusätzlichen Metrics-Interceptor hinzu. Sobald dieRestTemplate
erstellt ist, ist es zu spät für diese Konfiguration.
Die Idee ist, dass das uri
Tag immer noch den angefragten Pfad mit Pfadvariablen vor der Ersetzung enthält, damit du die Gesamtzahl und die Latenz der Anfragen an diesen Endpunkt ermitteln kannst, unabhängig davon, welche bestimmten Werte nachgeschlagen wurden. Dies ist auch wichtig, um die Gesamtzahl der Tags zu kontrollieren, die die http.client.requests
Metriken enthalten. Wenn du eine unbegrenzte Anzahl eindeutiger Tags zulässt, würde das Überwachungssystem irgendwann überfordert sein (oder richtig teuer für dich werden, wenn der Anbieter des Überwachungssystems nach Zeitreihen abrechnet).
Das Äquivalent für die nicht blockierende WebClient
ist in Beispiel 4-19 dargestellt.
Beispiel 4-19. Erlauben, dass der WebClient die Ersetzung von Pfadvariablen handhabt
@RestController
public
class
CustomerController
{
private
final
WebClient
client
;
public
CustomerController
(
WebClient
client
)
{
this
.
client
=
client
;
}
@GetMapping
(
"/customers"
)
public
Mono
<
Customer
>
findCustomer
(
@RequestParam
String
q
)
{
Mono
<
String
>
customerId
;
// ... Look up customer ID according to 'q', hopefully in a non-blocking way
return
customerId
.
flatMap
(
id
-
>
webClient
.
get
(
)
.
uri
(
"http://customerService/customer/{id}?detail={detail}"
,
id
,
"no-address"
)
.
retrieve
(
)
.
bodyToMono
(
Customer
.
class
)
)
;
}
}
.
.
.
@Configuration
public
class
WebClientConfiguration
{
@Bean
WebClient
.
Builder
webClientBuilder
(
)
{
return
WebClient
.
builder
(
)
;
}
}
Klingt ruchlos?
Stelle sicher, dass du Bean-Verdrahtungen für
WebClient.Builder
und nicht fürWebClient
erstellst. Spring Boot fügt eine zusätzliche MetrikWebClientCustomizer
an den Builder an, nicht an die fertigeWebClient
Instanz.
Der Standardsatz an Tags, den Spring Boot den Client-Metriken hinzufügt, ist zwar relativ vollständig, aber dennoch anpassbar. Besonders häufig werden Metriken mit dem Wert eines Request Headers (oder Response Headers) getaggt. Achte beim Hinzufügen von Tag-Anpassungen darauf, dass die Gesamtzahl der möglichen Tag-Werte überschaubar ist. Du solltest keine Tags für Dinge wie die eindeutige Kunden-ID (wenn du mehr als vielleicht 1.000 Kunden hast), eine zufällig generierte Anfrage-ID usw. hinzufügen. Erinnere dich daran, dass die Metriken dazu dienen, sich ein Bild von der Gesamtleistung zu machen, nicht von der Leistung einer einzelnen Anfrage.
Als etwas anderes Beispiel als das, das wir zuvor in der http.server.requests
Tag-Anpassung verwendet haben, könnten wir die Abrufe von Kunden zusätzlich nach ihrer Abonnementstufe taggen, wobei die Abonnementstufe ein Antwort-Header beim Abruf eines Kunden nach ID ist. Auf diese Weise könnten wir die Latenzzeit und die Fehlerquote beim Abruf von Premium-Kunden und Basiskunden getrennt darstellen. Vielleicht stellt das Unternehmen höhere Erwartungen an die Zuverlässigkeit oder Leistung von Anfragen an Premium-Kunden, was sich in einer strengeren Service-Level-Vereinbarung auf der Grundlage dieses benutzerdefinierten Tags niederschlägt.
Um die Tags für RestTemplate
anzupassen, füge deine eigenen @Bean RestTemplateExchangeTagsProvider
hinzu, wie in Beispiel 4-20 gezeigt.
Beachte, dass
response.getHeaders().get("subscription")
möglicherweisenull
zurückgeben kann! Egal, ob wirget
odergetFirst
verwenden, wir müssennull
irgendwie überprüfen .
Um die Tags für WebClient
anzupassen, füge deine eigenen @Bean WebClientExchangeTagsProvider
hinzu, wie in Beispiel 4-21 gezeigt.
Bis jetzt haben wir uns auf die Latenzzeit und Fehler konzentriert. Betrachten wir nun eine gängige Sättigungsmessung im Zusammenhang mit dem Speicherverbrauch .
Pausenzeiten der Speicherbereinigung
Pausen bei der Speicherbereinigung (GC) verzögern oft die Lieferung einer Antwort auf eine Benutzeranfrage und können ein Indikator für einen drohenden Ausfall der Anwendung sein, wenn der Speicher voll ist. Es gibt mehrere Möglichkeiten, diesen Indikator zu betrachten.
Maximale Pausenzeit
Lege einen festen Schwellenwert für die maximale GC-Pausenzeit fest, die du für akzeptabel hältst (wobei du weißt, dass eine GC-Pause auch direkt zur Antwortzeit für den Endnutzer beiträgt). Zeichne den Maximalwert aus dem jvm.gc.pause
Timer auf, um deine Schwellenwerte festzulegen, wie in Abbildung 4-25 gezeigt. Eine Heatmap der Pausenzeiten kann auch interessant sein, wenn deine Anwendung häufig pausiert und du wissen willst, wie das typische Verhalten im Laufe der Zeit aussieht.
Anteil der Zeit, die für die Speicherbereinigung aufgewendet wird
Da jvm.gc.pause
ein Timer ist, können wir seine Summe unabhängig betrachten. Wir können die Erhöhungen dieser Summe über ein Zeitintervall addieren und durch das Intervall dividieren, um zu ermitteln, wie viel Zeit die CPU mit der Speicherbereinigung beschäftigt ist . Und da unser Java-Prozess während dieser Zeit nichts anderes tut, ist eine Warnung gerechtfertigt, wenn ein signifikanter Anteil der Zeit mit GC verbracht wird. Beispiel 4-22 zeigt die Prometheus-Abfrage für diese Technik.
Beispiel 4-22. Prometheus-Abfrage nach der in der Speicherbereinigung verbrachten Zeit nach Ursache
sum( sum_over_time( sum(increase(jvm_gc_pause_seconds_sum[2m])[1m:] ) ) / 60
Summiert alle Einzelursachen, wie "Ende der kleinen GC".
Die Gesamtzeit, die in der letzten Minute für eine einzelne Sache aufgewendet wurde.
Dies ist das erste Mal, dass wir eine Prometheus-Subquery sehen. Sie ermöglicht es uns, die Operation an den beiden Indikatoren als einen Bereichsvektor zu behandeln, der in
sum_over_time
eingegeben wird.Da
jvm_gc_pause_seconds_sum
eine Einheit von Sekunden hat (und damit auch die Summen) und wir über einen Zeitraum von 1 Minute summiert haben, teilst du durch 60 Sekunden, um einen Prozentsatz im Bereich [0, 1] der Zeit zu erhalten, die wir in der letzten Minute in GC verbracht haben.
Diese Technik ist flexibel. Du kannst ein Tag verwenden, um bestimmte GC-Ursachen auszuwählen und z. B. nur den Anteil der Zeit auszuwerten, der auf die wichtigsten GC-Ereignisse entfällt. Oder du kannst, wie wir es hier gemacht haben, einfach alle Ursachen zusammenzählen und die gesamte GC-Zeit in einem bestimmten Intervall auswerten. Höchstwahrscheinlich wirst du feststellen, dass kleinere GC-Ereignisse nicht so stark zum Anteil der GC-Zeit beitragen, wenn du die Summen nach Ursachen trennst. Die in Abbildung 4-26 überwachte App führte jede Minute kleinere Sammlungen durch, und es überrascht nicht, dass sie trotzdem nur 0,0182 % ihrer Zeit mit GC-Aktivitäten verbrachte.
Wenn du kein Überwachungssystem verwendest, das Aggregationsfunktionen wie sum_over_time
anbietet, stellt Micrometer einen Meter Binder namens JvmHeapPressureMetrics
zur Verfügung (siehe Beispiel 4-23), der diesen GC-Overhead vorberechnet und ein Messgerät namens jvm.gc.overhead
liefert, das einen Prozentsatz im Bereich [0, 1] darstellt, gegen den du dann einen festen Schwellenwert als Alarm setzen kannst. In einer Spring Boot App kannst du einfach eine Instanz von JvmHeapPressureMetrics
als @Bean
hinzufügen und sie wird automatisch mit deinen Zählerregistrierungen verbunden.
Das Vorhandensein einer riesigen Zuweisung
Zusätzlich zur Auswahl einer der oben genannten Formen zur Überwachung der in der GC verbrachten Zeit ist es auch eine gute Idee, einen Alert auf das Vorhandensein riesiger Allokationen zu setzen, die die GC im G1-Collector verursacht, denn das zeigt an, dass du irgendwo in deinem Code ein Objekt allozierst, das mehr als 50 % der Gesamtgröße des Eden-Spaces ausmacht! Höchstwahrscheinlich gibt es eine Möglichkeit, die Anwendung so umzugestalten, dass eine solche Zuweisung durch Chunking oder Streaming von Daten vermieden wird. Eine riesige Zuweisung könnte z. B. beim Parsen einer Eingabe oder beim Abrufen eines Objekts aus einem Datenspeicher auftreten, der noch nicht so groß ist, wie die Anwendung theoretisch sehen könnte.
Konkret suchst du nach einem Wert ungleich Null für jvm.gc.pause
, bei dem der Tag cause
gleich G1 Humongous Allocation
ist.
In "Überwachung der Verfügbarkeit" haben wir bereits erwähnt, dass Sättigungsmetriken in der Regel den Auslastungsmetriken vorzuziehen sind, wenn du die Wahl zwischen beiden hast. Das gilt natürlich auch für den Speicherverbrauch. Die Zeit, die in der Speicherbereinigung verbracht wird, lässt sich leichter als Indikator für Probleme mit den Speicherressourcen heranziehen. Es gibt auch einige interessante Dinge, die wir mit Auslastungsmessungen machen können, wenn wir vorsichtig sind.
Heap-Auslastung
Der Java-Heap ist in mehrere Pools unterteilt, wobei jeder Pool eine bestimmte Größe hat. Java-Objektinstanzen werden im Heapspace erstellt. Die wichtigsten Teile des Heaps sind die folgenden:
- Raum Eden (junge Generation)
-
Alle neuen Objekte werden hier zugeordnet. Eine Speicherbereinigung findet statt, wenn dieser Platz gefüllt ist.
- Survivor Raum
-
Bei einer kleinen Speicherbereinigung werden alle lebenden Objekte (die nachweislich noch Referenzen haben und daher nicht eingesammelt werden können) in den Survivor Space kopiert. Bei Objekten, die den Survivor Space erreichen, wird das Alter erhöht und sie werden nach Erreichen einer Altersgrenze in die alte Generation befördert. Die Beförderung kann vorzeitig erfolgen, wenn der Survivor Space nicht alle lebenden Objekte der jungen Generation aufnehmen kann (die Objekte überspringen den Survivor Space und gehen direkt in die alte Generation). Diese letzte Tatsache ist entscheidend dafür, wie wir den gefährlichen Grad des Zuweisungsdrucks messen.
- Alte Generation
-
Hier werden Objekte gelagert, die lange überleben. Wenn Objekte im Eden-Raum gelagert werden, wird ein Alter für das Objekt festgelegt; und wenn es dieses Alter erreicht, wird das Objekt in die alte Generation verschoben.
Im Grunde wollen wir wissen, wann einer oder mehrere dieser Bereiche zu "voll" werden und bleiben. Das ist schwierig zu überwachen, denn die Speicherbereinigung der JVM setzt ein, wenn ein Speicherplatz voll ist. Wenn ein Speicherplatz voll ist, ist das also noch kein Anzeichen für ein Problem. Besorgniserregend ist jedoch, wenn er voll bleibt.
Der JvmMemoryMetrics
Meter Binder von Micrometer sammelt automatisch die Nutzung des JVM-Speicherpools sowie die aktuelle maximale Heap-Größe (da diese zur Laufzeit steigen und fallen kann). Die meisten Java-Webframeworks konfigurieren diesen Binder automatisch.
In Abbildung 4-27 sind mehrere Metriken abgebildet. Die einfachste Idee zur Messung des Heap-Drucks ist die Verwendung eines einfachen festen Schwellenwerts, z. B. eines Prozentsatzes des gesamten verbrauchten Heaps. Wie wir sehen können, wird der Alarm mit dem festen Schwellenwert viel zu häufig ausgelöst. Der früheste Alarm wird um 11:44 Uhr ausgelöst, lange bevor ein Speicherleck in dieser Anwendung auftritt. Auch wenn der Heap vorübergehend den von uns festgelegten Schwellenwert für den prozentualen Anteil am Gesamtheap überschreitet, bringen die Ereignisse der Speicherbereinigung den Gesamtverbrauch routinemäßig wieder unter den Schwellenwert.
In Abbildung 4-27:
-
Die durchgezogenen vertikalen Balken sind ein Stapeldiagramm des Speicherverbrauchs nach Bereichen.
-
Die dünne Linie um den Wert von 30,0 Mio. ist der maximal zulässige Heap-Speicherplatz. Beachte, wie dieser Wert schwankt, wenn die JVM versucht, den richtigen Wert zwischen der anfänglichen Heapgröße (
-Xms
) und der maximalen Heapgröße (-Xmx
) für den Prozess zu finden. -
Die fette Linie um die 24,0 Mio. stellt einen festen Prozentsatz dieses maximal zulässigen Speichers dar. Das ist der Schwellenwert. Es ist ein fester Schwellenwert im Verhältnis zum Höchstwert, aber dynamisch in dem Sinne, dass es ein Prozentsatz des Höchstwerts ist, der selbst schwanken kann .
-
Die helleren Balken stellen Punkte dar, an denen die tatsächliche Heap-Auslastung (der obere Teil des Stack-Diagramms) den Schwellenwert überschreitet. Dies ist die "Alarmbedingung".
Dieser einfache feste Schwellenwert wird also nicht funktionieren. Je nach den Möglichkeiten deines Zielüberwachungssystems gibt es bessere Optionen.
Rollende Zählung der Fälle, in denen der Heap-Speicher voll ist
Mit einer Funktion wie der rollenden Zählung in Atlas können wir nur dann eine Warnung ausgeben, wenn der Heap den Schwellenwert überschreitet - z. B. drei von fünf vorherigen Intervallen -, was bedeutet, dass der Heap-Verbrauch trotz der besten Bemühungen des Garbage Collectors weiterhin ein Problem darstellt (siehe Abbildung 4-28).
Leider haben nicht viele Überwachungssysteme eine Funktion wie die rollierende Zählung von Atlas. Prometheus kann so etwas mit seiner count_over_time
Funktion, aber es ist schwierig, eine ähnliche "drei von fünf" Dynamik zu erreichen.
Es gibt einen alternativen Ansatz, der ebenfalls gut funktioniert.
Geringer Pool-Speicher nach der Sammlung
Micrometer's JvmHeapPressureMetrics
fügt eine Anzeige jvm.memory.usage.after.gc
für den Prozentsatz des Heaps der alten Generation hinzu, der nach der letzten Speicherbereinigung verwendet wurde.
jvm.memory.usage.after.gc
ist ein Prozentsatz, der im Bereich [0, 1] liegt. Wenn er hoch ist (ein guter Schwellenwert für eine Warnung ist größer als 90 %), kann die Speicherbereinigung nicht viel Müll aufsammeln. Es ist also zu erwarten, dass langfristige Pausenereignisse, die beim Aufräumen von Old Generation auftreten, häufig vorkommen. Häufige langfristige Pausen verschlechtern die Leistung der App erheblich und führen schließlich zu OutOfMemoryException
fatalen Fehlern.
Eine subtile Variante zur Messung des niedrigen Poolspeichers nach dem Sammeln ist ebenfalls effektiv.
Geringer Gesamtspeicher
Bei dieser Technik werden die Indikatoren der Heap-Nutzung und der Speicherbereinigung gemischt. Ein Problem wird angezeigt, wenn beide Indikatoren einen Schwellenwert überschreiten:
jvm.gc.overhead
> 50%-
Beachte, dass dies ein niedrigerer Schwellenwert ist als der, der in "Pausenzeiten der Speicherbereinigung" für denselben Indikator vorgeschlagen wurde (wo wir 90% vorgeschlagen haben). Wir können bei diesem Indikator aggressiver vorgehen, weil wir ihn mit einem Auslastungsindikator kombinieren.
jvm.memory.used/jvm.memory.max
> 90% zu irgendeinem Zeitpunkt in den letzten 5 Minuten-
Jetzt wissen wir, dass der GC-Overhead ansteigt, weil einer oder mehrere der Pools immer voller werden. Du könntest dies auch nur auf den Old Generation Pool beschränken, wenn deine Anwendung unter normalen Umständen viel kurzfristigen Müll erzeugt.
Das Warnkriterium für den GC-Overhead-Indikator ist ein einfacher Test gegen den Messwert.
Die Abfrage der gesamten Speichernutzung ist etwas weniger offensichtlich. Die Prometheus-Abfrage wird in Beispiel 4-24 gezeigt.
Beispiel 4-24. Prometheus-Abfrage nach maximalem Speicherverbrauch in den letzten fünf Minuten
max_over_time( ( jvm_memory_used_bytes{id="G1 Old Gen"} / jvm_memory_committed_bytes{id="G1 Old Gen"} )[5m:] )
Um besser zu verstehen, was max_over_time
tut, zeigt Abbildung 4-29 die Gesamtmenge an Eden-Speicherplatz (in diesem Falljvm.memory.used{id="G1 Eden Space"}
), die zu verschiedenen Zeitpunkten verbraucht wird (die Punkte), und das Ergebnis der Anwendung einer einminütigen max_over_time
Abfrage auf dieselbe Abfrage (die durchgezogene Linie). Es handelt sich um ein gleitendes Maximalfenster über ein vorgegebenes Intervall.
Solange die Heap-Nutzung ansteigt (und nicht unter dem aktuellen Wert im Lookback-Fenster liegt), wird sie von max_over_time
genau verfolgt. Sobald eine Speicherbereinigung stattfindet, sinkt der aktuelle Wert und max_over_time
bleibt bei dem höheren Wert für das Rückblickfenster "hängen".
Dies ist auch das erste Mal, dass wir eine Warnung in Betracht ziehen, die auf mehr als einer Bedingung basiert. Alerting-Systeme erlauben in der Regel die boolesche Kombination mehrerer Kriterien. Wenn in Abbildung 4-30 der Indikator jvm.gc.overhead
für Abfrage A und der Nutzungsindikator für Abfrage B steht, kann in Grafana ein Alarm für beide zusammen konfiguriert werden.
Eine weitere gängige Messung der Auslastung ist die CPU, für die es kein einfaches Analogon zur Sättigung gibt.
CPU-Auslastung
Die CPU-Auslastung ist ein häufig verwendeter Auslastungsalarm, aber leider ist es schwierig, eine allgemeine Regel dafür aufzustellen, was eine gesunde Menge an CPU ist, da es verschiedene Programmiermodelle gibt, die weiter unten beschrieben werden - dies muss für jede Anwendung je nach ihren Eigenschaften bestimmt werden.
Ein typischer Java-Microservice, der auf Tomcat läuft und Anfragen mit einem blockierenden Servlet-Modell bedient, verbraucht in der Regel die verfügbaren Threads im Tomcat-Thread-Pool, bevor er die CPU überlastet. Bei dieser Art von Anwendungen ist eine hohe Speichersättigung weitaus häufiger (z. B. viel überschüssiger Speicherplatz, der bei der Bearbeitung jeder Anfrage erzeugt wird, oder große Anfrage/Antwort-Bodies).
Ein Java-Microservice, der auf Netty läuft und durchgängig ein reaktives Programmiermodell verwendet, akzeptiert einen viel höheren Durchsatz pro Instanz, so dass die CPU-Auslastung tendenziell viel höher ist. Tatsächlich wird die bessere Sättigung der verfügbaren CPU-Ressourcen häufig als Vorteil des reaktiven Programmiermodells angeführt!
Berücksichtige auf einigen Plattformen die CPU- und Arbeitsspeicherauslastung gemeinsam, bevor du die Instanzgröße änderst.
Ein gemeinsames Merkmal von Platform as a Service ist die Vereinfachung der Instanzgrößenbestimmung auf die gewünschte CPU- oder Speichermenge, wobei die andere Variable proportional mit der Größe wächst. Im Fall von Cloud Foundry wurde diese Proportionalität zwischen CPU und Speicher zu einer Zeit beschlossen, als ein blockierendes Modell der Anfragebearbeitung wie Tomcat fast universell war. Wie bereits erwähnt, wird die CPU bei diesem Modell tendenziell zu wenig genutzt. Ich habe einmal ein Unternehmen beraten, das ein nicht blockierendes reaktives Modell für seine Anwendung eingeführt hatte. Als ich feststellte, dass der Speicher deutlich zu wenig genutzt wurde, verkleinerte ich die Cloud Foundry-Instanzen des Unternehmens, um weniger Speicher zu verbrauchen. Aber die CPU wird den Instanzen auf dieser Plattform proportional zur Speicheranforderung zugewiesen. Indem das Unternehmen einen geringeren Speicherbedarf wählte, entzog es seiner reaktiven Anwendung ungewollt die CPU, die sie sonst so effizient ausgelastet hätte!
Micrometer exportiert zwei wichtige Metriken für die CPU-Überwachung, die in Tabelle 4-1 aufgeführt sind. Diese beiden Metriken werden vom Java-Betriebssystem MXBean (ManagementFactory.getOperatingSystemMXBean()
) gemeldet.
Metrisch | Typ | Beschreibung |
---|---|---|
system.cpu.usage |
Messgerät |
Die aktuelle CPU-Auslastung für das gesamte System |
process.cpu.usage |
Messgerät |
Die aktuelle CPY-Nutzung für den Prozess der virtuellen Java-Maschine |
Für den häufigsten Fall im Unternehmen, dass eine Anwendung Anfragen über ein blockierendes Servlet-Modell bedient, ist ein fester Schwellenwert von 80 % angemessen. Reaktive Anwendungen müssen empirisch getestet werden, um ihren Sättigungspunkt zu bestimmen.
Für Atlas kannst du die Funktion :gt
verwenden, wie in Beispiel 4-25 gezeigt.
Beispiel 4-25. Atlas CPU-Alarmschwelle
name,process.cpu.usage,:eq, 0.8, :gt
Für Prometheus ist Beispiel 4-26 nur ein Vergleichsausdruck.
Beispiel 4-26. Prometheus CPU-Alarmschwelle
process_cpu_usage > 0.8
Die CPU-Auslastung des Prozesses sollte als Prozentsatz dargestellt werden (wobei das Überwachungssystem eine Eingabe im Bereich 0-1 erwarten sollte, um die y-Achse entsprechend zu zeichnen). Die y-Achse in Abbildung 4-31 zeigt, wie dies aussehen sollte.
In Grafana ist "Prozent" eine der Einheiten, die auf der Registerkarte "Visualisierung" ausgewählt werden können. Achte darauf, dass du die Option "Prozent (0,0-1,0)" auswählst, wie in Abbildung 4-32 gezeigt.
Es gibt noch einen weiteren ressourcenbasierten Indikator, den du bei jeder Anwendung messen solltest: die Dateideskriptoren.
Datei Deskriptoren
Die Unix-Funktion "ulimits" begrenzt, wie viele Ressourcen ein einzelner Benutzer nutzen kann, einschließlich gleichzeitig geöffneter Dateideskriptoren. Dateideskriptoren werden nicht nur für den Dateizugriff verbraucht, sondern auch für Netzwerk- und Datenbankverbindungen usw.
Du kannst die aktuellen Ulimits deiner Shell mit ulimit -a
einsehen. Die Ausgabe ist in Beispiel 4-27 zu sehen. Auf vielen Betriebssystemen ist 1.024 die Standardgrenze für offene Dateideskriptoren. Szenarien, in denen die Anzahl der gleichzeitigen Threads das Limit des Betriebssystems überschreiten kann, wie z. B. bei jeder Dienstanfrage, die den Zugriff auf eine Datei zum Lesen oder Schreiben erfordert, sind für dieses Problem anfällig. Ein Durchsatz von mehreren Tausend gleichzeitigen Anfragen ist für einen modernen Microservice nicht unvernünftig, vor allem wenn es sich um einen nicht blockierenden Dienst handelt.
Beispiel 4-27. Ausgabe von ulimit -a in einer Unix-Shell
$ ulimit -a ... open files (-n) 1024 ... cpu time (seconds, -t) unlimited max user processes (-u) 63796 virtual memory (kbytes, -v) unlimited
Dies ist die Anzahl der zulässigen offenen Dateien, nicht die Anzahl der aktuell geöffneten Dateien.
Dieses Problem ist nicht unbedingt häufig, aber die Auswirkungen des Erreichens des Dateideskriptor-Limits können fatal sein und dazu führen, dass die Anwendung nicht mehr reagiert, je nachdem, wie die Dateideskriptoren verwendet werden. Anders als bei einem Out-of-Memory-Fehler oder einer fatalen Ausnahme blockiert die Anwendung oft einfach, scheint aber noch zu funktionieren. Da die Überwachung der Dateideskriptornutzung so billig ist, solltest du bei jeder Anwendung darauf achten. Anwendungen, die gängige Techniken und Web-Frameworks verwenden, werden wahrscheinlich nie mehr als 5 % der Dateideskriptoren auslasten (und manchmal sogar viel weniger).
Die Erfahrung mit dem Dateideskriptor-Problem beim Schreiben dieses Buches
Ich weiß schon seit einiger Zeit, dass ich dieses Problem beobachte, habe es aber bis zum Schreiben dieses Buches nie selbst erlebt. Ein Go-Build-Schritt, mit dem Grafana aus dem Quellcode erstellt wird, blieb immer wieder hängen und konnte nicht abgeschlossen werden. Offensichtlich schränkt der Go-Mechanismus zur Auflösung von Abhängigkeiten die Anzahl der offenen Dateideskriptoren nicht sorgfältig ein!
Eine Anwendung, die möglicherweise Sockets für Hunderte von Anrufern, HTTP-Verbindungen zu nachgelagerten Diensten, Verbindungen zu Datenquellen und Datendateien geöffnet hat, kann an die Grenze der Dateideskriptoren stoßen. Wenn einem Prozess die Dateideskriptoren ausgehen, ist das in der Regel kein gutes Ende. In Protokollen wie in Beispiel 4-28 und Beispiel 4-29 kannst du Fehler sehen.
Beispiel 4-28. Tomcat erschöpfte Dateideskriptoren, die eine neue HTTP-Verbindung annehmen
java.net.SocketException: Too many open files at java.net.PlainSocketImpl.socketAccept(Native Method) at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:398)
Beispiel 4-29. Java schlägt beim Öffnen einer Datei fehl, wenn die Dateideskriptoren erschöpft sind
java.io.FileNotFoundException: /myfile (Too many open files) at java.io.FileInputStream.open(Native Method)
Micrometer meldet zwei in Tabelle 4-2 dargestellte Metriken, um dich auf ein Dateideskriptorproblem in deinen Anwendungen aufmerksam zu machen.
Metrisch | Typ | Beschreibung |
---|---|---|
Prozess.max.fds |
Messgerät |
Maximal zulässige offene Dateideskriptoren, die der Ausgabe von |
process.open.fds |
Messgerät |
Anzahl der offenen Dateideskriptoren |
Normalerweise sollten die offenen Dateideskriptoren unter dem Maximum bleiben, daher ist ein Test gegen einen festen Schwellenwert wie 80% ein guter Indikator für ein bevorstehendes Problem. Diese Warnung sollte für jede Anwendung gesetzt werden, denn Dateilimits sind eine universell gültige Grenze, die deine Anwendung außer Betrieb setzt.
Für Atlas verwendest du die Funktionen :div
und :gt
, wie in Beispiel 4-30 gezeigt.
Beispiel 4-30. Atlas-Dateideskriptor-Alarmschwelle
name,process.open.fds,:eq, name,process.max.fds,:eq, :div, 0.8, :gt
Für Prometheus sieht das Beispiel 4-31 noch einfacher aus.
Beispiel 4-31. Prometheus-Dateideskriptor-Warnschwelle
process_open_fds / process_max_fds > 0.8
An dieser Stelle haben wir die Signale behandelt, die für fast jeden Java Microservice gelten. Die folgenden Signale sind zwar häufig nützlich, aber nicht so allgegenwärtig.
Verdächtiger Verkehr
Ein weiterer einfacher Indikator, der aus Metriken wie http.server.requests
abgeleitet werden kann, ist das Auftreten ungewöhnlicher Statuscodes. Eine schnelle Abfolge von HTTP 403 Forbidden (und ähnlichen) oder HTTP 404 Not Found kann auf einen Einbruchsversuch hinweisen.
Anders als bei der Aufzeichnung von Fehlern solltest du die Gesamtzahl der verdächtigen Statuscodes als Rate und nicht als Verhältnis zum Gesamtdurchsatz betrachten. Es ist wahrscheinlich sicher zu sagen, dass 10.000 HTTP 403 pro Sekunde genauso verdächtig sind, wenn das System normalerweise 15.000 Anfragen pro Sekunde oder 15 Millionen Anfragen pro Sekunde verarbeitet, also lass den Gesamtdurchsatz die Anomalie nicht verbergen.
Die Atlas-Abfrage in Beispiel 4-32 ähnelt der Abfrage der Fehlerquote, die wir bereits besprochen haben, betrachtet aber den status
Tag genauer als den outcome
Tag.
Beispiel 4-32. Verdächtige 403er in HTTP-Server-Anfragen in Atlas
name,http.server.requests,:eq, status,403,:eq, :and, uri,$ENDPOINT,:eq,:cq
Verwende die Prometheus-Funktion rate
, um das gleiche Ergebnis in Prometheus zu erzielen, wie in Beispiel 4-33.
Beispiel 4-33. Verdächtige 403er in HTTP-Server-Anfragen in Prometheus
sum( rate( http_server_requests_seconds_count{status="403", uri="$ENDPOINT"}[2m] ) )
Der nächste Indikator ist auf eine bestimmte Art von Anwendung spezialisiert, aber immer noch üblich genug, um ihn mit aufzunehmen.
Batch-Läufe oder andere langlaufende Aufgaben
Eines der größten Risiken einer langlaufenden Aufgabe ist, dass sie deutlich länger läuft als erwartet. Zu Beginn meiner beruflichen Laufbahn war ich regelmäßig für Produktionseinsätze auf Abruf, die immer nach einer Reihe von Batchläufen um Mitternacht durchgeführt wurden. Unter normalen Umständen hätte die Batch-Sequenz vielleicht um 1:00 Uhr nachts abgeschlossen sein sollen. Ein Netzwerkadministrator, der das verteilte Artefakt manuell hochlud (das war vor Kapitel 5), musste also um 1:00 Uhr nachts an einem Computer sitzen, um die Aufgabe zu erledigen. Als Vertreter des Entwicklungsteams des Produkts musste ich bereit sein, um ca. 1:15 Uhr einen kurzen Smoke-Test durchzuführen und bei der Behebung etwaiger Probleme zu helfen. Zu dieser Zeit lebte ich in einer ländlichen Gegend ohne Internetzugang, also fuhr ich auf einer Landstraße in Richtung eines Ballungszentrums, bis ich ein ausreichend zuverlässiges Handysignal hatte, um mich mit meinem Handy zu verbinden und eine VPN-Verbindung herzustellen. Wenn die Batch-Prozesse nicht in angemessener Zeit abgeschlossen wurden, saß ich manchmal stundenlang in meinem Auto auf einer Landstraße und wartete, bis sie abgeschlossen waren. An Tagen, an denen keine Produktionseinsätze stattfanden, wusste vielleicht niemand, dass der Batch-Zyklus bis zum nächsten Arbeitstag fehlschlug.
Wenn wir eine lang laufende Aufgabe in ein Mikrometer Timer
verpacken, wissen wir nicht, dass der SLO überschritten wurde, bis die Aufgabe tatsächlich abgeschlossen ist. Wenn die Aufgabe also nicht länger als 1 Stunde dauern sollte, sie aber tatsächlich 16 Stunden läuft, wird dies erst im ersten Veröffentlichungsintervall nach 16 Stunden auf dem Überwachungsdiagramm angezeigt, wenn die Probe im Timer aufgezeichnet wird.
Um lang laufende Aufgaben zu überwachen, ist es besser, die Laufzeit von laufenden oder aktiven Aufgaben zu betrachten. LongTaskTimer
führt diese Art der Messung durch. Wir können diese Art der Zeitmessung zu einer potenziell lang laufenden Aufgabe hinzufügen, wie in Beispiel 4-34.
Beispiel 4-34. Ein annotationsbasierter Lang-Task-Timer für einen geplanten Vorgang
@Timed
(
name
=
"policy.renewal.batch"
,
longTask
=
true
)
@Scheduled
(
fixedRateString
=
"P1D"
)
void
renewPolicies
()
{
// Bill and renew insurance policies that are beginning new terms today
}
Lange Task-Timer liefern verschiedene Verteilungsstatistiken: die Anzahl der aktiven Tasks, die maximale Dauer der In-Flight-Anfrage, die Summe aller In-Flight-Anfrage-Dauern und optional Perzentil- und Histogramm-Informationen über In-Flight-Anfragen.
Teste für Atlas gegen unsere Erwartung von einer Stunde in Nanosekunden, wie in Beispiel 4-35 gezeigt.
Beispiel 4-35. Atlas Long Task Timer maximaler Alarmschwellenwert
name,policy.renewal.batch.max,:eq, 3.6e12, :gt
Für Prometheus wird Beispiel 4-36 mit einer Stunde in Sekunden getestet.
Beispiel 4-36. Maximaler Alarmschwellenwert des Prometheus-Langzeittimers
policy_renewal_batch_max_seconds > 3600
Wir haben einige Beispiele für effektive Indikatoren kennengelernt und hoffen, dass du jetzt einen oder mehrere davon auf einem Dashboard dargestellt hast und aussagekräftige Einblicke erhältst. Als Nächstes befassen wir uns mit der Automatisierung von Warnmeldungen, wenn diese Indikatoren aus dem Ruder laufen, damit du nicht ständig auf deine Dashboards schauen musst, um zu wissen, dass etwas nicht stimmt.
Warnmeldungen mit Prognosemethoden erstellen
Feste Alarmschwellen sind oft schwer von vornherein festzulegen, und da die Leistung des Systems im Laufe der Zeit schwankt, muss sie möglicherweise ständig neu angepasst werden. Wenn die Leistung im Laufe der Zeit abnimmt (aber so, dass die Abnahme noch akzeptabel ist), kann ein fester Schwellenwert leicht zu geschwätzig werden. Wenn sich die Leistung tendenziell verbessert, ist der Schwellenwert kein zuverlässiges Maß für die erwartete Leistung mehr, wenn er nicht angepasst wird.
Um das maschinelle Lernen wird ein großer Hype gemacht, dass das Überwachungssystem automatisch Alarmschwellen bestimmt, aber es hat nicht die versprochenen Ergebnisse gebracht. Für Zeitreihendaten sind einfachere klassische statistische Methoden immer noch unglaublich leistungsfähig. Überraschenderweise ist das Papier von S. Makridakis et al., "Statistical and Machine Learning Forecasting Methods: Concerns and Ways Forward", dass statistische Methoden einen geringeren Vorhersagefehler haben (wie in Abbildung 4-33 dargestellt) als maschinelle Lernmethoden.
Beginnen wir mit der am wenigsten prädiktiven naiven Methode, die mit jedem Überwachungssystem verwendet werden kann. Spätere Ansätze werden von Überwachungssystemen weniger gut unterstützt, da ihre Mathematik so kompliziert ist, dass sie eingebaute Abfragefunktionen erfordert.
Naive Methode
Die naive Methode ist eine einfache Heuristik, die den nächsten Wert auf der Grundlage des letzten beobachteten Wertes vorhersagt:
Eine dynamische Alarmschwelle kann mit der naiven Methode bestimmt werden, indem ein Zeitreihen-Offset mit einem Faktor multipliziert wird. Dann können wir prüfen, ob die wahre Linie jemals unter die prognostizierte Linie fällt (oder sie überschreitet, wenn der Multiplikator größer als eins ist). Wenn die wahre Linie zum Beispiel den Durchsatz eines Systems misst, kann ein plötzlicher starker Rückgang des Durchsatzes auf einen Ausfall hindeuten.
Das Warnkriterium für Atlas ist dann, wenn die Abfrage in Beispiel 4-37 1
zurückgibt. Die Abfrage wurde gegen den Testdatensatz von Atlas entwickelt, so dass es für dich einfach ist, verschiedene Multiplikatoren auszuprobieren, um den Effekt zu beobachten.
Beispiel 4-37. Atlas-Warnkriterien für die naive Prognosemethode
name,requestsPerSecond,:eq, :dup, 0.5,:mul, 1m,:offset, :rot, :lt
Mit diesem Faktor wird die Schärfe der Schwelle festgelegt.
Blicke für die Vorhersage auf ein früheres Intervall "zurück".
Die Wirkung der naiven Methode ist in Abbildung 4-34 zu sehen. Der Multiplikationsfaktor (0,5 in der Beispielabfrage) steuert, wie nahe wir den Schwellenwert an den wahren Wert heranführen wollen, und reduziert gleichzeitig die Spikinesse der Vorhersage (d.h. je lockerer der Schwellenwert, desto weniger Spikinesse die Vorhersage). Da die Glättung der Methode proportional zur Lockerheit der Anpassung ist, wird die Warnschwelle in diesem Zeitfenster immer noch viermal ausgelöst (erkennbar an den vertikalen Balken in der Mitte des Diagramms), obwohl wir eine Abweichung von 50 % vom "Normalwert" berücksichtigt haben.
Um einen geschwätzigen Alarm zu verhindern, müssen wir die Anpassung der Prognose an unseren Indikator verringern (in diesem Fall schaltet ein Multiplikator von 0,45 den Alarm für dieses Zeitfenster aus). Dadurch können wir natürlich auch eine größere Abweichung vom "Normalwert" erreichen, bevor ein Alarm ausgelöst wird.
Einfach-exponentielle Glättung
Indem wir den ursprünglichen Indikator glätten, bevor wir ihn mit einem Faktor multiplizieren, können wir den Schwellenwert besser an den Indikator anpassen. Die einfach-exponentielle Glättung wird durch Gleichung 4-1 definiert.
Gleichung 4-1. Wobei
ist ein Glättungsparameter. Wenn werden alle Terme außer dem ersten auf Null gesetzt und es bleibt die naive Methode. Werte unter 1 zeigen, wie wichtig frühere Stichproben sein sollten.
Wie bei der naiven Methode ist das Warnkriterium für Atlas, wenn die Abfrage in Beispiel 4-38 1
zurückgibt.
Beispiel 4-38. Atlas-Warnkriterien für die einfach-exponentielle Glättung
alpha,0.2,:set, coefficient,(,alpha,:get,1,alpha,:get,:sub,),:set, name,requestsPerSecond,:eq, :dup,:dup,:dup,:dup,:dup,:dup, 0,:roll,1m,:offset,coefficient,:fcall,0,:pow,:mul,:mul, 1,:roll,2m,:offset,coefficient,:fcall,1,:pow,:mul,:mul, 2,:roll,3m,:offset,coefficient,:fcall,2,:pow,:mul,:mul, 3,:roll,4m,:offset,coefficient,:fcall,3,:pow,:mul,:mul, 4,:roll,5m,:offset,coefficient,:fcall,4,:pow,:mul,:mul, 5,:roll,6m,:offset,coefficient,:fcall,5,:pow,:mul,:mul, :add,:add,:add,:add,:add, 0.83,:mul, :lt,
Die Summierung ist eine geometrische Reihe, die gegen 1 konvergiert. Zum Beispiel für siehe Tabelle 4-3.
0 |
0.5 |
0.5 |
1 |
0.25 |
0.75 |
2 |
0.125 |
0.88 |
3 |
0.063 |
0.938 |
4 |
0.031 |
0.969 |
5 |
0.016 |
0.984 |
Da wir nicht alle Werte von T
einbeziehen, ist die geglättete Funktion bereits mit einem Faktor multipliziert, der der kumulativen Summe dieser geometrischen Reihe bis zu der von uns gewählten Anzahl von Termen entspricht. Abbildung 4-35 zeigt die Summen von einem und zwei Termen in der Reihe relativ zum wahren Wert (jeweils von unten nach oben).
Abbildung 4-36 zeigt, wie die verschiedenen Einstellungen von und auf den dynamischen Schwellenwert auswirken, und zwar sowohl in Bezug auf die Glättung als auch auf den ungefähren Skalierungsfaktor im Verhältnis zum wahren Indikator.
Universelles Skalierbarkeitsgesetz
In diesem Abschnitt werden wir von der Glättung von Datenpunkten aus der Vergangenheit (die wir als dynamische Warnschwellen verwendet haben) zu einer Technik übergehen, mit der wir vorhersagen können, wie die künftige Leistung aussehen wird, wenn die Gleichzeitigkeit bzw. der Durchsatz über das derzeitige Niveau hinaus ansteigt, indem wir nur eine kleine Anzahl von Stichproben verwenden, die zeigen, wie die Leistung bei bereits erreichten Gleichzeitigkeitsgraden aussah. Auf diese Weise können wir vorausschauende Warnungen ausgeben, wenn wir uns einem Service-Level-Ziel nähern, und so hoffentlich Probleme vermeiden, anstatt erst zu reagieren, wenn die Grenze bereits überschritten ist. Mit anderen Worten: Mit dieser Technik können wir einen vorhergesagten Service-Level-Indikatorwert mit unserem SLO bei einem Durchsatz testen, den wir noch nicht erlebt haben.
Diese Technik basiert auf einem mathematischen Prinzip, das als Little's Law und Universal Scalability Law (USL) bekannt ist. Wir werden die mathematische Erklärung hier auf ein Minimum beschränken. Das Wenige, das besprochen wird, kannst du einfach überspringen. Für weitere Details ist Baron Schwartz' frei erhältliches Buch Practical Scalability Analysis with the Universal Scalability Law (VividCortex) eine gute Referenz.
Anwendung des universellen Skalierbarkeitsgesetzes in der Lieferkette
Wir können nicht nur drohende SLA-Verletzungen in Produktionssystemen vorhersagen, sondern auch dieselben Telemetriedaten in einer Auslieferungspipeline verwenden, um eine Software mit Datenverkehr zu belasten, der nicht annähernd so hoch sein muss wie der maximale Datenverkehr in der Produktion, und vorhersagen, ob der Datenverkehr auf Produktionsniveau ein SLA erfüllen wird. Und das, bevor wir eine neue Version der Software in die Produktion einführen!
Das Little'sche Gesetz, Gleichung 4-2, beschreibt das Verhalten von Warteschlangen als eine Beziehung zwischen drei Variablen: Größe der Warteschlange (), Latenz () und dem Durchsatz (). Wenn dir die Anwendung der Warteschlangentheorie auf SLI-Vorhersagen etwas verwirrend vorkommt, mach dir keine Sorgen (denn das ist sie). Aber für unsere Zwecke bei der Vorhersage eines SLI, den Gleichzeitigkeitsgrad der Anfragen, die unser System durchlaufen, den Durchsatz und ein Maß für die Latenz, z. B. der Durchschnitt oder ein hoher Perzentilwert. Da es sich um eine Beziehung zwischen drei Variablen handelt, können wir von zwei beliebigen Variablen die dritte ableiten. Da wir uns für die Vorhersage der Latenz () interessiert, müssen wir diese in den beiden Dimensionen der Gleichzeitigkeit vorhersagen () und Durchsatz ().
Gleichung 4-2. Littlesches Gesetz
Das universelle Skalierbarkeitsgesetz, Gleichung 4-3, ermöglicht es uns stattdessen, die Latenzzeit in Bezug auf nur eine einzige Variable zu prognostizieren: entweder den Durchsatz oder die Gleichzeitigkeit. Diese Gleichung erfordert drei Koeffizienten, die aus einem von Micrometer gepflegten Modell abgeleitet und aktualisiert werden, das auf realen Beobachtungen der bisherigen Systemleistung basiert. Die USL definiert als die Kosten des Übersprechens, für die Kosten von Streitigkeiten und wie schnell das System unter unbelasteten Bedingungen arbeitet. Die Koeffizienten werden zu festen Werten, so dass Vorhersagen über Latenz, Durchsatz oder Gleichzeitigkeit nur von einem der drei anderen Faktoren abhängen. Micrometer veröffentlicht auch die Werte dieser Koeffizienten, wenn sie sich im Laufe der Zeit ändern, sodass du die wichtigsten Leistungsmerkmale des Systems im Laufe der Zeit vergleichen kannst.
Gleichung 4-3. Universelles Skalierbarkeitsgesetz
Mit einer Reihe von Substitutionen können wir Folgendes ausdrücken in Form von oder (siehe Gleichung 4-4). Auch hier musst du nicht lange über diese Beziehungen nachdenken, denn Micrometer erledigt diese Berechnungen für dich.
Gleichung 4-4. Voraussichtliche Latenzzeit als Funktion des Durchsatzes oder der Gleichzeitigkeit
Stattdessen erhalten wir eine schöne zweidimensionale Projektion, wie in Abbildung 4-37 dargestellt.
Die USL-Prognose ist eine Form der "abgeleiteten" Meter
in Micrometer und kann wie in Beispiel 4-39 gezeigt aktiviert werden. Micrometer veröffentlicht eine Reihe von Gauge
Zählern, die eine Reihe von Prognosen für verschiedene Durchsatz-/Gleichzeitigkeitsstufen für jedes Veröffentlichungsintervall bilden. Durchsatz und Gleichzeitigkeit sind korrelierte Messgrößen, also betrachte sie von nun an als austauschbar. Wenn du eine zusammenhängende Gruppe von Zeitmessern (die immer denselben Namen haben) auswählst, für die du eine Prognose veröffentlichen möchtest, veröffentlicht Micrometer mehrere zusätzliche Messwerte, die den gemeinsamen Messwertnamen als Präfix verwenden:
- timer.name.prognose
-
Eine Reihe von
Gauge
Messgeräten mit einem Tagthroughput
oderconcurrency
basierend auf der Art der ausgewählten unabhängigen Variable. Wenn du diese Messgeräte in einem bestimmten Zeitintervall aufzeichnest, entsteht eine Visualisierung wie in Abbildung 4-37. - timer.name.crosstalk
-
Ein direktes Maß für das Übersprechen des Systems (z. B. Fan-Out in einem verteilten System, wie es in der Arbeit von S. Cho et al. beschrieben wird, " Moolle: Fan-Out Control for Scalable Distributed Data Stores").
- timer.name.contention
-
Ein direktes Maß für die Systemkonkurrenz (z. B. das Sperren von relationalen Datenbanktabellen und generell jede andere Form der Synchronisierung von Sperren).
- timer.name.unloaded.performance
-
Es ist zu erwarten, dass Verbesserungen der idealen Leistung im unbelasteten Zustand (z. B. Verbesserungen der Rahmenleistung) auch unter Belastung zu Verbesserungen führen.
Beispiel 4-39. Universelles Skalierbarkeitsgesetz Prognosekonfiguration in Mikrometer
UniversalScalabilityLawForecast
.
builder
(
registry
.
find
(
"http.server.requests"
)
.
tag
(
"uri"
,
"/myendpoint"
)
.
tag
(
"status"
,
s
-
>
s
.
startsWith
(
"2"
)
)
)
.
independentVariable
(
UniversalScalabilityLawForecast
.
Variable
.
THROUGHPUT
)
// In this case, forecast to up to 1,000 requests/second (throughput)
.
maximumForecast
(
1000
)
.
register
(
registry
)
;
Die Vorhersage basiert auf den Ergebnissen einer Mikrometerzählersuche nach einem oder mehreren Timern mit dem Namen
http.server.requests
(erinnere dich daran, dass es mehrere solcher Timer mit unterschiedlichen Tag-Werten geben kann).Wir können die Menge der Zeitmesser, auf die sich die Vorhersage stützt, weiter einschränken, indem wir nur Zeitmesser mit einem bestimmten Schlüssel-Wert-Tag-Paar abgleichen.
Wie bei jeder Suche kann der Tag-Wert auch mit einem Lambda eingeschränkt werden. Ein gutes Beispiel ist die Einschränkung der Prognose auf alle "2xx" HTTP-Status.
Der Bereich des
Gauge
Histogramms ist entwederUniversalScalabilityLawForecast.Variable.CONCURRENCY
oderUniversalScalabilityLawForecast.Variable.THROUGHPUT
, standardmäßigTHROUGHPUT
.
Die Latenz, die eine Anwendung bei ihrem aktuellen Durchsatz in einer dieser Zeitscheiben erfährt, wird sich eng an die "vorhergesagte" Latenz aus der Prognose halten. Wir können einen Alert auf Basis eines hochskalierten Werts des aktuellen Durchsatzes setzen, um festzustellen, ob die vorhergesagte Latenz bei diesem hochskalierten Durchsatz immer noch unter unserem SLO liegt.
Neben der Vorhersage eines SLI bei erhöhtem Durchsatz sind die modellierten Werte für Crosstalk, Contention und unbelastete Leistung ein starker Indikator dafür, wo in einer Anwendung Leistungsverbesserungen vorgenommen werden können. Schließlich wirken sich die Verringerung von Übersprechen und Konflikten und die Erhöhung der unbelasteten Leistung direkt auf die vorhergesagte und tatsächliche Latenz des Systems bei verschiedenen Laststufen aus.
Zusammenfassung
In diesem Kapitel haben wir dir die Tools vorgestellt, die du brauchst, um jeden Java-Microservice mit Signalen, die in Java-Frameworks wie Spring Boot enthalten sind, auf Verfügbarkeit zu überwachen. Außerdem haben wir allgemeiner besprochen, wie du Metrik-Klassen wie Zähler und Timer überwachen und visualisieren kannst.
Obwohl du dich bemühen solltest, die Verfügbarkeit von Microservices anhand von geschäftsorientierten Kennzahlen zu messen, ist die Verwendung dieser grundlegenden Signale ein großer Fortschritt gegenüber der Betrachtung von Box-Metriken, wenn es darum geht, zu verstehen, wie dein Service funktioniert.
Organisatorisch hast du dich verpflichtet, ein Dashboarding-/Benachrichtigungstool einzurichten. In diesem Kapitel haben wir Grafana vorgestellt. Seine Open-Source-Verfügbarkeit und die Datenquellen für eine Vielzahl beliebter Überwachungssysteme machen es zu einer soliden Wahl, auf der du aufbauen kannst, ohne dich komplett an einen bestimmten Anbieter zu binden.
Im nächsten Kapitel werden wir uns mit der Automatisierung der Bereitstellung befassen und sehen, wie einige dieser Verfügbarkeitssignale genutzt werden, um Entscheidungen über die Eignung neuer Microservice-Releases zu treffen. Bei der effektiven Auslieferung geht es nicht nur um die Bewegung des Deployments, sondern darum, die Überwachung in die Tat umzusetzen.
Get SRE mit Java Microservices now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.