W-JAX BLOG

6. - 10. November 2017 in München
2
Aug

Microservices mit Java EE: Realistisch und pragmatisch

Microservices-Architekturen sind ein spannendes Konzept. Aber auch hier gilt es, pragmatisch zu bleiben. Denn nicht alles, was die großen Player machen, braucht man. Und für die Luftschlösser am Beginn eines Projekts sollte man keinen Platz einplanen. Ein praxisgerechter Ansatz sind Microservices mit Java EE in Kombination mit Docker.

Die überwältigende Mehrheit der Java-EE-Projekte wird nach höchst unwahrscheinlichen Annahmen realisiert. Ein Tausch der Datenbank während der Projektlaufzeit, magische Änderungen der Datenbank ohne jegliche Auswirkung auf das UI oder auch eine hohe Änderungshäufigkeit der Algorithmen werden als realistische Szenarien angenommen und bestimmen die Wahl der Patterns und das Design der Anwendung. Auch der hohe Grad der Wiederverwendbarkeit von internen Klassen wird ohne eine Betrachtung der Historie – oder einen Bezug zur Realität – eingeschätzt und resultiert in einer Explosion von Maven-Modulen und unnötigen Abstraktionen.

Alle diese Maßnahmen haben keinen spürbaren Mehrwert für den Benutzer, führen jedoch zu einem bedenklichen Verhältnis von Infrastruktur zu Fachcode. Die Verschleierung der Algorithmen, relevanten Domainklassen und somit auch eine schlechte Änderbarkeit und Wartbarkeit der Anwendung sind die Folge des Cargo Cult Programmings [1]. Die Fachabteilungen, Benutzer und auch Sponsoren sind unglücklich, und es wird dringend nach neuen Lösungen gesucht. Microservices-Architekturen werden als die Lösung des Bloat-Problems gesehen.

Neben den Brown-Field-Projekten [2] sind auch Green-Field-Projekte [3] an Microservices interessiert. Dabei werden Best Practices der großen Vorbilder wie Netflix, Amazon oder Google imitiert. Best Practices von globalen Unternehmen adaptieren auch lokal agierende Mittelstandsfirmen. Patterns und Frameworks für zehntausende elastische Netflix-Server [4] in den Wolken von Amazon werden auch gerne auf das eigene Rechenzentrum angewandt. Dabei hat selbst Netflix nur den sehr stark wachsenden Streaminganteil auf die Microservices-Architektur migriert. Die 5,3 Millionen von DVD-Kunden [5] werden nach wie vor vom eigenen Rechenzentrum und einer monolithischen Architektur bedient. Eine blinde Imitation der großen Vorbilder ist lediglich eine weitere Ausprägung des Cargo Cult Programmings.

Eine kleinere Gruppe von pragmatischen Projekten sucht Lösungen für konkrete, gut abgrenzbare Herausforderungen wie unterschiedlichen Releasezyklen innerhalb einer Anwendung, Zusammenarbeit von kleineren Teams, Einführung von moderneren UI, einfacheres Monitoring und Administrierbarkeit der Anwendungen und stößt nebenbei auf den Begriff Microservices.

 

 

Was ist eine Microservices-Architektur?

Eine Microservices-Architektur (MSA) ist schnell erklärt: pragmatisch kommunizierende Prozesse. Dabei muss es schon ein echter Prozess sein. Ein Thread reicht nicht aus. Hier sind sich Martin Fowler und auch Wikipedia einig. Fowler dazu: „In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery.“ [6] Und Wikipedia: „Microservices is a specialisation of an implementation approach for service-oriented architectures (SOA) used to build flexible, independently deployable software systems. Services in a microservice architecture (MSA) are processes that communicate with each other over a network in order to fulfill a goal. These services use technology-agnostic protocols.“ [7]

