Devoxx 2012 – Vert.x

Au milieu d’une journée plutôt orientée Web et Google, j’ai assisté à une présentation de Vert.x par Tim Fox (@timfox) de VmWare et ancien de JBoss ayant mis au point HornetQ.

Vert.x est un framework applicatif pour coder de manière asynchrone et qui est basé sur la JVM. Il a l’avantage d’être léger, simple et polyglotte, mais nous reviendrons plus en détails sur ces points dans l’article.

Qu’est ce que c’est ?

Vert.x se présente comme une plateforme applicative multi-usage. Vous pouvez l’utiliser pour faire du web, comme c’est le cas de la majorité des utilisateurs aujourd’hui, mais pas seulement.

Comme indiqué en introduction, le framework fonctionne sur la JVM et est polyglotte : vous pouvez coder en Java, Groovy, Javascript, CoffeeScript Ruby, Python. L’ajout de Scala est en cours de réalisation et Clojure devrait être le prochain langage sur la liste.

Le support de Javascript, Ruby et Python se fait via les implémentations Java que sont Rhino, JRuby et Jython.

L’objectif du framework est d’offrir un moyen simple (mais pas simpliste) et fun de coder une application asynchrone. Il est basé sur des principes assez proches de node.js et Akka. D’ailleurs Tim nous a avoué qu’il avait démarré le projet Vert.x car il était intéressé par node.js, mais qu’il pensait faire mieux en utilisant comme base la JVM.

Au niveau licences, Vert.x est en open source via la licence ASL 2.0 et le code source est disponible sur GitHub. Pour le moment l’équipe principale est petite, mais le projet attire de plus en plus l’attention (8ème projet Java le plus suivi sur GitHub) et le nombre de contributeurs augmente rapidement.

Développement Asynchrone

De manière générale, ce qu’on appelle le développement asynchrone est un peu différent de la gestion du multithreading et de la concurrence que l’on trouve dans les langages comme le Java, C, C#, etc.

Dans le modèle “classique”, vous créez vos threads vous-même et devez gérer plusieurs éléments pour éviter les problèmes au niveau du partage de mémoire, dans le cas où les données sont utilisées par plusieurs threads.

Vous allez rencontrer des problèmes types situation de compétition (race condition), interblocage (deadlocks) et de famine (livelocks) par exemple.

Pour résoudre ces problèmes, il faut mettre en place des mécanismes de synchronisation, de mutex, de lock, etc. Ces mécanismes sont en général assez coûteux en mémoire et il est recommandé de les éviter au maximum.

Une autre manière de voir les choses est de définir des entités dont les traitements sont indépendants les uns des autres et dont le code est isolé. Comme le code est isolé, il n’y a pas de partage de mémoire entre les threads et vous pouvez créer un thread par entité. Si vous avez besoin d’échanger des informations entre ces entités ou de déclencher un traitement, vous utilisez un système de messages ou d’évènements selon les besoins.

Le modèle de ce type le plus connu est celui des acteurs utilisé en Erlang et en Scala.

Images de Typesafe.com

Le gros avantage de cette manière de faire est qu’elle permet d’avoir beaucoup de threads en parallèle. De plus, associée à une file de message ou à un bus d’évènements, il est facile de réaliser de nombreux traitements avec un nombre limité de threads, car ceux-ci passent peu de temps à attendre.

L’inconvénient est que, pour fonctionner, il faut que les données transmises d’une entité à l’autre soient immuables et que le temps d’exécution de chaque traitement soit le plus rapide possible.

Et en Vert.x ça donne quoi ?

En Vert.x les entités autonomes sont appelées des “vertical”. Chaque verticale exécute donc un traitement isolé qui se déclenche à la réception d’un évènement.

En effet Vert.x est conçu autour d’un bus d’évènement particulièrement efficace basé sur le patron “reacteur” qui est notamment utilisé par node.js. Pour faire simple, il s’agit d’un thread d’écoute des évènements qui appelle la bonne entité (ici un vertical) lorsqu’un évènement est déclenché.

