Clients Python et HTTP rapides

De nos jours, si vous écrivez une sorte d'application Python, vous devrez très probablement l'équiper des fonctionnalités d'un client HTTP capable de communiquer avec les serveurs HTTP. L'omniprésence de l'API REST a fait des outils HTTP une fonctionnalité respectée dans d'innombrables projets logiciels. C'est pourquoi tout programmeur doit posséder des modèles visant à organiser un travail optimal avec des connexions HTTP.



Il existe de nombreux clients HTTP pour Python. Les plus courantes d'entre elles, et d'ailleurs celle avec laquelle il est facile de travailler, peuvent être appelées requêtes . Aujourd'hui, ce client est la norme de facto.

Connexions permanentes


La première optimisation à considérer lors de l'utilisation de HTTP est d'utiliser des connexions persistantes aux serveurs Web. Les connexions persistantes sont devenues standard depuis HTTP 1.1, mais de nombreuses applications ne les utilisent toujours pas. Cette faille est facile à expliquer, sachant que lors de l'utilisation de la bibliothèque de requests en mode simple (par exemple, en utilisant sa méthode get ), la connexion au serveur est fermée après avoir reçu une réponse de sa part. Pour éviter cela, l'application doit utiliser l'objet Session , qui permet de réutiliser les connexions ouvertes:

 import requests session = requests.Session() session.get("http://example.com") #    session.get("http://example.com") 

Les connexions sont stockées dans le pool de connexions (il s'agit par défaut de 10 connexions par défaut). La taille de la piscine peut être personnalisée:

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

La réutilisation d'une connexion TCP pour envoyer plusieurs requêtes HTTP confère à l'application de nombreux avantages en termes de performances:

  • Réduire la charge sur le processeur et réduire le besoin de RAM (en raison du fait que moins de connexions s'ouvrent en même temps).
  • Réduction des retards lors de l'exécution des requêtes les unes après les autres (il n'y a pas de procédure d'établissement de liaison TCP).
  • Des exceptions peuvent être levées sans délai supplémentaire pour fermer la connexion TCP.

HTTP 1.1 prend également en charge le traitement en pipeline des demandes. Cela vous permet d'envoyer plusieurs demandes au sein de la même connexion sans attendre les réponses aux demandes précédemment envoyées (c'est-à-dire, envoyer des demandes en "paquets"). Malheureusement, la bibliothèque de requests ne prend pas en charge cette fonctionnalité. Cependant, les demandes de pipelining peuvent ne pas être aussi rapides que leur traitement en parallèle. Et, en outre, il convient de prêter attention à cela: les réponses aux demandes de «paquets» doivent être envoyées par le serveur dans le même ordre dans lequel il a reçu ces demandes. Le résultat n'est pas le schéma de traitement des demandes le plus efficace basé sur le principe FIFO («premier entré, premier sorti» - «premier arrivé, premier laissé»).

Traitement parallèle des requêtes


requests présentent également un autre inconvénient sérieux. Il s'agit d'une bibliothèque synchrone. Un appel de méthode tel que requests.get("http://example.org") bloque le programme jusqu'à ce qu'une réponse complète du serveur HTTP soit reçue. Le fait que l'application doive attendre et ne rien faire peut être considéré comme un inconvénient de ce schéma d'organisation de l'interaction avec le serveur. Est-il possible de faire faire au programme quelque chose d'utile au lieu d'attendre simplement?

Une application intelligemment conçue peut atténuer ce problème en utilisant un pool de threads, similaire à ceux fournis par concurrent.futures . Cela vous permet de paralléliser rapidement les requêtes HTTP:

 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) 

Ce modèle très utile est implémenté dans la bibliothèque de requêtes-futures . Dans le même temps, l'utilisation des objets Session est transparente pour le développeur:

 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) 

Par défaut, un travailleur avec deux threads est créé, mais le programme peut facilement définir cette valeur en passant l'argument FuturSession ou même son propre exécuteur à l'objet FuturSession . Par exemple, cela pourrait ressembler à ceci:

 FuturesSession(executor=ThreadPoolExecutor(max_workers=10)) 

Travail asynchrone avec requêtes


Comme déjà mentionné, la bibliothèque de requests est complètement synchrone. Cela conduit au blocage des applications en attendant une réponse du serveur, ce qui affecte mal les performances. Une solution à ce problème consiste à exécuter des requêtes HTTP dans des threads séparés. Mais l'utilisation de threads est une charge supplémentaire sur le système. De plus, cela signifie l'introduction d'un schéma parallèle de traitement des données dans le programme, qui ne convient pas à tout le monde.

