Inhaltsverzeichnis
- Java – Speicher- und Prozessoranforderungen
- Testumgebung
- Java-Effizienz basierend auf dem Spring Framework
Geschäftsanwendungen werden immer komplexer, sie sollen zuverlässig und vollständig zugänglich sein (sogenannte Zero Downtime Deployment). Um das Unternehmen zu unterstützen, müssen wir sehr häufig komplexe Lösungen anwenden, z. B. mehrere Datenbanken (einschließlich relationaler und nicht-relationaler Datenbanken). Wir fordern geringe Kosten und ggf. hohe Verfügbarkeit, d. h. Auto-Scaling. Eines der Tools, die wir verwenden müssen, ist die Containerisierung mit dem Docker-Tool, das sich sehr gut in moderne Cloud-Lösungen integrieren lässt. In diesem Artikel konzentrieren wir uns auf die Containerisierung des sogenannten „Backend“ einer Anwendung, welches meistens in Java oder einer Programmiersprache geschrieben ist, die auf einer virtuellen Java-Maschine (JVM) funktioniert.
Java – Speicher- und Prozessoranforderungen
Bei der Entwicklung einer Anwendung in Java und anderen Sprachen müssen wir Prozessor- und Speicheranforderungen berücksichtigen. Eine große Auswahl an Frameworks macht die Aufgabe nicht einfacher. Angefangen bei den einfachsten Anwendungen, bei denen wir eigenverantwortlich verwalten und uns um den richtigen Lebenszyklus kümmern. Schließlich achten wir darauf, einen Code zu schreiben, der bestmöglichst keine Speicherlecks aufweist. Angefangen bei den einfachsten Frameworks wie javalin, Dropwizard oder RESTX, bis hin zu Lösungen wie „Enterprise“, die Standard Java EE oder Spring Framework verwenden. Verschiedene Server können verwendet werden, wie z. B. Web Apache Tomcat, Netty, sowie Enterprise: WildFly oder IBM WebSphere.
Durch die Auswahl einer der folgenden Lösungen sind wir in der Lage einen Server zu konfigurieren und Parameter für JVM hinzuzufügen, so wie wir hier die Parameter „Xmx“, „Xms“ und „Xss“ verwenden. Natürlich müssen wir eine bestimmte Java-Version berücksichtigen, auf der unsere Anwendung ausgeführt wird.
Die optimale Einstellung einer Java Virtual Machine ist gar nicht so wichtig. Wenn wir die Containerisierung verwenden, entsteht eine weitere Komplexitätsebene. Nehmen wir an, eine Anwendung, die in einem Container ausgeführt wird, sieht die Ressourcen des „Hosts“ und nicht die, die einem bestimmten Container zugewiesen sind, was in älteren Java-Versionen beobachtet werden kann. Anwendungsressourcen können über den für den Container gesetzten Rahmen hinausgehen – wie wird unsere Anwendung in einem solchen Fall reagieren? Was passiert mit der Anwendung? Leider gibt es keine eindeutige Antwort. Jeder Fall ist individuell aufgrund unterschiedlicher Java-Versionen oder eines anderen Problems in einem System.
Eines ist sicher – es ist eine unangenehme Situation sowohl für den Server als auch für die Programmierer. Sehr häufig kann ein kleines Problem großen Schaden anrichten. Nehmen wir an, wir haben einen Container, der auf ein solches Problem gestoßen ist. Es ist für einen Neustart in jeder problematischen Situation eingestellt. Wie wird es sich auf das gesamte System, andere Microservices und den Host oder die Cloud auswirken, auf der sich unsere Anwendung derzeit befindet?
Testumgebung
Ich werde einige Beispiele geben, die auf der folgenden Testumgebung „http://play-with-docker.com/“ basieren. Die Ergebnisse können abweichen, wenn Sie Ihre eigenen Umgebungen mit installiertem Docker oder sogar die im Artikel vorgestellte Testumgebung verwenden – ich kenne die genauen Regeln für die Zuweisung von Ressourcen zu einer bestimmten Instanz nicht.
Testen der alten Java-Version vs einer neueren Version, die Updates für Docker enthält
Unten finden Sie das Dockerfile, das während der Tests nützlich sein wird. Ich stelle zwei Tests vor, einen davon in einer neueren Version – Java 11, und den zweiten in einer älteren Version – Java 7, um das Problem darzustellen, das Java vor nicht allzu langer Zeit betraf. Der Quellcode in Java, der die Anzahl der verfügbaren Prozessoren und den maximal reservierten Speicher angibt - sichtbar für eine bestimmte Laufzeit.
Der Quellcode, der einen Container mit Hilfe einer einfachen Java-Anwendung (wie oben) erstellt, die auf einer bestimmten 7. Version einer virtuellen Java-Maschine arbeitet.
Der Quellcode, der einen Container mithilfe einer einfachen Java-Anwendung (wie oben) erstellt, die auf einer bestimmten 13. Version der Java Virtual Machine ausgeführt wird.
Die Befehle, die wir ausführen müssen, um einen Container basierend auf dem „Dockerfile“ zu erstellen, der sich im aktuellen Ordner befindet. Einen Container starten und als letzten Schritt Logs unseres einfachen Programms lesen. (für <version> verwenden Sie die derzeit getestete Java-Version, um eine Verwechslung der Containernamen zu vermeiden).
Die Ergebnisse für die gewählte Java Version:
Java version | 7 | 13 |
Number of processors | 8 | 2 |
Max memory | 7494172672 bytes ~ 7147 MB | 64880640 bytes ~ 62 MB |
Um die von uns durchgeführte Übung und die erzielten Ergebnisse zusammenzufassen, kann man feststellen, dass wir auf das zuvor beschriebene Problem stoßen.
Die beste Methode besteht darin, eine bestimmte Version der Java Virtual Machine zu überprüfen und zu testen, die gerade verwendet wird. Die Hauptversion von Java, z. B. 9, welche die von Docker gesteuerten Parameter nicht ausführt, können die sogenannten Patches erhalten, die das Problem in den nächsten Versionen mit dem Label "minor" beheben werden.
Java-Effizienz basierend auf dem Spring Framework
Um die Effizienz besser und zuverlässiger abzubilden und zu messen, diskutieren wir ein weiteres Beispiel. Wir werden eine Webanwendung mit Spring Framework erstellen. Wir verwenden den REST-Endpunkt, der eine Liste von zufälligen ganzen Zahlen für einen Anforderungsparameter erstellt und dann alle Primzahlen für jedes Element aus der Liste auswählt.
Die oben erwähnte Funktionalität zur Berechnung von Primzahlen und REST-Endpunkt. Basierend auf Spring Boot.
Für diesen Service bereiten wir ein „Dockerfile“ vor, welches ein „maven“-Projekt mit den erforderlichen Spring Boot-Abhängigkeiten erstellt. Es wird die zuvor vorbereitete Java-Datei kopieren, um alles im letzten Schritt auszuführen.
Dockerfile - REST-Anwendung Vorbereitung.
Um die Effizienz zu messen, werde ich das „ab“-Tool verwenden - Apache Bench. Apache Bench ermöglicht es, viele HTTP-Demands [–n Option] durch viele mehrere Threads [–c Option] zu erfüllen. Mit dem vorgestellten Tool bauen wir das passende Docker-Image.
Dockerfile mit Apache Bench und Curl-Tool.
Wir müssen alles mit dem Netzwerk verbinden, damit sich die Bilder sehen können. Wir müssen die Bilder auch darüber informieren, dass sie zum selben Netzwerk gehören.
Befehle, die zum Ausführen unseres Beispiels erforderlich sind.
Im nächsten Schritt lohnt es sich, darauf zu achten, dass alle Container miteinander kommunizieren können.
Eine Reihe von Befehlen, die das Netzwerk zwischen den Containern testen.
Lassen Sie uns die Ergebnisse besprechen, die unten zu finden sind. Wir führen Tests (genau 1000 Anfragen an den Server) zu unserer Methode zur Berechnung von Primzahlen durch – maximal 50 Anfragen parallel. Beide Container wurden auf diese Weise getestet, d. h. einer auf einen Thread beschränkt, der andere mit einer größeren Anzahl von Threads. Wir können leicht feststellen, dass der limitierte Container fast die Hälfte langsamer reagiert. Natürlich muss jeder Ansatz einzeln bewertet und Effizienztests durchgeführt werden – aber es kann sich sehr oft als die bessere Lösung herausstellen, d. h. die Ressourcen eines bestimmten Containers zu erhöhen, anstatt mehrere Instanzen zu erstellen.
Testergebnisse des Containers auf einen Thread beschränkt.
Testergebnisse des Containers, der „fast“ keine Grenzen kennt, 8 Threads.
Wie geht es weiter – Java, Docker, …
Offensichtlich ist das Thema sehr breit gefächert und deshalb werde ich nur die Richtungen skizzieren, die gewählt werden können. Das Beispiel ist sehr einfach – es soll das Problem darstellen. Ganz klar ist die Situation bei monolithischen Anwendungen – ein Container, eine virtuelle Version der Java-Maschine. Beim Microservice-Ansatz können wir viele zusätzliche Anwendungen aufweisen, von denen jede auf einer anderen Version einer virtuellen Maschine läuft oder sogar in verschiedenen Sprachen geschrieben ist, die auf JVM laufen, z. B. Java, Scala, Kotlin, Groovy. Die Komplexität steigt stetig. Ein weiteres Element des Puzzles ist die Orchestrierung. Wir haben einige zur Auswahl, da wir in der Docker-Umgebung zuerst wahrscheinlich an Swarm und Docker Enterprise denken. Der größte Player ist jedoch Kubernetes. In letzter Zeit hat es viele Marktanteile gewonnen und wird von den größten Plattformen wie AWS, Azure oder Google Cloud unterstützt. Orchestrierung bietet unter anderem die Verwaltung, Implementierung und automatische Skalierung einer Anwendung.