Jérémy Le Piolet - Blog

Petit retour d'expérience sur Node.js

Cet article a été mis à jour depuis sa première publication.

On trouve tellement d’informations et d’articles sur Node.Js que ce n’est pas toujours très simple de se faire une idée. Certains y voient le messie tandis que d’autres sont plutôt proches de l’Antéchrist. Comme souvent, la vérité est quelque part entre les deux.

J’ai commencé à utiliser Node.Js en 2011. C’était la douce époque des PoC, des prototypes et des sides projects rigolos en tout genre. La plateforme est ensuite montée en puissance, le nombre de ressources, de modules, d’usages a complètement explosé au point de faire de la communauté Node.Js l’une des premières communauté informatique au monde.

J’ai suivi cette progression, parfois en avance, parfois en retard sans complètement y basculer (je suis développeur front, pas back :) ). J’ai acquis une certaine connaissance de la plateforme que je livre ici.

Note : la première version de cet article reprenait les différentes caractéristiques de Node que j’analysais avec mon expérience. Outre le côté pompeux, c’était souvent incomplet et on trouve de meilleures ressources sur le net. Cette réécriture se concentre donc sur mes différentes expériences et suppose donc à minima de savoir à quoi sert Node.Js.

La fois où j’ai découvert Node.Js

Contexte

Mon premier projet Node.Js date de 2011. Je sortais de l’école, je ne connaissais pas la technologie et n’en avait même jamais entendu parlé. On m’avait demandé de faire un petit PoC dessus, pour l’évaluer.

Ce PoC, c’était de la collaboration à distance entre plusieurs personnes. En gros, il s’agissait d’une interface web que les utilisateurs manipulaient et dont les modifications étaient répercutées à tous, indépendamment de leur localisation. L’interface web était donc du javascript et la synchro était assurée par un serveur Node.Js via long polling HTTP.

Diagramme illsutratf du contexte de long polling. Une requ^ete est envoyée au serveur qui attend les données avant de renvoyer la réponse.

Principe du long polling - CC BySa 4.0 @ Mrharispe

Aujourd’hui, le sujet est extrêmement simple et répandu. Si l’on exclut les solutions de synchronisation comme Firebase, ce serait typiquement un sujet de TP d’une quelconque école d’ingénieur. On utiliserait certainement des websockets pour la synchronisation au lieu du long-polling avec une petite librairie qui va bien pour la partage des données et on en parle plus.

Mais à l’époque, c’était plus complexe. L’asynchrone se faisait encore par des callbacks (les promesses étaient quelque part dans la roadmap), Npm n’était pas encore inclu par défaut avec Node et le nombre de paquets disponibles était encore humainement comptabilisable, on était vraiment sur les premières versions, le défrichage, on mettait bien les mains dans le cambouis. Par exemple, j’ai utilisé les API bas niveau de Node pour créer et configurer directemet le serveur HTTP, chose que je n’ai presque plus fait depuis (merci Express et autres frameworks).

Pourtant, le développement du prototype a été assez rapide et sa mise en production tout autant. Et ça a même tourné pendant plusieurs années avant que le serveur ne soit définitivement coupé il y a 3-4 ans. J’ai depuis récupéré le code source et j’ai pu le faire tourner sur une version beaucoup plus récente de Node et là, surprise : ça fonctionnait encore sans problèmes ! Je dois avouer que j’en était le premier surpris.

Ce que j’ai appris

Cette première expérience avec Node m’a appris plusieurs choses :

  • Node, c’est super puissant
  • On peut mettre rapidement en place un server HTTP fonctionnel avec une réactivité et une rapidité inégalable par rapport à un serveur Apache. En pratique, c’était moins de 10 lignes de code (hors traitement propre à l’application).
  • Un nombre de nouveaux usages impressionnant s’ouvre pour le web avec la démocratisation du “temps réel”. C’est vraiment très très simple de mettre en place de la synchronisation et des échanges quasi-instantannés entre deux interfaces web, les possibilités offertes sont énormes.
  • La plateforme est pérenne et assure la rétro-compatibilité. Parce que mon code était développé sous une version 0.6 et continue de fonctionner sur une 12-13 presque 7-8 ans après. Certes, on était sur du code ultra basique mais les API standards étaient déjà stables. L’équipe Node n’a pas tout cassé entre temps (alors qu’ils auraient pu), ce qui est vraiment appréciable.