Übertragen auf Java bedeutet es mehrere Dinge: JVM kommunizieren innerhalb einer Anwendung miteinander. Bei pragmatischen Protokollen stehen uns in Java dafür unzählige Möglichkeiten zur Verfügung. Die grenzenlose Auswahl wird aber durch die Rahmenbedingungen der UI dramatisch eingeschränkt. Unter der Berücksichtigung der aktuellen Trends wie Single Page Applications (SPAs) und nativen, mobilen Apps bleiben nur HTTP(2)/JSON und WebSocket übrig. Genau diese Protokolle werden letztendlich auch für die Kommunikation zwischen den Prozessen verwendet. Je weniger Protokolle verwendet werden, desto einfacher und verständlicher der Code und desto wartbarer die Anwendung.

Mit oder ohne SOA

Bereits im ersten Satz der Wikipedia-MSA-Definition taucht der Begriff SOA auf. Es ist auch die meist gestellte Frage aus der Praxis: „Und was ist mit SOA?“ [8]. SOA war deutlich ambitionierter und versuchte gleich das Zusammenspiel von mehreren Anwendungen zu betrachten. In SOA spielt auch die Wiederverwendbarkeit von Services eine große Rolle. Obwohl reine SOA in der Theorie keine Protokolle vorschreibt, handelt es sich in der Praxis nahezu ausschließlich um SOAP. Die Schnittstellen werden mit WSDL (Web Services Description Language) beschrieben und die Clients aus dieser Beschreibungssprache generiert. Als Payload wird bei SOAP XML verwendet, oft mit XSD. XML-Nachrichten werden vollständig übertragen, ein Lazy Loading findet nicht statt. Die Aggregation der Nachrichten läuft serverseitig, oft mithilfe von ESB.

Die Ziele der Microservices sind dagegen deutlich bescheidener. Die Architektur einer Anwendung und nicht des gesamten Unternehmens steht dabei im Vordergrund. Microservices verwenden HTTP mit JSON, was Lazy Loading von Nachrichten hervorragend unterstützt. Eine zentrale Aggregation von Nachrichten ist nicht zwingend erforderlich.

Es gibt für die Beschreibung der MSA-Schnittstellen kaum Vorgaben. In der Praxis beschreiben sich fachlich getriebene REST-Schnittstellen erstaunlich gut selbst. Die HTTP-Spezifikation (RFC 2616 [9]) definiert nicht nur das Verhalten der HTTP-Methoden POST, PUT, GET, DELETE (…) und HTTP-Statuscodes mit ihrer Bedeutung, sondern auch noch den Umgang mit Caches und Concurrency-Problemen. Der Mangel an formalen Beschreibungsmitteln stört kaum in der Praxis. Eine fachlich getriebene Schnittstelle lässt sich mit minimalem Aufwand aufrufen, d. h. eine Zeile Code. Auch die JSON-Serialisierung bringt Java EE bereits mit.

 

Shared Deployment ist tot

Ein Sun-Starfire-E10K-Server [10] kam mit unglaublichen 464 MHz CPU, 128 GB RAM und kostete 1997 rund eine Million US-Dollar. Die E10K waren um die Jahrtausendwende auch in Deutschland oft im Einsatz. In größeren Projekten wurden gleich mehrere E10Ks im Clusterbetrieb eingesetzt. Applikationsserver kamen damals aus Kostengründen zum Einsatz. Nur ein Applikationsserver pro Server wurde installiert. Für die optimale Ausnutzung der Hardware wurden dabei möglichst viele Anwendungen (EARs) gleichzeitig deployt. Mit diesem Ansatz konnte man die extrem teure Hardware optimal ausnutzen. Die Rechnung ging auch noch bei hohen Lizenzkosten und komplizierten Installationsroutinen auf.

Heute kommt ein iPhone 7 mit einem Quad-Core-2,3-GHz-Prozessor und ist dabei rund achtmal schneller als die E10K. Einer der schnellsten Intel Xeon E7 CPU (E7-8890 v4) kommt heute mit 24 Cores sowie 48 Threads, kann mit bis zu 3 TB an RAM bestückt werden und kostet 7 000 Euro. Mit dem RAM von 2 GB kann ein iPhone zwar noch nicht mithalten, aber man bekommt bereits 64 GB Server Grade ECC RAM für wenige hundert Euro.

