Entwicklung und Betrieb von asynchronen Applikationen mit Vert.x in der Praxis

Vert.x im Unternehmenseinsatz

Sascha Möllering, Mariam Hakobyan, Björn Stahl

© iStockphoto.com/Kngkyle2

Ein aktuelles Hypethema in der Java-Community ist die asynchrone Programmierung sowie die dazugehörigen Plattformen und Frameworks. Diese bieten spannende Lösungswege und neue Denkansätze. Aber sind sie ausgereift genug für die Anforderungen einer Businessanwendung, die ein sehr hohes Maß an Stabilität, Wartbarkeit, Skalierbarkeit und niedrige Latenz erfordert? In vorliegendem Beitrag wollen wir ein Beispielprojekt beschreiben, das ZANOX auf Basis von Vert.x implementiert hat und diese Frage klar mit „ja“ beantwortet. Wir werden im Folgenden Vert.x kurz einführen, dann das Projektsetup und die Integration in die bestehende Infrastruktur erläutern und zum Ende hin auch noch einmal das Monitoring und die Anbindung an andere Messagingsysteme zeigen.

ZANOX hat sich dazu entschlossen, das hauseigene Tracking vollständig neu zu entwickeln. Ziel ist es dabei, die bestehende Tracking-Applikation auf eine neue Plattform zu heben, die performant ist (niedrige Latenz und hoher Durchsatz), gut skaliert und einfach erweitert werden kann. Um die Möglichkeiten zu evaluieren, sollte im ersten Schritt ein Prototyp entwickelt werden, der den gesamten HTTP-Traffic, der im Tracking anfällt, verarbeiten und selbst modular erweitert werden kann. ZANOX ist Europas größtes Netzwerk für Performance-Advertising. Dementsprechend hoch ist auch der Traffic: ca. 320 Millionen Requests pro Tag. Im Auswahlprozess eines für diese Anforderungen geeigneten Frameworks kamen folgende Technologien in Betracht: Node.js, Servlets (Java EE oder Spring), Vert.x, Akka und Go. Nach näherer Betrachtung und Evaluation der einzelnen Technologien hat sich das Entwicklerteam für Vert.x entschieden, da es den gewählten Einsatzzweck am besten abzudecken schien und eine schnelle Einarbeitung verspricht.

Performance-Advertising
Performance-Advertising gilt als Weiterentwicklung des klassischen Affiliate Marketings und basiert auf dem klar messbaren Erfolg aller eingesetzten Kanäle im Onlinemarketing. Ob Preisvergleichsseiten oder Shoppingportale, Coupons oder Cashback-Modelle, Display Advertising und Retargeting, E-Mail-Marketing, Suchmaschinen-, Mobile oder Social-Media-Marketing – der Advertiser bezahlt nur im Erfolgsfall.

Der größte Vorteil von Vert.x ist die Tatsache, dass die Plattform auf der JVM läuft, die über Jahre hinweg optimiert und verbessert wurde. Der inhärent polyglotte Ansatz, den das JDK 7 ermöglicht, ist ebenfalls sehr interessant, da dies die Migration von vorhandenen Anwendungen vereinfacht und den Umstieg auf eine andere Programmiersprache zum Kinderspiel macht: Innerhalb einer Vert.x-Anwendung können mehrere Sprachen verwendet werden. So wird eine schrittweise Migration zu einer anderen Sprache relativ einfach.

Im nächsten Schritt wurde vom Entwicklerteam ein erster Prototyp geschrieben, um zu verifizieren, dass der gewählte theoretische Ansatz auch in der Realität bestehen kann. Dieser bestand aus einem einfachen HTTP-Server, der sich mit wenigen Zeilen Code realisieren ließ.

Verticles
Verticles sind Klassen, die isoliert über einen eigenen Classloader geladen und von der Vert.x-Event-Loop abgearbeitet werden. Jedes Verticle wird (gleichzeitig) nur von lediglich einem Thread ausgeführt. Innerhalb der Verticles wird der Code Single-Threaded geschrieben, es existieren also keine Race Conditions und Deadlocks. Wichtig ist, dass es keine Aufrufe von blockierenden APIs innerhalb eines Verticles gibt, weil sonst die Event-Loop „steht“. Falls Blocking Calls trotzdem notwendig sind, müssen dafür so genannte Worker Verticles verwendet werden, die mit Vert.x-internen Thread-Pools verarbeitet werden. Die Kommunikation zwischen Verticles wird für gewöhnlich mit dem Event Bus abgebildet, einem Art Vert.x-internem Kommunikationskanal, über den Messages gesendet werden können. Der Event Bus unterstützt sowohl Kommunikation über Publish/Subscribe als auch Point-to-Point.

