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 Aleksandrovič Klimov

Alexander hat Ende 2013 mit einem Praktikum bei NETWAYS gestartet. 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 und seiner Ausbildung bei uns friedliche Wege.