Micronaut für Cloud-native JVM-Microservices

Micronaut ist ein neues Framework, das sich für das Erstellen Cloud-nativer JVM-Microservices eignet.

Werkzeuge  –  11 Kommentare
Micronaut für Cloud-native JVM-Microservices

(Bild: Foto von Jonas Verstuyft / Unsplash)

Micronaut ist ein junges Framework, das insbesondere für die Entwicklung modularer, einfach testbarer Twelve-Factor Microservice-Anwendungen und Self-Contained Systems auf der Java Virtual Machine (JVM) ausgelegt ist und die Sprachen Java, Kotlin und Groovy unterstützt. Es kommt mit vielen nützlichen Funktionen daher und ermöglicht es, schnelle Ergebnisse zu erzielen.

Der Artikel beinhaltet neben einer Einführung und Einordnung von Micronaut, eine Beschreibung der Cloud-native-Features des Frameworks. Ein weiterer Artikel wird sich den Features widmen, die allgemein für Webanwendungen hilfreich sind.

Microservices – Fluch und Segen

Das Architekturmuster der Microservices ist seit Jahren vor allem deshalb beliebt, weil es sich dazu eignet, die Geschwindigkeit zum Bereitstellen von Features zu optimieren. Durch die Beschränkung bei einem Microservice auf kleine autonome Komponenten und einen abgegrenzten fachlichen Bereich (häufig von der Idee des Bounded Context aus dem Domain-driven Design inspiriert), ist es meist möglich, Applikationen schneller zu entwickeln und durch dazu passende Maßnahmen wie Continuous Delivery beziehungsweise Deployment häufiger auszuliefern. Damit steht das Konzept im Gegensatz zum vermeintlichen Feindbild des allumfassenden Monolithen. Allerdings haben Autonomie und Geschwindigkeit ihren Preis. Eine Microservices-Architektur bringt eine Reihe Herausforderungen mit, die sich aus der Auslagerung von Funktionen aus einem Prozess in viele Prozesse ergeben, die miteinander kommunizieren müssen.

Der Herausforderung nehmen sich glücklicherweise viele Menschen an, weshalb die Zahl an Frameworks und Bibliotheken, die die Entwicklung von Anwendungen mit dem Architekturmuster vereinfachen sollen, zunehmend steigt. Nach über einem Jahr in der Entwicklung, ist Micronaut eines der Frameworks, die sich im JVM-Ökosystem einen Platz verschafft haben. Es verfolgt einen Ansatz, der sich von anderen Frameworks unterscheidet. Micronaut unterstützt alle Funktionen, die Java-Entwickler kennen und lieben: zum Beispiel Dependency Injection (DI) und aspektorierntierte Programmierung (AOP). Entwickler können sie einsetzen, ohne Kompromisse bei Startdauer, Performance und Speicherverbrauch einzugehen.

Im Folgenden beleuchtet der Artikel die Features von Micronaut, die speziell für die produktive Softwareentwicklung mit Microservices nützlich sind.

Entwicklungsteam und Historie

Object Computing, Inc. (OCI) und damit das Team hinter dem Grails-Framework haben Micronaut initiiert. Grails feierte Anfang des Jahres das zehnjährige Jubiläum der Version 1.0 und stammt damit aus einer Zeit, in der monolithische Anwendungen in der Softwareentwicklung überwogen.

Micronaut ist mit einem Blick auf Microservices und die Cloud konzipiert. Es ist ein leistungsstarkes und schlankes Framework. Offiziell hat Graeme Rocher auf der Greach Conference 2018 Micronaut angekündigt und am 23. Mai 2018 unter der Apache License 2 als Open-Source-Projekt veröffentlicht. Das erste Meilenstein-Release zur Version 1.0 erschien am 30. Mai 2018, und der erste Release Candidate am 29. September 2018. Das finale Release (1.0 GA) folgte am 23. Oktober 2018.

