Meteor / Node.js und DJB’s Daemontools
TL;DR: Hier das Repository mit der Konfiguration, die ich für Node.js / Meteor geschrieben habe. Simpel — wie ich das mag!
Annie43 / photocase.com
Wie betreibt man Node.js Applikationen am besten produktiv? Node.js ist ein Application Server, das heisst die Applikation wird zusammen mit dem Server (und zusätzlichen Modulen) entwickelt. Wenn mehrere unterschiedliche Node.js Web-Apps auf einem Server laufen sollen, muss auch Node.js mehrere male laufen — anders als zB bei PHP auf einem klassischem LAMP Stack.
Ein weiterer Unterschied zu PHP-Anwendungen besteht im Verhalten bei Fehlern: Ein LAMP-Server läuft in der Regel weiter, auch wenn er ein fehlerhaftes PHP-Skript ausgeführt hat — und dieses vielleicht abbrechen musste. Eine fehlerhafte Node.js-Anwendung stoppt alle Serverfunktionen.
Dass ein Dienst in so einem Fall crasht, anstatt weiter zu laufen, mag für Leute, die den Computer hauptsächlich zum Surfen und eMail Schreiben benutzen ein “Gschmäckle” haben — für mich ist es ein Feature (Siehe Candea und Fox: Crash-Only Software). Ich habe lieber einen Dienst, der bei einem unvorhergesehenem Zustand die Reißleine zieht, als einen, der einfach weiter läuft und im Hintergrund vielleicht die Datenbank korrumpiert.
Um trotzdem immer verfügbar zu sein, braucht ein solches Setup einen Wachhund, der sich darum kümmert, dass der Application-Server immer läuft. In komplexeren Systemen muss sich der Wachhund auch um die anderen benötigten Systembestandteile (Datenbank, Cache) kümmern. Sollte sich ein Dienst aufgrund eines unvorhergesehenen Fehlers selbst beenden, startet der Wachhund den Dienst neu.
Im Zusammenhang mit Node.js und Meteor wird für diese Aufgabe immer wieder auf Forever oder auch Upstart und Monit verwiesen.
- Forever ist selbst in Node.js geschrieben. Bisher ist Forever wohl nur für Node.js-Anwendungen geeignet; Ein Minuspunkt für uns. Viel wichtiger ist für mich allerdings, dass Forever mich schon hat hängen lassen: Forever meint, der Dienst läuft, dabei läuft er nicht — oder anders herum, beides schon gehabt. Außerdem bringt Forever eine lange Liste an Abhängigkeiten mit. Dafür, was es machen soll, braucht Forever verdächtig viel Code: Ein
du -s ~/apps/lib/node_modules/forever
auf das Forever-Verzeichnis sagt mir, dass die aktuelle Version 8,8 MB Plattenplatz benötigt. Kein Wunder, denn Forever lädt bei der Installation 55 Pakete herunter (WTF?!). Die hübschen Farben des Terminal-Outputs sind da leider nur ein kleiner Trost.
- Monit hat sich bei der ersten Konfiguration angefühlt als wär’s in Visual Basic geschrieben. Die Syntax des Konfigurationsfiles ist extrem kompliziert, und auch wenn ich Features wie “Monitoring eines HTTP Services” super finde und das der Grund ist, warum ich Monit zuerst benutzen wollte — Ich trau’ Monit nicht.
- Upstart ist Ubuntu-Only. Vielleicht ist es auch auf Debian Gnu/Linux zu installieren, aber das ist zu wenig. Upstart ist nicht leicht einzurichten wenn man keine Root-Rechte auf dem Server hat. Und Upstart ist noch ziemlich neu, und in den letzten Versionen wurde immer noch viel verändert.
- Die Daemontools von DJB schrecken zuerst vielleicht etwas ab — die Benutzung der Tools mag ungewohnt sein, wenn man bisher nur mit den gängigen Linux-Distributionen gearbeitet hat. Aber Daniel J. Bernstein ist für die Zuverlässigkeit seiner Software bekannt. Siehe beispielsweise QMail oder DJBDNS mit der dazu gehörigen Security Guarantee. Die Konfiguration passiert über ein einfaches Key/Value Schema: Eine Konfiguration besteht aus einem Verzeichnis, der Dateiname ist der Key, die erste Zeile der Datei der Value. Das ist sehr einfach zu parsen, d.h. es schleichen sich weniger leicht Fehler in den Parser-Code ein. Außerdem funktionieren die Daemontools auch ohne Root-Zugang — oft eine Erleichterung. Und nicht zuletzt: Die Daemontools laufen auf allen halbwegs unixoiden Systemen. Sollten wir uns entscheiden, zwecks der Extra-Coolness (d.h. wegen den coolen Solaris Containern (Zones), ZFS und DTrace) unsere Node.js Instanzen auf OpenSolaris oder SmartOS zu hosten, müssen wir den kompletten Stack (Node.js, MongoDB, evtl Memcached oder Nginx…) zwar neu kompilieren; Das sollte es dann aber auch gewesen sein. Dazu ein ander mal mehr…
Ich hab mich für die Daemontools entschieden. Wie sich heraus stellt passen Node.js und die Daemontools hervorragend zusammen: Node.js fork()ed nicht in den Hintergrund, Logging passiert — wenn man kein Logging-Paket (wie Winston oder Log4Js) einsetzt — auf STDOUT und STDERR, die Konfiguration kann leicht über Umgebungsvariablen angepasst werden (Siehe NConf oder app.settings.env für Express-Nutzer).
Tutorial: Daemontools konfigurieren für Node.js
Die Daemontools werden über eine Verzeichnishierarchie konfiguriert. Jeder Dienst bekommt ein Unterverzeichnis in einem Dienste-Verzeichnis:
mkdir -p service/nodejs
In das Verzeichnis legt man ein Shell-Script mit dem Dateinamen “run”, das die richtige Umgebung für den Daemon herstellt (Umgebungsvariablen, UID/GID setzen etc.) und den Daemon im Vordergrund startet. Mittels “exec” übernimmt Node.js den Shell-Prozessraum und kann direkt Signale empfangen.
[gist id=”4109730″]
- “exec 2>&1” leitet STDERR nach STDOUT um, so dass später beides in der selben Logdatei landet.
- “node ../../bundle/main.js” startet meine Node-Applikation (hier eine per “meteor bundle” erstellte Meteor-Anwendung).
- Das “envdir” Tool, das oben im Script erwähnt wird, setzt die Umgebungsvariablen, die es nach DJB-Schema aus den Dateien im service/nodejs/env Verzeichnis liest.
chmod +x service/nodejs/run
macht das ‘run’-Script ausführbar.
Die folgenden Zeilen konfigurieren das ‘envdir’-Tool. Dieses wird uns später beim Start unseres Dienstes die Umgebungsvariablen setzen.
[gist id=”4109761″]
Um die Ausgaben des Dienstes zu speichern bietet sich das “Multilog” Tool an. Multilog ist ebenfalls Teil der Daemontools. Ähnlich zu dem eigentlichen Dienst legt man ein weiteres Dienst-Konfigurationsverzeichnis für Multilog an:
mkdir service/nodejs/log
und legt auch darin ein Shell-Script mit dem Namen “run” an:
[gist id=”4109767″]
- “t” fügt einen Timestamp in jede Zeile ein,
- “s999999” legt die Größe bis zur nächsten Rotation der Logdatei fest und
- “n20” bestimmt, dass 20 alte Logfiles bereit gehalten werden. Ältere werden gelöscht.
Ältere Logfiles muss man sich, sollten wir sie denn benötigen, aus dem Backup holen. Vorteil: Es ist von Anfang an klar, wie viel Plattenplatz die Logs maximal belegen werden (hier: 20 MB).
Soweit. Wenn man auf einem System keine Root-Rechte besitzt, kann man mit einem einfachen
nohup svscan ~/service
einen Dienst starten, der das service-Verzeichnis regelmäßig scannt und für die darin konfigurierten Dienste supervise-Prozesse startet. Auf einem Server mit Root-Rechten installiert man die Daemontools auch systemweit und lässt einen svscan-Dienst laufen als der Benutzer, der Node.js ausführen soll. Mit dem Befehl
ps auxf
kann man sich sehr schön die Prozess-Hierarchie anzeigen lassen. Ein Beispiel:
root 1378 0.0 0.1 1752 524 ? Ss 20:32 0:00 /bin/sh /usr/bin/svscanboot
root 1380 0.0 0.0 1708 396 ? S 20:32 0:00 \_ svscan /etc/service
root 1387 0.0 0.0 1548 340 ? S 20:32 0:00 | \_ supervise svscan-fs
fs 1389 0.0 0.0 1708 396 ? S 20:32 0:00 | | \_ svscan /home/fs/service
fs 2792 0.0 0.0 1548 340 ? S 21:12 0:00 | | \_ supervise nodejs
fs 5197 0.1 3.4 63252 17860 ? Sl 21:42 0:01 | | | \_ node /home/fs/bundle/main.js
fs 4916 0.0 0.0 1548 340 ? S 21:38 0:00 | | \_ supervise log
fs 5139 0.0 0.0 1556 340 ? S 21:41 0:00 | | \_ multilog t s999999 n20 ./main
root 1388 0.0 0.0 1548 340 ? S 20:32 0:00 | \_ supervise log
root 1390 0.0 0.0 1688 412 ? S 20:32 0:00 | \_ multilog t ./main
root 1381 0.0 0.0 1536 304 ? S 20:32 0:00 \_ readproctitle service errors: ................................
fs 1393 0.0 0.0 1556 336 ? S 20:32 0:00 multilog t s999999 n20 ./main
So long! Ich werde das Repository mit der Daemontools-Konfiguration für Node.js aktualisieren, wenn ich nützliche Konfigurationsdateien für weitere von unseren Projekten benötigte Dienste erstellt und getestet habe. Bei dem Projekt, das mir den Anlass gegeben hat, die Daemontools einzusetzen, starte ich die systemweite MongoDB über /etc/init.d; Aber spätestens, wenn wir wieder ein Depoyment auf einer Maschine ohne Root-Rechte machen müssen, werden ein paar weitere Skripte dazukommen.
Zum Abschluss noch ein paar Links:
- Doku Daemontools – Sehr knapp und präzise, wenig Beispiele, auf jeden Fall auch die FAQ ansehen!
- Deutsche Anleitung mit Beispielen zu den Daemontools beim UberSpace
- Ein etwas abgefahreneres multilog-Beispiel
- Erklärung des Multilog-Matchers – Subtil ganz schön anders als RegEx
Wenn ich schon einen Wachhund für meine Dienste brauche, dann bitte auch einen zuverlässigen! Keinen zu fetten, keinen zu jungen, und keinen, der nur in seiner gewohnten Umgebung überhaupt Dienst tut.