Neues in Python 3.4

Seite 3: I/O und Internes

Inhaltsverzeichnis

Schon im Jahr 1999 stellte Dan Kegel das C10K-Problem vor: Wie skaliert man Webserver, sodass sie 10.000 Verbindungen gleichzeitig bedienen können? Heutzutage sind die Fähigkeiten der Server und auch die Anforderungen nochmals um einige Größenordnungen gewachsen, sodass das Problem, hochskalierende Webserver zu schreiben, aktueller denn je ist. Das traditionelle Modell von Webservern wie Apache, für jede Anfrage einen eigenen Thread (oder gar Prozess) zu eröffnen und nach Beendigung wieder zu schließen, stößt für sehr viele Verbindungen an seine Grenzen.

Das die Methode schlecht skaliert, liegt einerseits daran, dass jeder Thread einen eigenen Stack-Speicher reserviert, was die Speicheranforderungen linear mit den Verbindungen ansteigen lässt. Zudem wird das Umschalten zwischen Threads vom Betriebssystemkernel verwaltet, was einen großen Aufwand erfordert.

Viele moderne Frameworks vermeiden daher Kernel-Threads und implementieren asynchrone Modelle im User Space. Populäre hochskalierende Frameworks in Python umfassen Twisted und Tornado oder das auf greenlet aufbauende gevent. Bisher fehlte allerdings eine Unterstützung für solche Modelle in Pythons Standardbibliothek - wenn man vom veralteten asyncore einmal absieht.

Angesichts der Bedeutung von Webservern ist die wohl wichtigste Neuerung in Python 3.4 das neu hinzugekommene Modul asyncio. Es bietet ein vollständiges Framework für nichtblockierende und
asynchrone Ausführung. Es wurde jedoch mit einem Fokus auf Erweiterbarkeit geschrieben, sodass sich leicht auf die Funktionen anderer Frameworks zugreifen lässt. asyncio bietet im Vergleich zu Frameworks wie Node.js einen wesentlichen Unterschied: Statt auf Callbacks setzt [I]asyncio[/i] auf Koroutinen oder Futures – Callbacks nutzender Code wird allerdings unterstützt. Als Synchronisationspunkte dienen yield from-Ausdrücke. asyncio bringt einen eigenen Mainloop mit, der unter Linux mit epoll, unter BSD und Mac OS X mit kqueue und unter Windows mit IOCP implementiert ist. Hier darf ein "Hello World!"-Beispiel nicht fehlen:

@asyncio.coroutine
def greet_every_second():
while True:
print('Hello World')
yield from asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.run_until_complete(greet_every_second())

Im Gegensatz zu Systemen, in denen jedes Mal aufs Neue ein Callback aufgerufen wird, ruft man die Funktion greet_every_second() nur einmal auf, danach bleibt sie aktiv. Bei yield from asyncio.sleep(1) übergibt die Koroutine die Kontrolle an den Mainloop. Nach einer Sekunde setzt Letzterer die Ausführung der Koroutine an eben der Stelle fort. Der Vorteil gegenüber Callbacks ist das Vermeiden sogenannter Pyramids of Doom, also unleserlich ineinander verschachtelter Callbacks. Es ist nicht notwendig, genau zu wissen, was yield from eigentlich macht – es ist fast immer ausreichend, so zu tun, als wäre der Code in Wirklichkeit sequenziell und blockierend geschrieben.

Neben Koroutinen stellt das Modul Futures bereit, die ähnlich wie Twisteds Deferreds gestaltet sind. Futures sind Objekte, die den Status einer asynchron laufenden Aufgabe abbilden und Methoden bereitstellen, um das Resultat abzufragen oder die Aufgabe abzubrechen. Als dritten Datentyp bietet asyncio sogenannte Tasks an – das sind Futures, in die Koroutinen eingebettet sind. Die Terminologie mag anfangs gewöhnungsbedürftig sein (und ist in der Dokumentation nicht besonders ausführlich erklärt), doch die Handhabung ist erstaunlich intuitiv. Diese Datentypen haben nichts Netzwerkspezifisches an sich, sondern bilden nur die asynchrone Laufweise ab. Man könnte sie so beispielsweise für die Spieleprogrammierung nutzen.

Auf der untersten Abstraktionsebene des I/O-Systems sind Transporte und Protokolle angesiedelt. Diese Begriffe sind, wie einige Designentscheidungen von asyncio, aus Twisted übernommen worden.

