La mise en production d’un serveur Node.js : l’option « Phusion Passenger » (update)

TL ; DR : Passenger ce n’est pas indispensable, mais ça fait vraiment bien son job. C’est bon, mangez-en :) (et non, ce n’est pas un article sponsorisé, ça se saurait si j’étais foutu de négocier ce genre de truc).

Bon déjà, on va faire comme si c’était normal de n’avoir rien posté pendant près d’un an, et ne pas aborder le sujet 😛 Pas de bonne excuse à l’horizon, je vous ferai un bilan de mon 2013 l’année prochaine, vous allez voir qu’elle était sympa cette année 😉

Le sujet du jour est la mise en production de vos applications Node.js. On a pour ça plusieurs objectifs à atteindre :

  • Démarrage automatique de l’application au démarrage du serveur ;
  • Utilisation de plusieurs cores de CPU ;
  • Redémarrage automatique de l’application en cas de crash ;
  • Notification lors d’un crash ;
  • Monitoring de l’activité de l’application ;
  • Protection de l’application contre les attaques courantes HTTP ;
  • Optimisation du rendu des fichiers statiques ;
  • Une solution pour déployer les mises à jour ;

J’ai assisté lundi au dotJS 2013 qui était plutôt cool, même si ça manquait de conférences « inspirantes » comme le disait justement François Zaninotto. Je vous promettrais bien de faire un article sur mes impressions, mais d’une part vous en trouverez des dizaines déjà faits, et d’autre part c’est encore un article que je finirais par ne pas écrire. Donc non, aucune promesse 😛

Il y a quand-même deux talks qui m’ont particulièrement parlé (ah ah, jeu de mot bilingue, trop classe), et pas de bol tous deux étaient des lightning talks ! Le premier de David Bruant autour de la recherche des fuites mémoire dans nos applications JS et la présentation d’un outil auquel je vais m’empresser de contribuer (et non David, même si je sais que tu aimes ça, je ne te jetterai pas de pierres :P). Le deuxième par Hongli Lai présentait la solution Phusion Passenger pour simplifier la mise en production d’applications. Hop, parlons donc de Passenger, après un tour d’horizon de l’existant.

On fait comment pour l’instant ?

Mais d’abord, reprenons notre checklist et la réponse habituelle à chacune de ces problématiques. Un petit tour d’horizon ne sera pas du luxe 😉 Je ne vais parler que des solutions les plus connues.

Démarrage automatique

  • Utiliser un service initd : vous pouvez utiliser un petit script pour simplifier leur écriture (ou juste utiliser le template inclus sans faire confiance au générateur, ce que je vous conseille en production) et le rendre automatique avec sudo update-rc.d {{mon service}} defaults ;
  • Utiliser Upstart ;
  • supervisord est un outil en Python qui fait ça très bien et permet de déléguer la gestion de certains process à des utilisateurs non privilégiés ;

Utilisation de plusieurs cores de CPU

Redémarrage automatique

  • supervisord propose l’option « autorestart » ;
  • upstart propose l’option « respawn » ;
  • forever, un peu simpliste à mon avis, mais facilement installable ;
  • mon est un poil plus complet et générique ;

Notification de crash

Mon idéal : à chaque redémarrage non prévu (donc un crash suivi d’un autorestart) recevoir un mail avec tout ce qu’il y a eu comme sortie d’erreur (fichier ou /dev/stderr) depuis le dernier démarrage.

Je n’ai pas encore trouvé ce graal, mais en attendant déjà rien que le moyen d’exécuter une commande de son choix lors du restart est un bon point de départ.

Monitoring

Zabbix, Nagios, Monit… vous avez du choix et là j’avoue que je laisserai la parole au sysadmin (on peut connaître un peu le métier de chacun, n’empêche que ça reste un métier à part entière).

Idéalement aussi, il faut des logs. Donc là pas de mystère

Protection et fichiers statiques

Pour ça pas de mystère, l’idéal est vraiment de mettre un nginx devant, et l’application Node en proxy_pass.

upstream app_yourdomain {
    server 127.0.0.1:3000;
}
server {
    listen 0.0.0.0:80;
    server_name yourdomain.com;
    # les fichiers statiques sont servis directement par nginx
    root /path/to/app/public;
    # le reste = node
    location / {
      proxy_pass http://app_yourdomain/;
      proxy_redirect off;
    }
}

