Um eine Anwendung erfolgreich betreiben zu können, muss diese, neben der korrekten Umsetzung der fachlichen Anforderungen, eine Reihe von technischen Faktoren erfüllen. Hierzu gehören Logging, Metriken, Tracing und Health-Checks. In Kombination ermöglichen alle vier Faktoren zusammen den Betrieb einer Anwendung.
Health-Checks ermöglichen hierbei, direkt mit einem Blick auf die Anwendung zu sehen, wie deren aktueller Status ist. Somit lässt sich schnell erfassen, ob es Fehler gibt. Hierbei sind keine fachlichen Fehler oder fehlerhafte Eingaben gemeint, sondern vor allem Probleme mit externen Ressourcen der Anwendung. Externe Ressourcen sind hierbei die Festplatte, der Arbeitsspeicher oder die Verbindung zu anderen Systemen, wie Datenbanken, Message-Brokern oder anderen Anwendungen.
Ein Health-Check ist sowohl für Menschen als auch für Maschinen gemacht. Einem Menschen wird ermöglicht, im Fehlerfall auf einen Blick erkennen zu können, wo dieser entstanden sein könnte. Maschinen nutzen Health-Checks hingegen, um automatisiert entscheiden zu können, ob eine Instanz der Anwendung noch verwendbar ist. Sollte dies nicht mehr der Fall sein, kann dafür gesorgt werden, dass diese Instanz keinerlei Anfragen mehr bekommt oder die Instanz einfach gestoppt und eine neue gestartet wird.
Im Folgenden werden die Bestandteile von Health-Checks beleuchtet, anschließend gängige Herausforderungen aufgezeigt und im letzten Schritt wird die konkrete Implementierung in drei Frameworks für Java beschrieben.
Bestandteile
Für die vollständige Umsetzung von Health-Checks braucht es mehrere Bestandteile. Der erste Bestandteil sind die konkreten Health-Checks. Damit diese implementiert werden können, gibt es eine Programmierschnittstelle. Zwar unterscheidet diese sich im Detail zwischen den verfügbaren Bibliotheken, aber die Idee ist immer dieselbe. Jeder Health-Check implementiert ein Interface oder eine abstrakte Klasse, die eine Methode vorgibt. Diese Methode erwartet keine Argumente und gibt ein Ergebnisobjekt zurück, das den Status des Health-Checks abbildet. Neben dem Status kann das Objekt häufig auch noch Details enthalten, beispielsweise warum der Check zum konkreten Status gekommen ist. Manche Bibliotheken enthalten auch bereits fertige Health-Checks für Standardressourcen wie beispielsweise JDBC-Verbindungen.
Da der Gesamtzustand einer Anwendung jedoch meistens nicht mit nur einem Health-Check abgebildet werden kann, gibt es einen zweiten Bestandteil, meist Registry genannt. Bei dieser werden alle Health-Checks der Anwendung registriert. Anschließend ist sie dafür verantwortlich, alle Health-Checks auszuführen und aus allen Teil-Ergebnissen einen Gesamtzustand zu berechnen.
Der so gebildete Gesamtzustand soll natürlich von außerhalb der Anwendung abrufbar sein. Hierzu gibt es den dritten und letzten Bestandteil. Dieser implementiert eine von außen abrufbare Schnittstelle, zum Beispiel JSON über HTTP, und definiert damit auch, in welchem konkreten Format der Zustand von außen auslesbar ist.
Herausforderungen/Best Practices
Die Schnittstelle nach außen sollte die vom gewählten Protokoll zur Verfügung gestellte Semantik, um Fehler zu signalisieren, nutzen. Nutzt man beispielsweise HTTP als Protokoll, so sollte eine Abfrage, die einen fehlerhaften Gesamtzustand der Anwendung signalisiert, einen HTTP-Statuscode nutzen, der einen Server-Fehler signalisiert. In diesem konkreten Fall bietet sich 503, also „Service Unavailable“, an.
Je nach Plattform und Anforderung, wie schnell ein fehlerhafter Zustand erkannt werden soll, kann es passieren, dass der Gesamtzustand sehr häufig, gegebenenfalls alle paar Sekunden, abgefragt wird. Hierzu muss sichergestellt werden, dass dies nicht zu Instabilitäten führt und dass der Gesamtzustand schnell genug herausgefunden werden kann. Findet beispielsweise alle 5 Sekunden eine Abfrage statt, die Durchführung aller Health-Checks dauert jedoch länger, können Probleme entstehen. Dieser Herausforderung kann man mit Caching und der asynchronen Ausführung der Health-Checks begegnen. Zwar erhält man anschließend nicht mehr bei jeder Abfrage den wirklich aktuellen Zustand der Anwendung, dafür verbessert sich die Stabilität.
Die letzte Herausforderung besteht im Aspekt der Sicherheit. Health-Checks enthalten neben dem eigentlichen Status oft noch weitere Informationen. Diese helfen dabei zu verstehen, wieso der aktuelle Status zustande gekommen ist. Natürlich kann dies auch ein Sicherheitsrisiko sein, da hierüber zum Beispiel herausgefunden werden kann, welche Datenbank in welcher Version genutzt wird. Idealerweise unterstützt die gewählte Bibliothek hierfür ein Konzept, das anonymen Nutzern diese Details nicht anzeigt.
Nun kommen wir zu den drei Health-Check-Bibliotheken Metrics, Actuator und MicroProfile.
DropWizard Metrics
Metrics ist ein Sub-Projekt von DropWizard, das neben Metriken auch eine
Implementierung von Health-Checks bietet. Um einen eigenen Health-Check zu
implementieren, erbt man von der Klasse HealthCheck
und überschreibt die
Methode check
. Das als Rückgabewert erwartete Result
kann mit den
vorhandenen statischen Factory-Methoden oder mit einem Builder erzeugt werden.
Listing 1 zeigt eine Implementierung, welche den verfügbaren Festplattenplatz
überprüft und bei zu geringem Rest von Healthy auf Unhealthy umspringt.
Anschließend muss von diesem Health-Check noch eine Instanz erzeugt und bei
einer HealthCheckRegistry
registriert werden. Über diese kann der Health-Check
anschließend auch ausgeführt werden (s. Listing 2).
Aktuell werden also mit jedem Aufruf der Registry alle Health-Checks ausgeführt.
Um die Ausführung eines Health-Checks vom Aufruf der Registry zu entkoppeln,
kann man den Health-Check mit @Async
annotieren. Anschließend wird der Status
dieses Health-Checks asynchron abgefragt und bei Aufrufen auf der Registry wird
der letzte bekannte Wert genutzt (s. Listing 3).
Als Letztes stellt Metrics noch das HealthCheckServlet
zur Verfügung. Neben
dem Anzeigen aller Health-Check-Resultate ermittelt dieses auch einen
Gesamtzustand und setzt den HTTP-Statuscode auf einen passenden Wert.
Leider bringt Metrics bis auf einen Health-Check zur Erkennung von Threads, die
sich in einem Deadlock befinden (ThreadDeadlockHealthCheck
), keine weiteren
direkt nutzbaren Health-Checks mit.
Spring Boot Actuator
Innerhalb von Spring Boot kümmert sich Actuator um viele Belange rund um Betriebsaspekte. Teil davon sind auch Health-Checks.
Actuator bringt dazu bereits eine Menge fertiger Health-Checks für externe Ressourcen, vor allem Datenbanken, mit. Wird eine solche innerhalb von Spring Boot konfiguriert, wird innerhalb von Actuator auch der Health-Check automatisch registriert.
Reichen die vorhandenen Health-Checks nicht aus, kann entweder das Interface
HealthIndicator
implementiert oder von der abstrakten Klasse
AbstractHealthIndicator
geerbt werden, um einen neuen Health-Check
hinzuzufügen. Der zweite Weg hat dabei den Vorteil, dass im Falle einer
Exception der Status des Health-Checks automatisch auf einen fehlerhaften
gesetzt wird.
Dies ist ein Unterschied zu Metrics. Actuator legt großen Wert darauf, an allen
Stellen erweiterbar zu sein. Aus diesem Grund wird der Status eines
Health-Checks nicht über ein boolean ausgedrückt, sondern über die eigene Klasse
Status
. Standardmäßig werden dabei die vier Werte Unknown, Up, Down und
Out of Service unterstützt. Es ist jedoch auch möglich, eigene Werte zu
definieren, sofern man diese benötigt.
Um anschließend aus allen Health-Checks einen Gesamtstatus zu berechnen, nutzt
Actuator das Konzept eines HealthAggregator
. In diesem wird die Strategie
implementiert, um die verschiedenen Status-Werte auf einen Gesamtstatus zu
reduzieren. Im Standard nutzt Actuator den OrderedHealthAggregator
, in dem
eine Reihenfolge der Status-Werte definiert wird. Der Gesamtzustand entspricht
anschließend dem Status, der in der Reihenfolge als Erstes kommt und der in der
Liste aller Health-Checks gefunden wurde. Innerhalb von Spring Boot kann die
Reihenfolge per Konfiguration geändert und um eigene Status-Werte erweitert
werden. Außerdem kann natürlich auch eine eigene Strategie implementiert werden.
Ähnlich wie bei den Indikatoren gibt es auch hierfür eine abstrakte Klasse,
AbstractHealthAggregator
, die im Gegensatz zur direkten Implementierung des
Interface ein wenig Arbeit abnimmt.
Damit die Health-Checks von außen erreichbar sind, bietet Actuator zudem eine Komponente, die diese per HTTP oder JMX exponiert. Neben dem Verfügbarmachen setzt diese auch noch den Sicherheits- und Verfügbarkeitsaspekt um. Die Verfügbarkeit wird durch das Cachen des letzten Ergebnisses für eine bestimmte Zeit (im Standard 1 s) erledigt. Die Sicherheit wird durch diverse Mechanismen erhöht. Zum einen lässt sich der Endpunkt auf einem eigenen Port starten, sodass Health-Checks nur aus dem internen Netzwerk, nicht aber von außen abgefragt werden können. Eine andere Alternative besteht darin, den Endpunkt per Authentifikation abzusichern. Somit erhält ein anonymer Nutzer nur den Wert des Gesamtzustands, aber keinerlei Details. Authentifiziert sich der Nutzer vorher, zum Beispiel per HTTP Basic-Auth, kann er jedoch alle Details sehen.
MicroProfile: Service Healthchecks
Die Idee von Eclipse MicroProfile ist es, Java EE um ein Profil zu erweitern, das es ermöglicht, Microservices zu implementieren. In diesem Rahmen entstehen auch Spezifikationen für bisher noch fehlende technische Belange. Die Spezifikation für Health-Checks nennt sich „Service Healthchecks“.
Die Spezifikation besteht dabei aus zwei Teilen, dem API zum Implementieren von
Health-Checks und der Definition des Formates und Protokolls, über das die
Health-Checks von außen erreichbar sind. Eigene Health-Checks müssen hierzu das
Interface HealthCheck
implementieren und mit @Health
annotiert werden. Das
Ergebnis des Checks wird mittels der Klasse HealthCheckResponse
abgebildet und
kann durch die Verwendung von HealthCheckResponseBuilder
erzeugt werden. Dabei
stehen die beiden Status-Werte Up und Down zur Verfügung (s. Listing 4).
Als Protokoll sieht die Spezifikation HTTP vor, überlässt es allerdings jeder Implementierung, auch weitere Protokolle zu unterstützen. Der Payload wird in JSON repräsentiert und die Spezifikation enthält ein JSON-Schema, um diesen zu definieren. Alle weiteren Punkte sind in der Spezifikation optional. So kann jede Implementierung Sicherheitsmaßnahmen ergreifen, um den Health-Check-Endpunkt abzusichern. Und auch welche Strategie zur Berechnung des Gesamtzustands genutzt wird, ist Sache einer jeden Implementierung.