Les fois où j’ai connecté pleins d’équipements entre eux

Contexte

Sur plusieurs projets, il m’est arrivé de travailler sur des interfaces utilisateurs qui aggrégeaient des données de plusieurs équipements. Cela variait selon les projets mais la structure était assez similaire à chaque fois :

  • Une interface web qui affichait des données, réagissait aux données de capteurs ou commandait des actions sur des équipements
  • Des équipements IoT divers et variés qui remontaient des données ou déclenchaient des actions. Cela allait de simples beacons bluethooth à des lampes connectées en passant par des capteurs de températures ou de Co2
  • Des équipements mobiles complexes comme un smartphone ou une montre connectée.

Dans les différents cas, la structure centrale était un serveur Node.Js qui aggrégeait les données et qui les redistribuait. La communication s’adaptait selon les terminaux et possibilités offertes, pouvant aller de la simple API Rest pour envoyer des ordres à la Websocket qui se chargeait de faire la communication entre les parties et assurer la réactivité du tout.

Le nombre d’équipements connectés simultanément était variable mais impliquait toujours une communication à minima tripartite. Je ne suis pas toujours intervenu sur tous les modules Node mais j’en avais une assez bonne vision.

Et il faut bien avouer que ça fonctionne vraiment très bien. C’est assez rapide à mettre en place (socket.io permet d’avoir de bons résultats en très peu de temps) et relativement stable/fiable. Même en ayant connécté plus d’une douzaine d’équipements différents qui envoyaient des données quasi-continuellement, le serveur Node.Js tenait extrêmement bien la charge sans configuration ni optimisation particulière. Les réactions étaient quasi-instantannées, c’était vraiment temps réel pour des résultats assez bluffant.

Ce que j’ai appris

Là encore, ces projets IoT m’ont permis d’apprendre plusieurs choses sur la technologie Node :

  • Node.Js encaisse facilement des flux de données continus en provenance de différentes sources sans broncher et sans configuration particulière.
  • C’est particulièrement bien adapté dans la gestion d’équipement IoT qui envoient régulièrement des micros-données courtes à traiter rapidement.
  • Il y a des nombreuses possibilités de connexion avec des équipements connectés. Si nous avons surtout utilisé des websockets, c’est assez facile de faire de trouver des modules qui exploitent d’autres protocoles dans les repos Npm.

A noter toutefois que ces différents projets ont été réalisés dans le cadre de PoC ou autre prototypage. Je n’ai pas eu l’occasion personnellement de mettre ça en place sur de vrais projets de production, même si les conditions d’éxecution pour certains de ces projets étaient assez contraignantes (environnement hostile, connexion internet aléatoire, etc.).

Si je ne doute pas de la robustesse et des capacités d’encaissement du serveur, j’ai des doutes sur la sécurité de tels dispositifs. C’est une question assez rarement visible dans les documentations des différents modules de communication utilisé. Et vu le nombre de fuites de données et autres soucis du genre que l’on découvre régulièrement par rapport aux équipements IoT, je pense qu’il faut rester prudent dans un cadre de production. Je pense que ça doit se mettre en place mais je n’ai pas souvenir d’avoir facilement trouvé de la documentation sur ce cadre, c’est donc un point à garder à l’esprit si on se lance dans l’aventure.

La fois où j’ai mis à jour ma config générée par Vue Cli

Contexte

Node.Js est devenu en peu de temps l’outil de référence pour tout ce qui touche au front-end. Il est quasiment impossible de passer à côté : que ce soit pour la minification de css, pour du lint ou pour générer des projets javascript, Node est partout.

Vue.js propose ainsi un outil de build reposant sur Node (Vue-cli ) pour générer un projet de base, le compiler et le servir (entre-autre). C’est vraiment très pratique et ça permet de commencer de suite un projet sans perdre 3 jours en configuration. Je m’en suis donc tout naturellement servi pour démarrer un nouveau projet sur Vue.Js, un “gros” prototype.

