Wird auch mal Zeit, dass einer reagiert…

Dass unsere Gesellschaft es sich gut und gerne möglichst bequem macht hat Bernd schon hinreichend ausgeführt.

Dies gilt allerdings nicht nur im politischen Kontext, sondern auch im IT-Kontext.

Paradebeispiel

M$ Windows gehört aktuell der Löwenanteil des Desktop-PC-Marktes – aber nicht unbedingt der Qualität wegen, sondern u.a. aus folgendem Grund:

Einige (vor allem im gewerblichen Bereich) häufig unumgängliche “Killer-Apps” wie M$ Office oder Adobe Photoshop laufen schlichtweg nur auf Windows (oder Mac OS).

Um von diesem hoch unsicheren, kostenpflichtigen, spionierenden und nicht-quelloffenen Betriebssystem wegzukommen, muss der Benutzer zumindest folgende Hürden überwinden:

  • (darauf kommen, dass überhaupt ein Betriebssystem außer Windows und Mac OS existiert)
  • bspw. GNU/Linux herunterladen und auf CD/DVD/USB-Stick brennen
  • davon booten und das System installieren
  • sich in das System und die Unterschiede zu Windows einarbeiten
  • Alternativen für ihre Killer-Apps suchen (sofern überhaupt vorhanden!)
  • sich in letztgenannte einarbeiten…

In solchen Fällen habe ich volles Verständnis für den Sieg der Faulheit. Henry Ford hat mal gesagt:

Denken ist die schwerste Arbeit, die es gibt. Das ist wahrscheinlich auch der Grund, warum sich so wenige Leute damit beschäftigen.

Aber das geht auch viel einfacher – ohne als böser Raubkopierer vom rechten Weg abzukommen – mit …

ReactOS

ReactOS ist keine GNU/Linux-Distribution, die es sich zur Aufgabe gemacht hat, Windows äußerlich möglichst ähnlich zu sehen.

Es ist ein eigenständiges, von Grund auf geschriebenes Betriebssystem, dessen Ziel es ist, möglichst vollständig zu Windows kompatibel zu sein.

Ich selbst habe ReactOS ausprobiert und teile meine Erfahrungen hier mit euch…

Ran an die Buletten!

Wie üblich habe ich das OS in einer virtuellen Maschine ausprobiert – konkret mit VirtualBox. Nachdem ich das ISO-Abbild heruntergeladen habe erstellte ich eine neue virtuelle Maschine…

Es ist wichtig, dass das Modell der (virtuellen) Netzwerkkarte mit dem von mir gewählten übereinstimmt (3. Bild) – sonst kann es passieren, dass das Netzwerk überhaupt nicht funktioniert.

Sobald der Bootvorgang abgeschlossen war, fand ich einen Windows-XP-ähnlichen Assistenten vor – mit dem Unterschied, dass ich die Sprache frei wählen konnte. (Pfeiltasten oben/unten, dann Eingabetaste)

Sobald das vollbracht ist, muss eigentlich nur noch die Eingabetaste malträtiert werden, bis dieser Assistent durch ist…

Dann gilt es einen Neustart abzuwarten. Beim darauf folgenden Assistenten muss man fast genau so wenig den Kopf anstrengen.

Ich habe zwar Name und Organisation angegeben, hätte aber auch genau so gut immer nur direkt “weiter” drücken können…

Nachdem einem weiteren Neustart kann man auch schon loslegen. Fehlermeldungen wie diese habe ich für meinen Teil bedenkenlos weggeklickt:

Aber…

Da war noch was…

Ein gesunder Geist in einem gesunden Körper. Und ein guter Web-Browser auf einem guten Betriebssystem.

Ich habe die Firefox-Versionen 3.6, 28 und 45 aus dem ReactOS-Paketmanager (Ja, Paketmanager! 😀 ) ausprobiert und meine, dass 28 aktuell der beste Kompromiss aus Funktionalität und Stabilität ist…