C’est très efficace mais cela implique que les traitements soient très rapides pour ne pas bloquer le thread d’écoute, car ils sont appelés de manière synchrone.

En effet, chaque instance de Vert.x est par nature monothread pour éviter les problèmes de mémoire, conformément au patron réacteur.

Dans Vert.x ce patron est toutefois dérivé en ce que Tim appelle le “multi-reactor”. C’est le même principe mais avec plusieurs réacteurs en parallèle (par défaut un par core CPU) afin d’augmenter le nombre de traitements en parallèle.

Ce n’est donc pas une solution parfaite qui répond à tous les besoins, car vos verticals doivent être relativement rapides pour assurer un bon résultat. Nous verrons dans la partie montée en charge comment ce modèle prend tout son sens.

Cependant, tous les projets ont des parties qui nécessitent de bloquer l’exécution pendant un temps beaucoup trop long pour qu’il soit possible de bloquer le thread gérant les évènements (accès disque, requête à un serveur distant, calcul lourd, etc.). Pour ce genre de cas, il est possible de définir des “worker vertical” qui s’exécuteront en tâche de fond en utilisant un pool de thread réservé à cet effet.

Il faut cependant être vigilant et ne pas multiplier ce type de vertical, car ils ne supportent pas très bien la montée en charge en cas de nombreuses requêtes.

Les APIs fournies par Vert.x et utilisation

Le core de Vert.x fourni quelques APIs de base :

  • clients et serveurs TCP/SSL
  • clients et serveurs HTTP/HTTPS
  • clients et serveurs WebSockets
  • Gestion des évènements via le bus d’évènement
  • Actions planifiées via timer
  • Buffers
  • Flow control
  • Accès au système de fichier
  • Map et Set partagés
  • Logging
  • Gestion des fichiers de configuration
  • Ecriture d’un serveur SockJS
  • Déployer et détruire les verticals

Ce core est volontairement restreint et a vocation à le rester. Le reste est géré par un système de modules permettant d’étendre Vert.x, de déclarer des dépendances et de déployer ses modules sur un repository central accessible à tous.

Vous pouvez donc créer vos propres librairies et les réutiliser dans d’autres projets ou utiliser des modules mis à disposition par la communauté.

Pour utiliser Vert.x celui-ci met à disposition une ligne de commande permettant d’exécuter votre code directement :
vertx run server.js

En l’occurrence server.js est un simple fichier javascript contenant l’instanciation d’un serveur HTTP  qui sert des fichiers et redirige “/” vers index.html :

load('vertx.js')

vertx.createHttpServer().requestHandler(function(req) {
    var file = req.path === '/' ? 'index.html' : req.path;
    req.response.sendFile('webroot/' + file);
}).listen(8080)

 Déclarer un vertical

Un vertical doit être indépendant, il est donc logiquement composé d’un main (ou d’une classe principale en Java), de scripts annexes utilisés par le main, de ressources et de dépendances sous forme de Jars.

Chaque vertical peut être écrit dans un langage différent des autres.

Une application de base peut ne contenir qu’un seul vertical, puis quand elle se complexifie, en voir d’autres apparaître. Ceux-ci communiqueront par évènements au fur et à mesure des besoins.

Un vertical en Java est une simple classe Java étendant Vertical et implémentant la méthode start() :

public class Server extends Verticle {

  public void start() {
    // Do something...
  }
}

Dans votre vertical, vous avez ensuite accès aux api du core de Vert.x via l’objet vertx. Par exemple, pour créer un serveur écoutant sur le port 1234 :

public void start() {
    vertx.createNetServer().connectHandler(new Handler<NetSocket>() {
      public void handle(final NetSocket socket) {
        Pump.createPump(socket, socket).start();
      }
    }).listen(1234);
  }

