Webserver beenden

the next big thing  –  0 Kommentare

Einen Webserver in Node.js auszuführen, fällt ausgesprochen leicht: Es genügt, das http-Modul zu integrieren und dessen createServer-Funktion aufzurufen, um einen Webserver zu erhalten. Der Aufruf von dessen listen-Funktion wiederum genügt anschließend, um den Server an einen Port zu binden und zu starten:

'use strict';

var http = require('http');
http.createServer(function (res, res) {
// ...
}).listen(3000);

Die Vermutung liegt nahe, dass das Beenden eines Servers ebenso leicht von der Hand gehen müsse: Schließlich stellt Node.js am Server die vielversprechend klingende Funktion close zur Verfügung. Gemäß der Dokumentation verhindert der Aufruf dieser Funktion die Annahme neuer Verbindungen. Tatsächlich führt der Aufruf dieser Funktion dazu, dass der Webserver beendet wird und die Ausführung auf die Konsole zurückkehrt, wie sich mit Hilfe des optionalen Callbacks der close-Funktion leicht nachweisen lässt:

'use strict';

var http = require('http');
var server = http.createServer(function (req, res) {
// ...
}).listen(3000);

setTimeout(function () {
server.close(function () {
console.log('Server stopped');
});
}, 10 * 1000);

Auf die anfängliche Freude folgt jedoch schnell Ernüchterung, wenn man innerhalb der zehnsekündigen Pause einen Request an den Webserver schickt. In diesem Fall kehrt die Anwendung nämlich nicht wie erwartet nach zehn Sekunden zur Konsole zurück, sondern erst nach ungefähr zwei Minuten.

Die Ursache hierfür liegt im HTTP-Protokoll, das sogenannte persistente Verbindungen unterstützt: Wird eine derartige Verbindung aufgebaut, bleibt die zugrunde liegende TCP/IP-Verbindung auch nach dem Abschluss der HTTP-Datenübertragung geöffnet. Da in diesem Fall nicht für jeden einzelnen Request eine neue Verbindung aufgebaut werden muss, verbessert dies die Leistung zwischen Webbrowser und -server.

Node.js verwendet standardmäßig die Version 1.1 des HTTP-Protokolls, bei der alle Verbindungen per Definition persistent aufgebaut werden. Der interne Timeout beträgt für diese Verbindungen 120 Sekunden, weshalb der Webserver auch nach dem Aufruf der close-Funktion noch für ungefähr zwei Minuten ausgeführt wird.

Dies ist vollkommen konform zu dem, was in der Dokumentation beschrieben wird: Schließlich besagt diese lediglich, dass die close-Funktion die Annahme neuer Verbindung verweigert, nicht jedoch das Schließen bestehender Verbindungen verursacht.

Verschärft wird all dies dadurch, dass jede Aktivität auf einer persistenten Verbindung deren Timeout zurücksetzt, sodass es bei einem stark frequentierten Webserver unter Umständen unmöglich wird, diesen mithilfe der close-Funktion zu beenden.

Das Abschalten der Verbindungspersistenz löst das Problem zwar theoretisch, geht aber zugleich mit einem unerwünschten Leistungsverlust einher. Um diesen zu vermeiden, verbleibt als einzige Möglichkeit, den Timeout so weit herabzusenken, dass persistente Verbindungen frühzeitig geschlossen werden und die Chance für eine dauerhaft offene Verbindung in ausreichendem Maße sinkt.

Dies gelingt, indem man sich an das connection-Ereignis des Webservers anhängt und auf dem der neuen Verbindung zugrunde liegenden Socket die Funktion setTimeout mit der gewünschten Anzahl an Millisekunden aufruft:

'use strict';

var http = require('http');
var server = http.createServer(function (req, res) {
// ...
}).listen(3000);

server.on('connection', function (socket) {
socket.setTimeout(5 * 1000);
});

setTimeout(function () {
server.close(function () {
console.log('Server stopped');
});
}, 10 * 1000);

Wirklich gelöst wird das Problem dadurch jedoch nicht, zumal es unter Umständen durchaus wünschenswert sein kann, das sofortige Beenden des Webservers ohne Rücksicht auf Verluste zu erzwingen. Prinzipiell ist dies jedoch auf einem ähnlichen Weg möglich, sogar ohne den Timeout der persistenten Verbindungen senken zu müssen.

Die Lösung besteht darin, alle derzeit offenen persistenten Verbindungen in einer Liste zu verwalten und diese Verbindungen im Falle des Falles einzeln zu schließen. Erfolgt dies nach dem Aufruf der close-Funktion, ist sichergestellt, dass nach kurzer Zeit alle Verbindungen geschlossen wurden und der Server beendet werden kann:

'use strict';

var http = require('http');
var server = http.createServer(function (req, res) {
// ...
}).listen(3000);

var sockets = [];
server.on('connection', function (socket) {
sockets.push(socket);
socket.on('close', function () {
sockets.splice(sockets.indexOf(socket), 1);
});

});

setTimeout(function () {
server.close(function () {
console.log('Server stopped');
});
for (var i = 0; i < sockets.length; i++) {
sockets[i].destroy();
}
sockets = [];

}, 10 * 1000);

Das war's. Nach zehn Sekunden wird der Webserver nun wie gewünscht beendet, alle noch offenen Verbindungen werden geschlossen und die Liste der aktiven Verbindungen zurückgesetzt.

tl;dr: Das Beenden eines Webserver in Node.js ist nicht ganz so einfach wie das Starten: Persistente Verbindungen verhindern diesen Vorgang unter Umständen. Die Lösung liegt in der manuellen Verwaltung aller aktiven Verbindungen.