Le projet s’est doucement mis en place et, je ne sais pas vraiment ce qui m’a pris ce jour-là, je me suis fait un petit checkup de la configuration générée, surtout d’un point de vue sécurité pour voir s’il n’y avait pas des plugins/scripts à mettre à jour. Je n’ai pas été déçu : j’avais une guirlande de noël m’indiquant que je n’étais presque à jour sur rien !

J’ai donc fait un upgrade, comme recommandé dans la documentation, et ce qui devait arriver arriva : plus rien ne fonctionnait. Nous n’avions pas beaucoup dévié de la configuration initiale proposée par Vue-Cli qui reposait sur Webpack (que je ne maîtrisais pas vraiment à l’époque), les mises à jour n’étaient pas des majeurs, ça ne devrait pas prendre trop de temps. Bien entendu, je me trompais.

J’avais oublié à quel point l’écosystème autour de Node.Js était aléatoire et non fiable. Ce n’est pas parce qu’on met à jour une version mineure que les changements sont mineurs : en l’occurence, plusieurs propriétés webpack avaient été supprimées entre temps et ça ne fonctionnait plus (j’estime la durée de vie d’un élément de configuration de webpack à quelques mois). Ajoutons à ça des incompatibilités entre les nouvelles versions des plugins utilisés et on obtient un beau sac de noeuds.

Spaghetti

La config issue de Vue-Cli - allégorie

J’ai essayé de mettre à jour mais je dois bien avouer que j’ai laissé tomber après y avoir perdu 2 jours. Dès que je corrigeais quelque chose, ça tombait d’un autre bout, dès que je basculais sur un autre module, je repartais avec d’autres problèmes de compatibilité. Et la configuration d’un nouveau projet Vue via le Cli était identique à celle initialement crée, les problèmes de mises à jour de dépendances avec. Clairement, mon manque de connaissance de webpack mais aussi des différents plugins/modules utilisés m’a handicapé, de même que la non-documentation de la logique derrière.

Depuis, l’outil a été retravaillé (on doit être une majeur au dessus et je ne sais plus combien de mineur en plus) et les mises à jour sont à priori mieux gérées, avec signalement des incompatibilités au lieu d’une simple mise à jour. Je n’ai pas eu l’occasion de retenter l’expérience depuis, j’ose espérer que ça c’est amélioré dans le bon sens.

Ce que j’ai appris

Là encore, cette expérience m’a permis d’apprendre plusieurs choses :

  • Le Cli, c’est pratique pour démarrer un nouveau projet et faire les actions courantes. Là je parle de celui de Vue mais il y a la même chose avec Angular ou React. On simplifie vraiment le boulot et évite de se poser la question des modules à embarquer
  • Si on débute ou qu’on utilise la première fois, on n’a aucune idée de ce qu’il y a derrière. Et les explications sont souvent très succinctes : on trouve la documentation pour les commandes à exécuter, rarement les choix techniques effectués par rapport à tel ou tel module.
  • La mise à jour des configurations générées doit se faire par l’outil Cli. Les configuration sont pensées et configurées au petit oignon par les équipes et se lancer dans sa mise à jour seul peut vraiment être galère (cf. point précédent).
  • Les configurations/modules ne sont pas toujours à jour et mettre à jour implique parfois de revoir toute la pile d’exécution.
  • Connaître ce qu’il y a derrière les outils de build n’est pas du temps perdu. Car si ça pête un jour, le temps gagné offert par l’utilisation du Cli sera perdu.

La fois où j’ai bloqué Node

Contexte

Un projet sur lequel j’ai été amené à travailler devait proposer une interface utilisateur pour afficher et gérer des lignes de texte. A priori rien d’exceptionnel sauf que la volumétrie était conséquente et les données faiblement couplées. La question s’est donc posée d’utiliser une base de données relationnelle ou non et le choix s’est finalement porté sur une technologie de base de données non relationnelle (MongoDb ou Redis, je ne sais plus exactement).

Comme la technologie se mariait très bien avec Node.Js, c’est donc tout naturellement Node qui a été choisit comme serveur. Le serveur avait 3 choses à faire : servir les fichiers statiques de l’interface, traiter les appels à l’API pour accéder aux données et gérer quelques traitements un peu plus lourd sur celles-ci.