Der offizielle Leitfaden erklärt, dass die langjährige Erfahrung der Grails-Entwickler ein Fundament für Micronaut bildet. Grails baut seit Version 3 auf Spring Boot auf. Das Team hinter Micronaut unternahm keine Bemühung, die Ähnlichkeit zu der Anwendungsentwicklung mit Spring Boot zu verstecken. Micronaut profitiert stark von den positiven und negativen Erfahrungen, die die Entwickler im Laufe der Jahre bei der Entwicklung von Monolithen und Microservices unter Verwendung von Spring, Spring Boot und Grails machten. Es ähnelt außerdem dem Eclipse MicroProfile.

Vorteile gegenüber anderen Frameworks

Das Micronaut-Framework fokussiert sich auf einige Kriterien, die ihre zunehmende Relevanz in erster Linie durch die Anforderungen an Cloud-Anwendungen gefunden haben. Außerdem legt das Team wert auf eine gute Developer Experience, deren Messlatte sich aufgrund neuer Frameworks, häufig deklarativer Konfiguration und Programmierung auf Basis von Annotationen sowie zunehmend eleganteren APIs, stets erhöht.

Micronaut zielt auf folgende Aspekte von Anwendungen für die JVM ab und versucht, sie zu optimieren:

  • schneller Applikationsstart
  • reduzierter Laufzeit-Speicherbedarf
  • minimaler Einsatz von Java Reflection
  • minimaler Einsatz von Java Proxies
  • einfache, schnelle Applikationstests

Die schnelle Startzeit fällt sowohl bei der Entwicklung, bei der Ausführung von Integrationstests als auch bei einem Kaltstart als AWS-Lambda-Funktion positiv auf. Aufgrund des geringen Speicherbedarfs zur Laufzeit eignet sich Micronaut besonders für die Entwicklung von "Serverless"-Anwendungen. Damit verbunden wirbt das Entwicklungsteam vor allem mit dem inhärenten Cloud-Support, der keine zusätzlichen externen Abhängigkeiten benötigt, sondern Teil des Ökosystems ist.

Schneller Applikationsstart und geringer Speicherbedarf

Mit Micronaut sind die Startdauer von Anwendungen und deren Speicherverbrauch nicht an die Größe der Codebasis gebunden. Die Startdauer einer minimalen Micronaut-Anwendung liegt zurzeit bei etwa einer Sekunde. Mit Micronaut belegen Anwendungen einen Speicherplatz in Bereichen von zehn MBytes statt hunderten, ohne Kompromisse in Bezug auf zeitgemäße Framework-Funktionen eingehen zu müssen.

Bereits zur Kompilierzeit nutzt das Framework die deklarativen Informationen aus Annotationen, um zusätzliche Klassen zur Umsetzung der Funktionen zu generieren und sie zusammen mit dem Anwendungscode zu kompilieren. Proxies und Caches reduziert es entsprechend.

Die geringe Startdauer und der geringe Memory Footprint gelingen durch die Verwendung von Dependency Injection (DI) zur Kompilierzeit und der AOP-API (Aspect-Oriented Programming), die auf die Java Reflection API verzichten. Micronaut verwendet hingegen die Annotation Processor API für Java und Kotlin sowie AST-Transformationen für Groovy zur Kompilierzeit. Reflection-basierte IoC-Frameworks (Inversion of Control) wie Spring untersuchen den Classpath, laden und cachen Reflection-Daten für jedes Feld, jede Methode und jeden Konstruktor. Spring beispielsweise liest den Bytecode jeder Bean, die es zur Laufzeit findet, und erzeugt für sie diverse Metadaten. Das erzeugt Overhead.

Generell ist es bemerkenswert, wie viel Reflection-Logik in typischen Java-Frameworks existiert, obwohl die Sprache Java statisch typisiert ist. Während Prüfmechanismen zur Kompilierzeit bei Sprachen wie Java zur Verfügung stehen, um zu validieren, dass der Anwendungscode korrekt ist, unterstützen viele zeitgemäße Frameworks zur Kompilierzeit Entwickler wenig oder gar nicht. Sie führen die Überprüfungen stattdessen zur Laufzeit durch und erzeugen im Fehlerfall diverse Runtime Exceptions, um zu informieren, dass der vorliegende Code falsch ist.

Features für Cloud-native Microservices

