Vert.x 2: das reaktive, modulare Framework

Seite 3: NIO & EventBus

Inhaltsverzeichnis

Dass Verticles immer im gleichen Thread aufgerufen werden, bedeutet auch, dass alle Requests der Reihe nach, also sequenziell abzuarbeiten sind. Jeder, der mit I/O zu tun gehabt hat, wird sofort an folgendes Problem denken:

new BufferedReader(new FileReader("test.txt")).readLine()

new Socket("jenkov.com", 80).getInputStream().read()

In beiden Fällen sind die lesenden Aufrufe blockierend. Der Aufruf wird so lange blockieren, bis sich auch wirklich etwas zurückliefern lässt. Geht ein Verticle I/O so an, würde das ganze Konzept nicht funktionieren. Ein einziger lahmer Socket könnte die gesamte Anwendung gefährden. Vert.x umgeht dieses Problem, indem es gar nicht erst auf blockende APIs zurückgreift. Stattdessen wird die mit JDK 1.4 eingeführte NIO-API verwendet. Ein kurzes Code-Beispiel zeigt ihre Verwendung:

Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();

...

}
}

Statt Sockets beziehungsweise Files und blockierenden Lese-/Schreib-Operationen gibt es hier Channels und Selectors. Ein Selector lässt sich dabei mit beliebig vielen Channels registrieren. Der blockierende Aufruf selector.select() wartet dann, bis einer oder mehrere der Channels bereit sind. In diesem Fall bedeutet "bereit", dass sich aus dem Channel nichtblockierend lesen lässt. Statt also, wie im One-Thread-Per-Request-Prinzip, mit jeweils einem Thread an einem Socket zu lauschen, kann hier ein einzelner Thread alle Sockets überwachen.

Da der Umgang mit NIO nicht trivial ist und hier auch öfters Betriebssystemspezifika zuschlagen, gibt es mittlerweile eine ganze Reihe von Frameworks, die das Ganze mit einer ansprechenden API benutzbarer machen. Vert.x setzt auf Netty, das wohl bekannteste unter den NIO-Frameworks. Neben einer deutlich praktischeren Abstraktion der NIO-API liefert Netty auch Implementierungen vieler verschiedener Protokolle mit, die somit recht einfach von Vert.x aus zu verwenden sind.

Doch was passiert mit blockenden Aufrufen innerhalb eines Verticles? Was ist, wenn man eine blockierende API nutzen muss? Die Antwort ist einfach: Es darf unter keinen Umständen ein blockierender Call innerhalb eines Verticle durchgeführt werden. Das würde das gesamte Konzept aushebeln. Somit wird die Menge verwendbarer Standardbibliotheken recht überschaubar (und nur zur Klarstellung: JDBC ist blockend).

Glücklicherweise existiert dieses Problem nicht erst seit gestern. Für viele Datenbanken (PostgreSQL, MySQL, CouchDB, MongoDB, Neo4J ...) gibt es mittlerweile alternative Treiber als Vert.x-Module (Vert.x-Module-Registry), die ohne blockierende Aufrufe auskommen. Für die Fälle, in denen es keine entsprechende Implementierung gibt, sieht Vert.x den Einsatz von Worker Verticles vor. Für sie gilt eine gegenüber Standard-Verticles leicht abgeschwächte Zusicherung: Worker Verticles werden immer nur von einem Thread ausgeführt. Das hängt an der Tatsache, dass hier ein ThreadPool zu verwenden ist, da ein solches Verticle einen Thread auf unbestimmte Zeit blockieren kann.

Diese Option sei hier nur der Vollständigkeit halber erwähnt. Man sollte grundsätzlich versuchen, auf blockende Aufrufe innerhalb von Vert.x zu verzichten, da sie die Skalierbarkeit doch deutlich einschränken.

Was bisher gezeigt wurde, unterscheidet sich noch nicht signifikant von dem, was Node.js und Co. können. Der wirklich interessante Teil von Vert.x ist sein interner EventBus. Er basiert auf dem Hazelcast-Projekt und regelt die gesamte Kommunikation innerhalb einer Vert.x-Anwendung. Auch hier ein kurzes Beispiel zum besseren Verständnis:

public class PingVerticle extends Verticle{
@Override
public void start() {
vertx.setTimer(2000, new Handler<Long>() {
@Override
public void handle(Long event) {
vertx.eventBus().publish("ping", "PING!");
}
});
}
}
public class PongVerticle extends Verticle{
@Override
public void start() {
vertx.eventBus().registerHandler("ping", new Handler<Message>() {
@Override
public void handle(Message event) {
container.logger().info("Received ping: "+event.body());
}
});
}
}

Das PingVerticle startet einen Timer, der dafür sorgt, dass im Zwei-Sekunden-Takt eine Nachricht an alle an der Adresse "ping" registrierten Handler geschickt wird. Sein Gegenstück, das PongVerticle, registriert sich auf die "ping"-Adresse und schreibt für jeden eingehenden Ping eine Log-Meldung.

Neben der Möglichkeit eines "publish" (alle an der Adresse registrierten Handler werden benachrichtigt) gibt es Varianten von send mit Punkt-zu-Punkt-Vorgehen. Das Besondere hier ist, dass die Empfänger anhand eines nicht strikten Rundlaufverfahrens ausgewählt werden.

Als Nachrichten ist die Verwendung von primitiven Datentypen, Strings, Buffer und JSON-Objekten erlaubt, wobei letztere das bevorzugte Format darstellen – also nur Datentypen, die sich mit einfachen Mitteln serialisieren und über das Netz übertragen lassen. Diese Einschränkung ist wichtig, da der EventBus nicht auf den Nachrichtenaustausch innerhalb einer Vert.x-Instanz beschränkt ist. Tatsächlich werden Vert.x-Instanzen, die im gleichen Netz mit der Option -cluster gestartet werden, automatisch zu einem Cluster. Nachrichten werden ohne weiteren Aufwand transparent im gesamten Cluster verteilt. Und nicht nur im Cluster. Das Vert.x-Modul mod-web-server bietet die Möglichkeit, den EventBus via SockJS im Browser zu nutzen.

Die Kombination von einfachem Programmiermodell, schwacher Kopplung durch den EventBus und durchdachter Modularisierung führt zu einfach steuerbarer Skalierbarkeit. Falls ein einzelnes Verticle die Arbeit nicht mehr bewerkstelligen kann, deployt man einfach so lange zusätzliche, bis es funktioniert. Reicht dabei die lokale Maschine nicht mehr aus, kann man das auch auf beliebigen Instanzen im Cluster tun. Um den Rest kümmert sich der EventBus.

Mehr Infos

Was noch zu sagen wäre

Weitere, im Artikel nicht ausführlich behandelte Features von Vert.x sind:

  • Shared Data: Verwendung der von Vert.x bereitgestellten Shared Maps und Sets.
  • Unit-Testing: Vert.x bietet ein vollständiges Testframeworks.
  • Polyglot: Java, Groovy, JRuby, Jython, JavaScript und Scala werden mit eigenen APIs explizit unterstützt.
  • Hochverfügbarkeit: Der EventBus ermöglicht die einfache Umsetzung von Hochverfügbarkeitsszenarien
  • Gradle- und Maven-Support: Es gibt sowohl einen Maven-Archetypen als auch ein Gradle-Template für den schnellen Einstieg