L’un des traitements était l’import des données à partir de divers fichiers CSV (au passage, CSV est un très mauvais format pour l’échange de données, si tant est que l’on peut le qualifier de format vu que ce n’est pas normé). Tout naturellement, un petit bout de javascript est codé pour uploader le fichier, passer en revue chaque ligne et les insérer dans la base de données. Une bonne grosse boucle, simple mais efficace. De façon générale, Node.Js étant une technologie utilisée comme outil de build pour du front et traiter de nombreux fichiers (pour les minifications, concaténations, etc.), ça devait bien se passer.

Nous avions juste oubliés deux choses :

  • Les fichiers CSV étaient vraiment énormes. Mais vraiment. Les fichiers d’exemples représentatifs simples faisaient une centaine de Mo minimum, les plus gros flirtaient avec le Go (les données étaient ensuite tronçonnées en plusieurs sous-fichiers). 100 Mo de texte, ça fait des millions de lignes…
  • Node.Js est asynchrone avec I/O non bloquantes. Juste les I/O. C’est à dire que le reste bloque.

Quand on a testé les premiers imports, ça a bien fonctionné. Et on a commencé à jouer avec l’interface en même temps et là, on s’est rendu cmpte que ça ne répondait pas (ou alors très tardivement). C’était la première fois que nous étions confronté au problème, les projets réalisés auparavant n’avaient jamais eu ce genre de soucis. Cela a pris un peu de temps à analyser mais on a fini par comprendre : ce n’est pas fait pour les traitements longs.

Node.Js est mono-threadé et fonctionne avec une event-loop. Quand une demande arrive, elle est traité par l’event-loop et la suivante n’est traitée que lorsque la première n’est plus en cours de traitement. Plus en cours de traitement, ça veut dire ;

  • le traitement est terminé et la réponse a été envoyée
  • le traitement est en attente d’un retour d’un élément externe (base de données, système de fichiers, etc.) et s’occupe donc de la nouvelle requête en attendant (I/O non bloquantes)

Dans notre cas, le traitement des fichiers était effectué par l’Event Loop et 100 Mo de texte, ça prend du temps donc toutes les autres demandes étaient gelées tant que le traitement n’était pas fini (ou qu’il n’y avait pas un trou dans l’exécution). Cela ne nous était jamais arrivé auparavant car les données et requêtes traitées étaient courtes ou non bloquantes : des petits bouts de Json de quelques lignes qui transitent entre deux entités, c’est court, c’est simple, c’est rapide. Là, on avait un gros volume de données, c’était autre chose.

La solution a donc été de déporter ces traitements : on lançait une nouvelle instance de Node pour le faire, indépendante de la première. Cette seconde instance pouvait donc avoir son event-loop bloquée, c’était toujours la première qui traitait les demandes. Actuellement, on résoudrait surement ce genre de problématiques avec des Worker-Thread.

Ce que j’ai appris

Là encore, expérience enrichissante qui m’a permis de comprendre que :

  • Node.Js est I/O non bloquantes. Ce qui veut dire que pour le reste, ça bloque. C’est bête à dire, c’est marqué dessus mais je pense que tant que l’on n’a pas bloqué une fois son Event-Loop, on ne visualise pas ce que ça implique. J’étais personnellement habitué à un Apache qui lance des processus en plus si besoin, Node ne fonctionne pas pareil.
  • Il faut identifier les tâches longues et coûteuses et les isoler pour les déclencher de façon asynchrone.
  • Le point d’entrée du serveur doit être le plus simple possible.

La fois où j’ai failli voler du Bitcoin à mon insu

Contexte

Dans le cadre d’un projet, il fallait mettre en place un petit back-end avec Node.Js. Rien de foufou, le serveur devait faire deux choses :

  • offrir une API Rest qui allait ensuite taper sur plusieurs services différents, concaténer les résultats et les retourner (au final, cela devait se résumer à 4-5 requêtes GET, vraiment basique),
  • servir les fichiers statiques css et javascript qui permettaient l’affichage des résultats à l’utilisateur.

Vraiment basique. Pour faire simple et rapide, la solution retenue a été l’utilisation du framework Express avec quelques petits modules complémentaires pour une ou deux tâches plus complexes. Côté dépendances de developpement, un module pour la concaténation et la minification du code auquel on ajoute un ou deux petits utilitaires pratiques comme Nodemon, qui fait une sorte de Live-Reload sur le serveur. Bref, le fichier package.json ne devait pas référencer plus de 10 dépendances, on était vraiment sur quelque chose de simple.

