Healthendpoint aus theoretischer Sicht

Health Endpoint eines Services

Ich habe eine interessante Problemstellung bearbeitet: Wie könnte ein Health-Endpoint ausschauen? Nach kurzer Recherche im Internet:

  • the status of the connections to the infrastructure services used by the service instance
  • the status of the host, e.g. disk space
  • application specific logic

(Quelle: https://microservices.io/patterns/observability/health-check-api.html)

Meine Meinung dazu:

  • Punkt 1: zu oberflächlich
  • Punkt 2: aus meiner Sicht sind das Metriken – anderes Thema
  • Punkt 3: ok (obwohl nicht genau gesagt wurde, was hiermit gemeint ist)

.NET Core 2.2 hat es native enhalten: Health checks in ASP.NET Core. So wird z.B. bei SQL Server mit SELECT 1 überprüft, ob die Verbindung und die Authentifizierung funktioniert. Wie immer würde ich das gerne mit theoretischen Sichtweisen verschmelzen.

In der Theorie spricht man von:

  • Components: Beinhalten die Logik und das (optionale) Speichern von Daten. Haben eine klar definierte (abgegrenzte) Funktionalität. Komponenten sind Bausteine der Architektur. Die Schnittstelle werden über Ports definiert (alle öffentliche Events und Methoden), welche auch Stateful sein können.
  • Connectors Verbinden Components untereinander. Beispiele sind Local-Method-Call, RPC, SQL, SOAP over HTTP, …

Microservices grenzen sich klar ab:

„The high-level definition of microservice architecture (microservices) is an architectural style that functionally decomposes an application into a set of services. Note that this definition doesn’t say anything about size. Instead, what matters is that each service has a focused, cohesive set of responsibilities

Daher:

  • Ein Microservices hat auch wohl definierte Schnittstellen
  • Ein Microservices kann mehrere Komponenten beinhalten
  • Eine Komponente kann in mehreren Microservices vorkommen

Weitere Eigenschaften von Microservices:

  • Independent deployability: Einzelne Funktionalitäten können geändert werden (e.g. Billing) ohne dass andere Microservices auch deployed werden müssen (e.g. OrderManagament)
  • Keine Shared Databases: Im Regelfall halten Microservices ihre eigenen Daten in einem eigenen Storage.
  • Über Schnittstellen –> Datenaustausch
  • Microservice gehört einem Team
  • Viele Patterns im Dunstkreis der Microservices: https://microservices.io/i/MicroservicePatternLanguage.pdf

Zusammengefasst: Wir interessieren uns primär für den Connector. Weil ob die Festplatte voll ist oder der Cache vollläuft, sehe ich eher bei Metriken, welche dann ggf. einen Alarm triggern. Interna der Komponenten (Logik & Daten) sehe ich ebenfalls nicht beim Health-Endpoint.

Weiter geht es mit folgenden Definitionen:

  • Fault (Umgangssprachlich Bug): ist ein Defekt in der e.g. Software (e.g. falsch gesetzte Klammern bis hin zu falschen Schleifenabbruchbedingungen)
  • Errors: Werden von Fault(s) ausgelöst. Beispiele sind:
    • Timing or Race conditions
    • Infinite Loops
    • Protocol Error
    • Data inconsistency
    • Failure to Handle Overload condition
  • Failure: Wird von Errors herbeigeführt und es kommt zu einem Versagen des Systems / Verfehlung der Anforderungen

Errors sind dabei von größtem Interesse, da sie erkennt werden können, bevor sie zu einem Failure führen. Mich interessiert daher bei jedem System:

  • Socket: Ist das System noch erreichbar?
  • Protocol: Verstehe ich, was das System sagt? Speziell von Interesse, wenn man mit versionierten Schnittstellen arbeitet
  • Timing: Antwortet das System in der spezifizierten Zeit?

Im Umkehrschluss zu oben: Es gibt nichts schlimmeres als 1000 „java.net.ConnectException: Connection refused“ in den Logs zu haben. Besser finde ich, dieses Problem zu abstrahieren. E.g.:

{
    "type": "IntegrationPointFailure",
    "subtype": "ConnectionIssue",
    "detail": {
        "type": "kafka",
        "host": "broker1.kafka.somedomain",
        "status": "OPEN",
        "issueBegin": "2007-12-24T18:21Z",
        "codeReference": {
            "file": "MyKafkaSuperService.java",
            "line": 223
        }
    }
}

Hier kann man – so finde ich – viel saubere Metriken drauf machen. Weil für einen bestimmten Type: Connection-Issues = Max(Anzahl der Systeme) – viel übersichtlicher, als man schreibt stupide bei jedem Problem eine Metrik. Hat man den Fehler behoben:

{
    "type": "IntegrationPointFailure",
    "subtype": "ConnectionIssue",
    "detail": {
        "type": "kafka",
        "host": "broker1.kafka.somedomain ",
        "status": "SOLVED",
        "issueBegin": "2007-12-24T18:21Z",
        "issueEnd": "2007-12-24T18:21Z",
        "codeReference": {
            "file": "MyKafkaSuperService.java",
            "line": 223
        }
    }
}

Wie könnte jetzt also ein Healthendpoint ausschauen, der das oben alles berücksichtigt? Ein Beispiel:

{
    "currentServiceTime": "2007-12-24T18:21Z",
    "serviceName": "MyService",
    "hostname": "host123.somedomain",
    "deployed Version": "1.2.3-24534535",
    "integrationPoints": [
        {
            "id": "POSTGRES_APPDONAIN_CORE",
            "type": "database",
            "host": "pgsql2.somedomain",
            "connection": "ok",
            "protocol": "ok",
            "timing": "highResponseTime",
            "lastErrorMessages": [
                {
                    "referenceId": "d366b814-f188-4299-b99e-0bbc706015d8",
                    "type": "QueryThresholdExceeded",
                    "detail": {
                        "type": "storedProcedure",
                        "status": "open",
                        "issueBegin": "2007-12-24T18:21Z",
                        "codeReference": {
                            "file": "MyPgsqlRepository.java",
                            "line": 223
                        }
                    }
                }
            ]
        },
        {
            "id": "MAIL_SERVICE",
            "type": "service",
            "host": "someotherservice2.somedomain",
            "upstream": {
                "connection": "ok",
                "protocol": "parsingError",
                "timing": "ok"
            },
            "downstream": {
                "connection": "ok",
                "protocol": "ok",
                "timing": "ok"
            },
            "lastErrorMessages": [
                {
                    "referenceId": "15275afd-41f0-4efd-8648-331b6e2f8dec",
                    "type": "ProtocolFailure",
                    "subtype": "ParsingError",
                    "detail": {
                        "type": "TypeNotFound",
                        "status": "open",
                        "issueBegin": "2007-12-24T18:21Z",
                        "codeReference": {
                            "file": "MyParser.java",
                            "line": 223
                        }
                    }
                }
            ]
        }
    ]
}