Fazit

Ist ReactOS nun für den Produktivbetrieb zu empfehlen?

Meiner Meinung nach noch definitiv nicht.

Neben der o.g. mangelhaften Unterstützung von Netzwerkkarten ist das System (… wie sagt man da noch gleich politisch korrekt …) leicht absturzgefährdet.

Konkret habe ich versucht, mir Visual Studio 2015 Community herunterzuladen (erstmal nur herunterzuladen!) …

(-.-“)

Alexander Klimov

Autor: Alexander Klimov

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren – tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.

Schwarze Magie für GNU/Linux-Nerds

“Linux ist der beste Virenschutz”, heißt es in “Fachkreisen”. “Installiere Linux und Du wirst fortan ruhiger schlafen können!” Ähm… falsch.

Ja, Linux ist ein guter Anfang was die Herstellung der Sicherheit auf dem eigenen Rechner betrifft. Aber es hilft einem nichts ohne brain.exe. Vor allem wenn man es mit einem Magier zu tun bekommt…

Mögen die Spiele beginnen!

ps -fu nagios

Wer auf das Monitoring-System Zugriff hat, hat viel Macht.

Wo Bernd recht hat, hat er recht – nicht umsonst nimmt z. B. Gunnars Jabber-Notification-Skript Nutzername und Passwort via Umgebungsvariablen entgegen. Check- und Notification-Skripte, die diesem Beispiel nicht folgen, reißen eine Sicherheitslücke auf, die jeder Systemnutzer ganz einfach ausnutzen kann:

aklimov@icinga2:~$ ps -fu nagios
UID        PID  PPID  C STIME TTY          TIME CMD
nagios    8128     1  0 11:10 ?        00:00:00 /usr/lib/x86_64-linux-gnu/icinga2/sbin/icinga2 --no-stack-rlimit daemon -e /var/log/icinga2/error.log
nagios    8148  8128  0 11:10 ?        00:00:00 /usr/lib/x86_64-linux-gnu/icinga2/sbin/icinga2 --no-stack-rlimit daemon -e /var/log/icinga2/error.log
nagios    8433  8148  0 11:17 ?        00:00:00 /usr/lib/nagios/plugins/check_ping -6 -H ::1 -c 200,15% -w 100,5%
nagios    8434  8433  0 11:17 ?        00:00:00 /bin/ping6 -n -U -w 10 -c 5 ::1
nagios    8435  8148  0 11:17 ?        00:00:00 /usr/lib/nagios/plugins/check_ping -4 -H 127.0.0.1 -c 200,15% -w 100,5%
nagios    8436  8148  0 11:17 ?        00:00:00 /usr/lib/nagios/plugins/check_ping -H 127.0.0.1 -c 5000,100% -w 3000,80%
nagios    8437  8435  0 11:17 ?        00:00:00 /bin/ping -n -U -w 10 -c 5 127.0.0.1
nagios    8438  8436  0 11:17 ?        00:00:00 /bin/ping -n -U -w 30 -c 5 127.0.0.1

Schwarze Magie ist hier noch nicht im Spiel. (Aber dieses Beispiel ist auch nur zum Aufwärmen.)

strace -p

Was tut ein Programm so alles wenn ich gerade mal nicht hinschaue? Um diese Frage zu beantworten, wird nicht zwangsläufig der Quellcode benötigt. In vielen Fällen reicht bereits strace:

aklimov@WS-AKlimov:~$ echo $$  # Prozess-ID der Shell
20901
aklimov@WS-AKlimov:~$ strace -p20901
strace: Process 20901 attached
wait4(-1,

Der Ausgabe können wir entnehmen, dass die Shell gerade darauf wartet, dass ein beliebiger Kindprozess (z. B. strace) sich beendet.

Diese Art der Hexenkunst hat sich bereits rumgesprochen und die Kernel-Entwickler haben ihre Wirkung eingedämmt. Daher konnte ich ohne weiteres nur den Elternprozess von strace als Beispiel nehmen.

gdb -p

Ich erinnere mich noch genau an diese eine AWP-Unterrichtsstunde in der Berufsschule:
Es ging darum, dass ein C/C++-Programm auf mehrere Module aufgeteilt werden kann. Mit dem static-Schlüsselwort sei es möglich, “modulglobale” Variablen zu erstellen – d. h. Variablen, die zwar global, aber nur für das eigene Modul sichtbar sind. Ein Argument des Lehrers für solche Variablen war allen Ernstes die Sicherheit. Und die Übung zu diesem Thema bestand darin, in einem Modul ein Passwort vor den anderen Modulen zu verbergen.
Wenn der wüsste…

Zunächst starte ich in einem neuen Terminal cat:
aklimov@WS-AKlimov:~$ cat

Damit haben wir auch schon unseren “Opfer”-Prozess, der im konkreten Fall auf eingehende Daten wartet.

Darauf hin öffne ich ein zweites Terminal (mit den gleichen Benutzerrechten!) und starte gdb:
aklimov@WS-AKlimov:~$ ps -ef |grep cat
aklimov  10217 10149  0 12:25 pts/11   00:00:00 cat
aklimov  11088 10298  0 12:26 pts/12   00:00:00 grep cat
aklimov@WS-AKlimov:~$ gdb -p 10217
GNU gdb (Debian 7.11.1-2+b1) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
(...)
Attaching to process 10217
(...)
(gdb)

Die schwarze Magie daran…

… besteht darin, dass ich nun innerhalb dieses Prozesses schalten und walten kann wie ich will. (Und sämtlichen Speicher aller Module auslesen.)

Beispielsweise kann ich den Standard-Eingabe-Datenstrom (Stdin) des Prozesses on-the-fly durch /dev/null ersetzen (ohne ihn zu verlieren):

Aktion (Beschreibung) Aktion (GDB) Datei-Deskriptoren
/dev/null öffnen (gdb) p open("/dev/null", 0)
$1 = 3
0 /dev/pts/1
3 /dev/null
Stdin sichern (gdb) p dup(0)
$2 = 4
0, 4 /dev/pts/1
3 /dev/null
Datenstrom umleiten (gdb) p dup2(3, 0)
$3 = 0
4 /dev/pts/1
0, 3 /dev/null
Redundanten Deskriptor schließen (gdb) p close(3)
$4 = 0
4 /dev/pts/1
0 /dev/null
Programm-Ausführung fortsetzen (gdb) c
Continuing.
[Inferior 1 (process 10217) exited normally]
(gdb)
N/A

Fazit

Linux nimmt einem keinerlei Sicherheitsfragen vollständig ab. Es gibt einem lediglich die Möglichkeit, sich selbst um diese zu kümmern (und sie nicht der NSA zu überlassen).

Gehet hin und sichert euch!

Alexander Klimov

Autor: Alexander Klimov

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren – tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.

In Datenbanken Indizes benutzen Du sollst!

… hat Bernd schon verlauten lassen.

Das heißt aber nicht, dass pauschal jede Spalte jeder Tabelle indiziert gehört – und es funktioniert. Denn bei jeder Schreiboperation müssen auch alle betroffenen Indizes mit aktualisiert werden. Dies sollte berücksichtigt werden, wenn in eine Tabelle potenziell viel geschrieben wird, aber die Lesevorgänge sich in Grenzen halten. (Beispiel: Icinga 2 DB IDO)

Im folgenden möchte ich genauer auf die Anwendungsfälle von DB-Indizes eingehen. Zwecks Demonstration habe ich eine SQLite-DB mit folgendem Schema angelegt:

CREATE TABLE myheavytable0 (
  c0 INTEGER, c1 INTEGER, c2 INTEGER, c3 INTEGER, c4 INTEGER,
  c5 INTEGER, c6 INTEGER, c7 INTEGER, c8 INTEGER, c9 INTEGER
);
CREATE TABLE myheavytable1 (
  c0 INTEGER, c1 INTEGER, c2 INTEGER, c3 INTEGER, c4 INTEGER,
  c5 INTEGER, c6 INTEGER, c7 INTEGER, c8 INTEGER, c9 INTEGER
);

(2 Tabellen mit je 10 Ganzzahlen-Spalten)

Diese Tabellen habe ich mit je 10.000.000 Zeilen Zufallszahlen befüllt. Im übrigen ist die Indizierung numerischer Spalten potenziell besser als die Indizierung von Text-Spalten.

Anwendungsfall #1: Joins

Die Herstellung von Beziehungen zwischen verschiedenen Entitätsmengen mit Hilfe von Fremdschlüsseln ist bei relationalen Datenbanken das Tagesgeschäft. Es kann sich durchaus lohnen, die entsprechenden Spalten zu indizieren.

Zwecks Demonstration wird das Schema wie folgt ergänzt:

CREATE INDEX myheavyindex0c0 ON myheavytable0 (c0);
CREATE INDEX myheavyindex0c1 ON myheavytable0 (c1);
CREATE INDEX myheavyindex1c0 ON myheavytable1 (c0);
CREATE INDEX myheavyindex1c1 ON myheavytable1 (c1);

(Die ersten 2 Spalten beider Tabellen werden jeweils einzeln indiziert.)

Die Auswirkungen können mit Messungen der Ausführungszeiten folgender Abfragen veranschaulicht werden:

Abfrage Zeit
SELECT COUNT(*) FROM myheavytable0 m0 INNER JOIN myheavytable1 m1 ON m0.c2 = m1.c3 1m 6.519s
SELECT COUNT(*) FROM myheavytable0 m0 INNER JOIN myheavytable1 m1 ON m0.c0 = m1.c3 20.836s
SELECT COUNT(*) FROM myheavytable0 m0 INNER JOIN myheavytable1 m1 ON m0.c0 = m1.c1 4.468s

Daraus folgt: Werden 2 Tabellen mit Hilfe von je einer Spalte, von denen eine indiziert ist, gejoint, ist die Abfrage etwa 3x so schnell wie mit 2 nicht-indizierten Spalten. Sind beide Spalten indiziert, ist die Abfrage fast 15x so schnell.

Anwendungsfall #2: Verrechnung mehrerer Spalten

Manchmal müssen 2 Attribute einer Entität in einer Abfrage miteinander verrechnet werden. Viele Datenbanken bieten lobenswerterweise auch die Möglichkeit, Ausdrücke zu indizieren:

CREATE INDEX myheavyindex0e0 ON myheavytable0 (c4 + c5);
Abfrage Zeit
SELECT COUNT(*) FROM myheavytable0 WHERE c0 + c1 > 8000000 1.324s
SELECT COUNT(*) FROM myheavytable0 WHERE c4 + c5 > 8000000 0.513s

Daraus folgt: Wenn ein indizierter Ausdruck abgefragt wird, ist die Abfrage fast 3x so schnell wie wenn alle am Ausdruck beteiligten Spalten einzeln indiziert sind.

Anwendungsfall #3: Filter auf mehrere Spalten

Wenn abgefragte Daten häufig auf mehreren Spalten basierend gefiltert werden, kann es sich lohnen, diese Spalten zusammen zu Indizieren:

CREATE INDEX myheavyindex0m0 ON myheavytable0 (c6, c7);
Abfrage Zeit
SELECT COUNT(*) FROM myheavytable0 WHERE c0 < 8000000 AND c1 > 8000000 12.130s
SELECT COUNT(*) FROM myheavytable0 WHERE c6 < 8000000 AND c7 > 8000000 0.445s

Daraus folgt: Bei zusammen indizierten Spalten ist die Abfrage 27x so schnell wie bei einzeln indizierten.

Fazit

Bei Nutzung von Indizes hängt viel vom Einzelfall ab. Im Zweifelsfall sollte ein Fachmann zu Rate gezogen werden.

Alexander Klimov

Autor: Alexander Klimov

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren – tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.

Ich arbeite nicht mit Git – Git arbeitet für mich

Immer wenn ich daran denke, was für ein praktisches Werkzeug Git doch ist, setzt es noch einen drauf. Als ob mir git bisect nicht schon genug Arbeit abnehmen würde, bin ich jetzt auch noch über git rerere gestolpert.

Was ist git rerere?

git rerere ist standardmäßig inaktiv und kann mit folgendem Befehl aktiviert werden:

$ git config --global rerere.enabled true

Sobald das getan ist, zeichnet Git auf, wie der Entwickler Zusammenführungs-Konflikte auflöst und verwendet diese Aufzeichnungen ggf. wieder.

Erklärung am Beispiel

Wir beginnen mit einem leeren Repository, das wir mit ein paar Commits füllen:

$ mkdir rerere-demo && cd $_
$ git init
Initialisierte leeres Git-Repository in /home/aklimov/rerere-demo/.git/
$ echo A >Buchstabensalat.txt
$ git add Buchstabensalat.txt
$ git commit -m A
[master (Basis-Commit) 4d1a53f] A
 1 file changed, 1 insertion(+)
 create mode 100644 Buchstabensalat.txt
$ echo B >>Buchstabensalat.txt
$ git commit -am B
[master fe71b2b] B
 1 file changed, 1 insertion(+)
$ git checkout HEAD^
$ echo C >>Buchstabensalat.txt
$ git commit -am C
[losgelöster HEAD 181ad2e] C
 1 file changed, 1 insertion(+)
$ git checkout -

Nun sollten wir so einen Baum vorfinden:

$ git --no-pager log --oneline --graph --decorate=full master 181ad2e
* 181ad2e C
| * fe71b2b (HEAD -> refs/heads/master) B
|/
* 4d1a53f A

Jetzt versuchen wir, die Commits B und C zusammenzuführen, was natürlich erfolgreich fehlschlagen wird. Wir müssen also den Konflikt manuell auflösen:

$ git merge 181ad2e
automatischer Merge von Buchstabensalat.txt
KONFLIKT (Inhalt): Merge-Konflikt in Buchstabensalat.txt
Recorded preimage for 'Buchstabensalat.txt'
Automatischer Merge fehlgeschlagen; beheben Sie die Konflikte und committen Sie dann das Ergebnis.
$ git --no-pager diff
diff --cc Buchstabensalat.txt
index 35d242b,8ec30d8..0000000
--- a/Buchstabensalat.txt
+++ b/Buchstabensalat.txt
@@@ -1,2 -1,2 +1,6 @@@
  A
++<<<<<<< HEAD
 +B
++=======
+ C
++>>>>>>> 181ad2e
$ git --no-pager diff
diff --cc Buchstabensalat.txt
index 35d242b,8ec30d8..0000000
--- a/Buchstabensalat.txt
+++ b/Buchstabensalat.txt
@@@ -1,2 -1,2 +1,3 @@@
  A
 +B
+ C
$ git add Buchstabensalat.txt
$ git commit --no-edit
Recorded resolution for 'Buchstabensalat.txt'.
[master 279715c] Merge commit '181ad2e'
$ git --no-pager log --oneline --graph --decorate=full
*   279715c (HEAD -> refs/heads/master) Merge commit '181ad2e'
|\
| * 181ad2e C
* | fe71b2b B
|/
* 4d1a53f A

Die Wirkung von git rerere macht sich bereits bei den rot hervorgehobenen Zeilen bemerkbar. An diesen Stellen wird aufgezeichnet, wie die Konflikt-Datei vorher bzw. nachher ausgesehen hat. Um zu sehen, was uns das bringt, verwerfen wir den Merge-Commit und führen erneut zusammen:

$ git reset --hard HEAD^
HEAD ist jetzt bei fe71b2b B
$ git --no-pager log --oneline --graph --decorate=full master 181ad2e
* 181ad2e C
| * fe71b2b (HEAD -> refs/heads/master) B
|/
* 4d1a53f A
$ git merge 181ad2e
automatischer Merge von Buchstabensalat.txt
KONFLIKT (Inhalt): Merge-Konflikt in Buchstabensalat.txt
Resolved 'Buchstabensalat.txt' using previous resolution.
Automatischer Merge fehlgeschlagen; beheben Sie die Konflikte und committen Sie dann das Ergebnis.
$ git --no-pager diff
diff --cc Buchstabensalat.txt
index 35d242b,8ec30d8..0000000
--- a/Buchstabensalat.txt
+++ b/Buchstabensalat.txt
@@@ -1,2 -1,2 +1,3 @@@
  A
 +B
+ C
$ git add Buchstabensalat.txt
$ git commit --no-edit
[master cb5c96b] Merge commit '181ad2e'
$ git --no-pager log --oneline --graph --decorate=full
*   cb5c96b (HEAD -> refs/heads/master) Merge commit '181ad2e'
|\
| * 181ad2e C
* | fe71b2b B
|/
* 4d1a53f A

Wie wir sehen können, hat Git, unmittelbar nachdem es einen Konflikt entdeckt hat, diesen automatisch mit Hilfe der vorher gespeicherten Aufzeichnung aufgelöst.

Da geht noch was

Selbstverständlich ist die Funktionalität von git rerere nicht auf so einfache Fälle beschränkt. Nehmen wir einmal an, dass wir nicht alleine an diesem Repository arbeiten und ein Kollege bereits etwas in den master-Zweig hochgeladen hat:

$ git checkout fe71b2b
$ vi Buchstabensalat.txt
$ git commit -am D
[losgelöster HEAD 779312b] D
 1 file changed, 1 insertion(+)
$ git checkout -
$ git --no-pager log --oneline --graph --decorate=full master 779312b
* 779312b D
| *   cb5c96b (HEAD -> refs/heads/master) Merge commit '181ad2e'
| |\
|/ /
| * 181ad2e C
* | fe71b2b B
|/
* 4d1a53f A

Wir müssen also unseren Merge-Commit über D legen, bevor wir pushen können:

$ git rebase --preserve-merges 779312b
Resolved 'Buchstabensalat.txt' using previous resolution.
automatischer Merge von Buchstabensalat.txt
KONFLIKT (Inhalt): Merge-Konflikt in Buchstabensalat.txt
Automatischer Merge fehlgeschlagen; beheben Sie die Konflikte und committen Sie dann das Ergebnis.
Error redoing merge cb5c96bcb52306dfbdcc8c728c6d356efa3ed9da
$ git --no-pager diff
diff --cc Buchstabensalat.txt
index f5a7bda,8ec30d8..0000000
--- a/Buchstabensalat.txt
+++ b/Buchstabensalat.txt
@@@ -1,3 -1,2 +1,4 @@@
 +D
  A
 +B
+ C
$ git add Buchstabensalat.txt
$ git rebase --continue
[losgelöster HEAD 5096395] Merge commit '181ad2e'
Successfully rebased and updated refs/heads/master.
$ git --no-pager log --oneline --graph --decorate=full
*   5096395 (HEAD -> refs/heads/master) Merge commit '181ad2e'
|\
| * 181ad2e C
* | 779312b D
* | fe71b2b B
|/
* 4d1a53f A

Fazit

Ich kann nicht für alle erdenklichen Teams sprechen, aber wir bei Icinga Web 2 werden uns jedenfalls nicht mehr absprechen müssen, wer gerade Konflikte auflöst und wer pushen darf. 🙂

Alexander Klimov

Autor: Alexander Klimov

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren – tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.

Jetzt wird der lahmen Schlange der Prozess gemacht

Python poweredEiner meiner Kollegen ist in diesem Beitrag bereits auf Python-betriebene Web-Server (WSGI) eingegangen. Ein mit make_server() erstellter Standard-WSGI-Server kann zur selben Zeit leider nur eine Anfrage bearbeiten, da weder Multithreading, noch Multiprocessing Verwendung finden. Mit dem Wissen wie es geht ist dies relativ einfach nachzurüsten und genau darauf will ich im folgenden eingehen.

Threads oder Prozesse?

Die Threadsicherheit von Python steht außer Frage. Die Umsetzung lässt allerdings zu wünschen übrig: Der Global Interpreter Lock verhindert gleichzeitiges Ausführen von Python-Bytecode in mehreren Threads. Somit sind Threads in Python mehr oder weniger für die Katz und es bleiben nur noch die Prozesse.

Diese können etwas umständlicher zu verwalten sein, bringen aber dafür zumindest einen wesentlichen Vorteil mit sich: Die einzelnen Prozesse sind voneinander komplett unabhängig.

Daraus folgt: Wenn ein Prozess abstürzt, bleibt der Rest davon unberührt. Daraus wiederum folgt: Wenn ein Prozess “amok läuft” kann er problemlos “abgeschossen” werden, um die System-Ressourcen nicht weiter zu belasten. (Im realen Leben undenkbar.)

Ein Server für alle Netzwerk-Schnittstellen

Wer weiß, dass seine Anwendung immer auf allen Netzwerk-Schnittstellen (oder nur auf einer) wird lauschen sollen, der hat ungleich weniger Aufwand bei der Implementierung:

from socket import AF_INET
from SocketServer import ForkingMixIn
from wsgiref.simple_server import make_server, WSGIServer

class ForkingWSGIServer(ForkingMixIn, WSGIServer):
    address_family = AF_INET

def app(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    yield 'Hallo Welt!'

if __name__ == '__main__':
    make_server('0.0.0.0', 8080, app, server_class=ForkingWSGIServer).serve_forever()

 

Erklärung

Nach der Python-üblichen Unmenge an Importen wird die Klasse ForkingWSGIServer definiert – ein WSGIServer kombiniert mit dem ForkingMixIn. Dies funktioniert einwandfrei, da WSGIServer von BaseHTTPServer.HTTPServer erbt und letztgenannter wiederum ein SocketServer.TCPServer ist. HTTPServer, WSGIServer und ForkingMixIn überschreiben völlig unterschiedliche Methoden und kommen sich somit nicht in die Quere.

Darauf folgt eine beispielhafte WSGI-Anwendung, die theoretisch durch alles erdenkliche ersetzt werden kann.

Schlussendlich wird ein ForkingWSGIServer erstellt und er wird angewiesen, für einen unbestimmten Zeitraum die beispielhafte WSGI-Anwendung auf Port 8080 anzubieten.

Arbeitsweise

Der Server wird für jede ankommende HTTP-Anfrage einen neuen Prozess starten und diesen sie bearbeiten lassen. Während dessen kann er bereits einen neuen Prozess für die nächste schon wartende Anfrage erzeugen, usw..

Den Server sauber herunterfahren

Wenn der oben beschriebene Server z.B. via Strg-C o.ä. beendet wird, werden alle aktiven Verbindungen abrupt abgebrochen. Wenn das unerwünscht ist, kann der Server wie folgt erweitert werden:

import os
import sys
from signal import signal, SIGTERM
if __name__ == '__main__':
    signal(SIGTERM, (lambda signum, frame: sys.exit()))
    
    try:
        make_server('0.0.0.0', 8080, app, server_class=ForkingWSGIServer).serve_forever()
    finally:
        try:
            while True:
                os.wait()
        except OSError:
            pass

 

Erklärung

Kurz vor dem Start des Servers wird ein Signal-Handler registriert. Dieser sorgt dafür, dass der Python-Interpreter sich sauber beendet (sys.exit()) wenn er ein TERM-Signal erhält.

Diese Art der Beendigung wird abgefangen, um vor dem tatsächlichen Stopp auf alle noch laufenden Kindprozesse zu warten.

Ergebnis

Strg-C funktioniert zwar immer noch nicht so wie es soll, aber dafür der kill-Befehl.

Ein Server pro Netzwerk-Schnittstelle

Obwohl die gerade beschriebene Implementation einfach und schnell zu bewerkstelligen ist, kommt es vor, dass eine Anwendung auf mehreren, aber nicht auf allen Netzwerk-Schnittstellen lauschen soll. Hierzu muss der Haupt-Prozess für jede Schnittstelle einen weiteren Prozess, den eigentlichen WSGI-Server, erstellen.

import os
import sys
from multiprocessing import Process
from signal import signal, SIGTERM
from socket import AF_INET, AF_INET6
from SocketServer import ForkingMixIn
from time import sleep
from wsgiref.simple_server import make_server, WSGIServer


def print_msg(prefix, msg):
    print >>sys.stderr, '<{0}> {1}'.format(prefix, msg)


class ForkingWSGIServer(ForkingMixIn, WSGIServer):
    address_family = AF_INET


class ForkingWSGI6Server(ForkingWSGIServer):
    address_family = AF_INET6


def app(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    yield 'Hallo Welt!'


def run_forking_server(host, port, address_family=AF_INET):
    pid = os.getpid()

    print_msg(pid, 'Serving on {0}:{1}..'.format(host, port))

    server_class = ForkingWSGI6Server if address_family == AF_INET6 else ForkingWSGIServer
    try:
        make_server(host, port, app, server_class=server_class).serve_forever()
    finally:
        print_msg(pid, 'Shutting down..')

        try:
            while True:
                os.wait()
        except OSError:
            pass

        print_msg(pid, 'Exiting..')


if __name__ == '__main__':
    signal(SIGTERM, (lambda signum, frame: sys.exit()))

    processes = []

    port = 8080

    for (address_family, host) in (
        (AF_INET, '127.0.0.1'), (AF_INET6, '::1')
    ):
        p = Process(target=run_forking_server, args=(host, port, address_family))
        p.daemon = False
        p.start()
        processes.append(p)

    prefix = '{0} (root)'.format(os.getpid())

    print_msg(prefix, 'Waiting for SIGTERM..')
    try:
        while True:
            sleep(86400)
    finally:
        print_msg(prefix, 'Shutting down..')

        for p in processes:
            print_msg(prefix, 'Terminating {0}..'.format(p.pid))
            p.terminate()

        for p in processes:
            print_msg(prefix, 'Joining {0}..'.format(p.pid))
            p.join()

        print_msg(prefix, 'Exiting..')

 

Erklärung

Nach Registrierung des uns schon bekannten Signal-Handlers wird für jede Schnittstelle ein Prozess erstellt. Danach legt sich der Haupt-Prozess schlafen und wartet darauf, von einem SIGTERM aufgeweckt zu werden. Wenn das eintritt, terminiert er alle seine Kindprozesse und wartet, bis sie sich beendet haben.

Ein jeder dieser Kindprozesse startet wie gehabt einen WSGI-Server und wartet auf die Beendigung seiner Kindprozesse wenn ein SIGTERM eintrifft.

Fazit

Der vorgestellte Code ist rein demonstrativer Natur und entsprechend minimalistisch aufgebaut. Der Ursprung aller Dinge ist klein und dementsprechend ist noch viel Luft nach oben. Viel Spaß beim ausprobieren!

Vorausgesetzte Python-Version: 2.6 oder 2.7

Alexander Klimov

Autor: Alexander Klimov

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren – tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.