Pour le déploiement, le client avait une plateforme cloud avec conteneur. Un peu avant une des livraisons, je fais des tests de déploiement dans un conteneur équivalent en local, ça se passe bien, aucun soucis, ça va passer tout seul. On met le script pour la livraison en place, c’est aussi configuré côté client également parce qu’on a déjà fait un test de livraison quelques semaines avant, il n’y a plus qu’à envoyer le bébé sur la bonne branche 2-3 jours plus tard.

Forcément, ça ne s’est pas bien passé : on ne dépassait même pas l’étape de téléchargement des dépendances. Aïe. L’un des modules ne semble pas passer et la plateforme cloud n’étant pas très verbeuse, je teste en local. Re-Aïe : ce qui marchait bien deux jours avant ne fonctionnait plus. Pourtant, il n’y a pas eu de modification de notre côté (je n’avais pas remis à jour mon code local, c’était le même que celui des tests précédents).

Je débguggue et je tombe sur un message curieux m’indiquant que le module event-stream avait été supprimé pour des questions de sécurité. Le message était un retour de Npm : c’était bizarre parce que ce module, je ne savais même pas qu’il était dans mon projet. J’ai donc commencé à parcourir le web à la recherche d’informations et j’ai trouvé la source du problème.

Illustration bitcoin

Bye Bye Bitcoin

Pour faire court : un module Node.Js avait été repris par une personne malveillante qui y avait rajouté dedans un code qui essayait de dérober des portefeuilles Bitcoin (pour plus de détail : https://schneider.dev/blog/event-stream-vulnerability-explained/). Et ce petit module à priori anodin était en fait très utilisé comme dépendance d’autres modules. Et effectivement dans mon cas, c’était une dépendance de dépendance de dépendance de Nodedemon, l’outil de rechargement automatique du serveur Node.

Comme c’était de la dépendance de développement, je l’ai retiré vite et tout a fonctionné comme il fallait. Comme c’était une dépendance de dev, que mon serveur ne présentait pas les pré-requis pour le code se déclenche et que, de toute façon, Nodemon n’avait jamais été installé sur le conteneur client, pas de soucis. J’ai d’ailleurs pu réintégrer NodeDemon un peu plus tard, quand il a été mis à jour.

Mais cette attaque a vraiment pris tout le monde de court : l’équipe de Npm a réagit très vite en bloquant quasi directement le module incriminé, ce qui a impacté beaucoup d’autres modules du jour au lendemain. Si à mon petit niveau, j’ai été impacté, je pense que des projets plus gros ont du avoir quelques sueurs froides car le module était vraiment beaucoup utilisé et sur des modules beaucoup plus sensible que Nodemon.

Ce que j’ai appris

Là encore, une expérience bien formatrice pour laquelle je retiens que :

  • On n’a absolument aucune idée de ce qu’il y a dans les arbres de dépendances que l’on utilise en Node.Js. Vraiment aucune. Je le savais un peu mais ça a confirmé mon point de vue. Le problème de ces arbres de dépendances, c’est qu’ils sont tellement énormes que c’est impossible de tout voir et tout connaître. Regardez un jour votre package.lock :)
  • La gestion des modules est vraiment aléatoire. Ici, c’est suite à un abandon de package que le “pirate” a pu commettre son forfait. Celui-ci, ça s’est vu mais combien d’autres modules tombent dans l’oubli, sont repris par de parfaits inconnus qui font n’importe quoi sans qu’ils soient repérés ? C’est le jeu de l’Open Source, c’est sa beauté mais aussi sa faiblesse, d’autant qu’ils sont légions les modules Node maintenu par une seule personne. Des fois, plutôt que de forker à tout va, certaines personnes devraient peut-être contribuer à des modules existants plutôt que de réinventer la roue.
  • La sécurité autour des modules Node.Js est catastrophique. La solution pour l’équipe Npm a été de couper l’accès à la version incriminée, impactant ainsi des centaines de modules, du jour au lendemain. Bon, au moins, ça se voyait qu’il y avait un problème mais c’est quand même plus que limite…

Conclusion et perspective