Das Verticle des initialen Prototyps implementiert einen HTTP-Server, der auf verschiedene URLs reagiert und entsprechend eines Regelsatzes mit einem Redirect antwortet (Definition der Verticles siehe Kasten: „Veticles“). Im Prinzip ist das bereits der ganze Anwendungsfall. Die Businesslogik für die Ermittlung des Redirects ist jedoch recht komplex. Erste Tests auf einem sog. TeamDevServer sollten zeigen, ob die grundlegende Performance ausreicht, um dem erwarteten Traffic standzuhalten. Ein initialer Lasttest wurde mit einem eher rudimentären Setup durchgeführt: Basis war ein virtueller Server mit zwei Cores, 4 GB RAM und ein Lastgenerator auf der lokalen Entwicklermaschine. Dieser wurde ebenfalls mit Vert.x entwickelt; er erzeugt HTTP-Requests und wartet auf einen korrekten Redirect. Erst danach wird dieser Request als erfolgreich gewertet (weitergehende Informationen und Implementierungsbeispiele dazu lassen sich in der Example-Sektion des Vert.x-Projekts abrufen). Begrenzt wurde die Performance des Testsetups durch die Anzahl der gleichzeitigen Verbindungen und die CPU-Leistung. Mit fünf gleichzeitig gestarteten Verticles war es möglich, mit einer einfachen Entwicklermaschine 28 000 HTTP-Requests pro Sekunde zu erzeugen. Dieses Ergebnis, das mit diesem minimalen Setup erreicht wurde, war für uns vollkommen ausreichend, um das Projekt weiter zu verfolgen. Die Anforderung war, 17 000 Requests pro Sekunde verarbeiten zu können, d. h. dass wir in der Produktivumgebung – mit entsprechend größeren Servern – mehr als genug Leistungskapazitäten haben.

Vert.x: Setup und Build

Nach diesen ersten Gehversuchen wurde eine geeignete Build-Infrastruktur aufgesetzt – nicht zuletzt, um den Firmenrichtlinien gerecht zu werden. Es wurde viel Arbeit in die Build-Infrastruktur, bestehend aus Maven, Nexus und Jenkins, gesteckt, um einen Entwicklungsprozess nach den Prinzipien des Continuous Delivery bzw. in der Staging-Umgebung auch ein Continuous Deployment zu ermöglichen.

Das initiale Projektsetup ist seit Vert.x 2.0 denkbar einfach gestaltet: zuerst den Maven-Aufruf mvn archetype:generate -Dfilter=io.vertx: und dann den gewünschten Archetype auswählen (derzeit ist nur einer verfügbar). Dieser Schritt erzeugt ein Maven-Projekt, das für jede unterstützte Programmiersprache ein Beispiel-Verticle beinhaltet. Diese können – falls nicht benötigt – natürlich gelöscht werden. Ein einfaches mvn package baut das gesamte Projekt und erstellt ein Zip-File, das später im Vert.x-Server ausgeführt werden kann. Für den nächsten Schritt zum Continuous Deployment wird im Jenkins-Server ein Job erstellt, der bei jedem Push in das entsprechende GitHub-Repository das Bauen des Projekts anstößt. Das gebaute Paket wird dann vom Jenkins- in den Nexus-Server hochgeladen und der Nexus-URL zum erstellten Zip-File an das Deployment-Skript übergeben (zur genauen Infrastruktur selbst kommen wir im nächsten Abschnitt). Das Deployment-Skript wiederum kopiert das soeben erstellte ZIP-Paket vom Nexus auf den Zielserver und startet diesen neu. Mit dieser Variante des Continuous Deployments ist zum einen sichergestellt, dass exakt die gebaute Version auf den Zielserver kopiert und getestet wird. Zum anderen hat der Server durch den Neustart immer einen sauberen Zustand. Darüber hinaus ist der Turnaround sehr kurz. Das heißt, nach dem Einchecken steht die neue Version innerhalb von 30 Sekunden auf dem Testserver zur Verfügung. Ziel ist es, für alle Stages der Infrastruktur (Production, Staging, Testing etc.) genau das gleiche Paket zu deployen und zu testen. Hierfür sind alle installierten JVMs mit einem Parameter versehen, der angibt, in welcher Stage sich der Server befindet. Anhand dieser Einstellung wird die richtige Properties-Datei mit den Einstellungen bezüglich Datenbankverbindung, Porteinstellung usw. ausgewählt. In dem ausgelieferten Paket befindet sich also für jede Umgebung genau eine Properties-Datei (dazu im weiteren Verlauf mehr).

