Nanoservices – kleiner als Microservices

Seite 2: Nanoservices Java EE, OSGi, Lambda

Inhaltsverzeichnis

Die Frage ist nun, ob eine geeignete technische Basis die mögliche Größe eines Microservice reduzieren kann. Ein häufiges Fundament für Microservices sind Docker-Container. Sie nutzen Linux-Container zur Isolation der Services und bieten ein effizientes Dateisystem, bei dem sich mehrere Services gemeinsame Snapshots des Systems teilen können. Aber es ist für jeden Microservice immer noch ein eigenes Dateisystem zu erstellen, und jeder hat einen eigenen Betriebssystemprozess. Für einige wenige Zeilen Code kann das nicht mehr vertretbar sein.

Um die Größe eines Microservice zu reduzieren, sind Kompromisse denkbar. Zentrale Eigenschaft eines Microservice ist das unabhängige Deployment. Kompromisse in diesem Bereich sind also kaum sinnvoll, aber bei der Isolation der Microservices gegeneinander oder der freien Wahl der Technologien durchaus denkbar. Um diese Idee klar von klassischen Microservices zu unterscheiden, nutzt der Autor den Begriff Nanoservice: Die Services sind kleiner als klassische Microservices, aber sie gehen auch einige Kompromisse ein, sodass sie keine echten Microservices mehr sind.

Die Java Enterprise Edition (Java EE) ist ein Standard aus dem Java-Bereich. Er definiert APIs aus unterschiedlichen Bereichen wie Servlets, JSF (JavaServer Faces) und JSP (JavaServer Pages) für Webanwendungen sowie JTA (Java Transaction API) für Transaktionen und JPA (Java Persistence API) für Persistenz. Bei der Implementierung der APIs spielen auch Features wie die Verwaltung von Netzwerkverbindungen oder Threads eine Rolle. Außerdem standardisiert Java EE ein Deployment-Modell. Webanwendungen lassen sich in ein WAR (Web Archive) verpacken. JARs (Java Archives) können Logik in EJBs (Enterprise JavaBeans) und Bibliotheken enthalten. In einem EAR (Enterprise Archive) können JARs und WARs zu einer Anwendung verpackt werden.

Ursprünglich war die Idee der Application Server, dass sich mehrere Java-Anwendungen die Infrastruktur eines Servers teilen. Mittlerweile benötigen viele Anwendungen aber einen Cluster von Anwendungsservern, sodass ein Deployment mehrere Applikationen in einem einzigen Server kaum noch zeitgemäß ist.

In Java EE bedeutet Anwendung ein WAR, JAR oder EAR. Der Code aus der einen ist für die anderen Anwendungen nicht sichtbar. Allerdings reicht die Isolation nicht besonders weit: Wenn eine Applikation die CPU stark belastet oder viel Speicher verbraucht, beeinflusst das die anderen Anwendungen. Auch beim unabhängigen Deployment ergeben sich Herausforderungen: In der Praxis werden Application Server nach dem Deployment einer neuen Anwendungsversion neu gestartet, um zu garantieren, dass die alte Version vollständig aus dem Speicher entfernt worden ist.

Da der Code der Anwendungen vollständig voneinander getrennt ist, können sie keinen gemeinsamen Code haben und auch nicht direkt über Methodenaufrufe kommunizieren. Also müssen die Nanoservices genauso wie ihre größeren Vorbilder über das Netzwerk und damit über HTTP, REST oder Messaging kommunizieren.

Ebenso ist die Technologiewahl eingeschränkt: Es lassen sich nur Programmiersprachen nutzen, die auf der JVM (Java Virtual Machine) zur Verfügung stehen. Aber nicht nur die Wahl der Programmiersprache ist begrenzt – selbst die der Plattform. Java-Anwendungsserver nutzen ein synchrones Modell für das Behandeln von Anfragen. Für jede Anfrage verwendet der Application Server einen Thread, der auch für den Aufruf anderer Systeme genutzt wird und durch das Warten auf diese Daten blockiert ist.