En vrai il y a bien d’autres options à ajouter pour faire ça bien, mais c’est l’idée :)

Déploiement des mises à jour

Là c’est un peu à part, tant qu’il y a moyen de déclencher un restart après le déploiement, on est content.

On utilise typiquement Capistrano, mais vous en trouverez mille autres facilement. Pour le redémarrage ça dépendra de l’outil utilisé pour gérer le process (upstart, initd, supervisord, un restart manuel…).

Conclusion

On voit des outils qui se démarquent nettement : nginx me semble indispensable puisqu’il tient plusieurs rôles (protection, fichiers statiques, load balancer, mais aussi des logs de requêtes). Pour la gestion des processus j’aime bien m’en tenir à upstart (autostart, respawn, notifications).

Et avec Phusion Passenger ça se passe comment ?

Passenger promet plein de choses, voyons la réalité 😉 Déjà un point positif : une version open-source gratuites pour les radins les bricoleurs et une version payante « Enterprise » avec des options en plus et du support pour ceux qui n’ont pas que ça à foutre les entreprises.

Installation

Pour l’installation, j’ai suivi le guide d’installation en tant que copain de nginx. Cette méthode va remplacer le paquet « nginx », ça m’emballe moyennement mais pourquoi pas… [update 11/12] en effet passenger se présente alors sous la forme d’un module nginx, et ce dernier ne supportant pas le chargement dynamique de modules, bien obligé de le recompiler. Ils fournissent donc par confort le binaire directement.

Il est aussi possible de l’installer en « standalone » pour ne pas toucher à son installation de nginx : dans ce cas Passenger fait directement serveur HTTP (basé sur nginx à ce que j’ai compris), mais il faut gérer le démarrage auto du service et probablement faire un proxy_pass sur la version locale de nginx, l’intérêt me semble du coup plus limité.

Autre solution : « Installing as a normal Nginx module without using the installer », ça doit être un très bon compromis, mais pas testé pour l’instant [update 11/12] mais ça impose de recompiler nginx sur son serveur.

sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 561F9B9CAC40B2F7
sudo apt-get install apt-transport-https
# Testé sur Ubuntu 13.10
echo 'deb https://oss-binaries.phusionpassenger.com/apt/passenger saucy main' | sudo tee /etc/apt/sources.list.d/passenger.list
sudo apt-get update

Après un sudo apt-get install nginx-extras passenger qui m’a remplacé mon nginx sans dommages, on édite /etc/nginx/nginx.conf pour activer Passenger (j’ai juste décommenté les lignes, j’ai tout laissé par défaut) :

  ##
  # Phusion Passenger config
  ##
  # Uncomment it if you installed passenger or passenger-enterprise
  ##
 
  passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
  passenger_ruby /usr/bin/ruby;

Oui, on sent que Passenger est plutôt un copain historique de Ruby :) Mais comme on a les mêmes problématiques dans Node, c’est normal qu’on finisse avec les mêmes copains.

Utilisation

À partir de là en suivant le tutorial pour Node.js ça roule tout seul.

Déjà, en intro, des promesses (on parle d’un truc commercial quand-même, il faut bien un minimum de bullshit) :

  • « The power of nginx » → oui, forcément vu qu’il est devant, OK ;
  • « Multitenancy […] easily and without hassle » → C’est justement ce qu’on va voir ;) ;
  • « Process management and supervision » → OK, ça va donc remplacer mon « upstart » ou mon « initd + mon » & co ;
  • « Statistics and insight » → Waouh ça semble trop super génial 😀 en fait c’est juste la (déjà très cool et surtout utile) possibilité de voir le nombre de requêtes traités, et le nombre de requêtes en cours de traitement ;
  • « Scaling and load balancing » → au départ je ne vois pas l’intérêt, je préfère laisser faire le module cluster, mais j’ai peut-être tort (spoiler : vous verrez que ça a quand-même un vrai intérêt de passer ça en instruction)… en tous cas ça n’empêche que le côté « coucou votre appli pourra magiquement profiter de tous vos cores de CPU » est évidemment faux, ça ne dispense pas de concevoir son application pour être « scalable », ce qui n’est pas automatique ;
  • « I/O security », « System security », « Static file acceleration » → Ouaip, enfin tout ça c’est nginx qui le gère, mais c’est bien de rappeler pourquoi on le met devant quand-même ;