À partir de Python 3.5, les fonctionnalités de langage standard incluent la programmation asynchrone utilisant asyncio . La bibliothèque aiohttp fournit au développeur un client HTTP asynchrone basé sur asyncio . Cette bibliothèque permet à l'application d'envoyer une série de demandes et de continuer à fonctionner. Dans le même temps, pour envoyer une autre demande, vous n'avez pas besoin d'attendre une réponse à une demande envoyée précédemment. Contrairement au pipelining des requêtes HTTP, aiohttp envoie des requêtes en parallèle en utilisant plusieurs connexions. Cela évite le «problème FIFO» décrit ci-dessus. Voici à aiohttp ressemble aiohttp :

 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) 

Toutes les approches décrites ci-dessus (en utilisant Session , streams, concurrent.futures ou asyncio ) offrent différentes façons d'accélérer les clients HTTP.

Performances


Le code suivant est un exemple dans lequel le client HTTP envoie des requêtes au serveur httpbin.org . Le serveur prend en charge une API qui peut, entre autres, simuler un système qui met longtemps à répondre à une demande (dans ce cas, c'est 1 seconde). Ici, toutes les techniques discutées ci-dessus sont mises en œuvre et leurs performances sont mesurées:

 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)])) 

Voici les résultats obtenus après le démarrage de ce programme:

 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 

Voici un tableau des résultats.

Les résultats d'une étude des performances de différentes méthodes pour effectuer des requêtes HTTP

Il n'est pas surprenant que le schéma d'exécution de requête synchrone le plus simple se soit avéré le plus lent. Le point ici est qu'ici les requêtes sont exécutées une par une, sans réutiliser la connexion. Par conséquent, il faut 12 secondes pour terminer 10 requêtes.

L'utilisation de l'objet Session et, par conséquent, la réutilisation des connexions, économise 8% du temps. C'est déjà très bien, et pour y parvenir, c'est très simple. Quiconque se soucie des performances doit utiliser au moins l'objet Session .

Si votre système et votre programme vous permettent de travailler avec des threads, c'est une bonne raison de penser à utiliser des threads pour paralléliser les requêtes. Cependant, les flux créent une charge supplémentaire sur le système, ils ne sont pour ainsi dire pas «gratuits». Ils doivent être créés, exécutés, vous devez attendre la fin de leur travail.

Si vous souhaitez utiliser le client HTTP asynchrone rapide, alors si vous aiohttp pas sur des versions plus anciennes de Python, vous devriez porter la plus grande attention à aiohttp . Il s'agit de la solution la plus rapide, la plus évolutive. Il est capable de gérer des centaines de demandes simultanées.

Une alternative à aiohttp , pas une alternative particulièrement bonne est de gérer des centaines de threads en parallèle.

Traitement des données en continu


Une autre optimisation du travail avec les ressources réseau, qui peut être utile en termes d'amélioration des performances des applications, consiste à utiliser des données en streaming. Le schéma de traitement des demandes standard ressemble à ceci: l'application envoie une demande, après quoi le corps de cette demande est chargé en une seule fois. Le paramètre stream , qui prend en charge la bibliothèque de requests , ainsi que l'attribut content de la bibliothèque aiohttp , vous permet de vous éloigner de ce schéma.

Voici à quoi ressemble l'organisation du traitement de données en continu à l'aide de requests :

 import requests #  `with`          #     . with requests.get('http://example.org', stream=True) as r:    print(list(r.iter_content())) 

Voici comment diffuser des données en utilisant 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]) 

L'élimination de la nécessité de charger instantanément le contenu de la réponse complète est importante dans les cas où vous devez éviter la possibilité potentielle d'allocation inutile de centaines de mégaoctets de mémoire. Si le programme n'a pas besoin d'accéder à la réponse dans son ensemble, s'il peut fonctionner avec des fragments individuels de la réponse, alors il est probablement préférable de recourir à des méthodes de diffusion en continu avec des requêtes. Par exemple, si vous souhaitez enregistrer des données de la réponse du serveur à un fichier, la lecture et l'écriture partielles seront beaucoup plus efficaces en termes d'utilisation de la mémoire que la lecture de l'ensemble du corps de la réponse, allouant une énorme quantité de mémoire puis écrivant le tout sur le disque.

Résumé


J'espère que mon exposé sur les différentes façons d'optimiser le fonctionnement des clients HTTP vous aidera à choisir ce qui convient le mieux à votre application Python.

Chers lecteurs! Si vous connaissez encore d'autres moyens d'optimiser le travail avec les requêtes HTTP dans les applications Python, veuillez les partager.


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


All Articles