Für einen lokalen Test kann direkt ein Run-Job in der IDE erstellt werden. An dieser Stelle möchten wir das gerne beispielhaft für IntelliJ zeigen (Abb. 1).

Abb. 1: Konfiguration von IntelliJ

Das Vorgehen ist aber leicht auf andere IDEs übertragbar. Im entsprechenden Menü wird eine neue Konfiguration für den Start von Vert.x erzeugt. Als Main-Class muss org.vertx.java.platform.impl.cli.Starter angegeben werden, als „Program Arguments“ runzip targetmyproject-0.0.1-SNAPSHOT-release.zip und als Verzeichnis das Projektverzeichnis. Die Maven Dependencies von Vert.x müssen auf compile gesetzt werden, damit die entsprechenden Klassen im Classpath sind.

Der Nachteil hierbei ist, dass die entsprechenden Vert.x JARs ebenfalls in der ZIP-Datei landen. Im ZANOX-Setup wird Vert.x wie ein Application Server genutzt, es wird also davon ausgegangen, dass Vert.x auf dem Zielsystem installiert ist. Somit sind die JARs an dieser Stelle überflüssig. Falls dies in der produktiven Stage verhindert werden soll, kann an dieser Stelle mit einem Maven Buildprofile gearbeitet werden. Egal für welche Variante sich im Endeffekt entschieden wird, der Turnaround von der Codeänderung zum laufenden Ergebnis ist sehr kurz, d. h. mvn compile mit dem anschließenden Startbefehl runzip benötigt bei diesem Projekt lediglich 3 bis 5 Sekunden, je nach Rechnerausstattung.

Infrastruktur

Eine der ZANOX-Regeln ist es, dass neue Infrastruktur – sofern es geplant ist, diese produktiv zu betreiben – in Codeform vorliegen muss (sog. Infrastructure as Code), in diesem Fall als Chef-Cookbooks. Wir müssen also auch an dieser Stelle so früh wie möglich evaluieren, ob und wie wir Vert.x in unsere bestehende Infrastruktur und Cookbooks integrieren können.

Chef ist ein Konfigurationsmanagementtool, das mithilfe von Ruby implementiert ist. Es bietet eine DSL für das Erstellen von Systemkonfigurationen. Das Tool wurde von der Firma Chef (früher Opscode) [2] implementiert und steht unter der Apache License 2.0. Es arbeitet mit drei verschiedenen Komponenten:

  • Chefserver, auf dem zentral die Konfiguration der Infrastruktur abgelegt wird
  • Chefworkstation, von der aus der Administrator Chefskripte startet
  • Nodes, das heißt Systeme, die von Chef gemanagt werden

Für die Konfiguration der einzelnen Nodes werden so genannte Cookbooks verwendet. Es enthält u. a. Bestandteile wie Recipes, Attribute-Dateien, Templates oder zusätzliche Konfigurationsartefakte.

Jedes Cookbook muss mindestens ein Recipe („default-recipe“) enthalten. Ein Node besitzt eine so genannte Run List. Im einfachsten Fall ist das eine Liste von Recipes bzw. Cookbooks, die der Node abarbeiten wird. Recipes sind das Herzstück von Chef, da sie die Infrastrukturdefinition enthalten. Falls im Recipe abhängige Recipes definiert worden sind, werden diese Abhängigkeiten korrekt aufgelöst und die Recipes automatisch ausgeführt. Zu beachten ist: In der Reihenfolge, in der die Recipes in die Liste übergeben worden sind, werden sie auch gestartet.

