Vert.x 2: das reaktive, modulare Framework

Werkzeuge  –  3 Kommentare

Viele sind von den Konzepten von Node.js begeistert, fühlen sich aber von JavaScript als zugrunde liegender Sprache abgeschreckt. Gut, dass es da Alternativen wie Vert.x gibt.

Beim ersten Blick auf die Vert.x-Website wird man zunächst von einem Buzzword-Sturm überrollt: Mit "reactive", "polyglot", "event-driven", "scalable", Actor-based concurrency" und "modular" fehlt kaum ein Begriff, über den nicht derzeit diskutiert wird. Die tragischen Erfahrungen mit der J2EE (Java 2 Enterprise Edition) haben Java-Entwickler gelehrt, mit solchen Versprechungen vorsichtig umzugehen. Bei genauerer Betrachtung der Schlagwörter fallen einem auch gleich eine ganze Reihe anderer Frameworks ein, die in dieser Ecke zu verorten sind: etwa das erwähnte Node.js oder Akka und Quasar.

Es gibt einen triftigen Grund für diese neue Generation von Frameworks, die sich stark von allem unterscheiden, was Entwickler auf der JVM (Java Virtual Machine) gewohnt sind.

Zur Erklärung der Motivation für diese neuen Techniken seien drei Grundannahmen formuliert:

  • Entwickler sind eigentlich ziemlich mies beim Umgang mit vielen CPUs.
  • Es ist schwer, Anwendungen mit Shared Mutable State zu implementieren.
  • Selbst wenn Programmierer damit "richtig" umgehen, geht es nach hinten los.

Die erste Annahme grenzt ja schon fast an eine Beleidigung. Schließlich schreiben Entwickler nicht erst seit gestern hochgradig parallele Anwendungen. Allerdings haben sich die Anforderungen in den letzten Jahren drastisch verändert. Wo man vorher mit vergleichsweise wenigen, aber großen Requests zu tun hatte, dominieren nun – dank AJAX, REST und mobilen Anwendungen – viele kleine Requests. Die Prozessorhersteller haben das Problem erkannt und sorgten für eine stetig wachsende Zahl verfügbarer CPUs pro Maschine.

Leider haben die Programmiertechniken nicht wirklich mit dieser Entwicklung Schritt halten können. Der Gleichung "mehr CPUs gleich mehr Threads" folgend, vergrößerten Entwickler Jahr für Jahr ihre Thread Pools. Irgendwann sahen sie aber, dass statt einer Beschleunigung ihrer Anwendungen das Gegenteil eintrat. Request Laufzeiten vergrößerten sich bis zu dem Punkt, an dem das ganze System unbenutzbar wurde. Wie immer gab es Menschen, die das Problem schon früh erkannt und ihm auch einen Namen gegeben hatten: C10K.

C10K zeigt, wie "Kleinigkeiten" in großer Menge zu einer echten Herausforderung werden. Die problematische Kleinigkeit ist in diesem Fall der Thread-Kontextwechsel. Jedes Mal, wenn ein Thread auf eine I/O-Ressource warten muss oder sein Time Slice abgelaufen ist, findet dieser statt. Die Operation ist billig und bewegt sich im Bereich zwischen Nano- und Mikrosekunden.

Bis heute dominiert bei Servern das One-Thread-per-Request-Prinzip: Ein Thread lauscht an einem Server Socket und reagiert auf eingehende Requests, indem er sich einen Thread aus dem Pool holt und diesem die Verarbeitung übergibt. Durch die ständig wachsende Zahl an Requests stieg auch die Zahl der Threads und somit die Menge an Kontextwechseln. Irgendwann beschäftigen sich die CPUs mehr mit Kontextwechseln und Warten als mit ihren eigentlichen Aufgaben.

Die zweite Annahme lässt sich mit zwei einfachen Fragen bestätigen: Welche der folgenden Dinge haben Entwickler bisher verwendet und immer richtig eingesetzt: AtomicBoolean, Lock, ReentrantLock, StampedLock, synchronized oder volatile.

Shared State ist ein schwer zu zähmendes Biest. Ständig stellt sich die Frage, ob das Stück Code, an dem Entwickler gerade arbeiten, thread-safe sein muss oder nicht. Manche werden regelrecht panisch, packen alles in synchronized-Blöcke und sehen ihrer Anwendung beim Einschlafen zu. Andere ignorieren das Problem und verbringen irgendwann viel Zeit mit der Jagd nach Heisenbugs. Aber auch die, die wirklich verstehen, was sie da tun, finden sich von Zeit zu Zeit in einer der beiden Ecken wieder und zweifeln an ihrer Ausbildung.

Bleibt noch Annahme 3. Hier sei auf eine Schrift zu Synchronisierungskosten zurückgegriffen. Im Disruptor-Paper beschrieben die Leute von LMAX ihre Motivation hinter der Entwicklung eines hocheffizienten Ringpuffers. Hierzu verglichen sie die verschiedenen, auf der JVM verfügbaren Varianten. Den Test hat der Autor mal nachgebaut (Code gibt es hier). Es werden 500.000.000-Schreiboperationen auf eine long-Variable mit verschiedenen Contention-Szenarien durchgeführt. Die Ergebnisse finden sich in der folgenden Abbildung.

Contention-Kosten verschiedener Konstrukte (Abb. 1)

Für einige Leute dürften vor allem die Kosten, der oft als Allheilmittel gepriesenen CAS-Operationen (Compare-and-swap) "überraschend" sein. Somit ist klar: Selbst wenn sie Shared Mutable State richtig implementieren, fangen sie sich nicht wegzudiskutierende Kosten ein.