D’après le tuto, l’application doit impérativement suivre cette structure :

app.js
tmp/
public/

Ce n’est plus vrai depuis la 4.0.25 mais il est obligatoire d’avoir un script avec un seul serveur HTTP dedans (si vous en avez plusieurs, il faudra autant d’instances) et un dossier temporaire par application.

Cela dit, cette structure est celle de toutes mes applications web (sauf pour le dossier tmp), donc ça roule. Je teste sur la dernière en date :

# /etc/nginx/sites-enabled/client-projet.local
server {
    server_name client-projet.local;
    root /home/nchambrier/Projects/Client/Projet/public;
    passenger_enabled on;
}

Après un petit echo 127.0.0.1 client-projet.local | sudo tee /etc/hosts et un sudo service nginx restart on va visiter http://client-projet.local : bim, 403 !

Ah, j’avais oublié de créer le dossier tmp (Passenger ne le fait pas pour nous). Pas vraiment un souci, il faut juste le savoir.

On re-teste : bim, une page d’erreur qui me dit qu’il ne trouve pas « node » !

Premier démarrage

La page est plutot bien faite, elle en profite pour nous lister les variables d’environnement :

Variables d'environnement

Effectivement, le PATH est incomplet : j’utilise nvm et je n’ai pas d’installation globale de node. Du coup il faut que je signale à Passenger où trouver le binaire : ça se fait avec l’instruction passenger_nodejs ou en définissant la variable d’environnement $NODE_PATH.

J’aurais aussi pu lui dire de faire exécuter mon .bashrc ce qui aurait du modifier le PATH sur ma version courante de node, avec l’instruction passenger_load_shell_envvars mais ça n’a eu aucun effet, je n’ai pas creusé la question.

J’ajoute donc passenger_nodejs /home/nchambrier/.nvm/v0.10.22/bin/node; dans ma config, je relance nginx et… bim ! une erreur, applicative cette fois. En effet mon application est lancée en environnement de production, eu lieu de « development ». Bizarre sachant que j’ai défini export NODE_ENV=development dans mon .bashrc… ah mais oui c’est vrai qu’il n’est pas chargé, grumpf. On peut définir la variable d’environnement avec passenger_set_cgi_param NODE_ENV development.

Dans la doc j’ai vu des instructions pour faire ça de manière raccourcie : rails_env development ou rack_env development, mais pas de node_env ou un générique passenger_env. Un peu frustrant d’ajouter une ligne pour Ruby dans sa conf alors que ça n’a rien à voir, mais ça marche aussi. [update 11/12] Dans la 4.0.25 on peut utiliser passenger_app_env, plus neutre, et qui met à jour NODE_ENV aussi :)

Restart, OK ça roule :)

Le fichier nginx final :

server {
    server_name docatl.local;
    root /home/nchambrier/Projects/Toog/DocGA/public;
    passenger_enabled on;
 
    passenger_nodejs /home/nchambrier/.nvm/v0.10.22/bin/node;
    rack_env development;
}

C’est light, plutôt agréable.

Conclusion