Gleichzeitig haben alle Applikationsserver deutlich abgespeckt. Die Installationsdatei ist selten größer als 200 MB, und der RAM-Bedarf ist gering. Alle aktuellen Java-EE-7-Applikationsserver begnügen sich mit weniger als 100 MB RAM (40 bis100 MB) pro Instanz [11] und liegen in der Praxis noch deutlich darunter (50 bis 64 MB). Für die Installation muss lediglich eine ZIP-Datei oder ein JAR entpackt werden. Die Hardware kostet nur einen Bruchteil, ist aber um ein Vielfaches leistungsfähiger. Gleichzeitig sind die meisten Applikationsserver frei verfügbar. Der Support und die damit verbundenen Kosten sind bei der Mehrheit der Hersteller optional.

Tatsächlich könnte man zwar immer noch wenige Euro mit dem Shared Deployment einsparen. Der Betrag lässt sich relativ einfach abschätzen: 64 GB ECC RAM kosten rund 500 Euro. Ein Applikationsserver im Ruhezustand benötigt etwa 64 MB an Arbeitsspeicher. Theoretisch – man bräuchte eine CPU mit hunderten von Cores – könnte man auf einen Server mit 64 GB eintausend leere Applikationsserver deployen. Eine Instanz eines Applikationsservers kostet also etwa 0,50 Euro – Tendenz sinkend. Die Rechnung ist keinesfalls exakt, da sie nicht die Betriebskosten und Hardwarekosten berücksichtigt. Deutlich wichtiger als die Korrektheit der Rechnung ist der langfristige Trend. Denn es sind keine Preissteigerungen bei Hardware zu erwarten. In den meisten Projekten sollte sich eine Ersparnis von 50 Cent nicht lohnen. Für Netflix mit zehntausenden von elastischen Servern und hunderttausenden von Serviceinstanzen lohnt sich die Entwicklung von eigener Infrastruktur dennoch.

Shared Nothing lebt

Das Enterprise Archive (EAR) wurde für das Deployment der Anwendungen verwendet. Das EAR bestand aus einem UI-Anteil, dem WAR (Web Archive) und der Geschäftslogik – zumindest einem JAR. Jegliche Modularisierung innerhalb des EAR war zwecklos. Das EAR wurde immer komplett mit allen zugehörigen WAR und JAR deployt. Mit der Einführung des Continuous Deployments (CI) wurde bei jedem Commit das gesamte EAR gebaut und im Optimalfall auch deployt. Jeder Bestandteil des Archivs musste einzeln gebaut und gezippt werden, um an Ende der Pipeline doch in dem EAR zu landen. Je mehr Maven-Module gebaut werden mussten, desto langsamer war auch die Pipeline. Mit der steigenden Anzahl der Module sinkt also die Qualität des CI, da das Feedback nur verzögert den Entwickler erreicht. Mit sinkender Performance der Pipeline sinkt auch die Begeisterung der Entwickler für häufige Commits.

Bereits vor zehn Jahren war die Hardware relativ leistungsfähig und günstig. Man musste aber kaum Rücksicht auf die SPA nehmen. Damals wurde ein EAR auf einen Applikationsserver deployt. Die EAR haben miteinander über RMI/IIOP, Protokolle wie Hessian [12] oder manchmal SOAP miteinander kommuniziert. Mehrere Server wurden auf einem Server in einer oder mehreren virtuellen Maschinen (VMware) betrieben. Der Begriff Microservices war noch nicht geprägt. Deswegen hatten wir eine solche Architektur „Shared Nothing Architecture (SNA)“ [13] genannt.

Docker + Java EE = 42

