Pourquoi donc changer ?

À ses débuts, Doctrine.fr était sous Ruby on Rails. Plus par habitude que par choix, car nous étions tous des amateurs de Rails. Un jour, Tariq KRIM nous a dit : « Les gars, faut passer à Node.js ! »

Node.js est certes à la mode depuis quelques années comme le prouve Google Trends, mais pour autant, une transition valait-elle le coup ?

                         Google Trends : Node.js dépasse Ruby on Rails en 2013.

Tout d’abord, tout le monde connaît (un peu) de JavaScript. Si vous avez Angular, Backbone ou Ember en front, vous communiquez avec votre serveur en JSON. Ainsi, avoir Node.js sur le serveur évite des conversions permanentes. Un développeur front pourra plus facilement passer en back, et inversement, utile dans de petites équipes. Les employeurs ne s’y sont pas trompés : depuis 2015, les développeurs Node.js sont plus recherchés que les développeurs Ruby on Rails sur Indeed.

               Offres d’emploi sur Indeed : Node.js a dépassé Ruby on Rails en 2015.

Un des défauts de Node.js (né en 2009), par rapport à Ruby on Rails (2005), serait sa jeunesse. Néanmoins, la communauté Node est extrêmement active. Résultat : le merveilleux dépôt npm compte depuis 2014 plus de modules que Rubygems.

Et sur Stack Overflow, Node.js est depuis novembre 2015 plus débattu que Rails :

         Stack Overflow : Node.js dépasse Rails fin 2015 en termes de popularité.

L’avantage de Node.js est surtout sa rapidité : le moteur V8, développé par Google pour Chrome, compile et exécute le JavaScript très rapidement. Surtout, le fonctionnement de Node.js est non bloquant (ou asynchrone), ce qui permet de réaliser un maximum d’opérations en parallèle et accélère le chargement des pages.

Ces dernières années, de nombreuses entreprises ont migré vers Node.js :

Forts de ces enseignements, nous nous sommes dit qu’il serait intéressant de benchmarker Node.js, d’abord sur une partie du site puis de migrer totalement si l’essai était concluant.

Le test de performances

Pour mieux comprendre, quand tout était écrit en Rails, l’application de Doctrine.fr était séparée en 2 serveurs : la partie search qui répond aux requêtes AJAX du navigateur pour les recherches, et celle qui envoie au client toutes les autres pages (HTML, images, scripts, CSS…). Comme les requêtes pour afficher les pages étaient parfois longues en Rails cela permettait aux recherches d’être rapides.

L’idée était dans un premier temps de benchmarker la partie search. Choix naturel, car cette partie ne fait que répondre aux requêtes AJAX en JSON à la partie écrite en AngularJS sur la machine de l’utilisateur.

Résultats du benchmark

Après un jour de travail à la réécriture du search en Node.js, nous avons comparé les performances de Node.js et Rails avec un serveur ayant un CPU et 1 GB de RAM. Avec 50 recherches par seconde, Node.js et Rails arrivent tous les deux à tenir la charge, Rails utilise alors 50% du CPU et Node.js seulement 30%. Mais il est impossible pour Rails de traiter plus de recherches, alors qu’avec Node.js on peut monter à 120 par seconde (46% du CPU utilisé). Au-delà, Node.js plante car il n’y a plus assez de mémoire. Dans tous les cas, le facteur limitant est la RAM et non le le CPU.

Ainsi, avec des machines identiques, Node.js permettrait de servir deux fois plus d’utilisateurs. À l’échelle, cela signifie une division par deux des coûts.

Comment fonctionne Node.js ?

Si on peut économiser autant de RAM c’est grâce au fonctionnement non bloquant de Node.js, bien différent des langages comme Rails.

Dans Rails, quand vous consultez une décision de justice sur notre site, le programme effectue par exemple les opérations suivantes dans l’ordre :

  1. Chercher la décision dans la base de données ;
  2. Chercher les commentaires de la décision dans la base de données ;
  3. Traiter le contenu de la décision ;
  4. Générer la page web à afficher.

