Zero downtime deployment avec Node.js et Express, une première étape ...

Zero downtime deployment avec Node.js et Express, une première étape ...

Lorsqu’on souhaite stopper ou redémarrer un serveur, différentes solutions s’offrent à nous. Parmi elles, la possibilité d’envoyer un signal de type SIGTERM au processus.Cette solution est couramment utilisée, malheureusement cela entraîne la coupure des connexions en cours sans permettre au serveur d’honorer les requêtes en cours de traitement.Dans ...

Nginx

Lorsqu’on souhaite stopper ou redémarrer un serveur, différentes solutions s’offrent à nous. Parmi elles, la possibilité d’envoyer un signal de type SIGTERM au processus.

Cette solution est couramment utilisée, malheureusement cela entraîne la coupure des connexions en cours sans permettre au serveur d’honorer les requêtes en cours de traitement.

Dans l’objectif de fournir une meilleure qualité de service, il est important d’honorer toute requête entrante. Comment faire, donc, pour permettre le redémarrage d’un serveur en douceur, sans couper brutalement les connexions en cours ?

Le protocole HTTP permet au serveur de répondre aux requêtes entrantes par un status code 503 qui signifie que le service n’est pas disponible (Service Unavailable). L’idée est donc de renvoyer ce status code dès lors que le process a reçu un signal SIGTERM, tout en laissant le temps au serveur de terminer le traitement des requêtes HTTP en cours, puis de stopper le serveur une fois que les requêtes en cours sont traitées. Il est toujours possible de killer le process du serveur si cela met trop longtemps.

Node.js

Avec Node.js, il est possible d’écouter les signaux reçus par le système et d’y réagir. Il est donc possible de mettre en place une mécanique qui instruit le serveur de répondre aux nouvelles requêtes entrantes par un status code 503, puis de couper le serveur une fois les requêtes en cours traitées.

Cela donne le code suivant:

start = new Date()

# Express
app = express()

gracefullyClosing = false

app.configure ->
    app.set 'port', process.env.PORT or 8000
app.use (req, res, next) ->
    return next() unless gracefullyClosing
	res.setHeader "Connection", "close"
	res.send 503, "Server is in the process of restarting"

app.use app.router

app.get '/', (req, res) -> 
	res.send 200, 'OK'

httpServer = app.listen app.get('port')

process.on 'SIGTERM', ->
    logger.info "Received kill signal (SIGTERM), shutting down gracefully."
    gracefullyClosing = true
    
httpServer.close ->
    logger.info "Closed out remaining connections."
    process.exit()
    
setTimeout ->
    console.error "Could not close connections in time, forcefully shutting down"
    process.exit(1), 30 * 1000

L’appel de la fonction close sur l’instance de serveur HTTP renvoyée Express, permet au serveur de terminer le traitement des requête en cours avant de s’arrêter.

Si votre application Node.js est correctement redondée avec un reverse proxy (Nginx, HAProxy) devant les différentes instances, les requêtes entrantes seront redirigées vers d’autres instances en état de traiter les requêtes. Cela sera le cas dès lors que votre application répondra aux requêtes entrantes avec des status code 502 ou 503 par exemple.

Nginx

Si votre application est déployée derrière un reverse proxy tel que Nginx, il suffit de configurer celui-ci avec plusieurs flux upstream vers différentes instances de votre application pour qu’il soit capable de passer la main à une autre instance lorsqu’il reçoit un code erreur 502 ou 503 en réponse à une requête transmise à une des instances.

upstream my_app_upstream {
	server 127.0.0.1:7000;
	server 127.0.0.1:8000;
	server 127.0.0.1:9000;
}

Ensuite, il faut déclarer ce que Nginx doit faire lorsqu’il reçoit une réponse 502 ou 503 et le tour est joué. Ici, nous indiquons à Nginx via la directive proxy_next_upstream de faire suivre la requête au prochain flux upstream lorsqu’il reçoit en réponse une erreur, un timeout ou bien un code HTTP 502 ou 503:

location /app {
	...
	proxy_next_upstream error timeout http_502 http_503;
	...
	proxy_pass http://my_app_upstream;
}

Limitations

Ce fonctionnement décrit dans cet article répond bien aux besoins d’applications traitant des requêtes HTTP simples, néanmoins il ne répond pas au problème des configurations ayant activé l’option de keepalive pour les connexions HTTP, ni aux applications utilisant les websockets. Il faudra dès lors trouver une solutions adaptée.

Conclusion

La mise en oeuvre de la notion de Gracefully Closing lors d’un redémarrage pour raison de déploiement d’une nouvelle version, est une première étape importante pour arriver à faire du Zero Downtime Deployment. Cela permet, non seulement d’honorer les requêtes en cours de traitement, mais également à vos reverse proxy de prendre connaissance de l’absence de service et redispatcher les requêtes sur d’autres serveurs avant que le flux upstream soit coupé.