Micronaut bietet alles, was Anwender heute von einem Cloud-nativen Microservice-Framework erwarten. Dazu gehören Features wie Dependency Injection, Convention over Configuration und Configuration Sharing, Service Discovery, Routing, Circuit Breaker, Tracing und vieles andere mehr. Die dafür notwendigen Bibliotheken sind über die Deklaration der Abhängigkeit als Feature in Micronaut-Anwendungen integrierbar.

Service Discovery

Unterliegen Umgebungen einer geringen Anpassungsdynamik, können Entwickler Namensauflösungen zu Diensten statisch konfigurieren. In einer Infrastruktur, die aus vielen Diensten besteht, die man stets verändert und neu ausrollt, ist eine dynamische Auflösung von Dienstnamen zu spezifischen Netzwerkadressen notwendig. Die Service-to-Service Discovery ist mit gängigen Diensten wie Consul, Eureka, Kubernetes, Googles Cloud Plattform oder AWS Route 53 möglich. Consul beispielsweise ist ein verteiltes Service Mesh zum Verbinden, Sichern und Konfigurieren von Diensten. Micronaut bietet eine eigene Consul-Client-Implementierung, denn die Mehrheit der Consul- und Eureka-Clients sind blockierend. Zudem beinhalten sie eine Vielzahl externer Abhängigkeiten, die schließlich die auszuliefernden JAR-Dateien aufblähen. Der Discovery Client von Micronaut verwendet den nativen Micronaut HTTP-Client, der den Bedarf an externen Abhängigkeiten stark reduziert und eine reaktive API für die Nutzung beider Discovery-Server bereitstellt.

Resilience

Resilienz bezeichnet die Widerstandsfähigkeit technischer Systeme. Sie hat als Ziel, bei einem Teilausfall standhaft zu bleiben und nicht vollständig zu versagen. Fallen etwa abhängige Microservices in einer Kommunikationskette aus, antworten nicht oder unerwartet spät, ist eine adäquate Reaktion notwendig. Verbindungsprobleme wie Zeitüberschreitungen durch hohe Last und die temporäre Nichterreichbarkeit angebundener Systeme sind Aspekte, die Entwickler in Microservices-Infrastrukturen immer berücksichtigen müssen. In der Praxis treten die Probleme häufig auf, weshalb man auf sie vorbereitet sein sollte. Mechanismen, um ihnen zu entgegnen, sind Wiederholungsversuche (Retries), Rückfallmechanismen (Fallbacks) und Schutzschalter (Circuit Breaker).

Hystrix ist eine Bibliothek für den Umgang mit Latenzzeiten und Fehlertoleranz, die Micronaut unterstützt und von Netflix entwickelt wurde, um Zugriffe auf entfernte Systeme, Dienste und Bibliotheken von Drittanbietern zu isolieren und damit kaskadierende Fehler zu vermeiden.

Ein erneuter Verbindungsaufbau zu einem abhängigen Dienst ist in Architekturen von Microservices manchmal sinnvoll. Mit der Annotation @Retryable kann man den Aufruf einer Operation mehrfach versuchen, bevor der Fehlerfall eintritt:

@Retryable(attempts="3", delay="1s")

In einigen Fällen ist die Verwendung des Circuit-Breaker-Musters eine bessere Wahl. Leistungsschalter finden sich im hauseigenen Stromkasten. Das Prinzip in der Softwareentwicklung ist ähnlich. Circuit Breaker lassen eine bestimmte Anzahl fehlerhafter Anforderungen zu, bis sie mit dem Öffnen eines "Schaltkreises" reagieren, der für einen definierten Zeitraum offen bleibt, bevor er zusätzliche Wiederholungsversuche zulässt. Die Annotation des Circuit Breaker ist eine Variation der Annotation @Retryable. Sie gibt an, wie lange der Kreis offen bleiben soll (standardmäßig 20 Sekunden).

Der Einsatz eines Circuit Breakers mit maximal fünf Wiederholungsversuchen im Abstand von fünf Millisekunden und 300 Millisekunden zum Reset ist mit Micronaut per gleichnamiger Annotation möglich:

@CircuitBreaker(attempts="5", delay="5ms", reset="300ms")