Es gibt auf der JVM alternative Modelle wie Vert.x oder das Play Framework, die asynchron arbeiten. Sie haben einen Event-Loop, bei dem ein Thread Ereignisse abarbeitet. Ein Aufruf an ein anderes System blockiert also keinen Thread, sondern sorgt nur dafür, dass ein neuer Event eingestellt wird, wenn die Daten des anderen Systems verfügbar sind. Weil der Application Server aber die Behandlung von Threads und Netzwerkverbindungen übernimmt, ist ein solcher Ansatz in einem Java-EE-Umfeld nicht ohne weiteres umsetzbar. Integrationen asynchroner Techniken in Java EE gibt es zwar, aber weite Teile des Ansatzes sind dennoch synchron.

Java-EE-Nanoservices (Abb. 3)

Letztlich würde also ein Nanoservice so aussehen, wie Abbildung 3 es zeigt: Jeder Service ist eine WAR-Datei. Die Services nutzen untereinander REST und können jeweils für den Nutzer HTML über HTTP zur Verfügung stellen. Ein passendes Beispiel gibt es ebenfalls. Die Umsetzung hat jedoch ihre Nachteile: Wenn der Application Server ausfällt, sind gleich alle Services ausgefallen. Bei einem Microservice-Ansatz ließe sich das verhindern, weil man die Services auf unterschiedlichen virtuellen Servern installiert. Natürlich kann es mehrere Instanzen des Application Server geben – aber wenn jeder Server ein WAR mit einem Speicherleck hat, nützt das nur wenig. Weil ein Application Server verschiedene Services beheimatet, lassen sie sich auch nicht mehr
unabhängig voneinander skalieren.

Bei OSGi handelt es sich ebenfalls um einen Standard. Es ist ein Modularisierungsansatz für die JVM. Die Module heißen Bundles und basieren auf JAR-Dateien, die Java EE ebenfalls nutzt. Allerdings können bei OSGi die JARs Code exportieren und importieren. Im Gegensatz zu Java EE kann es also gemeinsam genutzten Code geben. Allerdings werden dadurch auch Änderungen an den Bundles schwieriger: Wenn ein Bundle Code exportiert und ein anderes ihn importiert, ist bei einer Änderung des exportierenden Bundles auch das importierende neu zu starten. Das bedeutet aber, dass ein Deployment eines Bundles zusätzlich andere Bundles beeinflusst. Daher lassen sie sich scheinbar nicht so ohne weiteres zur Umsetzung von Micro- oder Nanoservices nutzen.

Bundles können Services exportieren. Das sind letztlich Java-Objekte, die von anderen Bundles aus aufgerufen werden können. Um die Implementierung eines Services zur Laufzeit ändern zu können, hat sich in der OSGi-Welt ein Pattern etabliert:

  • Das Schnittstellen-Bundle enthält den Code für die Schnittstelle des Services. Jedes Bundle, dass den Services nutzen will, importiert die Schnittstelle.
  • Das Service-Bundle enthält die Implementierung des Service. Es exportiert ihn, aber keinen Code. Die Schnittstelle importiert es aus dem Schnittstellen-Bundle.
  • Ein Client-Bundle importiert den Service aus dem Service-Bundle und die Schnittstelle aus dem Schnittstellen-Bundle.

Wenn nun eine Änderung am Service notwendig ist, ist nur das Service-Bundle neu auszuliefern. Dann steht zwar der Service für einige Zeit nicht zur Verfügung, aber das Client-Bundle ist nicht neu zu starten. Nur wenn Änderungen an der Schnittstelle notwendig sind, muss man alle drei Bundles neu installieren und starten. OSGi-Services erzwingen also keine Deployment-Abhängigkeiten und können als Umsetzung von Nanoservices dienen.

Allerdings gilt auch für OSGi wie schon für Java EE, dass sich Bundles zwar theoretisch einzeln neu starten lassen, aber in der Praxis oft das gesamte System einen Neustart erfordert. Die Entwicklungsumgebung Eclipse ist ein solches Beispiel: Sie ist zwar in OSGi Bundles aufgeteilt, aber bei einem Update werden dann nicht nur die einzelnen Bundles neu gestartet, sondern der gesamte Prozess. Also ist das unabhängige Deployment einzelner Bundles oft so nicht realistisch nutzbar.

