Date Tags Java / JUG

​Am 15.5. traf sich die JUG-DA zu einem Vortrag von Norman Maurer zum Thema "Netty 4". Norman Maurer ist für Red Hat als Principal Software Engineer tätig, ist Core Committer zu Netty (inzwischen auch bei vert.x) und beschäftigt sich bei Red Hat vornehmlich mit dem Thema NIO. Er ist Autor diverser Artikel im JavaMagazin (zum Thema Netty gibt es eine interessante Artikelserie), Speaker auf der JAX und arbeitet gerade an einem Buch zum Thema ("Netty in Action"), das voraussichtlich Ende des Jahres bei Manning erscheinen wird.

Netty ist ein Framework, mit dem man non-blocking I/O betreiben kann. Es basiert - ähnlich wie andere Vertreter dieser Kategorie - auf einem Event-basierten Ansatz. Mit Netty arbeitet man vorzugsweise asynchron; es bietet allerdings verschiedene Modelle an, so dass man ohne große Schwierigkeiten auf einen blockierenden Modus zurückfallen kann, sofern es das Einsatzszenario erfordert. Im Gegensatz zu Frameworks wie node.js, die nur einen Thread für user-supplied-code benutzen, kann Netty mehrere Threads nutzen und somit mehrere Kerne besser auslasten. Das Framework bringt Support für viele Protokolle mit. Die wohl typischsten Vertreter sind HTTP, WebSockets, SSL, TCP, UDP, UDT (sowas wie UDP, allerdings mit persistenten Verbindungen), SCTP (nur unter Linux), SPDY, Protobufs.

Netty ist für Hochlastszenarien ausgelegt. Um eine gute Skalierbarkeit zu erreichen, bedienen sich die Netty-Entwickler vieler Optimierungen im Detail. So wird z. B. ähnlich wie bei Java Fork/Join mit sun.misc.Unsafe gearbeitet, um ohne JVM-seitige Sicherheitsüberprüfungen in den Heap zu schreiben. Insbesondere bringt Netty eigene Byte Buffer Implementierungen mit, die denen des JDK hinsichtlich ihrer Performanz deutlich überlegen sind. Das ist zum Einen durch die Benutzung von sun.misc.Unsafe begründet, zum Anderen aber auch dadurch, dass Java-ByteBuffer finalizer benutzen und damit zwei GC-Zyklen notwendig sind, um die korrespondierenden Objekte vollständig abzuräumen. Das ist insbesondere in Hochlastszenarien mit potentiell kurzlebigen Verbindungen ein Problem, da der GC durch eine hohe Frequenz von Objektallokationen / Objektfreigaben viel Arbeit hat und man sich die zusätzlichen GC-Zyklen durch finalizer gerne spart. Benutzern von Netty ist es allerdings freigestellt, ob sie den Unsafe-Modus benutzen möchten. Über ein Property kann dieser Modus komplett abgeschaltet werden. Wie stark die Performanzeinbußen dadurch sind, konnte der Speaker allerdings nicht beziffern. Aber in einem Szenario mit 1 Million konkurrierender Verbindungen (ja, das geht mit Netty) zählt tatsächlich jedes Byte und jeder GC-Zyklus, den man einsparen kann - und die Entwickler haben sich intensiv Gedanken darüber gemacht, wo man Speicherverbrauch und CPU-Zeit einsparen kann.

Man muss sich natürlich immer die Frage stellen, ab wann der Einsatz eines nicht-blockierenden Frameworks Sinn macht. Wenn ich in meiner Applikation zu jedem Zeitpunkt max. zehn konkurrierende Verbindungen habe, dann ist ein blockierender Ansatz durchaus ausreichend (das ist er in der Regel auch noch, wenn wir von weitaus mehr Verbindungen sprechen!). Im klassischen blockierenden Modell unter Java ordnet man jedoch eine Verbindung einem Thread zu. Das skaliert bei vielen Verbindungen nicht mehr, da zum Einen die Anforderungen an den Speicherplatz enorm steigen (1 Thread belegt typischerweise zwischen 256 KB - 1 MB Heap) und der Overhead für Kontextwechsel massiv zunimmt. Ab einer gewissen Anzahl zu erwartender Verbindungen ist es daher sinnvoll, sich mit den Möglichkeiten nicht-blockierender Ansätze zu beschäftigen. Diese benutzen einen Thread um mehrere Verbindungen zu bearbeiten - das skaliert deutlich besser, erfordert allerdings, dass man sich mit einem asynchronen Programmiermodell auseinandersetzt. Allerdings bekommt man auch hier nichts geschenkt: NIO zeigt oft eine höhere Latenz als blockierende I/O, so dass sich der positive Effekt tatsächlich erst bei hoher Verbindungsanzahl bemerkbar macht. Synchrone Operationen sollte man nicht benutzen, da sie die Abarbeitung aller anderer Verbindungen auf dem gleichen Thread blockieren würden - und damit den Sinn und Zweck eines NIO-Frameworks zunichte machen.

Grundsätzlich trifft man die Entscheidung, ob blockierende I/O oder nicht-blockierende I/O, relativ früh im Projekt - typischerweise bei der Auswahl des Frameworks. Die APIs, die Java hierfür zur Verfügung stellt, arbeiten grundverschieden, so dass Entwicklungsaufwand entsteht, wenn man irgendwann feststellen sollte, dass der blockierende Ansatz nicht mehr trägt und man zu einem nicht-blockierenden Framework wechseln muss. Netty spielt an dieser Stelle seine Stärken durch eine unifizierte API aus. Ein Wechsel vom blockierenden zum nicht-blockierenden Ansatz ist daher durch die Anpassung weniger Zeilen und damit ohne signifikanten Entwickleraufwand möglich.

Interessant ist, dass Netty bereits von einigen Firmen benutzt und in vielen Frameworks die technische Basis für effizientes NIO stellt. Das komplette Backend von Twitter basiert bspw. auf Netty. Typesafe benutzt Netty als Bestandteil von Akka (wobei allerdings insbesondere Aktoren demnächst eine eigene NIO-Implementierung erhalten, da Netty nicht 100%ig in diesem Szenario passt), vert.x basiert auf Netty (schlägt auch in die gleiche Kerbe wie Netty, aber auf einem deutlich höheren Abstraktionsgrad), Play! sowie ElasticSearch.

Ich hoffe, diese Beschreibung hat Lust gemacht, sich Netty einmal genauer anzusehen. Obwohl große Namen wie Red Hat, Twitter etc. maßgeblich die Weiterentwicklung von Netty pushen, ist das Projekt sehr Community-orientiert. Das heißt, dass jeder seine Ideen einbringen und durch prototypische Implementierungen unterstützen kann - ist die Idee gut, dann besteht eine gute Chance, dass die Umsetzung angenommen wird. Über die Website kann man sich bei Interesse zusätzliche Informationen holen.