Au final, Node.Js est une technologie extrêmement intéressante et offrant de véritables atouts. Ce n’est d’ailleurs pas pour rien qu’elle est autant utilisée et qu’elle fascine autant les grands groupes (Paypal, Netflix, Google, etc.).

De mon expérience, Node se révèle particulièrement efficace dès lors qu’il faut gérer et traiter de nombreuses tâches courtes (remontée d’équipements IoT, traitement de multiples requêtes HTTP, traitement à la chaîne de multiples fichiers). Les demandes sont traitées rapidement avec une faible consommation de ressources, ce qui, couplé avec une architecture en micro-services, permet de tenir de fortes charges à moindre coûts. Dans ce domaine, Node excelle.

En revanche, Node se révèle un très mauvais choix dès lors qu’il faut faire du travail long. La technologie n’est pas pensée pour ça et on perd tout son bénifice dès qu’un traitement est un peu long. S’il est possible de déporter certains traitements (par exemple via des worker thread), on complexifie beaucoup et on essaye de détourner son usage initiale. Des technologies plus adaptées et éprouvées sont disponibles, inutile de réinventer la roue : Node.Js peut très bien se coupler avec et le tout n’en sera que meilleur.

A titre d’exemple, Netflix utilsie beaucoup Node. Quand on regarde plus en détail, on se rend compte qu’il est surtout utilisé pour servir des fichiers et encaisser les demandes, beaucoup de traitement annexes sont traités en python.

Si d’aventure vous vous demandiez si Node est fait pour vous, mon meilleur conseil reste d’essayer et de se faire sa propre opinion. C’est une technologie intéressante, qui a fait ses preuves chez de grands noms mais qui peut vous amener droit dans un très beau mur si mal appréhendée. Et là où c’est vicieux par rapport aux autres technologies, c’est qu’on ne va pas s’en rendre compte de suite : Node permet d’obtenir très rapidement des résultats intéressants, ammenant une certaine confiance. Et ce n’est qu’une fois bien avancé qu’on se rend compte que les modules utilisés sont à changer, que l’architecture n’est pas si bonne que ça, que la sécurité n’est pas là, etc.

D’autant que la communauté Node est hyper prolifique (sans doute trop), elle avance très vite (là encore trop à mon avis) mais la qualité n’est pas toujours au rendez-vous. Quand on débute, il est assez difficile de distinguer les bonnes ressources des mauvaises et on peut vite être noyé dans le milion de module disponible sur Npm : une partie n’est pas maintenu à jour, une autre élude certaines problématiques (la sécurité, c’est vraiment un point qui pose problème je trouve), …

Il est également nécéssaire de se donner les moyens d’appréhender et de mettre en place correctement la technologie. Si une personne est suffisante pour faire un petit prototype, il faut quand même compter sur une véritable équipe pour une application en production. Si des sociétés comme LinkedIn ou Paypal utilisent Node avec succès, ce n’est pas une personne à 40% qui maintient mais une vraie équipe de plusieurs personnes. Une personne seule ne pourra pas tout suivre : rien que faire le suivi des configurations et des mises à jour est chronophage et demande un minimum de temps, sous peine d’être vite dépassé, là où Java bouge beaucoup (beaucoup) plus lentement.

Bref, Node est une technologie pertinente et adaptée dans certains cas mais qui cache de nombreux pièges (et peut-être un peu trop portée par de la hype). Mon expérience reste assez positive mais s’est beaucoup résumée à des prototypes/PoC auquel Node se prétait bien. Comme beaucoup d’autre technologies, il faut s’assurer que la réponse est bien celle du problème.

Dernier point : j’ai expérimenté quelques plateformes Paas (Plateform A A Service) de type Firebase dont certaines reposent en partie sur Node.Js : ça marche très très bien, c’est rapide à souhait et fournit des services performants. Si vous n’avez pas des problématiques de sécurité impliquant de ne pas les utiliser (Firebase, ça reste Google quand même, tout n’est peut-être pas bon à y héberger), ces plateformes peuvent vous fournir des réponses clés en mains pour le nombreux services back (authentification, database, synchronisation temps réel) sans devoir passer par du développement Node.Js. Ca peut être une alternative intéressante plutôt que de redévelopper son propre service Node.Js