Ansätze wie OSGi Blueprints oder OSGi Declarative Services vereinfachen die Implementierung von OSGi-Services und können einen vorübergehenden Ausfall eines Services kompensieren. Auf jeden Fall unterstützen OSGi-Services lokale Kommunikation – also Methodenaufrufe in der JVM ohne Kommunikation über das Netzwerk. Das ist effizienter als die verteilte Kommunikation von Microservices.

Auch bei OSGi ist die Nutzung der Technologien eingeschränkt: Es lassen sich nur JVM-Technologien verwenden. OSGi greift zwar nicht wie ein Application Server in die Handhabung von Threads und Netzwerkverbindungen ein, aber das Laden von Klassen funktioniert anders als in einfachen Java-Systemen, um Bundles und den Im- und Export von Klassen zu unterstützen. Das erzwingt eine Unterstützung in den Bibliotheken.

Amazon Lambda ist ein Dienst in Amazons Cloud-Umgebung. Er ist in sämtlichen Rechenzentren des Cloud-Betreibers verfügbar. Mit ihm lassen sich einzelne Funktionen installieren und ausführen. Lambda unterstützt für die Implementierung der Funktionen Java, JavaScript mit Node.js und Python. Jede Funktion lässt sich einzeln deployen, und jede Ausführung einer Funktion wird einzeln abgerechnet. Der Aufwand für die Infrastruktur ist also minimal: Es ist lediglich ein Skript zum Deployment aufzurufen. Die Metriken und Logs der Funktionen lassen sich mit dem Dienst Cloud Watch überwachen. Auch die Definition von Alarmen ist möglich, wenn bestimmte Werte kritisch werden. Die Funktionen können Anwender auf verschiedene Weisen starten:

  • Die Funktion lässt sich direkt per Kommandozeile aufrufen.
  • In S3 (Simple Storage Service) kann man große Dateien ablegen und herunterladen. Solche Aktionen lösen Ereignisse aus, auf die eine Amazon-Lambda-Funktion reagieren kann.
  • Mit Amazon Kinesis lassen sich Datenströme verwalten und verteilen. Diese Technologie ist auf die Echtzeitverarbeitung großer Datenmengen ausgelegt. Lambda-Funktionen können als Reaktion auf neue Daten in diesen Strömen aufgerufen werden.
  • DynamoDB ist eine NoSQL-Datenbank in Amazons Cloud. Sie kann bei Änderungen am Datenbestand der Datenbank Lambda-Funktionen aufrufen, sodass diese praktisch zu Datenbank-Triggern werden.
  • Der Simple Notification Service (SNS) wird oft genutzt, um einen Alarm aus dem Monitoring per E-Mail oder SMS weiterzuschicken. Auch SNS kann Lambda-Funktionen aufrufen.
  • Amazons Simple Email Service (SES) kann E-Mails verschicken und empfangen. Als Reaktion auf eine E-Mail lässt sich eine Lambda-Funktion aufrufen.
  • Mobile Daten wie Spielstände kann Amazon Cognito verwalten. Auch hier ist ein Aufruf einer Lambda-Funktion als Reaktion auf eine Änderung von Daten denkbar.
  • Amazon CloudWatch Logs kann Lambda-Funktionen nutzen, um die Logs von Anwendungen zu analysieren.
  • Schließlich lassen sich Lambda-Funktionen als Teil von CloudFormation-Skripten nutzen, die eine Umgebung in der Amazon-Cloud aufbauen.

Allerdings ist es nicht ohne weiteres möglich, eine Lambda-Funktion als Reaktion auf einen REST-Zugriff zu aktivieren. Eine Erweiterung mit anderen Technologien ist aber denkbar: Beispielsweise lassen sich mit EC2 virtuelle Rechner nutzen oder mit Elastic Beanstalk Anwendungen in Sprachen wie Java oder Python betreiben. Also können Bereiche, die Lambda nicht abdeckt, in anderen Technologien umgesetzt werden. Die Isolation der einzelnen Lambda-Funktionen ist gut, denn schließlich muss die Infrastruktur auch die Amazon-Kunden gegeneinander isolieren.

Für den Einstieg gibt es ein Tutorial.