Ein Exit ist kein Urteil
Ein Monitor, der auf die Abwesenheit eines Prozesses hin alarmiert, beantwortet eine Frage, deren Antwort du längst siehst. Ob ein Job weg ist, lässt sich billig feststellen und ist fast nie das, worauf es ankommt. Ob er fehlgeschlagen ist oder absichtlich beendet wurde, durch ein Deploy, einen Neustart, einen Operator, das ist es, woran ein Alarm hängt, und ein bloßer Exit trägt davon nur einen Bruchteil.
Der Alarm kam kurz nach Tagesanbruch, und er war falsch. Ein periodischer Sync-Job, einer aus einer Klasse, die alle zwanzig Minuten läuft, ein wenig Arbeit erledigt und sich beendet, hatte seinen Failure-Hook ausgelöst und Alarm geschlagen. Die Benachrichtigung sagte, der Prozess sei getötet worden, Signal fünfzehn, eine schlichte Beendigung. Kaputt war in Wahrheit nichts. Über Nacht hatte die Maschine ihr unbeaufsichtigtes Upgrade gefahren, und irgendwo in diesem Upgrade-Zyklus ging ein Stopp-Signal an alle Units hinaus, die gerade liefen, als es eintraf; der Sync hatte dreißig Sekunden zuvor gestartet und wurde mitten im Lauf erwischt. Sein nächster Tick, zwanzig Minuten später, lief sauber, und jeder danach ebenso. Der Job war keine Sekunde lang ungesund gewesen. Und die Benachrichtigung war nicht leer: Sie trug die ganze Geschichte, die Signalnummer, die Tatsache, dass der Prozess getötet wurde und nicht etwa einen Fehler zurückgab. Der Monitor hatte alles davon und nannte ein absichtliches Herunterfahren trotzdem einen Fehlschlag, weil für diese Art von Unit die Regel, die er anwandte, ein Beendigungssignal als Fehler las.
Das ist der Fehlermodus, der im Billigsten steckt, was ein Monitor tun kann, nämlich zu bemerken, dass etwas weg ist. Abwesenheit ist leicht zu erkennen. Sie ist auch fast immer das Falsche, worauf man alarmieren sollte, denn eine Abwesenheit ist keine Ursache, sie ist ihr Schatten, und die Ursachen zerfallen in zwei Haufen, die von dort, wo der Monitor steht, gleich aussehen. Im einen Haufen ist die Arbeit kaputtgegangen: ein Bug, eine schlechte Eingabe, eine Abhängigkeit, die verschwunden ist. Im anderen wurde die Arbeit von außen beendet, meist von etwas, das sie beenden darf: ein Deploy, das die Flotte durchrollt, ein Neustart, ein Operator, der von Hand einen Stopp fährt, ein Idle-Sweep, der zurückholt, was er für einen abgestandenen Prozess hielt, das Upgrade, das meinen Sync erwischte. Ein Monitor, der nur fragt, ob der Prozess noch da ist, alarmiert für beide Haufen zugleich, weil diese Frage sie nicht auseinanderhalten kann. Ein Monitor, der stattdessen an das eigene Fehlschlag-Urteil der Unit angeschlossen ist, ist nur so gut wie dieses Urteil, und der Alarm im Morgengrauen ist, wie ein fehlkalibriertes Urteil aussieht. So oder so sollte ein Alarm nicht an der Tatsache der Abwesenheit hängen, sondern an ihrer Herkunft: was den Prozess beendet hat, und ob es das sollte.
Der Reflex, wenn ein Pager laut wird, ist der Griff zur Zeit. Alarmiere nicht auf eine einzelne Messung; warte ab, ob es hält. Dieser Instinkt ist richtig, und er steckt in den Werkzeugen: Eine Prometheus-Alarmregel mit einer for-Klausel hält eine erfüllte Bedingung in einem Pending-Zustand und befördert sie erst dann zu einem echten Alarm, wenn die Bedingung ununterbrochen für die ganze Dauer wahr geblieben ist, während eine Regel ohne for schon bei der ersten Auswertung losgeht, die sie auslöst. Und Zeit ist die richtige Achse für eine Form von Nicht-Fehlschlag. Ein periodischer Poll gegen eine externe API läuft einmal in einen Timeout, weil ein Netzwerk nun mal ein Netzwerk ist, und heilt sich beim nächsten Versuch von selbst; einen Menschen für diesen einen Aussetzer zu alarmieren ist der Weg, ihm beizubringen, den Pager wegzuwischen, ohne ihn zu lesen, also wartest du stattdessen auf eine Serie, eine Stunde Unerreichbarkeit, bevor du entscheidest, dass es echt ist. Das ist Zeit, die genau die Arbeit tut, für die sie taugt, ein Vorübergehendes zu einem Trend herunterzufiltern.
Aber Zeit ist für den Alarm im Morgengrauen bestenfalls eine halbe Lösung, und es lohnt sich, genau zu sagen, warum, denn es sieht so aus, als müsste sie die ganze Antwort sein. Eine for-Klausel sortiert Abwesenheiten danach, wie lange sie dauern, und Dauer ist nur ein Stellvertreter für Legitimität, niemals eine Ablesung davon. Ein Teil des Stellvertreters trägt: Ein Deploy, das einen langlaufenden Dienst durchrollt, ist Sekunden weg, ein Neustart eine Minute oder zwei, und ein for von wenigen Minuten sitzt beides aus, was wirklich nützlich ist und einen guten Teil der routinemäßigen Stopps abdeckt. Aber das erkauft es nur, indem es jeden echten Alarm um dasselbe Fenster verzögert, und es wird blind in dem Moment, in dem ein legitimer Stopp nicht kurz ist. Eine Unit, die für die Wartung heruntergefahren und unten gelassen wird, ist eine saubere, absichtliche Abwesenheit, die Stunden dauert. Ein periodischer Job, der mitten im Lauf gestoppt wird, bleibt weg bis zu seinem nächsten Tick, was für meinen Zwanzig-Minuten-Sync eine lange, saubere Stille ist, durch die ein Zehn-Minuten-for glatt hindurchsegelt und trotzdem alarmiert. Länger zu warten trennt das Vorübergehende vom Anhaltenden. Es kann das Fehlgeschlagene nicht vom Beabsichtigten trennen, denn ein legitimer Stopp kann genauso anhaltend sein wie ein Absturz, und oft mehr noch.
Die Unterscheidung, die der Pager tatsächlich braucht, ist nicht länger, sie ist tiefer: Er muss wissen, warum der Prozess weg ist. Das Überraschende ist, dass das Betriebssystem einen Teil dieser Antwort bereits mit sich trägt und ihn dann, für eine Art von Unit, wegwirft. systemd behandelt, wenn es einen gewöhnlichen langlaufenden Dienst überwacht, eine Beendigung durch SIGTERM standardmäßig als Erfolg. Seine Exit-Status-Regeln zählen den normalen Exit-Code null als sauber, und für jeden Dienst, der kein Oneshot ist, zählen sie auch die Signale SIGHUP, SIGINT, SIGTERM und SIGPIPE als sauber. Diese Voreinstellung ist der Supervisor, der eine Herkunft festhält, die der Prozess nicht fälschen kann: nicht das, was das Programm über sich selbst sagte, sondern welches Signal eintraf, und dass ein Signal und kein schlechter Rückgabewert es beendet hat. Ein Stopp-Signal, das an einen Dienst geht, den du zu stoppen batest, ist kein Fehler, und der Kontrakt sagt genau das. Aber diese Voreinstellung ist mit einer Ausnahme geschrieben, für den Oneshot-Typ, und ein Job, der startet, ein begrenztes Stück Arbeit erledigt und sich beendet, ist genau ein Oneshot. Für diese Units fällt das Stopp-Signal wieder aus dem sauberen Satz heraus, sodass genau das SIGTERM, das für einen Daemon geordnetes Herunterfahren bedeutet, für den periodischen Job Fehlschlag bedeutet, und der Failure-Hook feuert. Die Reparatur besteht darin, die Unterscheidung von Hand wiederherzustellen, indem man über SuccessExitStatus=SIGTERM erklärt, dass ein Beendigungssignal auf dieser Unit als sauberes Ende zählt, woraufhin ein externer Stopp den Alarm nicht mehr auslöst, während ein echter Fehler, ein Exit-Code ungleich null, den der Job zurückgab, ein Out-of-Memory-Kill durch das härtere SIGKILL, weiterhin alarmiert. Das bringt den Alarm nicht zum Schweigen. Es gibt dem Alarm eine Regel an die Hand, die für die eine Unterscheidung einsteht, die er allein nicht ziehen konnte, fehlgeschlagen gegen beendet, und wie gut diese Regel als Einstand taugt, erweist sich als die Frage, auf der der Rest hier ruht.
Es wäre leicht, das neben ein Problem einzusortieren, dem es ähnelt, also lohnt es sich, die Grenze zu ziehen. Es gibt ein nahes Argument, den Fall, dass eine Anwesenheitsprüfung der falsche Weg ist, einen langlaufenden Agenten zu überwachen, weil sie einen angestrengt denkenden Agenten nicht von einem für immer verklemmten unterscheiden kann, und die Reparatur dort besteht darin, nicht mehr zu fragen, ob der Prozess existiert, und den Fortschritt an der dauerhaften Spur abzulesen, die die Arbeit hinterlässt. Das ist eine echte Lösung, und sie funktioniert, solange die Arbeit noch läuft und noch eine Spur zum Ablesen hinterlässt. Ein Alarm ist die umgekehrte Lage. Das Ding ist weg. Es wird keine Spur mehr geschrieben, weil der Prozess, der sie schriebe, nicht da ist, und es gibt keinen Fortschritt zu befragen, weil nichts voranschreitet. Alles, was bleibt, ist die Art des Weggehens: der Exit-Code, das Signal, was auch immer der Supervisor darüber festgehalten hat, wer es sandte. Für eine Abwesenheit ist Herkunft nicht ein Signal unter mehreren, die du zu lesen wählen könntest. Sie ist das einzige Indiz, das es gibt, und der Teil davon, dem zu trauen sich lohnt, ist genau der Teil, den der abgetretene Prozess nicht selbst verfasst hat. So treten die beiden Entscheidungen auseinander und bleiben auseinander: Die Health-Prüfung fragt, ob das lebende Ding sich bewegt, und liest eine Spur, um es herauszufinden; der Pager fragt, ob das Ende ein Fehlschlag oder eine Wahl war, und hat nichts zu lesen als die Form des Endes selbst.
Tritt von systemd zurück, und das Prinzip ist schlicht, und ein wenig unbequem. Ein Exit-Code ist eine Ein-Byte-Zusammenfassung einer Geschichte, die der Monitor nie mit angesehen hat, und schlimmer, eine Zusammenfassung, die das Subjekt über sich selbst schrieb. Das ist die alte, schwer erkämpfte Vorsicht aus dem Betrieb verteilter Dinge: Du fragst eine Komponente nicht, ob sie gesund ist, und glaubst der Antwort einfach, denn die Komponente, die kaputt ist und es nicht weiß, ist die, deren Wort am wenigsten wert ist. Herkunft ist genau in dem Maße vertrauenswürdig, in dem etwas anderes als der Prozess sie festgehalten hat. SuccessExitStatus kann seine Arbeit nur tun, weil der Supervisor die Out-of-Band-Hälfte der Geschichte behielt, welches Signal eintraf und dass der Kernel, nicht das Programm, es zustellte. Die Hälfte, die das Programm wählt, der Rückgabewert, den es auf dem Weg hinaus abgibt, erbt jeden Grund, dem Bericht eines Dings über sich selbst nicht zu trauen. Der Zug, der sich also auszahlt, wo du ihn hinbekommst, ist, auf eine Tatsache hin zu alarmieren, die etwas anderes als der fehlschlagende Prozess festgehalten hat, und die bloße Abwesenheit, die, an der keine solche Tatsache hängt, als Frage zu halten statt als Urteil. Eine Unit, deren Supervisor einen Out-of-Memory-Kill protokolliert hat, ist auf eine Weise fehlgeschlagen, hinter die du dich stellen kannst. Eine Unit, die bloß weg ist, ohne dass etwas über das Warum festgehalten wäre, hat eine Frage aufgeworfen, und eine Frage ist noch kein Alarm. Das ist die älteste Disziplin der Alerting-Welt, auf die Abwesenheit gewendet: Weck einen Menschen für das, worauf er handeln muss, nicht für jedes Ereignis, das eine Schwelle überschreitet.
Aber lass das nicht zur Erlaubnis erstarren, still zu bleiben, wenn ein Job schlicht nie läuft, was die gefährlichste Abwesenheit von allen ist, weil sie kein Signal erzeugt, das man klassifizieren könnte. Das Gegenstück dazu, nie auf bloße Abwesenheit zu alarmieren, ist, auf das Ausbleiben eines Erfolgs zu alarmieren. Ein Zeitstempel der letzten erfolgreichen Fertigstellung, der veraltet ist, ist eine positive, zuschreibbare Tatsache, die Arbeit war seit einem Zeitpunkt, den du nennen kannst, nicht mehr erfolgreich, und sie macht den Job, der still aus dem Zeitplan fiel, oder der fehlschlug und mit null endete, wieder laut, ohne darauf zu warten, dass das Ding selbst eine Beschwerde einreicht, die es vielleicht nie einreicht.
In dieser Umklassifizierung muss die Ehrlichkeit wohnen, denn Herkunft über die Form eines Signals zu kodieren ist nicht dasselbe, wie sie zu beobachten. Weite den sauberen Satz aus, sodass er SIGTERM einschließt, und du wettest, dass nichts Kaputtes je durch genau diese Tür hinausgeht. Meist geht nichts: Ein echter Fehler neigt dazu, einen Code ungleich null zurückzugeben oder der Maschine den Speicher wegzunehmen und den härteren Kill zu kassieren, den deine Regel nicht abdeckt. Aber die Wette hat Ränder. Ein Fehler, der zufällig über SIGTERM hinausgeht, ein Handler, der das Signal schluckt und sauber zurückkehrt, ein Stopp, der aus einem Grund erteilt wurde, der eigentlich falsch war, ein Idle-Sweep, der einen Prozess zurückholt, der nur ruhig war, erbt jetzt die Unschuld, die die Regel dem Signal zusprach. Ein Hänger hinterlässt gar kein Signal, also sieht diese Regel ihn nie; ein Oneshot ohne gesetzten Start-Timeout kann für immer verklemmt sitzen, ohne je fehlzuschlagen, was das Problem der Health-Prüfung nebenan ist, nicht das des Pagers. Und der Out-of-Memory-Kill, den du im Alarm-Haufen behalten hast, ist selbst eine Ermessensfrage, denn ein Prozess, der für den Appetit eines Nachbarn getötet wurde, ist weniger kaputtgegangen als einen Streit über Kapazität verloren zu haben. Nichts davon ist ein Grund, den Tausch auszulassen. Du hast eine große und sichere Quelle falscher Alarme, jedes Deploy und jeden Neustart über das Leben der Unit, gegen eine kleine und seltene Quelle falscher Stille getauscht, und für einen Job, der sich beim nächsten Tick selbst heilt, ist das fast immer der richtige Tausch. Was es nicht ist, ist eine Ablesung. Wenn du es besser kannst als eine Wette, dann geht das so: Hör auf, Herkunft zu erschließen, und fang an, sie an der Quelle festzuhalten: Lass das Deploy, den Sweep, das Wartungsfenster aufschreiben, dass es diese Unit gestoppt hat, zu dieser Zeit, aus diesem Grund, und die Abwesenheit ist überhaupt keine Frage mehr.
Der Pager ist das eine Instrument im ganzen Stack, dessen ganzer Zweck es ist, die Aufmerksamkeit eines Menschen auszugeben, und er ist das, das am häufigsten so gebaut ist, dass es auf das billigste Signal feuert, das es kriegen kann, nämlich bloße Abwesenheit. Ein Alarm, der jedes Mal losgeht, wenn etwas absichtlich gestoppt wird, ist keine Sicherheitsvorrichtung. Er ist eine Maschine, die ihrem Operator geduldig und wirksam beibringt, dass der Alarm nichts bedeutet, und sie unterrichtet am besten im Morgengrauen am Morgen nach einem routinemäßigen Upgrade. Was er stattdessen beantworten sollte, ist die Frage, die der Mensch in dem Moment tatsächlich hat, in dem der Pager ihn weckt, die nicht lautet, ist es weg, etwas, das er selbst sehen kann, sondern ist es fehlgeschlagen oder hat etwas es beendet, und, an der einen Stelle, an der sich die beiden wirklich nicht auseinanderhalten lassen, zuzugeben, dass die Antwort eine Wette ist und keine Ablesung. Ein Exit ist kein Urteil. Er ist ein Indiz, und die ganze Disziplin liegt darin, sich zu weigern zu alarmieren, bis du weißt, wofür es ein Indiz ist.