Plutôt simple même pour un langage par nature assez verbeux comme Java.

La montée en charge

Le but de Vert.x est de pouvoir gérer beaucoup de connexions en parallèle. Or nous l’avons vu, le patron réacteur limite le nombre de threads et donc de traitements possibles dans une instance de Vert.x.

Si l’on veut monter en charge, il suffit donc de lancer plusieurs instances de Vert.x. Le bus de données est distribué et est donc capable de transmettre vos évènements d’une instance à l’autre, y compris si elles sont dans des JVM séparées et/ou sur des machines différentes. Évidemment, une configuration est nécessaire pour que le cluster se mette en place et puisse communiquer.

Petit bonus sympathique, si vous lancez deux instances d’un vertical créant un serveur sur un port identique, Vert.x va répartir la charge de l’ensemble automatiquement pour vous.

De plus, le serveur HTTP se base sur Netty et est donc extrêmement efficace pour gérer de nombreuses connexions simultanées.

Le bus d’évènement

Le point névralgique de Vert.x est son bus d’évènements distribué. Celui-ci est basé sur Hazelcast. et permet de communiquer de plusieurs manières :

Autre point expliquant pourquoi Vert.x est très intéressant pour le web, est que le bus permet également de communiquer avec le navigateur directement via Websocket, et considère votre partie web comme un vertical auquel il envoie des évènements.
L’implémentation côté client est basée sur sockJS qui est un équivalent un peu moins connu de socket.io et permet de simuler les Websockets sur les navigateurs ne les supportant pas.

Cela permet de faire un site web moderne et “temps réel” très facilement. Il suffit d’ajouter la librairie javascript de Vert.x pour se simplifier la vie ou d’utiliser directement les Websockets pour écouter les évènements.

Un conseil que donne Tim est d’utiliser le format JSON pour transmettre des données entre verticals, afin de faciliter les échanges entre entités réalisés dans des langages différents.

La suite à venir

La première priorité est l’ajout de Scala et Clojure dans les langages supportés. Des travaux sont déjà en cours donc cela devrait arriver rapidement.

Une autre priorité est une meilleure administration des clusters via une console, et éventuellement une interface de gestion plus avancée.

Ensuite, un support des IDE et des outils de test serait vraiment un plus, l’équipe en est consciente et travaille sur le sujet également.

D’autres sujets sont à l’étude sur l’intégration d’un système de promises efficace. Des expérimentations ont déjà été faites, mais sans résultat concluant, et des réflexions sont en cours pour voir comment gérer ce type de problèmes. Ce type de fonctionnalité pose notamment de gros problèmes de montée en charge.

Conclusion

Le framework a l’air très intéressant avec une approche rafraîchissante de la programmation concurrente qui sort un peu des habitudes.

Le tout est beaucoup moins poussé que Akka, mais offre une alternative à node.js sur la JVM pour faire des serveurs web redoutablement efficaces et plus proches du modèle web basé sur des évènements, le tout en gardant une empreinte mémoire assez réduite.

Cependant, tout n’est pas parfait car, comme pour node.js, ce type de développement est extrêmement difficile à déboguer. En effet, ici la pile d’exécution repose sur des évènements qui se déclenchent en parallèle, il n’y a pas d’exécution séquentielle des verticals.

Dans tous les cas, il devient de plus en plus important de connaître ce type de développement. En effet, il va probablement devenir prépondérant dans les traitements hautement parallélisés, que ce soit pour les serveurs web qui s’y prêtent très bien, mais également pour des traitements coté serveur.

Nombre de vue : 162

COMMENTAIRES 2 commentaires

  1. Petite correction, Tim a bossé sur HornetQ chez JBoss, pas RabitMQ.

  2. Mathieu PARISOT dit :

    Oups en effet, je me suis pris les pieds dans le tapis entre les solutions VMWare et JBoss…

    Merci !

AJOUTER UN COMMENTAIRE