Die Revolution brachte Java EE 6 vor rund sieben Jahren. Nun konnte man WARs mit UI und Geschäftslogik und ohne weitere Unterteilung bauen. Da Java-EE-Server bereits die wichtigsten APIs für die Entwicklung von Web-Apps enthalten, besteht ein WAR lediglich aus reiner Geschäftslogik und ist schlank. In der Praxis ist es immer noch mehrere MB groß, da in vielen Projekten noch die UI-Assets (JSF/PrimeFaces) mitdeployt werden. Docker bringt ein Build-System (ähnlich Maven, Dockerfiles sind ähnlich zu POMs), eine Registry (ähnlich Maven Repository Manager), einer Versionierung (ähnlich Git) und einem Laufzeitsystem (ähnlich zu OS-Virtualisierung, nur um Vielfaches schneller, effizienter und gut automatisierbar). Mit Docker geht man noch einen Schritt weiter. Man verpackt das Betriebssystem, die JVM, den Applikationsserver und schließlich das WAR in ein ausführbares Image und liefert es in dieser Form in die Produktion [14]. Das ausführbare Image wird in eine Registry eingecheckt (docker push) und muss nur noch für die Einführung in die Produktion aus der Registry geladen (docker pull) und gestartet werden.

Java EE 7: der letzte Schliff

Mit der Einführung 2013 von Java EE 7 kam direkte Unterstützung von JSON, WebSocket, Concurrency Utilities und weitere Verbesserungen der bereits bestehenden API. Gleichzeitig forderten die SPA (HTML5, Angular) JSON für die Serialisierung der Nutzdaten und HTTP als Übertragungsprotokoll. Nun hatten JAX-RS und JSON-P auch die vielen proprietären RPC-Protokolle ersetzt. Zusätzlich kam mit WebSocket eine interessante Möglichkeit hinzu, Clients proaktiv zu benachrichtigen und Events asynchron zu pushen.

SPA hatten in manchen Fällen JSF verdrängt. Somit wurden die UI-Assets in den WARs nicht mehr benötigt. Das hat zu einer weiteren Größenreduktion geführt. Die WAR sind nun im Kilobytebereich, also echte Thin WAR [15]. Je kleiner die WAR, desto schneller die CI-Pipeline und auch das Deployment [16]. Das gesamte Image des Applikationsservers lässt sich in rund 100 Millisekunden mit Docker bauen.

Das Concurrency-Utilities-API in Java EE 7 ermöglicht die Definition, Konfiguration und Injection von Thread-Pools und Thread Factories. Die Zuweisung von vorkonfigurierten Thread-Pools einzelner Kommunikationskanäle erhöht die Stabilität der Anwendung ohne nennenswerten Mehraufwand – die Injection ist ein Einzeiler: @Resource ManagedExecutorService threadPool. Die Partitionierung von globalen Ressourcen in kleinere Bereiche zur Erhöhung der Robustheit wird auch Bulkheads genannt und war immer schon eine Stärke der Applikationsserver: JDBC-Pools, JMS-Pools, Request Queues und Pools sind Bulkheads [17].

Ein zu langsamer oder instabiler Service lässt sich gleichzeitig mit einem Circuit Breaker [18] schützen. In Java EE ist es lediglich ein Interceptor, der die ausgehenden Aufrufe eines Service überwacht. Bei bestimmter Anzahl von Fehlern oder Time-outs ruft der Interceptor das externe System nicht mehr auf und kehrt sofort zurück. Ein Circuit Breaker lässt sich mit einer Klasse und einem if-Statement implementieren.

 

Ein glücklicher Zufall

Seit 2002 und JSR 77 (Management and Monitoring) [19] müssen alle Applikationsserver die Monitoringdaten via JMX bereitstellen. Glücklicherweise werden die Daten über verschiedene Protokolle zur Verfügung gestellt, auch über HTTP/REST. Mit dem „One WAR, one Server, one JVM“-Ansatz erhalten nützliche Monitoringdaten, wie Zustand der Thread-Pools/Bulkheads und Performance aller relevanten Methoden sowie HTTP, JDBC, JMS, Statistiken ohne jeglichen Mehraufwand.