Auf der Workstation und den Nodes befindet sich der Chefclient. Ein Teil davon ist das Programm Knife: ein sehr mächtiges CLI-Tool, das mit dem Server über eine REST-Schnittstelle kommuniziert. Wenn der Chefclient gestartet wurde, werden die Recipes, die sich in der Run List des Clients befinden, auf die Nodes transferiert. Sie werden in einer DSL basierend auf Ruby implementiert.

Nach der Implementierung unseres ZANOX-spezifischen Cookbooks haben wir der Community eine etwas generalisierte Version für Vert.x zur Verfügung gestellt [3]. Es ist sowohl für Debian-kompatible Linux-Versionen (Debian, Ubuntu) als auch für die RHEL-Familie (CentOS, Red Hat, Fedora, Amazon Linux) geeignet. Das Chef-Cookbook installiert Vert.x in der Version 2.0.2 und hat Abhängigkeiten zu dem Java [4] und Ark Community Cookbook [5]. Letzteres ist für das Management von Softwarepaketen gedacht, d. h. Ark kümmert sich um Download, Entpacken, Konfiguration, Build und Installation dieser Pakete. Um das Vert.x Cookbook nutzen zu können, müssen alle drei genannten Cookbooks auf dem entsprechenden Chefserver bzw. im Filesystem liegen, falls knife-solo eingesetzt wird.

Was passiert nun genau in dem Vert.x Cookbook? Initial wird JDK 7 als Basis für Vert.x installiert und zusätzlich ein runtime-User angelegt, unter dem der Java-Prozess später laufen wird. Dieser User hat aus Gründen der Sicherheit keine root-Rechte, was naturgemäß zu Problemen beim bind auf Port 80 führen kann. Unter Debian existiert das Paket authbind [6], mit dem dieses Problem elegant umschifft werden kann: authbind erlaubt es dem Administrator, bestimmten Usern und Gruppen Berechtigungen für Ports unter 1024 (TCP und UDP) zu geben. In dem Cookbook ist das folgendermaßen umgesetzt:

touch /etc/authbind/byport/80
chown vertx:vertx /etc/authbind/byport/80
chmod 755 /etc/authbind/byport/80

Nach der Installation von Vert.x unter /usr/local durch Ark wird noch zusätzlich ein Softlink unter /srv/vertx angelegt, der auf die Installation zeigt. Die Grundidee des Cookbooks war es, Vert.x als Container, ähnlich wie z. B. JBoss, zu nutzen, um das Deployment und die Administration zu vereinfachen. Aus diesem Grund liegen unter /etc/default bzw. /etc/sysconfig noch die beiden Dateien vertx und vertx_deploy, wobei vertx einige grundlegende JVM-Parameter (-Xmx, -XX:+UseG1GC etc.) enthält und in vertx_deploy vom Jenkins-Job beim Deployment das Module definiert wird, mit dem Vert.x startet. Beim Start wird bei Vert.x der runzip-switch gesetzt, d. h., es wird davon ausgegangen, dass das zu startende Module als ZIP-Datei vorliegt. Diese wird durch einen Maven-Build via Jenkins erzeugt, in Nexus abgelegt und über ein Fabric-Script [7] auf den jeweiligen Server kopiert. Fabric ist im Prinzip eine Art Python-Wrapper um SSH, um das Deployment von Applikationen bzw. die Systemverwaltung deutlich zu vereinfachen. Ein simples Beispiel (die Ausführung von uname) sieht folgendermaßen aus:

from fabric.api import run

def host_type():
    run('uname -s')

Darüber hinaus werden noch einige andere Konfigurationsdateien, die sich in der Vert.x-ZIP-Datei befinden, durch das Cookbook – in Abhängigkeit vom Linux-Flavor – überschrieben. Die wohl wichtigste Datei ist das Init-Script für Vert.x, das auch als Template vorliegt und in Abhängigkeit vom jeweiligen Linux erstellt wird. An dieser Stelle werden alle Informationen, die in den Konfigurationsdateien abgelegt worden sind, zusammengeführt und Vert.x sauber gestartet bzw. gestoppt. Falls es Probleme beim Starten oder Stoppen des Prozesses gab, werden entsprechende Fehlermeldungen nach /srv/logs/console.log geschrieben (Listing 1).

