Wenn Sie heutzutage eine Art Python-Anwendung schreiben, müssen Sie diese höchstwahrscheinlich mit der Funktionalität eines HTTP-Clients ausstatten, der mit HTTP-Servern kommunizieren kann. Die Allgegenwart der REST-API hat HTTP-Tools zu einer angesehenen Funktion in unzähligen Softwareprojekten gemacht. Aus diesem Grund muss jeder Programmierer Muster besitzen, um die optimale Arbeit mit HTTP-Verbindungen zu organisieren.

Es gibt viele HTTP-Clients für Python. Die häufigste unter ihnen und außerdem die, mit der man leicht arbeiten kann, kann als
Anfrage bezeichnet werden . Heute ist dieser Kunde der De-facto-Standard.
Permanente Verbindungen
Die erste Optimierung, die bei der Arbeit mit HTTP berücksichtigt werden muss, ist die Verwendung dauerhafter Verbindungen zu Webservern. Permanente Verbindungen sind seit HTTP 1.1 zum Standard geworden, werden jedoch von vielen Anwendungen immer noch nicht verwendet. Dieser Mangel ist leicht zu erklären, da bei Verwendung der
requests
im einfachen Modus (z. B. mithilfe der
get
Methode) die Verbindung zum Server nach Erhalt einer Antwort von diesem Server geschlossen wird. Um dies zu vermeiden, muss die Anwendung das
Session
Objekt verwenden, mit dem offene Verbindungen wiederverwendet werden können:
import requests session = requests.Session() session.get("http://example.com")
Verbindungen werden im Verbindungspool gespeichert (standardmäßig werden 10 Verbindungen verwendet). Die Poolgröße kann angepasst werden:
import requests session = requests.Session() adapter = requests.adapters.HTTPAdapter( pool_connections=100, pool_maxsize=100) session.mount('http://', adapter) response = session.get("http://example.org")
Die Wiederverwendung einer TCP-Verbindung zum Senden mehrerer HTTP-Anforderungen bietet der Anwendung viele Leistungsvorteile:
- Reduzierung der Prozessorlast und Reduzierung des RAM-Bedarfs (aufgrund der Tatsache, dass weniger Verbindungen gleichzeitig geöffnet sind).
- Reduzieren von Verzögerungen bei der Ausführung von Anforderungen, die nacheinander eingehen (es gibt keine TCP-Handshake-Prozedur).
- Ausnahmen können ohne zusätzliche Zeit ausgelöst werden, um die TCP-Verbindung zu schließen.
HTTP 1.1 unterstützt auch das
Pipelining von Anforderungen. Auf diese Weise können Sie mehrere Anforderungen innerhalb derselben Verbindung senden, ohne auf Antworten auf zuvor gesendete Anforderungen zu warten (dh Anforderungen in "Paketen" zu senden). Leider unterstützt die
requests
diese Funktion nicht. Pipelining-Anforderungen sind jedoch möglicherweise nicht so schnell wie die parallele Verarbeitung. Außerdem ist es angebracht, darauf zu achten: Antworten auf "Paket" -Anfragen sollten vom Server in derselben Reihenfolge gesendet werden, in der er diese Anfragen erhalten hat. Das Ergebnis ist nicht das effizienteste Anforderungsverarbeitungsschema nach dem FIFO-Prinzip („first in, first out“ - „first come, first Leave“).
Parallele Abfrageverarbeitung
requests
auch einen weiteren schwerwiegenden Nachteil. Dies ist eine synchrone Bibliothek. Ein Methodenaufruf wie
requests.get("http://example.org")
blockiert das Programm, bis eine vollständige HTTP-Serverantwort empfangen wird. Die Tatsache, dass die Anwendung warten und nichts tun muss, kann als Minus dieses Organisationsschemas für die Interaktion mit dem Server angesehen werden. Ist es möglich, das Programm dazu zu bringen, etwas Nützliches zu tun, anstatt nur zu warten?
Eine intelligent gestaltete Anwendung kann dieses Problem durch die Verwendung eines Thread-Pools verringern, der dem von
concurrent.futures
bereitgestellten ähnelt. Auf diese Weise können Sie HTTP-Anforderungen schnell parallelisieren:
from concurrent import futures import requests with futures.ThreadPoolExecutor(max_workers=4) as executor: futures = [ executor.submit( lambda: requests.get("http://example.org")) for _ in range(8) ] results = [ f.result().status_code for f in futures ] print("Results: %s" % results)
Dieses sehr nützliche Muster ist in der
Request-Futures- Bibliothek implementiert. Die Verwendung von
Session
ist jedoch für den Entwickler transparent:
from requests_futures import sessions session = sessions.FuturesSession() futures = [ session.get("http://example.org") for _ in range(8) ] results = [ f.result().status_code for f in futures ] print("Results: %s" % results)
Standardmäßig wird ein Worker mit zwei Threads erstellt. Das Programm kann diesen Wert jedoch einfach festlegen, indem das Argument
FuturSession
oder sogar sein eigener Executor an das
FuturSession
Objekt übergeben wird. Zum Beispiel könnte es so aussehen:
FuturesSession(executor=ThreadPoolExecutor(max_workers=10))
Asynchrone Arbeit mit Anfragen
Wie bereits erwähnt, ist die
requests
vollständig synchron. Dies führt dazu, dass Anwendungen blockiert werden, während auf eine Antwort vom Server gewartet wird, was sich negativ auf die Leistung auswirkt. Eine Lösung für dieses Problem besteht darin, HTTP-Anforderungen in separaten Threads auszuführen. Die Verwendung von Threads ist jedoch eine zusätzliche Belastung für das System. Darüber hinaus bedeutet dies die Einführung eines parallelen Datenverarbeitungsschemas in das Programm, das nicht für jeden geeignet ist.
Ab Python 3.5 gehören zu den Standardsprachenfunktionen die asynchrone Programmierung mit
asyncio
. Die
aiohttp- Bibliothek bietet dem Entwickler einen asynchronen HTTP-Client, der auf
asyncio
basiert. Mit dieser Bibliothek kann die Anwendung eine Reihe von Anforderungen senden und weiterarbeiten. Um eine weitere Anfrage zu senden, müssen Sie nicht auf eine Antwort auf eine zuvor gesendete Anfrage warten. Im Gegensatz zum Pipelining von HTTP-Anforderungen sendet
aiohttp
Anforderungen parallel über mehrere Verbindungen. Dies vermeidet das oben beschriebene "FIFO-Problem". So sieht die Verwendung von
aiohttp
aus:
import aiohttp import asyncio async def get(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: return response loop = asyncio.get_event_loop() coroutines = [get("http://example.com") for _ in range(8)] results = loop.run_until_complete(asyncio.gather(*coroutines)) print("Results: %s" % results)
Alle oben beschriebenen Ansätze (mit
Session
, Streams,
concurrent.futures
oder
asyncio
) bieten verschiedene Möglichkeiten, um HTTP-Clients zu beschleunigen.
Leistung
Der folgende Code ist ein Beispiel, in dem der HTTP-Client Anforderungen an den
httpbin.org
Server sendet. Der Server unterstützt eine API, die unter anderem ein System simulieren kann, dessen Beantwortung einer Anfrage lange dauert (in diesem Fall 1 Sekunde). Hier werden alle oben diskutierten Techniken implementiert und ihre Leistung gemessen:
import contextlib import time import aiohttp import asyncio import requests from requests_futures import sessions URL = "http://httpbin.org/delay/1" TRIES = 10 @contextlib.contextmanager def report_time(test): t0 = time.time() yield print("Time needed for `%s' called: %.2fs" % (test, time.time() - t0)) with report_time("serialized"): for i in range(TRIES): requests.get(URL) session = requests.Session() with report_time("Session"): for i in range(TRIES): session.get(URL) session = sessions.FuturesSession(max_workers=2) with report_time("FuturesSession w/ 2 workers"): futures = [session.get(URL) for i in range(TRIES)] for f in futures: f.result() session = sessions.FuturesSession(max_workers=TRIES) with report_time("FuturesSession w/ max workers"): futures = [session.get(URL) for i in range(TRIES)] for f in futures: f.result() async def get(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: await response.read() loop = asyncio.get_event_loop() with report_time("aiohttp"): loop.run_until_complete( asyncio.gather(*[get(URL) for i in range(TRIES)]))
Hier sind die Ergebnisse, die nach dem Start dieses Programms erzielt wurden:
Time needed for `serialized' called: 12.12s Time needed for `Session' called: 11.22s Time needed for `FuturesSession w/ 2 workers' called: 5.65s Time needed for `FuturesSession w/ max workers' called: 1.25s Time needed for `aiohttp' called: 1.19s
Hier ist eine Tabelle der Ergebnisse.
Die Ergebnisse einer Studie zur Leistung verschiedener Methoden zum Erstellen von HTTP-AnforderungenEs ist nicht überraschend, dass sich das einfachste Ausführungsschema für synchrone Abfragen als das langsamste herausstellte. Der Punkt hier ist, dass hier die Abfragen einzeln ausgeführt werden, ohne die Verbindung wiederzuverwenden. Daher dauert es 12 Sekunden, um 10 Abfragen abzuschließen.
Durch die Verwendung des
Session
und die Wiederverwendung von Verbindungen werden 8% der Zeit gespart. Das ist schon sehr gut und das zu erreichen ist sehr einfach. Jeder, der sich um die Leistung kümmert, sollte mindestens das
Session
Objekt verwenden.
Wenn Ihr System und Ihr Programm es Ihnen ermöglichen, mit Threads zu arbeiten, ist dies ein guter Grund, über die Verwendung von Threads zum Parallelisieren von Anforderungen nachzudenken. Streams verursachen jedoch eine zusätzliche Belastung des Systems, sie sind sozusagen nicht „frei“. Sie müssen erstellt und ausgeführt werden. Sie müssen auf den Abschluss ihrer Arbeit warten.
Wenn Sie den schnellen asynchronen HTTP-Client verwenden möchten und nicht auf älteren Python-Versionen schreiben, sollten Sie
aiohttp
die größte Aufmerksamkeit
aiohttp
. Dies ist die schnellste und am besten skalierbare Lösung. Es ist in der Lage, Hunderte von gleichzeitigen Anforderungen zu verarbeiten.
Eine Alternative zu
aiohttp
, keine besonders gute Alternative, besteht darin, Hunderte von Threads parallel zu verwalten.
Datenverarbeitung streamen
Eine weitere Optimierung der Arbeit mit Netzwerkressourcen, die zur Verbesserung der Anwendungsleistung hilfreich sein kann, ist die Verwendung von Streaming-Daten. Das Standard-Anforderungsverarbeitungsschema sieht folgendermaßen aus: Die Anwendung sendet eine Anforderung, wonach der Hauptteil dieser Anforderung auf einmal geladen wird. Mit dem
stream
Parameter, der die
requests
sowie das
content
der
aiohttp
Bibliothek unterstützt, können Sie sich von diesem Schema entfernen.
So sieht die Organisation der Streaming-Datenverarbeitung mithilfe von
requests
aus:
import requests
So streamen Sie Daten mit
aiohttp
:
import aiohttp import asyncio async def get(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.content.read() loop = asyncio.get_event_loop() tasks = [asyncio.ensure_future(get("http://example.com"))] loop.run_until_complete(asyncio.wait(tasks)) print("Results: %s" % [task.result() for task in tasks])
In Fällen, in denen Sie die potenzielle Möglichkeit einer nutzlosen Zuweisung von Hunderten von Megabyte Speicher verhindern müssen, ist es wichtig, dass der vollständige Antwortinhalt nicht sofort geladen werden muss. Wenn das Programm keinen Zugriff auf die Antwort als Ganzes benötigt und mit einzelnen Fragmenten der Antwort arbeiten kann, ist es wahrscheinlich am besten, auf Streaming-Methoden mit Anfragen zurückzugreifen. Wenn Sie beispielsweise Daten aus der Antwort des Servers auf eine Datei speichern möchten, ist das Lesen und Schreiben in Teilen in Bezug auf die Speichernutzung viel effizienter als das Lesen des gesamten Antwortkörpers, das Zuweisen einer großen Menge an Speicher und das anschließende Schreiben aller Daten auf die Festplatte.
Zusammenfassung
Ich hoffe, dass meine Geschichte über verschiedene Möglichkeiten zur Optimierung des Betriebs von HTTP-Clients Ihnen bei der Auswahl der für Ihre Python-Anwendung am besten geeigneten Methode hilft.
Liebe Leser! Wenn Sie noch andere Möglichkeiten zur Optimierung der Arbeit mit HTTP-Anforderungen in Python-Anwendungen kennen, teilen Sie diese bitte mit.
