Python und schnelle HTTP-Clients

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") #    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-Anforderungen

Es 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 #  `with`          #     . with requests.get('http://example.org', stream=True) as r:    print(list(r.iter_content())) 

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.


Source: https://habr.com/ru/post/de472858/


All Articles