start)
        echo "Starting Vert.x 2.0.2"
        <% if node[:platform_family] == "debian" -%>
        START_SCRIPT="authbind /srv/vertx/bin/vertx runzip /srv/zxdeploy/packages/$VERTX_MODULE"
        <% else %>
        START_SCRIPT="/srv/vertx/bin/vertx runzip /srv/zxdeploy/packages/$VERTX_MODULE"
        <% end %>
        if [ -z "$SUBIT" ]; then
          $START_SCRIPT > /srv/zxlog/console.log &
          echo ONLINE > $STATUS_FILE
        else
          touch /srv/zxlog/console.log
          chown vertx:vertx /srv/zxlog/console.log
          $SUBIT "$START_SCRIPT" > /srv/zxlog/console.log &
          $SUBIT "echo ONLINE > $STATUS_FILE"
        fi
    ;;

Vert.x in der Wolke

Die volle Leistungsfähigkeit kann Chef aber erst ausspielen, wenn neben dem eigenen Datacenter zusätzlich noch ein Cloud-Service wie z. B. Amazon Web Services genutzt wird. Beispielsweise kann eine neue Instanz einer Linux-VM mit Vert.x mit folgendem knife-Command in kürzester Zeit erzeugt werden, wobei beachtet werden muss, dass die Cookbooks bereits auf dem entsprechenden Chefserver liegen müssen:

knife ec2 server create -I ami-c7c0d6b3 -i <your/pem/file.pem> -S knife -r "recipe[vertx]"

knife

knife ist ein Kommandozeilentool, das eine Verbindung zwischen dem lokalen Repository und dem Chefserver darstellt. Eine EC2-Erweiterung gibt es unter [9].

Als Basis wird ein Amazon Linux Image im Datacenter in Irland genutzt. Zur Gewährleistung der bereits erwähnten flexiblen Anpassung sind noch einige Dinge wie Autoscale (automatisch skalierende Infrastruktur) und Cloudwatch (Überwachung der AWS-Cloud-Ressourcen) zu implementieren bzw. zu konfigurieren. Wie das genau funktioniert, kann im Blogpost „Elastic JBoss AS 7 clustering in AWS using EC2, S3, ELB and Chef“ auf dem AWS-Blog [9] nachgelesen werden, der ein ähnliches Setup auf Basis von JBoss 7 beschreibt.

Konfiguration

Wie bereits oben angedeutet, existiert für jede Stage eine eigene Konfigurationsdatei, die u. a. die Konfiguration der Datenbank und der Messaging-Systeme enthält. Damit dem Service bekannt ist, auf welcher Stage er sich befindet, existiert eine Umgebungsvariable, die durch Chef beim Ausrollen der Infrastruktur geschrieben wird:

JAVA_OPTS="$JAVA_OPTS -Dstage=<%= node.chef_environment %>"

Diese Zeile ist Teil des Templates für die JVM-Parameter. Im Java-Code wird diese Umgebungsvariable folgendermaßen verwendet:

String stage = System.getProperty("stage");
Properties prop = getPropertiesFromClasspath("server_" + stage + ".properties");

Damit ist gewährleistet, dass ein Paket, das alle Konfigurationen der verwendeten Stages enthält, unverändert auf jeder Stage verwendet werden kann.

Monitoring

Wenn Services bzw. Infrastruktur sinnvoll produktiv betrieben werden sollen, stellt sich naturgemäß die Frage nach dem Monitoring des Service. Da wir uns auf der Java-Plattform befinden, ist JMX das Tool der Wahl für das Monitoring. Neben den Standardmetriken der JVM ermöglicht JMX, auch Vert.x-spezifische Metriken des Thread-Pools wie z. B. ActiveCount, TaskCount, CompletedTaskCount und QueueSize abzufragen. In dem bereits angesprochenen Cookbook werden alle notwendigen JVM-Parameter gesetzt, um per JMX die Metriken auslesen zu können (Listing 2).

JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=5566"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.local.only=false"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=true"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.password.file=/srv/vertx/conf/jmxremote.password"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.access.file=/srv/vertx/conf/jmxremote.access"

In diesem Fall werden die JMX-Metriken über den Port 5566 ausgegeben und es ist möglich, Remote diese Werte abzugreifen. Der Zugriff auf die Daten ist mit Username und Passwort abgesichert, beide werden in den Dateien jmxremote.password und jmxremote.access abgelegt (Abb. 2).

Abb. 2: Vert.x MBeans in JConsole

Das Auslesen der Metriken kann mit einem Tool wie z. B. jmxtrans [10] durchgeführt werden. Es verbindet JMX-Produzenten mit einem Logging- oder Monitoring-System, ist sehr flexibel aufgebaut und kann über JSON konfiguriert werden. Bislang existieren Adapter für Graphite, StatsD, Ganglia, cacti, Textdateien und stdout. Eine Beispielkonfiguration für Graphite [11] kann unter [12] gefunden werden.

Neben den Metriken stellt sich die Frage, wie ein Vert.x-Server in eine bestehende Load-Balancer-Infrastruktur integriert werden kann. Dazu wurde ein eigenes Verticle implementiert, das sich an Port 80 bindet (aus diesem Grund wird Authbind benötigt) und lediglich ein File (/var/www/zxmonitor/status.txt) an den Load Balancer zurückliefert. Der Load Balancer ist so konfiguriert, dass erkannt wird, ob in dem File der String „ONLINE“ oder „OFFLINE“ abgelegt ist. Im Falle von „OFFLINE“ wird kein Traffic an den jeweiligen Node geschickt. Der Inhalt des Files kann über ein eigenes Init-Skript gesteuert werden, sodass kontrolliert Traffic auf die unterschiedlichen Nodes gebracht werden kann (Listing 3).

public class MonitoringVerticle extends Verticle {
  @Override
  public void start() {
    vertx.createHttpServer().requestHandler(new Handler() {

        @Override
        public void handle(HttpServerRequest request) {
          if (request.path().equals(Constants.ZXMONITOR_PATH)) {
            request.response().sendFile(Constants.ZXMONITOR_FILE);
          }
        }
    }).listen(80, NetworkUtil.getMyIPAddress());
  }
}

Analoges Verhalten kann auch bei AWS mit dem Elastic Load Balancer (ELB) erreicht werden, dazu muss zunächst eine entsprechende ELB-Instanz erzeugt werden:

aws elb create-load-balancer --load-balancer-name vertx-elb --listeners Protocol=http,LoadBalancerPort=80,InstancePort=80 --availability-zones "eu-west-1a"

und die entsprechende Health-Check-Konfiguration:

aws elb configure-health-check --load-balancer-name vertx-elb --health-check Target=http:80/zxmonitor/status.txt,Interval=30,Timeout=3,UnhealthyThreshold=2,HealthyThreshold=2

Integration mit Messaging-Systemen

Der konkrete Anwendungsfall sah vor, dass zusätzlich Messages aus RabbitMQ [13] gelesen und in Apache Kafka [14] geschrieben werden sollen. RabbitMQ sendet Messages falls Datenänderungen von externen Systemen aufgetreten sind. In Kafka werden – basierend auf den Eingangsdaten – Events für andere Consumer generiert. Wie bereits oben beschrieben, kommunizieren Verticles miteinander über den Event Bus. Ein Verticle produziert ein Event, schreibt dieses in den Event Bus (mit der Zieladresse des Kafka Verticles) und das Kafka Producer Verticle nimmt diesen Event und schreibt den Inhalt in das Kafka-Messaging-System. Das Senden eines Events ist denkbar einfach:

vertx.eventBus().publish("your_eventbus_address", "Hello World!");

Die Verarbeitung derselben Nachricht im Ziel-Verticle sieht schematisch wie in Listing 4 aus.

vertx.eventBus().registerHandler("your_eventbus_address", new Handler<Message<String>>() {
    @Override
    public void handle(Message<String> event) {
      logger.info("Received event '{}' from EventBus." + event.body());
    }
});