Während die Transporte für I/O und Pufferung zuständig sind, werden das Parsen der hereinkommenden Daten und die Schreibanfragen von den Protokollen geregelt. Von Haus aus bringt asyncio Transporte für TCP, UDP, SSL und Pipes mit.

Auf einer höheren Abstraktionsebene befindet sich die Stream-API, die viele Implementierungsdetails versteckt. Ein anschauliches Beispiel ist in der offiziellen Dokumentation zu finden. Es demonstriert nicht nur die verhältnismäßig einfache API, sondern auch die gute Lesbarkeit des Koroutinen-Konzepts.

Vermutlich werden viele Python-Nutzer asyncio nicht direkt nutzen, da es sich bewusst auf Basis- Funktionen beschränkt. Beispielsweise ist keine Implementierung des HTTP-Protokolls zu finden. Stattdessen sieht es sich als Grundlage für Webserver und moderne Web-Frameworks. Obwohl es noch jung ist, gibt es bereits auf asyncio aufbauende Projekte, etwa rainfall, das Tornado ähnelt, oder Vase, das von Flask inspiriert ist. Auch Autobahn hat ein asyncio-Backend.

Neben den schon erwähnten Modulen haben die Python-Entwickler in einer ganzen Reihe weiterer Module Verbesserungen vorgenommen. Alle diese Änderungen im Detail aufzulisten würde allerdings den Rahmen des Artikels sprengen, daher werden nun die wichtigsten Verbesserungen in Kürze beschrieben:

Das Serialisierungsformat pickle hat eine Reihe von Optimierungen erfahren (PEP 3154). In der aktuellsten Version des Formats verbrauchen serialisierte Daten weniger Speicherplatz, zudem ist die Serialisierung wie die Deserialisierung schneller. Um Abwärtskompatibilität zu gewährleisten, wird das neue Format nicht standardmäßig benutzt. Wer es verwenden möchte, muss explizit die Version 4 angeben oder pickle.HIGHEST_PROTOCOL verwenden:

with open("hello.pickle", "wb") as f:
pickle.dump("Hello World!", f, pickle.HIGHEST_PROTOCOL)

Neben pickle beherrscht Python ein weiteres Serialisierungsformat namens marshal, das vor allem intern benutzt wird, um kompilierten Python-Bytecode als .pyc-Datei zu speichern. Anders als bei pickle ist nicht garantiert, dass sich mit marshal serialisierte Daten in anderen Python-Versionen korrekt deserialisieren lassen. In Version 3.4 ist das von marshal verwendete Format deutlich kompakter geworden und benötigt so weniger Speicherplatz. Die Einsparungen belaufen sich auf etwa ein Viertel des vorher verwendeten Speichers.

Wie fast alles in Python ist auch das Importverhalten zur Laufzeit änderbar. Nachdem die Entwickler die Importlogik für Python 3.3 als importlib-Modul neu geschrieben hatten, wurde das Modul in der aktuellen Version um die Klasse ModuleSpec erweitert (PEP 0451). Die Klasse bündelt und strukturiert den Status importierbarer Module. Zwar war es auch vorher möglich, eine eigene Importlogik zu implementieren, doch lässt sich mit ModuleSpec einiges an unübersichtlichem Code vermeiden.

Eine ganze Reihe an sicherheitsrelevanten Änderungen hat das Modul ssl erfahren. Neben der Unterstützung von TLSv1.1 und TLSv1.2 gibt es neue Funktionen und Methoden für SSLContext, darunter create_default_context(), das einen SSLContext mit "einer vernünftigen Balance aus Sicherheit und Kompatibilität" erstellt. Es wird angeraten, den Rückgabewert der Funktion zu benutzen, wann immer ein context als Argument der API benötigt wird. Des Weiteren ließ sich die Fähigkeit zum Umgang mit Zertifikaten durch einige neue Funktionen erweitern. Nur für Windows sind die Funktionen enum_certificates() und enum_crls(), die Zertifikate und CRLs vom Windows cert store abfragen können.

Unter Unix unterstützt multiprocessing nun neben fork zwei weitere Arten, einen neuen Prozess zu starten: spawn, das unter Windows die Standardeinstellung ist, und forkserver, in dem ein separater Server-Prozess erstellt wird, von dem sich neue Prozesse per fork abspalten lassen, sobald sie benötigt werden. Die jeweilige Art kann man mit set_start_method() festlegen. Durch diese Änderungen werden potenzielle Sicherheitsprobleme vermieden, die damit zusammenhängen, dass mit fork erstellte Prozesse die Ressourcen des Elternprozesses erben. Zudem kann dessen Einsatz im Zusammenhang mit Multithreading problematisch sein.