Das Deployment-Modell der Applikationsserver ist besonders in einer Microservices-Umgebung interessant. Mit dem Deployment wird stabile Infrastruktur von der fluktuierenden Fachlichkeit strikt getrennt. Applikationsserver, Betriebssystem oder JVMs werden lediglich wenige Male im Jahr aktualisiert. Das WAR selbst ändert sich bei jedem Commit. Der Thin-WAR-Ansatz führt zu extrem schneller Bereitstellung (rund 100 Millisekunden) der binären Images.

Die Größe eines Docker-Images mit JVM, Applikationsserver (z. B. Payara, WildFly, TomEE oder WebSphere Liberty) und einem Thin WAR beträgt zwischen 400 und 700 MB. Der aufmerksame Leser stellt sich hier die Frage: „Auf welcher Maschine kann man 700 MB in 100 ms bauen?“

Docker unterstützt Vererbung (Layering) von Images. Die Images werden nach Änderungshäufigkeit voneinander vererbt. Das oberste Image enthält das OS mit JVM, davon erbt das Image des Applikationsservers, davon der konfigurierte Applikationsserver und letztendlich das Thin WAR. Das WAR ändert sich bei jedem Commit, die projektspezifische Konfiguration des Applikationsservers wenige Male pro Woche, die Applikationsserver werden nur wenige Male im Jahr aktualisiert und das Betriebssystem und die JVM noch seltener [20]. Mit Docker und Java EE wird nur das gebaut, was sich tatsächlich ändert. In der Praxis wird dieser Ansatz mit Builds in Millisekunden belohnt.

Monolith, Microservices und Microliths

Eines der Prinzipien der agilen Entwicklung ist die Verzögerung aller nicht essenzieller Entscheidungen auf einen späteren Zeitpunkt. Kurze Zyklen erlauben einen pragmatischen Umgang mit der Softwareentwicklung. Im Java-EE-Umfeld fängt man einfach die Entwicklung ohne jegliche Vorbereitung mit einer einzigen Abhängigkeit zum Java-EE-7-API an [21]. Falls der Funktionsumfang des Applikationsservers und des JDK nicht ausreichen sollten, kann man in begründeten Fällen zu einem späteren Zeitpunkt eine Third-Party-Abhängigkeit integrieren. Mit Java EE 7 werden alle unproduktiven Meetings über die Wahl von möglichen Libraries oder Frameworks auf einen späteren Zeitpunkt verschoben. Oft finden solche esoterischen Diskussionen überhaupt nicht mehr statt. Man fängt mit der ersten Zeile des Codes mit der Realisierung der Fachlichkeit an.

Ein kleines Eine-Pizza-Team fängt mit der Implementierung des ersten WAR an. Ein bis drei Personen entwickeln ein WAR, das dann auf einen Applikationsserver innerhalb eines Docker-Containers deployt wird. Werden weitere zusammenhängende Konzepte identifiziert, können zusätzliche Teams mit der Arbeit an unabhängigen WAR mit einem fachlichen API beginnen.

Ein kleines Team kann extrem produktiv sein. Im Idealfall schafft man es, die Anwendung mit einem einzigen Thin WAR zu realisieren. Ein WAR lässt sich kaum als MSA bezeichnet – vielmehr ist es ein schlanker Monolith: ein Microlith. „Don‘t distribute“ ist eines der wichtigsten Patterns in verteilten Architekturen und so auch bei MSA. Falls man mit einem einzigen WAR die Fachlichkeit effizient abbilden kann, muss man sich nicht mit den Herausforderungen der verteilten Programmierung wie Konsistenz vs. Skalierbarkeit (CAP-Theorem) beschäftigen.

Papier ist geduldig und der Code einfach

Leider lässt sich die Effizienz der Java-EE-basierten Microservices nur schlecht mit Text beschreiben. Aus diesem Grund werden wir im nächsten Beitrag zu dieser Serie einen Java-EE-7-Microservice implementieren und als Docker-Image in Betrieb nehmen – mit viel Code und wenig Text.

 

