Die daemontools sind eine Sammlung von kleinen Tools von Dan J. Bernstein, der auch schon für den von unser eingesetzten Mailserver verantwortlich zeichnet. Sie sind eine ideale Möglichkeit, um Programme, die dauerhaft laufen sollen, zu verwalten und vor allem auch zu überwachen - das gilt auch für den Einsatz von runwhen, was wir als Alternative zu Cron empfehlen; aber auch jeder andere Daemon kann prima auf diese Art gesteuert werden, beispielsweise wenn du MongoDB auf deinem Uberspace laufen lassen willst.
Mit dem Befehl uberspace-setup-svscan kannst du dein persönliches ~/service-Verzeichnis aktivieren, das den Betrieb eigener Daemons oder auch für den Einsatz von runwhen ermöglicht. So sieht's aus:
[annette@argon ~]$ uberspace-setup-svscan
Creating the /etc/run-svscan-annette/run script
Symlinking /etc/run-svscan-annette to /service/svscan-annette to start the service
Waiting for the service to start ... 1 2 3 4 started!
Congratulations - your personal ~/service directory is now ready to use!
Webhosting hat heute nicht mehr viel mit dem Ablegen ein paar statischer Dateien zu tun, und auch der Einsatz von PHP-Scripts ist nicht das Ende der Fahnenstange. Komplexere Webapplikationen laufen inzwischen viel eher als eigene Dienste, seien es Catalyst-Applikationen mit Perl oder Rails-Applikationen mit ruby. Das hat entscheidende Vorteile: Eine komplexe Applikation muss nur einmal gestartet werden und kann dann laufend Anfragen beantworten und dabei Datenbankverbindungen und Logfiles offenhalten, es müssen nicht laufend wieder Codeschnipsel von Platte gelesen und neu interpretiert werden … mit dem Ergebnis, dass die Antwortzeiten solcher Applikationen oft sogar deutlich schneller sind als der Abruf statischer Dateien.
Die meisten Dokumentationen erläutern allerdings nur, wie man seine Applikation auf einem eigenen Server persistent laufen lassen sollte, wobei häufig root-Rechte vorausgesetzt werden und die Applikation in den Boot-Prozess des Servers integriert wird. Das halten wir bei Uberspace.de nicht mehr für zeitgemäß, denn solche Aufgaben lassen sich viel sauberer innerhalb des Userspaces realisieren - und dabei möchten wir dich gerne unterstützen.
Der ideale Daemon …
sollte beim Booten automatisch starten.
sollte zur Laufzeit problemlos angehalten oder neu gestartet werden können oder sonstige Signale erhalten können
sollte überwacht werden und im Fall eines Crashs automatisch neu gestartet werden
Am dritten Punkt scheitern klassische Initscripts direkt - Service-Monitoring ist mit ihnen nicht zu leisten. Und der zweite Punkt ist oftmals auch sehr hakelig, weil er beim klassischen SysVinit darauf basiert, dass ein Daemon beim Starten seine Prozess-ID in eine definierte Datei schreibt und sich dann selbst in den Hintergrund verabschiedet, und wenn man den Dienst stoppen oder ihm irgendein Signal geben will, muss man erstmal die Prozess-ID aus dieser Datei auslesen. Wenn Software einmal crasht, bleibt die Datei mit der Prozess-ID bestehen, und nicht selten macht dann ein Neustart Probleme, wenn die Software aufgrund der bestehenden Datei meint, bereits zu laufen. Im schlimmsten Fall ist die Software abgebrochen, hat die Prozess-ID-Datei zurückgelassen, und ein anderer Prozess besitzt inzwischen die fragliche Prozess-ID und bekommt dann die eigentlich für einen ganz anderen Daemon gedachten Signale.
Die daemontools lösen diese Probleme mit einem ganz simplen Prinzip: Der fragliche Daemon bleibt im Vordergrund, als Kind-Prozess unter der unmittelbaren Kontrolle eines Prozess-Supervisors. Dieser kann ihm jederzeit beliebige Signale senden, und kann sich auf Wunsch auch direkt um das Logging kümmern. Als kleinen Nebeneffekt lässt sich der Prozess in der Prozessliste auch noch sehr schön strukturiert betrachten. Hier ein praktisches Beispiel:
$ ps fuxwww
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
uber 27647 0.0 0.0 91052 1828 ? S 23:13 0:00 sshd: uber@pts/2
uber 27648 3.5 0.0 67668 3156 pts/2 Ss 23:13 0:00 \_ -bash
uber 27698 0.0 0.0 65568 932 pts/2 R+ 23:13 0:00 \_ ps fux
uber 11843 0.0 0.0 3832 392 ? S Jan14 0:00 /command/svscan /home/uber/service
uber 11931 0.0 0.0 3660 336 ? S Jan14 0:00 \_ supervise uberspace
uber 27529 3.3 1.2 182388 81112 ? S 23:13 0:01 | \_ perl /home/uber/Uberspace/script/uberspace_fastcgi.pl --listen /var/www/virtual/uber/uberspace.sock --keeperr
uber 11932 0.0 0.0 3660 336 ? S Jan14 0:00 \_ supervise log
uber 11949 0.0 0.0 3804 408 ? S Jan14 0:00 \_ multilog t ./main
Der Prozess svscan ist gewissermaßen das Herzstück: Er überwacht ein Verzeichnis (hier entsprechend /home/uber/service) mit Diensten. Findet es einen Dienst vor, so wie hier den Dienst uberspace, startet es ihn unter einem supervise-Prozess. supervise führt den Dienst dann als Kind-Prozess - in diesem Fall eine in Perl entwickelte Catalyst-Applikation. Auf gleicher Hierarchie findet sich außerdem ein log-Prozess, der alle Ausgaben des Diensts mit multilog loggt.
Letztlich folgen die daemontools also der Unix-Devise, dass jedes Tool nur einen ganz bestimmten Job erledigen sollte. Damit erleichtert es gleichzeitig die Entwicklung von Daemons, denn jene brauchen keinen Code mehr, um Pid-Files zu schreiben und sich in den Hintergrund zu schieben, und sie können sich jeglichen Codes fürs Logging schenken, denn es reicht, einfach auf der Standardausgabe zu schreiben.
Du kannst von uns auf Wunsch problemlos dein eigenes ~/service-Verzeichnis bekommen, das genauso funktioniert wie das „große“ unter /service, unter dem die von uns betriebenen Dienste laufen. Das geht ganz einfach durch die Eingabe von uberspace-setup-svscan wie im Abschnitt Für Ungeduldige beschrieben. Dieser Vorgang ist eine einmalige Aktion - unter der für dich dann laufenden svscan-Instanz kannst du in Eigenregie beliebig viele Dienste einrichten. Und so geht's:
Jeder Dienst besteht aus einem Verzeichnis, das ein Script namens run beinhaltet. Dieser Name ist fest vorgegeben. Im einfachsten Fall ist run ein Shell-Script, das etwas in der Art exec was-auch-immer beinhaltet, mehr nicht.
Möchtest du Logging? Dann legst du in dem Dienstverzeichnis noch ein Unterverzeichnis log an und dort dann ebenfalls ein run-Script. Bei dieser Kombination erstellt supervise automatisch eine Pipe zwischen der Standardausgabe des Daemons und der Standardeingabe des Log-Prozesses, der dann typischerweise multilog einsetzt (dazu gleich mehr).
Fertig? Sind die Scripts ausführbar? Dann legst du einen Symlink dieses Verzeichnisses nach ~/service an. Innerhalb weniger Sekunden wird dein Daemon gestartet.
In Code ausgedrückt könnte das so aussehen, wenn du einen Daemon namens ~/bin/my-daemon entwickelt hast, der jetzt unter ~/service laufen soll:
[uber@helium ~]$ mkdir ~/etc/run-my-daemon
[uber@helium ~]$ cat <<__EOF__ > ~/etc/run-my-daemon/run
#!/bin/sh
exec ~/bin/my-daemon 2>&1
__EOF__
[uber@helium ~]$ chmod +x ~/etc/run-my-daemon/run
[uber@helium ~]$ mkdir ~/etc/run-my-daemon/log
[uber@helium ~]$ cat <<__EOF__ > ~/etc/run-my-daemon/log/run
#!/bin/sh
exec multilog t ./main
__EOF__
[uber@helium ~]$ chmod +x ~/etc/run-my-daemon/log/run
[uber@helium ~]$ ln -s ~/etc/run-my-daemon ~/service/my-daemon
Du kannst das mit unserem Setup-Tool auch abkürzen, dem du einfach den Namen des Service (hier: my-daemon) sowie den Pfad zum auszuführenden Daemon (hier: ~/bin/my-daemon) mitgibst:
[uber@helium ~]$ uberspace-setup-service my-daemon ~/bin/my-daemon
Creating the ~/etc/run-my-daemon/run service run script
Creating the ~/etc/run-my-daemon/log/run logging run script
Symlinking ~/etc/run-my-daemon to ~/service/my-daemon to start the service
Waiting for the service to start ... 1 2 3 started!
Congratulations - the ~/service/my-daemon service is now ready to use!
Das war's! Nicht nur, dass dein Daemon jetzt läuft; er wird auch beim Booten automatisch gestartet, und sollte er einmal crashen (was ja nun mal auch den Besten von uns passieren kann), so wird er automatisch neu gestartet.
Wichtig für deinen Daemon ist eben: Er darf sich nicht in den Hintergrund schieben (sonst ist supervise der Meinung, er habe sich beendet, und startet eine neue Instanz).
Sobald der Daemon einmal gestartet wurde, kannst du ihn mittels des Tools svc (service control) verwalten. Du gibst ihm mit einem Schalter das gewünschte Signal an und danach das Verzeichnis, in dem der Dienst läuft, den du steuern willst, wobei du auch gleich mehrere hintereinander angeben kannst. Die gängigsten Schalter sind:
-u | up, also Dienst starten |
-d | down, also Dienst beenden |
-h | hup, ein HUP-Signal senden (Reload) |
Ein vollständiger Aufruf sähe also so aus:
[uber@helium ~]$ svc -h ~/service/my-daemon
Du kannst Switche auch kombinieren, beispielsweise Stop und anschließend einen erneuten Start:
[uber@helium ~]$ svc -du ~/service/my-daemon
Bitte beachte, dass das Logging (wenn du dafür auch einen Dienst angelegt hat) unter einem separaten supervise-Prozess läuft. Änderst du also etwas am log/run-Script, musst du anschließend das Logging neu starten:
[uber@helium ~]$ svc -du ~/service/my-daemon/log
Es gibt noch einige weitere Switche, die du im Alltag aber eher selten brauchen wirst. Die Dokumentation zu svc verrät dir mehr dazu.
Im obigen Beispiel haben wir direkt multilog fürs Logging eingesetzt. Es nimmt auf der Standardeingabe zu loggende Daten entgegen, kann diese auf Wunsch noch mit einem Timestamp versehen und schreibt sie in eine Datei. Dabei nimmt es eine automatische Log-Rotation vor, die on the fly funktioniert: Erreichen Logs eine bestimmte Größe, werden sie automatisch rotiert; die Anzahl der Logs kannst du ebenso selbst bestimmen. Außerdem kannst du mit multilog auch einfaches Pattern Matching machen, um bestimmte Meldungen aus den Logs einfach direkt rauszuhalten, noch bevor sie geschrieben werden. Und schließlich bietet es auch noch die Möglichkeit, Logs vor der Rotation durch einen anderen Prozess zu schicken, der sie beispielsweise irgendwie statistisch auswertet oder komprimiert.
Im obigen Beispiel hatten wir den einfach Fall, bei dem fast nur Standardeinstellungen verwendet werden; lediglich Timestamps haben wir ergänzt; dafür sorgt das t:
multilog t ./main
Das angegebene Verzeichnis ./main ist das, wohin multilog dann die Logs schreibt. Hierbei loggt es stets in eine Datei namens current, die bei Erreichen der vorgegebenen Größe dann in eine Datei umbenannt wird, die einen TAI64N-Zeitstempel als Name trägt (auch die Timestamps innerhalb des Logs, die mit t aktiviert werden, tragen dieses Format).
Die Logs werden standardmäßig beim Erreichen einer Größe von 99999 Bytes rotiert (zumindest ungefähr, weil multilog immer versucht, sauber an Zeilenumbrüchen zu rotieren), und es werden standardmäßig 10 Logfiles aufbewahrt. Auf diese Weise belegen die Logfiles von Haus aus maximal ca. 1 MB Speicherplatz in deinem Uberspace. Möchtest du größere Logs und/oder mehr davon aufbewahren, kannst du das mit den s- und n-Schaltern regeln (size bzw. number):
multilog t s999999 n20 ./main
Werden die Logs größer, möchtest du vielleicht zumindest die rotierten Logfiles komprimieren - gut, dass sich über die Preprozessor-Option ! unter anderem auch gzip einbinden lässt, was ohne weiteren Angaben die Standardeingabe liest, komprimiert und auf die Standardausgabe schreibt:
multilog t s999999 n20 '!/bin/gzip' ./main
Bitte beachte, dass du diese Angabe in einfache Anführungszeichen schreiben musst, weil ansonsten das Ausrufezeichen bereits von der Shell und nicht von multilog interpretiert wird.
Noch ein abschließendes Wort zu den Timestamps: Mit TAI64N hattest du vermutlich bisher wenig zu tun, und offen gesagt ist es auch in erster Linie für Maschinen lesbar. Die daemontools beinhalten aber auch das Tool tai64nlocal, das - als Pipe eingesetzt - die TAI64N-Timestamps in lokale Zeitangaben umwandeln. Du könntest dir deine Logs also mit konvertierten Zeitstempeln beispielsweise so anschauen:
[uber@helium ~]$ cat ~/service/my-daemon/log/main/* | tai64nlocal | less
Wenn du den von dir eingerichteten Dienst nicht mehr brauchst, kannst du ihn natürlich auch wieder entfernen. Das ist ganz simpel: Du wechselst in das Verzeichnis, in dem der Dienst liegt, löschst dann den dafür Symlink und stoppst anschließend die noch laufenden Prozesse (also sowohl den Dienst selbst als auch das zugehörige multilog als auch die beiden überwachenden supervise-Instanzen). Beispiel:
[uber@helium ~]$ cd ~/service/my-daemon
[uber@helium my-daemon]$ rm ~/service/my-daemon
[uber@helium my-daemon]$ svc -dx . log
[uber@helium my-daemon]$ rm -rf ~/etc/run-my-daemon
Auf diese Weise wird der Dienst sauber entfernt. In der vorletzten Zeile findest du ein Beispiel für kombinierte svc-Switches: Der Dienst soll erst gestoppt werden (-d) und anschließend wird auch der überwachende supervise-Prozess gestoppt (-x). Im Alltag gibt es keinen Anlass, das zu tun, weshalb wir -x auch nicht in der obigen Tabelle aufgeführt haben; es ergibt ausschließlich beim Entfernen eines Daemons Sinn. Außerdem siehst du hier ein Beispiel, dass du svc auch mehrere Dienste übergeben kannst, im konkreten Fall erst . (also das aktuelle Verzeichnis, in das wir ja gerade gewechselt haben) und dann log (also das Unterverzeichnis log relativ zum aktuellen Verzeichnis).