Der Speicherverbrauch einer Python-Anwendung lässt sich mit dem neu hinzugekommenem Modul tracemalloc mit wenigen Zeilen Code schnell aufschlüsseln. Anders als die meisten Module der Standardbibliothek lässt es sich unter Umständen von anderen Python-Implementierungen nicht (oder nicht sinnvollerweise) bereitstellen.

CPython (die mit Abstand am meisten benutzte Implementierung von Python) verwendet intern sogenannte Hash Maps – nicht nur für den eingebauten Datentyp dict, sondern für die gesamte Objekt- und Klassenlogik. Der verwendete Hash-Algorithmus ist daher von zentraler Bedeutung. Als veröffentlicht wurde, dass sich Kollisionen in Pythons Hash-Algorithmus zu DoS-Attacken nutzen lassen, entschloss man sich, statt des bisherigen Algorithmus (einer modifizierten Fowler-Noll-Vo-Funktion) den kryptographisch sichereren SipHash-Algorithmus zu verwenden (PEP 456). Er ist ein wenig langsamer, bietet aber zuverlässigen Schutz gegen Kollisionsattacken.

Ebenfalls im Bereich der Sicherheit zu verorten ist die Entscheidung, alle geöffneten Dateihandles als "nichtvererbbar" zu markieren. Bislang erbten alle (beispielsweise mit dem Modul subprocess) erzeugten Subprozesse sämtliche vom Elternprozess geöffneten Datei-Handles, was zu Sicherheitslücken führen kann. Mit dieser Änderung (PEP 446) werden Dateihandles, wie sie etwa open(), os.pipe() oder socket.socket() zurückgeben, standardmäßig nicht an Kindprozesse vererbt. Um ein Datei-Handle explizit als vererbbar zu kennzeichnen, lässt sich die Funktion os.set_inheritable() einsetzen.

Um es Anwendungen zu ermöglichen, einen eigenen Speicher-Allokator zu verwenden, stellt CPython nun eine API für die Speicherverwaltung bereit. Denkbare Einsatzgebiete sind Anwendungen im Embedded-Bereich, die mit eigenen Allokatoren eine effizientere Speichernutzung erreichen wollen. Zudem lassen sich dadurch einfacher Debugging-Werkzeuge entwickeln, die Fehler oder Ineffizienzen in Anwendungen ermitteln können. Das bereits erwähnte Modul tracemalloc ist auf Basis dieser API implementiert.

CPython benutzt als Methode zur Garbage Collection (der automatischen Speicherbereinigung) einfaches Reference Counting. Da sich mehrere Objekte jeweils gegenseitig referenzieren können, bringt CPython außerdem einen Cycle Collector mit, der solcherlei zyklische Referenzen erkennen und mit Hilfe des Moduls gc auflösen kann. Er stieß jedoch an seine Grenzen, wenn während der Finalisierung eines Objekts (typischerweise in der __del__()-Methode) eine neue Referenz zu einem Objekt desselben Referenzzyklus erzeugt wurde. Mit einer Änderung der Logik in der Objektfinalisierung von CPython ist die Limitierung nun beseitigt worden. Dadurch fällt die bisher geltende Regel "Objekte mit __del__() -Methode sollten nicht Teil eines Referenzzyklus sein" weg. Da sie vielen Python-Nutzern nicht gerade geläufig war, mag der eine oder andere mysteriöse Fehler in bestehender Software dadurch behoben worden sein.

Auch an der Performance von CPython arbeiteten die Entwickler. Die Zeit, die der Interpreter zum Starten benötigt,ließ sich um fast ein Drittel reduzieren. Der Grund dafür ist (neben dem bereits erwähnten optimierten marshal-Format) vor allem darin zu finden, dass weniger Module standardmäßig zu laden sind. Auch der Speicherbedarf von CPython kann dadurch in vielen Fällen etwas sinken. Der Dekoder für die (eher selten verwendete) Unicode-Kodierung UTF-32 ist nun drei- bis viermal so schnell. Bereits in Version 3.3 wurden UTF-8- und UTF-16-Enkodierung und -Dekodierung sowie der Speicherverbrauch von Unicode-Strings optimiert. Damit sind die Geschwindigkeitsnachteile, die Python 3 gegenüber Python 2 durch die Umstellung von Byte- auf Unicode-Strings ursprünglich aufwies, weitestgehend ausgeglichen.