bien_adam_sw.tif

Adam Bien arbeitet mit Java und LiveScript seit JDK 1.0 und 1995 als Entwickler, Consultant, Trainer und manchmal als Autor und hat immer noch großen Spaß dabei.

Web: www.adam-bien.com

Links & Literatur

[1] Cargo Cult Programming: https://en.wikipedia.org/wiki/Cargo_cult_programming

[2] Brown Field Project: https://en.wikipedia.org/wiki/Brownfield_(software_development)

[3] Green Field Project: https://en.wikipedia.org/wiki/Greenfield_project

[4] Brodkin, John: „Netflix finishes its massive migration to the Amazon cloud“: http://arstechnica.com/information-technology/2016/02/netflix-finishes-its-massive-migration-to-the-amazon-cloud/

[5] Steel, Emily: „Netflix Refines Its DVD Business, Even as Streaming Unit Booms“: https://www.nytimes.com/2015/07/27/business/while-its-streaming-service-booms-netflix-streamlines-old-business.html

[6] Fowler, Martin: „Microservices: a definition of this new architectural term“: https://martinfowler.com/articles/microservices.html

[7] Microservices Wikipedia: https://en.m.wikipedia.org/wiki/Microservices

[8] Bien, Adam: „The Difference Between SOA and Microservices Architectures“: http://www.adam-bien.com/roller/abien/entry/the_difference_between_soa_and

[9] HTTP-Spezifikation/RFC 2616: https://www.ietf.org/rfc/rfc2616.txt

[10] Sun Starfire E10K: https://en.wikipedia.org/wiki/Sun_Enterprise

[11] Bien, Adam: „The Overhead of Java EE Application Servers“: https://www.youtube.com/watch?v=OSERh3l1vK8

[12] Hessian Protocol: http://hessian.caucho.com

[13] Shared Nothing Architecture: https://en.wikipedia.org/wiki/Shared_nothing_architecture

[14] Bien, Adam: „Why not one Application per Server“: http://www.adam-bien.com/roller/abien/entry/why_not_one_application_per

[15] Bien, Adam: „Productive Java EE 7 on Java 8 at Commerzbank“: http://www.adam-bien.com/roller/abien/entry/productive_java_ee_7_on

[16] Bien, Adam: „EARs, WARs and Size Matters“: http://www.adam-bien.com/roller/abien/entry/ears_wars_and_size_matters

[17] Bulkhead: https://en.wikipedia.org/wiki/Bulkhead_(partition)

[18] Fowler, Martin: „CircuitBreaker“: https://martinfowler.com/bliki/CircuitBreaker.html

[19] JSR 77: J2EE Management and Monitoring: https://jcp.org/en/jsr/detail?id=77

[20] Minimalistische Docker-Images: https://github.com/AdamBien/docklands

[21] Bien, Adam: „The Only Dependency You Need“: http://www.adam-bien.com/roller/abien/entry/the_only_one_dependency_you

[22] Bien, Adam: „A Minimalistic Circuit Breaker Pattern Implementation for Java EE“: http://www.adam-bien.com/roller/abien/entry/a_minimalistic_cirquit_breaker_pattern

[23] CAP-Theorem: https://en.wikipedia.org/wiki/CAP_theorem

Leave a Reply

Alles zur JAX:
Alles zur JAX:

Behind the Tracks of W-JAX 2017

Agile & Culture
Teamwork & Methoden

Big Data & Machine Learning
Speicherung, Processing & mehr

Clouds, Container & Serverless
Alles rund um Cloud

Core Java & JVM Languages
Ausblicke & Best Practices

DevOps & Continuous Delivery
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Web Development & JavaScript
JS & Webtechnologien

Performance & Security
Sichere Webanwendungen

Serverside & Enterprise Java
Spring, JDK & mehr

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Software Architecture
Best Practices