Wir haben bewiesen, dass jeder Alarm auslösen kann. Die meisten konnten es nicht.
Monitoring hat eine Eigenschaft, die es einzigartig gefährlich macht: Es ist der Code, der nur während einer Katastrophe läuft. Jeder andere Teil deines Systems läuft ständig, vor den Augen der Nutzer, und wirft Fehler, die dir auffallen. Deine Alarmierung läuft an dem Tag, an dem die Datenbank vollläuft, und keinen Moment früher, und wenn sie kaputt ist, erfährst du es genau in dem Augenblick, in dem du dich auf sie verlassen hast, und zwar auf genau die Weise, die garantiert, dass niemand hingesehen hat. Ein stummer Alarm ist schlimmer als gar kein Alarm, denn von einem fehlenden Alarm weißt du, dass du ihn nicht hast, von einem stummen aber glaubst du, dass du ihn hast.
Also haben wir aufgehört, unseren Alarmen zu vertrauen, nur weil wir sie geschrieben hatten, und ein Prüfgerüst gebaut, um es zu beweisen. Für jede Alarmregel, die wir ausrollen, treibt das Prüfgerüst ein echtes Signal, das sie auslösen sollte, und dann tut es das, worauf es wirklich ankommt: Es wartet, bis die Benachrichtigung physisch in einem Auffang-Relais eintrifft, einem kleinen Dienst, der stellvertretend für das Telefon steht, das sonst vibrieren würde. Dass es in der Oberfläche des Monitoring-Werkzeugs selbst auslöst, zählt nicht. Die Zustellung zählt. Der vollständige Weg vom Signal bis zur Nachricht in der Hand, oder es ist ein Fehlschlag.
Der erste echte Lauf ließ die meisten Regeln durchfallen. Hier ist, was sich hinter grünen Dashboards verbarg.
Der wichtigste Alarm war tot
Die wichtigste Regel, die wir besitzen, ist die, die auslöst, wenn die Fehlerrate steigt: zu viele HTTP-500er, alarmiere jemanden. Das Prüfgerüst warf vierhundertfünfzig echte Serverfehler dagegen, verifizierte, dass mindestens neunzig Prozent davon tatsächlich 500 zurückgaben, und wartete. Nichts kam an. Der eine Alarm, dem wir am meisten vertrauen wollten, hatte kein einziges Mal ausgelöst und hätte auch bei einem echten Vorfall nicht ausgelöst.
Die Ursache war ein Name. Unsere Metriken stammen aus OpenTelemetry, wo Namen mit Punkten geschrieben werden, wie http.server.request.duration. Irgendwo zwischen dem Lesen eines Tutorials und dem Schreiben der Abfrage hatten wir angenommen, das Monitoring-Backend würde diese Punkte zu Unterstrichen normalisieren, so wie es viele Prometheus-Werkzeuge tun, und die Alarmabfrage gegen die Unterstrich-Form geschrieben. Das Backend normalisiert nicht. Es behält die Namen mit Punkten genau so, wie sie gesendet wurden. Also fragte der Alarm eine Metrik ab, die nicht existierte, traf auf nichts, berechnete für immer eine Fehlerrate von null und saß ruhig und grün da, während die echte Metrik unter ihrem echten, mit Punkten geschriebenen Namen jeden einzelnen dieser Fünfhunderter aufzeichnete. Die Abfrage war nicht auf eine Weise falsch, die einen Fehler wirft. Sie war auf eine Weise falsch, die still die leere Menge zurückgibt, was die Arithmetik dann in eine völlig plausible Null verwandelt.
Der Fehler, der jeden Log-Alarm mit sich riss
Das war der schlimme Fall. Der nächste war schlimmer, und das Prüfgerüst fing ihn nur, weil es auf Zustellung besteht und nicht darauf, dass die Regel korrekt aussieht.
Eine ganze Kategorie unserer Alarme ist log-basiert: Finde eine Zeile in den Logs, löse aus. Jeder einzelne von ihnen wertete über nichts aus, und der Grund hatte nichts mit den Alarmregeln zu tun. Es war eine Aufbewahrungseinstellung.
Wir gleichen unsere Log-Aufbewahrung über die Einstellungs-API des Backends ab. Diese API war auf eine neue Version mit einer neuen Anfrageform umgezogen, und wir sendeten immer noch die alte Form. Der alte Body wurde nicht abgelehnt. Der Decoder auf der anderen Seite ignoriert stillschweigend Felder, die er nicht erkennt, also las er unsere nun unbekannten Felder als abwesend, nahm den Standardwert für die Aufbewahrung, die er nicht finden konnte, und dieser Standardwert war null. Der Aufruf gab ein fröhliches 200 zurück. Und eine Aufbewahrung von null Tagen bedeutet sofortiges Ablaufen: Neu geschriebene Log-Daten wurden so gestempelt, dass sie sofort abliefen, sodass der Speicher, den die Log-Alarme abfragten, praktisch immer leer war. Die Aufnahme sah die ganze Zeit völlig gesund aus, denn Logs kamen an. Sie alterten nur in genau dem Moment aus, in dem sie landeten.
Zwei Systeme taten jeweils etwas Vertretbares. Die Einstellungs-API akzeptierte eine Anfrage, die sie teilweise nicht verstand, statt zu scheitern, was eine gängige und oft sinnvolle Entscheidung ist. Unser Deployment sendete einen Body, der gegenüber einem Versionssprung veraltet war. Keines von beiden schlug Alarm. Das Ergebnis war, dass die einzige folgenreichste Zahl in unserer Log-Pipeline, wie lange ein Log lebt, still auf null gesetzt worden war, und das einzige sichtbare Symptom war, dass jeder Log-Alarm dauerhaft und gesund aussehend stumm blieb. Hätten wir getestet, ob die Regeln richtig aussehen, sahen sie perfekt aus. Nur zu testen, dass ein echtes geloggtes Ereignis eine zugestellte Alarmierung erzeugt, konnte es ans Licht bringen, denn nur dieser Test liest tatsächlich wieder aus dem Speicher, der geleert worden war.
Die Alarme, die für immer auslösten, was dasselbe ist wie nie
Ein dritter Fehler lief in die entgegengesetzte Richtung, und er ist der subtile. Eine Handvoll Audit-Alarme löste ständig aus. Das klingt nach dem Gegenteil eines stummen Alarms, hat aber genau dieselbe Wirkung, und zu verstehen, warum, ist den Umweg wert.
Wenn einer dieser Alarme auslöste, schrieb das Monitoring-Backend eine Log-Zeile über die Auswertung, eine Brotkrume, und diese Zeile enthielt den eigenen Filterausdruck des Alarms als Text. Diese Brotkrume wurde selbst als Log aufgenommen, wie alles andere. Also fand die Regel bei der nächsten Auswertung eine passende Zeile: ihren eigenen Fußabdruck vom letzten Mal. Die Regel war selbsterhaltend geworden. Sie traf bei jeder Auswertung ihren eigenen Schatten, für immer.
Ein Alarm, der immer auslöst, wechselt nie von nicht-auslösend zu auslösend, und dieser Wechsel ist es, der eine Benachrichtigung sendet. Also unterdrückte die Alarmierungsschicht, die eine Bedingung sah, die bereits wahr war und wahr blieb, weitere Benachrichtigungen, genau wie vorgesehen, um dir nicht jede Minute eine Meldung über etwas zu schicken, worüber du bereits informiert wurdest. Die Folge ist, dass ein echtes Ereignis, eine echte Audit-Aktion, die eine Alarmierung wert ist, in eine Regel gelangte, die bereits festhing, und deshalb nichts sagte. Ein Alarm, der dauerhaft an ist, ist genauso stumm wie einer, der dauerhaft aus ist. Die Lösung war, diese Regeln auf Log-Zeilen zu beschränken, die von unseren eigenen Diensten stammen, damit sie nicht mehr das Geplauder des Backends über sich selbst treffen, mit einer bewussten Ausnahme, deren echtes Signal tatsächlich von außerhalb unserer Dienste kommt.
Die Regel, die den ersten Atemzug eines Pods für normal hielt
Der letzte ist eine kleine, scharfe Lektion über Ratenberechnungen. Ein Alarm, der auf der Änderungsrate einer Metrik beruht, kann aus dem allerersten Messwert einer Reihe keine Rate berechnen, weil es davor nichts gibt, womit man vergleichen könnte. Als also ein frischer Pod seinen ersten Schwall an Fehlern ausstieß, wurde dieser Schwall zur Grundlinie statt zu einer Spitze, und die Regel, die ihn hätte fangen sollen, sah nur einen Ausgangspunkt. Wir haben es im Prüfgerüst behoben, indem wir solche Signale in zwei Wellen treiben, eine kleine, um die Reihe zu etablieren, und eine echte danach, was auch eine treffende Beschreibung dessen ist, was Produktionsverkehr von allein tut und was ein synthetischer Test bewusst nachahmen muss.
Jeder Fehler versteckte sich in einer Naht, die man nur findet, indem man sie beansprucht
Jeder einzelne davon war für die Inspektion unsichtbar. Die Abfragen lasen sich korrekt. Die Regeln waren ausgerollt. Die Dashboards waren grün. Die Aufnahme war gesund. Jeder Fehler lebte in einer Naht zwischen zwei Dingen, die jeweils für sich in Ordnung waren: ein Metrikname und eine Annahme über Normalisierung, ein Anfrage-Body und ein Versionssprung, ein Alarm und das Log, das sein eigenes Auslösen erzeugt, eine Ratenfunktion und eine Reihe, die gerade erst begonnen hat. Man kann eine Naht nicht lesen. Man muss sie beanspruchen.
Behandle Monitoring also als das, was es ist, nämlich Code, und zwar Code, dessen einziger Produktionslauf der Notfall ist. Code, der nur im Notfall läuft, braucht Tests, die an jedem anderen Tag laufen, denn es gibt kein sanftes erstes Scheitern, das dich warnt. Treibe ein echtes Signal von Anfang bis Ende, und prüfe das Artefakt ganz am Ende der Kette, die zugestellte Benachrichtigung, nicht die Regel, die in der Mitte gesund aussieht. Alles darunter testet, dass du einen Alarm geschrieben hast, was du bereits wusstest, statt zu testen, dass er auslöst, was das Einzige ist, was du wirklich wolltest.
Wenn du möchtest, dass dein Identitätsanbieter sich bereits auf diese Weise selbst überwacht: Authagonal betreibt seine eigene Alarmierung durch genau dieses Prüfgerüst, damit an dem Tag, an dem etwas kaputtgeht, die Meldung, die auslösen sollte, es auch tatsächlich tut.