Der Fallback-Mechanismus ist eine dritte Möglichkeit, auf Ausnahmesituationen zu reagieren. Ein Fallback ermöglicht es, Standardwerte gemäß fachlicher Anforderungen in Ausnahmesituationen zurückzugeben. Über die Annotation @Fallback können Anwender eine Bean definieren, die automatisch als alternative Implementierung dient, wenn ein angebundener Service nicht antwortet. Eine Klasse oder eine Methode, die einen solchen Fallback nutzen soll, ist mit @Recoverable annotiert. Mehr ist nicht zu tun.

Ein solcher Fallback-Mechanismus kann ebenso zur Entwicklungszeit von Microservices nützlich sein. Bei einem korrekten Microservice-Systemschnitt ist es üblich, dass man für ein Feature an nur einem Microservice arbeitet. Ist das Feature jedoch abhängig von Daten anderer Dienste, ist es sinnvoll, dass sie nicht zur Entwicklungs- und Testzeit erreichbar sein müssen. In einem solchen Fall kann etwa eine Fallback-Implementierung das Verhalten des zur Laufzeit in Produktion angebundenen Services simulieren.

Client-side Load-Balancing

Um die Zugriffszeit auf Funktionen von Diensten zu optimieren oder mögliche Lastspitzen abzufangen, beziehungsweise die zuvor genannte Resilienz zu erzielen, skalieren Entwickler Microservices bei Bedarf auf mehrere Instanzen. Das ist durch den Einsatz von Tools wie Docker und Kubernetes schon lange kein Problem mehr, sondern gängige Praxis. Die HTTP-Client-Implementierung von Micronaut nutzt zur Verteilung von Requests eine clientseitige Round-Robin-Strategie. Sie kann Anfragen an andere Instanzen des Dienstes weiterleiten, bevor sie überlastet ist. Ribbon von Netflix ist eine Bibliothek, die Micronaut statt des internen Mechanismus zum Load Balancing einsetzen kann.

Distributed Tracing

Anfragen in Microservice-Architekturen landen mitunter bei vielen Diensten. Eine gut durchdachte Architektur sollte gewährleisten, Anfragen von Anfang bis Ende zu verfolgen und Interaktionen zwischen Komponenten zu visualisieren. Tracing-Angebote ermöglichen es, Abhängigkeiten und Latenzprobleme in Architekturen verteilter Systeme über Grenzen hinweg zu identifizieren und zu analysieren.

Micronaut erlaubt mit den Annotationen @NewSpan und @ContinueSpan das Erzeugen und Weiterführen sogenannter Spans. Das sind benannte, zeitlich gekennzeichnete Operationen, die ein zusammenhängendes Segment von Ablauflogik darstellen. Die genannten Annotationen stellen den Einsprung in ein solches Segment und dessen Weiterführung über mehrere Methodenaufrufe dar. Die Annotation @SpanTag zeigt zusätzlich einen Methodenparameter, der den Span taggt. Dadurch können Nutzer verfolgen, wann und wo welche Funktion mit welchen Parametern aufgerufen beziehungsweise welches Segment abgearbeitet wurde. Mehrere Spans in Reihe ergeben zusammen in einem gerichteten azyklischen Graphen einen sogenannten Trace.

Falls die beiden Annotationen für diese Zwecke nicht ausreichend sind, können Entwickler Implementierungen der Open Tracing API verwenden, die weitere Möglichkeiten bereitstellen. Die bekanntesten Vertreter sind Zipkin von Twitter und Jaeger von Uber, das von Dapper und Zipkin inspiriert wurde. Zipkin Brave ist die zugehörige Bibliothek, die beim Sammeln und Übermitteln von Spans zum Einsatz kommt. Die Open-Tracing-Spezifikation und die Zipkin-Dokumentation beschreiben die Funktionsweise des verteilten Tracings im Detail.

Serverless Functions mit Micronaut

Ein Teil von Serverless-Architekturen sind Serverless Functions beziehungsweise Functions as a Service (FaaS). Entwickler erstellen dabei Funktionen, die die Cloud-Umgebung vollständig verwaltet und in ephemeren Prozessen ausführt. Das besondere ist, dass ein FaaS-Server eine Funktion typischerweise für einen bestimmten Zeitraum mit einem Kaltstart auf Touren bringt und sie für eine gewisse Dauer "warm" hält. Das bedeutet, dass insbesondere ein schneller Start einer Funktion kritisch ist, um sie sinnvoll für eine Anfrage bereitzustellen und auszuführen.