J’ai rencontré quelques problèmes, vite résolus, et majoritairement dus à des cas particuliers de mon installation. Je suis plutôt content d’avoir rencontré tous ces problèmes maintenant, ça aurait été moins rigolo le jour d’une mise en prod. Vérifions notre checklist :

  • Démarrage automatique : ça démarre avec nginx ;
  • Utilisation de plusieurs cores de CPU : instruction passenger_min_instances ;
  • Redémarrage automatique : géré ;
  • Notification de crash : pas trouvé :( [update 11/12] on va pouvoir faire ça avec des hooks dans la 4.0.28 (pas encore publié à l’heure où j’écris ces lignes) ;
  • Monitoring : avec passenger-status et les logs nginx ;
  • Protection et fichiers statiques : via nginx ;
  • Déploiement des mises à jour : redémarrage avec un touch /path/to/tmp/restart.txt (ne cherchez pas, le fichier n’est pas supprimé, Passenger se base sur sa date de modification) ;

Les promesses sont donc globalement tenues :

  • Ajouter une app est facile, c’est vrai. Tout est dans nginx donc on n’a plus rien à faire d’autre qu’ajouter un vhost, et hop on a d’office l’auto-start (l’application est réellement lancée à la toute première requête sur le vhost) et l’auto-restart en cas de crash ;
  • La commande passenger-status tient elle aussi ses promesses : passenger-status ;
  • On peut lui dire de relancer l’application à chaque requête (pratique en dev) avec un touch /path/to/tmp/always_restart.txt (il peut aussi presque remplacer supervisor du coup) ;
  • On peut lui faire démarrer plusieurs instances qu’il va passer en load balancing avec l’instruction passenger_min_instances et là où c’est vraiment balaise c’est que comme on peut mettre cette instruction n’importe où on peut aussi imaginer avoir un nombre d’instance différent pour certaines URI ;

On est toujours obligé d’écrire un vhost nginx, et franchement le fichier standard proxy_pass + load n’est pas si méchant, donc côté nginx je considère le gain inexistant… par contre on n’a plus besoin d’upstart, ni de forever, ni rien d’autre ! Évidemment il faut faire confiance à Passenger 😉 mais de ce côté là pas de souci non plus car ça fait quelques années qu’ils l’utilisent du côté de chez Rails.

Restent quelques améliorations à apporter (j’imagine que certaines existent déjà, en version Enterprise ou cachées dans la doc) :

  • Je n’ai rien trouvé pour me faire notifier des restart :( À part une ligne d’erreur spécifique « [ 2013-12-04 16:21:33.0185 3025/7fd8ba9b2700 Pool2/Pool.h:717 ]: Process (pid=3117, group=/home/nchambrier/Projects/Client/Projet#default) no longer exists! Detaching it from the pool. » juste après le crash, dont on doit pouvoir faire quelque-chose, on n’a rien à se mettre sous la dent, dommage [update 11/12] OK avec les hooks dans la 4.0.28 à venir ;
  • Le coup du touch pour redémarrer c’est cool mais je me demande comment ça va se passer pour un admin qui débarque sur le projet, et qui par habitude cherchera un script initd ou upstart, ne trouvant pas va chercher dans ps et tomber sur /home/nchambrier/.nvm/v0.10.22/bin/node /usr/share/passenger/helper-scripts/node-loader.js (qui ne fait nulle part référence à mon app.js)… je crains qu’il ne mette un moment avant de comprendre comment redémarrer l’application si on ne lui a pas dit exactement où ça se passait (ce qui est le problème à la base dans ce scénario, n’empêche que ça arrive souvent) [update 11/12] Apparemment c’était un bug qui doit déjà être corrigé, le nom du processus est censé contenir le chemin vers le app.js, ce qui aurait bien aidé notre pauvre admin 😉 ;
  • L’installation qui remplace mon nginx, ça me chatouille forcément un peu donc il faudra étudier l’installation en tant que simple module sans passer par leur dépôt [update 11/12] en fait comme on doit recompiler nginx pour ajouter un module, même si en terme de sécurité on préfèrera sans doute le recompiler soi-même, il faudra de toute façon remplacer l’installation courante ;
  • On sent bien, ne serait-ce que par l’absence de certaines fonctions, que l’intégration de Node est récente et je me méfie donc des cas limites qui ne manqueront pas d’apparaître… je n’ai notamment pas vérifié comment il réagit avec une app utilisant « cluster », à suivre…
  • J’aimerais des benchmarks pour me rassurer sur le fait qu’il ne coûte rien (ou très peu) en performance par rapport au traditionnel prox_pass ;
  • Ça parle beaucoup de monitoring, du coup je m’attendais à un passenger-status en mode « temps réel » (comme un top), après un watch passenger-status fait très bien le job aussi ;

Une impression globalement très positive. Je pense vraiment l’utiliser pour le déploiement des mes prochains projets, d’autant plus que l’équipe semble très réactive (quelques tweets échangés qui renforcent cette bonne impression) donc je ne doute pas que certaines de mes attentes seront comblées (la notification de restart me semble vraiment vitale, et l’ajout d’une instruction node_env semble trop facile pour s’en passer).

À bientôt (je vous parlerai de débuggage tiens).

3 réflexions au sujet de « La mise en production d’un serveur Node.js : l’option « Phusion Passenger » (update) »

  1. lionelB

    Pour l’avoir utiliser avec ruby en version 2, le projet était déjà très actifs et le fait qu’ils soient encore là en tenant les même promesses pour node qu’a l’époque avec ruby (simplifier le déploiement) présage de la qualité.

    Répondre

Laisser un commentaire