NodeJS Server Neustart ohne Downtime? Kein Problem!

Sorry, this entry is only available in German. For the sake of viewer convenience, the content is shown below in the alternative language. You may click the link to switch the active language.

Webserver sollten heutzutage möglichst 24/7 verfügbar sein. Oft trifft das sogar für firmeninterne Server und staging Backends zu. Was aber passiert, wenn ich mein Backend auf eine neue Version updaten will?Es gibt eben Situationen, wo ein Neustart der Applikation unumgänglich ist. Wir sehen uns mal an, wie du das für simple NodeJS/Express Anwendungen lösen kannst, und so dem Benutzer diesen Screen ersparst:

Nodejs server down

Prozess Manager

Um den NodeJS Prozess am Server neu zu starten, empfiehlt sich ein Prozess Manager, wie pm2. Dieser beendet den “alten” Prozess, und startet sofort den neuen. Ein einfacher Befehl reicht:

pm2 restart <prozess_name>

pm2 übernimmt auch weitere Funktionalität, wie Neustart bei Crashes (welche natürlich nicht vorkommen sollten), reboot safety u. s. w.

“Schnelles” Neustarten erfüllt jedoch unsere Anforderungen noch lange nicht. Das hat folgende Gründe:

  1. Beim Start des neuen NodeJS Prozesses dauert es eine Weile, bis die Applikation auch auf den Port hört, und somit Aufrufe entgegennehmen kann. Vom Stoppen des alten Prozesses bis zu diesem Zeitpunkt ist das Backend also nicht erreichbar.
  2. Das Verarbeiten von Aufrufen kann eine Weile dauern. Müssen zum Beispiel umfangreiche Datenbankabfragen ausgeführt, oder große Dateien hochgeladen werden, kann dies in Sonderfällen Minuten dauern. Wird der alte Prozess während der Verarbeitung einfach beendet, brechen auch gerade zu bearbeitende Anfragen ab. Stell dir vor, du musst auf einer Webseite eine Datei hochladen, und der Upload bricht nach zehn Minuten plötzlich ab!

 

Cluster Mode und Prozessstart

Wir sehen also, dass wir mit einer einzelnen Instanz bei jedem Neustart Downtime erzeugen. Wir brauchen daher mindestens eine zweite Instanz, die läuft, während sich die Erste neu startet. Glücklicherweise besitzt pm2 einen “Cluster Mode”, der das Starten und Verwalten mehrerer Instanzen vereinfacht. Intern verwendet pm2 bei diesem Modus das Cluster Modul von NodeJS. Das erlaubt uns, auf einfache Weise mehrere Instanzen unserer Applikation auf einen Port hören zu lassen. Die Anfragen werden dann auf die Instanzen verteilt. Wir müssen also das Backend wie folgt starten:

pm2 start ./bin/www -i 2

Das ./bin/www ist in diesem Fall das Startup Skript, das je nach Projektstruktur variieren kann. Der -i Parameter gibt die Anzahl der zu starteten Prozesse an. Wenn du so viele Prozesse wie CPU’s haben willst, kannst du auch -i maxangeben.

Um Problem 1. zu beheben, muss pm2 jedoch wissen, ab wann ein Prozess bereit ist, Anfragen zu empfangen. Erst dann kann es mit dem Neustart des zweiten Prozesses beginnen. pm2 hört zu dem Zweck auf die ‘ready’ Message, die wir in der Applikation absenden, sobald auf den Port gehört wird:

const http = require('http');
const server = http.createServer(app);
// some other initialization...
// using 'listening' event
// (https://nodejs.org/api/net.html#net_event_listening)
server.on('listening', () => {
  // use if since we process.send is only defined when this
  // is a child process.
  if (typeof process.send === 'function') {
    process.send('ready');
  }
});

Dann sagen wir pm2 beim Start noch, es soll auf die Message hören:

pm2 start ./bin/www -i 2 --wait-ready --name my_server

# reload instead of restart
pm2 reload my_server

Mit pm2 reload achtet der Prozessmanager immer darauf, das zumindest eine Instanz zu jeder Zeit verfügbar ist.

Laufende Anfragen

Problem 2 haben wir damit jedoch noch nicht gelöst. Der Prozessmanager kann nicht wissen, ob eine Anfrage gerade in Bearbeitung ist. Hier können wir uns Unix Signale zu Nutze machen. Wird ein Prozess auf Unixoiden beendet, empfängt dieser vorher ein sogenanntes TERM Signal. Darauf kann der Prozess reagieren, was wir uns zu Nutze machen. Erstmal müssen wir in unserer Express Applikation aber einmal erkennen, ob alle Anfragen abgeschlossen sind. Dazu verwenden wir eine Express Middleware, die “mitzählt”, sowie einen Listener auf SIGINT

// count for pending requests
let pendingRequests = 0;
// set by SIGINT listener
let shouldExit = false;
// determine node version used later
const nodeMajorVersion = parseInt(process.version.match(/v([0-9]*)/)[1], 10);
// this should be the first registered middleware to track all requests
app.use((req, res, next) => {
  // incrementing count
  pendingRequests += 1;
  function reqEnd() {
    // decrementing count
    pendingRequests -= 1;
    // exit if SIGINT was already received
    if (shouldExit && pendingRequests === 0) {
      process.exit();
    }
  }
  // either 'finish' (regularly) or 'close' (e.g. when running into
  // timeouts) is emitted
  if (nodeMajorVersion < 11) {
    // such node versions do not emit 'close' after 'finish'
    res.on('finish', reqEnd);
  }
  res.on('close', reqEnd);
  next();
});
process.on('SIGINT', () => {
  if (pendingRequests === 0) {
    process.exit();
  } else {
    // there are pending request, so let other code path exit
    shouldExit = true;
  }
});

Auch pm2 schickt der Instanz zuerst das SIGINT Signal. Dann wartet es 1600ms, bis die Instanz beendet wird. Da aber Anfragen, wie schon erwähnt, viel länger dauern können, müssen wir diesen Zeitraum erhöhen, in diesem Beispiel auf eine Minute:

pm2 start ./bin/www -i 2 --wait-ready --name my_server --kill-timeout 60000

Die Länge des Timeouts hängt sehr von der Applikation ab. Wählt man es zu lange, kann das Update möglicherweise sehr lange dauern (bis zu der Timeout Zeit). Wählt man es zu kurz, ist das Risiko höher, einen Benutzer z. B. beim Hochladen einer großen Datei zu unterbrechen.

Ausblick

Dies ist nur eine von mehreren Möglichkeiten, die Verfügbarkeit eines Backends auch beim Neustart zu gewährleisten. Für hoch frequentierte Seiten sollte man auch Load Balancing, Autoscaling und ähnliche Techniken in Betracht ziehen. Die hier beschriebene Vorgangsweise ist jedoch ein einfacher Start und für viele Applikationen ausreichend.

Noch Fragen? Melde dich einfach bei uns!