Pour éviter de bloquer le serveur pendant les phases d’attente (entre 1 et 2 et entre 2 et 3), Rails lance en parallèle différents threads permettant ainsi de répondre à d’autres utilisateurs pendant ces phases d’attente. Mais chaque thread nécessite une quantité importante de RAM.

Node.js procède différemment, vous ne dites pas “fais ceci, puis cela, puis cela” mais plutôt “fais ceci et voilà ce que tu devras faire quand ce sera fini, mais en attendant je n’ai plus rien à te demander”. Cette petite différence, en apparence, signifie que Node.js sait explicitement la différence entre les phases “calcul” (comme 3 et 4) et les phases “attente” (comme 1 et 2). Le programme ci-dessus se réécrit de la façon suivante :

  1. Demander la décision à la base de données et quand elle sera disponible, faire tout ce qui suit :
  2. En parallèle : chercher les commentaires de la décision et traiter le contenu de la décision ;
  3. Générer la page web une fois les deux opérations précédentes terminées.

Pour l’étape 2, Node.js triche et ne réalise pas vraiment l’opération en parallèle. En réalité, il demande les commentaires de la décision à la base de données, voit alors que “là il n’y a plus qu’à attendre” et traite ensuite le contenu car on a indiqué dans le programme que les deux pouvaient être faits en parallèle. Une fois cela fait, il voit qu’il faut que les deux opérations soient terminées et qu’on attende encore la réponse de la base de données (en général plus lente que le traitement du contenu). Node.js peut donc “faire autre chose” en attendant, par exemple traiter une recherche.

Cela a deux avantages :

  • Le chargement est plus rapide pour l’utilisateur ;
  • On utilise moins de RAM car garder en mémoire “ce qu’il faudra faire quand l’opération est finie” est moins gourmand en RAM qu’un thread séparé.

Réécriture en Node.js : observations diverses

Nous avons donc décidé de réécrire la totalité du site en Node.js. Lors de la réécriture, j’ai fait les observations suivantes, qui peuvent être utiles si vous migrez aussi de Rails à Node.js :

  • La gestion des erreurs est totalement différente, on ne lève pas d’exceptions à tout bout de champ. À la place, on passe les erreurs au callback. La syntaxe “if (error) return callback(error)” devient un classique, présent partout dans le code.
  • Pour réécrire les vues en HTML, la transition Rails vers Node est aisée car il est possible d’utiliser EJS, dont la syntaxe <%= code %> est identique à celle de Rails.
  • Le framework ExpressJS permet d’organiser son application plus librement que Rails. On peut ainsi facilement découper en plusieurs fichiers n’importe quel programme, alors que l’utilisation des modules/concerns de Rails est parfois fastidieuse. Cela signifie par contre qu’il faut planifier soi-même l’organisation de son application en différents dossiers.
  • Node.js empêche de mettre le code des contrôleurs dans les vues (pratique fréquente en PHP mais découragée en Ruby on Rails). En fait, on ne peut pas utiliser les callbacks dans la vue, ce qui empêche de faire des requêtes à la base de données par exemple. Ici, Node.js impose donc une plus grande discipline.
  • Dans Rails, la protection contre les attaques CSRF est gérée automatiquement. Avec Node.js, la parade est d’abandonner les requêtes POST avec paramètres classiques tels qu’envoyées par des formulaires sans JavaScript. La protection contre les CSRF devient alors inutile. À la place, on n’utilise plus que du AJAX en JSON. Il suffit d’encoder les requêtes POST en JSON côté client et de laisser le serveur analyser le JSON (tout en désactivant bodyParser.urlencoded pour qu’on ne puisse plus utiliser les paramètres classiques).

Conclusions sur notre transition

Grâce au passage en Node.js nous avons gagné :

  • Une division par deux des coûts fixes (plus besoin de séparer les deux parties search et web) ;
  • Une division par deux des coûts variables ;
  • Un affichage deux fois plus rapide pour l’utilisateur ;
  • La possibilité de facilement choisir d’exécuter du code côté client ou serveur (JavaScript dans les deux cas).

Essayez Doctrine !