Micronaut bietet Unterstützung für die Entwicklung und Bereitstellung von Funktionen für AWS Lambda und jedes FaaS-System, das Funktionen in Form von Containern unterstützt, wie OpenFaaS oder Fn. Funktionen melden sich gegebenfalls beim konfigurierten Service-Discovery-Dienst an, und ein mit @FunctionClient annotierter Client kann sie ansprechen. Entwickler können sie isoliert oder über einen HTTP-Server testen.

Cloud Configuration Sharing

Ein weiteres Feature von Micronaut ist das Cloud Configuration Sharing. Damit ist es möglich, Konfigurationen extern abzulegen, damit sich in einem Cloud-Umfeld skalierte Instanzen eines Services eine gemeinsame Konfiguration teilen können und Änderungen direkte Auswirkungen auf alle Instanzen haben.

Abhängig von der Infrastruktur der Applikation kann man beispielsweise unterschiedliche Beans aktivieren. Micronaut erkennt Umgebungen wie Android, Test, Cloud, Amazon EC2, Google Compute, Kubernetes, Heroku, Cloud Foundry, Azure oder die IBM Cloud. Neben einem internen Mechanismus zum manuellen Auflösen verteilter Konfigurationen können Dienste wie Consul oder AWS Systems Manager Parameter Store verwendet werden.

Message-Driven Microservices

Für die Verarbeitung verteilter Datenströme und Echtzeitdaten ist in der Praxis Apache Kafka beliebt. Mit der Unterstützung von Micronaut für Kafka können Entwickler Message-driven Microservices in wenigen Schritten mit Compile-Time-AOP-Annotationen wie @KafkaClient, @KafkaListener, @Topic, @Body, @Header, @KafkaKey und wenigen Zeilen YAML-Konfiguration verwirklichen. Der Micronaut Guide enthält eine umfassende Anleitung. Eine gleichwertige Unterstützung ist für RabbitMQ angedacht.

Fazit

Obwohl Micronaut viele Funktionen bietet, die zweifellos für das Erstellen von Microservices und den Umgang mit Herausforderungen verteilter Systemen von Vorteil sind, ist Micronaut ein universelles Framework. Einige der Features machen Micronaut daher nicht zuletzt auch für zeitgemäße Webanwendungen interessant, die nicht in einem Cloud-Umfeld als Microservice laufen.

Durch die Ahead-Of-Time-Kompilierung ist Micronaut in der Lage, einen Großteil der Aufgaben wie Dependency Injection und aspektorientiere Programmierung zur Kompilierzeit durchzuführen, während sie in den meisten Frameworks erst zur Laufzeit stattfinden. Dadurch büßen andere Micro-, MicroProfile- oder Full-Stack-Frameworks an Zeit und Speicherverbrauch ein. Mit anderen Worten: Je mehr Microservices in Betrieb sind, desto mehr Ressourcen sind nötig und desto höhere Betriebskosten entstehen. Den ohnehin schnellen Start einer Micronaut-Anwendung von etwa einer Sekunde kann der Einsatz von GraalVM sogar auf einen zweistelligen Millisekundenbereich reduzieren.

Der zweite Artikel beleuchtet die Features, die nicht spezifisch für Cloud-native Microservices, sondern ebenso für die Entwicklung und den Betrieb von Applikationen jedes anderen Makroarchitektur-Musters hilfreich sind. Wer bereits jetzt Lust auf mehr bekommen hat, wird im offiziellen Micronaut Guide seinen Informationsdurst stillen können. (bbo)

Jonas Havers
ist freiberuflicher Softwareentwickler und Dozent für Softwareentwicklung. Er entwickelt Webanwendungen in agilen Teams, seit mehreren Jahren ausschließlich remote und vorwiegend in Projekten mit dem Branchenschwerpunkt E-Commerce. Technologisch setzt er auf einen bunten Mix aus Java, Kotlin, Groovy, TypeScript und JavaScript.