Ein weiterer Vorzug dieser Plattform ist die Modularität, die mit so genannten Modules erreicht wird. Ein Module im Vert.x-Kontext ist ein JAR-File, das zur Laufzeit nachgeladen wird und zusätzliche Funktionalität bereitstellt. Dieses Module muss sich natürlich im lokalen Maven Repository oder bei Maven Central befinden. Für viele Modules ist eine zusätzliche Konfiguration notwendig, die meist in Form von JSON-Objekten bzw. Files abgelegt werden muss.

Da bislang noch kein Kafka Module für Vert.x existierte, hat ZANOX aus der eigenen Implementierung eine generelle Open-Source-Implementierung unter der Apache-2-Lizenz geschrieben. Dieses Module befindet sich unter [15], ist bereits in Maven Central [16] und dem Vert.x Module Registry [17] verfügbar.

Angenommen, jemand möchte das Kafka Module nutzen, dann ist folgender Schritt im Starter-Verticle notwendig, um das Module in der eigenen Applikation zu nutzen:

container.deployModule("com.zanox.vertx~mod-kafka~1.0.0", config);

Das Module wird über ein JSON-Objekt konfiguriert, wobei folgende Konfigurationsparameter für ein korrektes Funktionieren notwendig sind:

JsonObject config = new JsonObject();
config.putString("address", "event_bus_address");
config.putString("metadata.broker.list", "localhost:9092");
config.putString("kafka-topic", "kafka_topic");
config.putString("kafka-partition", "kafka_partition");
config.putString("request.required.acks", "whether_you_require_acknowledgment_from_server_after_message_was_received");

Benutzt wird das Module, indem eine Message im JSON-Format an die zuvor konfigurierte Adresse über den Event Bus geschickt wird (in diesem Fall wäre das „event_bus_address“):

JsonObject jsonObject = new JsonObject();
jsonObject.putString("content", "your message goes here");
vertx.eventBus().publish("event_bus_address", jsonObject);

Die Integration in RabbitMQ ist ähnlich gelöst: glücklicherweise bietet RabbitMQ einen asynchronen Zugriff auf die Queues [18], somit ist es nicht notwendig, ein Worker Verticle zu implementieren, das in einer Endlosschleife die entsprechende Queue pollt (Listing 5).

channel.basicConsume(createQueue, true, "vertxcreateconsumer",
  new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag,
                               Envelope envelope,
                               AMQP.BasicProperties properties,
                               byte[] body)
    throws IOException {

      String strBody = new String(body);
      logger.debug("Sending " + strBody + " to " + TargetVerticle.class.getSimpleName());
      vertx.eventBus().send(TargetVerticle.class.getSimpleName(), strBody);
    }
});

In der überschriebenen start()-Methode des Verticles wird dieser Consumer an der Queue registriert und Messages können asynchron aus RabbitMQ gelesen und an ein Target Verticle weitergeleitet werden.

Optimierungen der applikationsinternen Kommunikation

Mit wachsenden Anforderungen an die Applikation und steigender Komplexität der Businesslogik wächst natürlich auch die Anzahl der Verticles. Damit nicht der Überblick verloren geht, wurde ein System konzipiert, bei dem nicht die einzelnen Verticles den Datenfluss bestimmen, sondern ein Datenobjekt, das von Verticle zu Verticle gesendet wird. Dieses Verfahren hat den Vorteil, dass trotz einer großen Anzahl von Verticles immer klar ist, welchen Weg jedes Datenobjekt nehmen wird.

Jedes Verticle wird mit seinem einfachen Klassennamen an dem Event Bus registriert. Das Datenobjekt, das von Verticle zu Verticle versendet wird, enthält eine einfache sortierte Liste von Adressen (also Klassennamen der einzelnen Verticles). Diese werden nach dem Stackprinzip abgearbeitet, d. h. es wird immer die erste Adresse gelesen und gelöscht und danach das Datenobjekt an die entsprechende Adresse versendet.

JsonObject dataObject {
  JsonArray verticleAddresses = new JsonArray();
  verticleAddresses.add(ParserVerticle.class.getSimpleName());
  verticleAddresses.add(DataProcessorVerticle.class.getSimpleName());    
}

