En mai de cette année, Vert.x a fait son entrée dans le monde des serveurs asynchrones. Tim Fox, le créateur, a pour ambition de fournir un système à la fois polyglotte et scalable tout en proposant un modèle concurrentiel simple.
Restons polyglottes
Il s’agit bien ici de parler plusieurs langages : Java, Groovy, Javascript, Coffeescript, Ruby et Python. En ce qui concerne Java, c’est tout naturel pour une application construite autour du framework asynchrone Netty. Et quand on parle Java, Groovy coule de source. Pour avoir testé le framework essentiellement avec ce dernier, on a apprécié la richesse fonctionnelle associée à la productivité de Groovy. S’agissant de Javascript, Vert.x a été clairement conçu pour venir concurrencer Node.js ; Tim Fox a d’ailleurs publié un benchmark qui a généré beaucoup de commentaires. Ruby et Python s’appuient sur les librairies JRuby et Jython et positionnent Vert.x à l’intersection de plusieurs communautés.
De l’ULM au quadri-moteur
En plus de rajouter une couche de simplification vis-à-vis de Netty, Vert.x apporte toute une variété de fonctionnalités qui, mises bout à bout, confèrent une grande cohérence au modèle. Tout comme Node.js, Vert.x applique le pattern reactor, les tâches à traiter sont effectuées sur un même thread qui dispatche ensuite la réponse. On parle même de pattern multi-reactor, car en réalité Vert.x est capable de gérer plusieurs event loops à la demande, mettant à profit le nombre de processeurs à disposition. On nomme verticle chacune de ces boucles. Ce modèle impose qu’aucune action ne soit bloquante dans le thread principal, au risque de ralentir tout le monde. Dans la vraie vie ce n’est pas toujours possible, ne serait-ce que pour accéder à un module de persistance. Dans ce cas, Vert.x s’inspire du pattern Actor, popularisé par Erlang et Scala entre autres (Akka a été une des sources d’inspiration de Vert.x). Un message est publié dans un bus d’évènements, ce message sera traité par un worker, géré dans un pool de threads dédié. Un handler traitera la réponse dans le thread principal. Dans la suite de cet article nous allons balayer les différentes fonctionnalités importantes. Il ne s’agit pas réellement d’un tutorial, pour cela le site (http://vertx.io) de Vert.x est très bien fait, mais plutôt d’une visite guidée.
Un serveur web
Ce qu’on aime dans Vert.x, c’est sa simplicité. On a besoin d’un serveur http, il suffit d’une ligne de code (tous les exemples de cet article seront en groovy) :
vertx.createHttpServer().requestHandler { request -> println “hello world”}.listen(8080,”localhost”)
Rapidement on aura envie d’introduire la notion de contrôleur avec un mécanisme de routage. Vert.x fournit un route matcher :
def routeMatcher = new RouteMatcher() routeMatcher.get(“/path/:parameter/”) { req -> req.response.end “parameter is ${req.params[‘parameter’]}” }
L’ensemble des méthodes http sont disponibles (put, get, delete…). Nous voici donc avec un serveur REST à moindre frais. Vert.x gère également https. Comme tout bon serveur moderne, Vert.x est capable de créer des websockets :
vertx.createHttpServer().webSocketHandler { ws -> println “connected”}.listen(8080,”localhost”)
Il existe une librairie javascript, vertxbus.js, qui se combine avec la librairie SockJS pour créer une connexion websocket. SockJS permet de defaulter sur un autre protocole si le navigateur ne supporte pas les websockets. Ce qui est intéressant ici c’est la réutilisation du bus d’évènements afin de construire un pont entre le client et le serveur. Le pont est déclaré côté serveur :
def server = vertx.createHttpServer() def config = [“prefix”: “/eventbus”] vertx.createSockJSServer(server).bridge(config, [],[]) server.listen(8080,”localhost”)
Le préfixe contenu dans config définit l’url utilisée côté client pour établir la connexion. Les autres paramètres de la méthode bridge, ici des tableaux vides, permettent de filtrer les requêtes entrantes et sortantes pour plus de sécurité. Côté client :
<script type="text/javascript" src="/js/sockjs-0.2.1.min.js"></script> <script type="text/javascript" src='/js/vertxbus.js'></script> <script type="text/javascript"> var evtBus = new vertx.EventBus(window.location.protocol + '//' + window.location.hostname + ':' + window.location.port + '/eventbus'); evtBus.onclose = function() {console.log('event bus closed')} evtBus.onopen = function() { console.log('event bus opened'); evtBus.registerHandler('some-address', function(message) { console.log(‘message received’); }); evtBus.send(‘some-address’, {a_value: “a_key”}); } </script>
L’élément le plus important à noter ici est la communication par le bus d’évènements. Il s’agit du même bus d’évènements utilisé côté serveur, apportant une grande cohérence au modèle. Le client javascript peut donc émettre un message qui sera consommé par un module côté serveur, par exemple un module de persistance, ou bien un autre client javascript, sans avoir même besoin de définir un contrôleur côté serveur. C’est la beauté du modèle.
Scalabilité
Le bus d’évènements permet d’échanger des données d’un verticle à l’autre, essentiellement pour évacuer des tâches longues vers les workers. Cependant, il existe un certain nombre de situations où il serait plus pratique de partager des données. Pour cela on dispose de maps ou de sets partagés :
vertx.sharedData.getMap(‘my.shared.map’) vertx.sharedData.getSet(‘my.shared.set’)
Ces données seront obligatoirement des objets immutables ou des objets Buffer (voir plus bas). Ces données seront donc accessibles par toutes les instances de verticle ainsi que tous les noeuds d’un cluster. Tout d’abord, pour démarrer un serveur Vert.x, il faut utiliser l’exécutable vertx :
vertx run mon-script.groovy
Au sein du script principal, on va déployer un certain nombre de verticles ou de modules (qu’on peut considérer comme des verticles spécialisés) :
container.deployVerticle(‘un-job.groovy’) container.deployModule(‘un-module.groovy’)
Comme évoqué plus tôt, chaque verticle peut être instancié plusieurs fois, il suffit juste pour ça de :
- soit le préciser en ligne de commande (pour le verticle principal) :
vertx run mon-script.groovy -instances 10
- soit l’écrire programmatiquement :
container.deployVerticle(‘un-job.groovy’,10)
Si par exemple un verticle correspond à un serveur http sur le port 8080, toutes les instances écouteront sur ce même port. Vert.x s’occupe de faire le load balancing entre les différentes instances. On peut également aller plus loin en créant un cluster de serveurs Vert.x.
vertx run mon-script.groovy -cluster -cluster-host host1 -cluster-port 1234 vertx run mon-script.groovy -cluster -cluster-host host2 -cluster-port 1234
Le port défini ici permet à chaque nœud de se mettre en écoute des autres nœuds ; on peut ainsi héberger plusieurs nœuds sur un même serveur ou sur des serveurs différents. Qu’il s’agisse des instances ou des nœuds du cluster, d’un point de vue logique, il s’agira du même bus d’évènements et des mêmes données partagées. On a une transparence sur la scalabilité sous-jacente. Pour le mode cluster, c’est Hazelcast qui est utilisé. Une connexion TCP est établie entre les différents nœuds. Il est important de le savoir car une limitation d’un verticle défini en tant que worker est qu’il n’est justement pas possible d’établir une connexion réseau.
Une glue réseau
Il est également possible de créer un serveur tcp :
vertx.createNetServer().listen(1234,”localhost”)
On associe un handler pour chaque nouvelle connexion :
server.connectHandler { sock -> println “new client connected”}
La variable sock instancie l’objet NetSocket. Afin de lire le contenu de la socket, on définit un dataHandler :
sock.dataHandler { buffer -> println “${buffer.length}” }
Le buffer instancie l’objet Buffer, fournit par Vert.x, qui est un array java extensible. NetSocket implémente à la fois ReadStream et WriteStream pour lire et écrire dans la socket. Vert.x ajoute un certain nombre de fonctionnalités intéressantes. Par exemple il est possible de faire un pipe entre deux sockets :
Pump.createPump(socket1, socket2).start()
Lorsqu’on écrit dans une socket, si à l’autre bout le client est relativement lent, le buffer interne de la socket peut être rempli et on risque de perdre des informations. Il est possible de tester l’état du buffer :
sock.writeQueueFull()
De la même façon si le lecteur lit trop vite, on peut lui dire de faire une pause puis de redémarrer :
sock.pause() sock.resume()
La méthode pause stoppe l’appel au dataHandler. Dans le cas par exemple d’une communication bidirectionnelle (A écrit à B qui répond à A…), on peut utiliser drainHandler sur le WriteStream qui sera appelé quand le buffer ne sera plus plein et ainsi reprendre la communication :
sock.drainHandler { sock.resume() }
Enfin pour détecter la fin de la transmission (EOF), on dispose de endHandler. Ces contrôles sont aussi utilisables pour les transmissions http, websocket ou fichier.
Un facteur dans ma maison
Avec la notion de bus d’évènements, la similitude avec un Middleware Oriented Message (MOM) est évidente. Vert.x fait par exemple la distinction entre un envoi point à point et du publish/subscribe :
vertx.EventBus.send(“adresse”, “mon message”) vertx.EventBus.publish(“adresse”, “mon message”)
Dans le premier cas, au plus un handler récupérera le message. Dans le deuxième cas, tous les handlers en écoute à cette adresse recevront le message. Il sera également possible de faire du request/reply :
vertx.EventBus.registerHandler(“adresse”, { msg -> msg.reply “ma réponse” })
Par contre l’analogie avec un MOM s’arrête là. Un message contient juste un payload et une adresse de réplication mais pas de header par exemple. Il n’existe pas de mécanisme d’acquittement, de persistance ou même de notion de file de messages. Il s’agit plus d’un outil de messaging intra-process qui se révèle être très pratique.
Mais encore…
Tout ça est bien joli mais quels sont les cas d’utilisation ? Assez naturellement tous les cas adressés par Node.js sont candidats. Les points forts de Vert.x sont :
- capacité d’encaisser beaucoup de requêtes en parallèle grâce au modèle reactor. Bien sûr cela demande à être testé en fonction des utilisations.
- possibilité de scaler en fonction du nombre de processeurs et de scaler sur plusieurs serveurs ;
- très bonne productivité surtout si on s’appuie sur un langage dynamique.
On a dégagé quelques exemples qui nous semblent pertinents :
- proxy/reverse proxy/cache : il ne s’agit pas bien sûr de concurrencer les nombreuses solutions existantes et déjà très performantes. Par contre dans certaines situations où le métier peut être un peu compliqué, Vert.x apporte une solution simple pour prendre la main sur la distribution des requêtes ou si on veut mettre en place un cache de second niveau.
- bouchon : dans le même ordre d’idée que le point précédent, Vert.x peut servir à remplacer un serveur d’application pour des tests d’intégration ou de performance. On pourra simuler des délais en fonction des situations.
- transformation/sérialiseur/désérialiseur : on a vu que Vert.x permet un contrôle assez fin du flux. Vert.x peut faire le lien entre deux applications fonctionnant sur des protocoles réseaux ou de sérialisations différents, un peu à la manière de Flume.
- worker : il arrive qu’un projet ait besoin d’exécuter des tâches longues qui n’ont pas besoin d’être synchrones avec le flux courant, par exemple pour un envoi de mail de notification, la génération de pdf, l’écriture sur disque des données ou l’envoi sur le réseau. Vert.x peut très bien répondre à ce besoin. Néanmoins il faut faire attention au niveau d’intégrité voulu.
- monitoring : Vert.x permet de construire très rapidement une console web d’administration ou de monitoring qu’on pourra faire évoluer ou adapter sans connaissance approfondie sur les applications web. Les websockets sont un petit plus, par exemple pour mettre à jour des graphes d’indicateurs.
Conclusion
Avant tout, Vert.x nous aura impressionnés par la richesse de ses fonctionnalités et la cohérence de son modèle. Certes rien n’est vraiment nouveau dans ce framework, chaque brique est une réutilisation de frameworks ou de concepts déjà existants. Mais mis bout à bout, Vert.x propose un outil puissant et très productif. Au sein de nos journées d’échange Xebia (XKE), nous avons proposé un Hands-on (disponible sur github ici) afin de se familiariser avec Vert.x. De notre retour d’expérience, nous avons été confrontés à des problèmes de compatibilité mineurs entre la version 1.2.3 et la version 1.3.0. Nous avons également utilisé le module vertx-mongo-persistor à qui il manque encore un peu trop de fonctionnalités pour être vraiment production ready. Ces problèmes peuvent être attribués à la relative jeunesse de l’application.