Dieses Datenobjekt wird in jedem Verticle entweder mit Daten angereichert oder vorhandene Daten werden modifiziert. Diese Operationen sollten am Besten idempotent sein, da die Verticles prinzipiell stateless sind. Damit bringt ein mehrmaliges Abarbeiten (versehentlich oder absichtlich) durch ein Verticle keine unerwünschten Nebenwirkungen mit sich.

Don’t block

Die wichtigste Erfahrung, die wir im Umgang mit Vert.x gerade unter Performancegesichtspunkten gemacht haben, ist; dass man genau wissen sollte; welche Aktionen blocking/non-blocking sind. Getreu dem Motto „Don’t block the Event-Loop“ sollten alle langwierigen oder blockierenden Operationen wie JDBC-Calls und Dateizugriffe in einem Worker Verticle ausgeführt werden. Selbst mit dem Logging sollte eher sparsam umgegangen werden: Unsere Performanceanalysen haben gezeigt, dass Logging-Operationen die Event-Loop stoppen, selbst mit asynchronen Loggern. Es gibt zwei Möglichkeiten damit umzugehen: Entweder man setzt das Applikations-Logging sehr sparsam ein oder man lagert das eigentliche Logging an ein eigenes Verticle aus, das über den Message Bus gespeist wird. Der Nachteil der zweiten Option ist, dass die Nachrichten nicht unbedingt in der richtigen Reihenfolge geloggt werden.

Fazit

Projekte mit Vert.x haben ihren ganz eigenen Reiz: Auf der einen Seite sind sie vom Java-EE-Ballast befreit, auf der anderen Seite ist man den Restriktionen bei Blocking Calls unterworfen. Das Entwickeln von Software auf Basis von asynchronen Frameworks erfordert eine gänzlich andere Denkweise als wie wir sie aus der klassischen Programmierwelt kennen: Jedes Verticle ist stateless und die Daten können nicht einfach als Objekte an andere Verticles übergeben werden. Alles wird über den Event Bus geschickt und dieser kann nur mit primitiven und JSON-Objekten arbeiten. Entwickler müssen bei der Nutzung von Vert.x also ganz genau überlegen, welche Daten wann an welches Verticle geschickt werden sollen. Falls auf Events reagiert werden soll, müssen z. B. mit Java Callbacks geschrieben werden, was nicht immer die Lesbarkeit des Codes verbessert. In JavaScript ist derselbe Code deutlich kompakter und lesbarer.

Auf der anderen Seite ist die Asynchronität die große Stärke dieses Frameworks. Jedes Verticle arbeitet vollkommen unabhängig und kann daher in (nahezu) beliebiger Anzahl gestartet werden, wenn es der Use Case verlangt. Ein weiteres Plus ist, dass man sich selbst mit mehreren hundert Verticles keinerlei Gedanken um Concurrency Issues machen muss.

Was die Infrastrukturseite betrifft, muss bei Vert.x nahezu alles selbst implementiert werden. Glücklicherweise ist Vert.x sehr offen, was das Setup signifikant erleichtert. Die Integration mit Messaging-Systemen ist denkbar einfach, speziell wenn bereits asynchrone Treiber existieren.

Im Endeffekt bleibt zu sagen: Falls ein High-Performance-System mit geringer Latenz und einfacher Skalierbarkeit entwickelt werden soll, dann sollte Vert.x ganz oben auf der Liste der zu evaluierenden Frameworks stehen.

Geschrieben von
Sascha Möllering
Sascha Möllering
Sascha Möllering arbeitet als Solutions Architect bei der Amazon Web Services Germany GmbH. Seine Interessen liegen in den Bereichen Automation, Infrastructure as Code, Distributed Computing, Container und JVM.
Mariam Hakobyan
Mariam Hakobyan
Mariam Hakobyan ist Senior Software Engineer bei der ZANOX AG mit über sieben Jahren Erfahrung in den Bereichen Enterprise Java Development, SaaS- und IaaS-Applikationen auf Basis von Java, Java EE 7, Spring, Hibernate und BPMN.  
Björn Stahl
Björn Stahl
Björn Stahl ist Senior Developer bei der ZANOX AG und arbeitet dort im Bereich Java EE. Darüber hinaus beschäftigt er sich mit Build-Infrastrukturen, insbesondere mit Maven und Nexus und interessiert sich für das Performancetuning von Java-Anwendungen.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: