¿Cuál es el significado de pool_connections en requests.adapters.HTTPAdapter?

Al inicializar una Session solicitudes, se HTTPAdapter dos HTTPAdapter se HTTPAdapter en http y https .

Así es como se define HTTPAdapter :

 class requests.adapters.HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False) 

Si bien entiendo el significado de pool_maxsize (que es el número de sesiones que puede guardar un grupo), no entiendo qué significa pool_connections o qué significa. Doc dice:

 Parameters: pool_connections – The number of urllib3 connection pools to cache. 

Pero, ¿qué significa “almacenar en caché”? ¿Y cuál es el punto usando múltiples agrupaciones de conexiones?

Las solicitudes utilizan urllib3 para administrar sus conexiones y otras características.

Reutilizar las conexiones es un factor importante para mantener el rendimiento de las solicitudes HTTP recurrentes. El README de urllib3 explica :

¿Por qué quiero reutilizar las conexiones?

Actuación. Cuando normalmente hace una llamada a urllib, se crea una conexión de socket separada con cada solicitud. Al reutilizar los sockets existentes (admitidos desde HTTP 1.1), las solicitudes ocuparán menos recursos en el extremo del servidor y también brindarán un tiempo de respuesta más rápido al final del cliente. […]

Para responder a su pregunta, “pool_maxsize” es el número de conexiones que se deben mantener por host (esto es útil para aplicaciones de múltiples subprocesos), mientras que “pool_connections” es el número de grupos de hosts que hay que mantener. Por ejemplo, si se conecta a 100 hosts diferentes y pool_connections=10 , solo se reutilizarán las últimas conexiones de los 10 hosts.

Escribí un artículo sobre esto. pegado aquí

Secreto de solicitudes: pool_connections y pool_maxsize

Requests es una de las bibliotecas de terceros de Python, si no la más conocida, para los progtwigdores de Python. Con su API simple y su alto rendimiento, las personas tienden a usar solicitudes en lugar de urllib2 proporcionada por la biblioteca estándar para solicitudes HTTP. Sin embargo, es posible que las personas que usan solicitudes todos los días no conozcan los pool_connections internos, y hoy quiero presentarles dos: pool_connections y pool_maxsize .

Empecemos con la Session :

 import requests s = requests.Session() s.get('https://www.google.com') 

Es bastante simple Probablemente sepa que la Session las solicitudes puede persistir cookie. Guay. ¿Pero sabes que la Session tiene un método de mount ?

mount(prefix, adapter)
Registra un adaptador de conexión a un prefijo.
Los adaptadores se clasifican en orden descendente por longitud de clave.

¿No? Bueno, de hecho, ya ha utilizado este método cuando inicializa un objeto Session :

 class Session(SessionRedirectMixin): def __init__(self): ... # Default connection adapters. self.adapters = OrderedDict() self.mount('https://', HTTPAdapter()) self.mount('http://', HTTPAdapter()) 

Ahora viene la parte interesante. Si ha leído el artículo Rebashs en solicitudes de Ian Cordasco, debe saber que HTTPAdapter se puede usar para proporcionar la funcionalidad de rebash. Pero, ¿qué es realmente un HTTPAdapter ? Cita del doc :

class requests.adapters.HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False)

El adaptador HTTP incorporado para urllib3.

Proporciona una interfaz de caso general para que las sesiones de Solicitudes se pongan en contacto con las URL de HTTP y HTTPS mediante la implementación de la interfaz del Adaptador de transporte. Esta clase generalmente será creada por la clase Session bajo las cubiertas.

Parámetros:
* pool_connections – El número de grupos de conexiones urllib3 a caché. * pool_maxsize : el número máximo de conexiones para guardar en el grupo. * max_retries(int) : el número máximo de rebashs que debe intentar cada conexión. Tenga en cuenta que esto se aplica solo a búsquedas de DNS fallidas, conexiones de socket y tiempos de espera de conexión, nunca a solicitudes donde los datos han llegado al servidor. De forma predeterminada, las solicitudes no reintentan las conexiones fallidas. Si necesita un control granular sobre las condiciones bajo las cuales reintentamos una solicitud, importe la clase Reintentar de urllib3 y pase eso. * pool_block : si la agrupación de conexiones debe bloquear las conexiones. Uso:

 >>> import requests >>> s = requests.Session() >>> a = requests.adapters.HTTPAdapter(max_retries=3) >>> s.mount('http://', a) 

Si la documentación anterior lo confunde, esta es mi explicación: lo que hace el Adaptador HTTP es simplemente proporcionar diferentes configuraciones para diferentes solicitudes de acuerdo con la url de destino . ¿Recuerdas el código de arriba?

 self.mount('https://', HTTPAdapter()) self.mount('http://', HTTPAdapter()) 

Crea dos objetos HTTPAdapter con el argumento predeterminado pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False , y se monta en https:// y http:// respectivamente, lo que significa que la configuración del primer HTTPAdapter() será se usa si intenta enviar una solicitud a http://xxx , y el segundo HTTPAdapter() se usará para las solicitudes a https://xxx . En este caso, las dos configuraciones son las mismas, las solicitudes a http y https aún se manejan por separado. Veremos lo que significa después.

Como dije, el propósito principal de este artículo es explicar pool_connections y pool_maxsize .

Primero echemos un vistazo a las pool_connections de pool_connections . Ayer hice una pregunta sobre stackoverflow porque no estoy seguro de si mi comprensión es correcta, la respuesta elimina mi incertidumbre. HTTP, como todos sabemos, se basa en el protocolo TCP. Una conexión HTTP también es una conexión TCP, que se identifica mediante una tupla de cinco valores:

 (, , , , ) 

Digamos que ha establecido una conexión HTTP / TCP con www.example.com , suponga que el servidor admite Keep-Alive , la próxima vez que envíe una solicitud a www.example.com/a o www.example.com/b , simplemente podría usa la misma conexión porque ninguno de los cinco valores cambia. De hecho, la sesión de solicitudes lo hace automáticamente por usted y reutilizará las conexiones siempre que sea posible.

La pregunta es, ¿qué determina si puede reutilizar la conexión antigua o no? Sí, ¡ pool_connections !

pool_connections – El número de pools de conexiones urllib3 a caché.

Lo sé, lo sé, no quiero traer tantas terminologías tampoco, esta es la última, lo prometo. Para facilitar la comprensión, un grupo de conexión corresponde a un host , eso es lo que es.

Aquí hay un ejemplo (las líneas no relacionadas son ignoradas):

 s = requests.Session() s.mount('https://', HTTPAdapter(pool_connections=1)) s.get('https://www.baidu.com') s.get('https://www.zhihu.com') s.get('https://www.baidu.com') """output INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.baidu.com DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 None INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2621 INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.baidu.com DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 None """ 

HTTPAdapter(pool_connections=1) se monta en https:// , lo que significa que solo una agrupación de conexiones persiste a la vez. Después de llamar a s.get('https://www.baidu.com') , el grupo de conexiones en caché es connectionpool('https://www.baidu.com') . Ahora s.get('https://www.zhihu.com') y la sesión descubrió que no puede usar la conexión en caché anterior porque no es el mismo host (un grupo de conexiones corresponde a un host, ¿recuerdas?). Por lo tanto, la sesión tuvo que crear una nueva agrupación de conexiones, o una conexión si lo desea. Como pool_connections=1 , la sesión no puede mantener dos grupos de conexiones al mismo tiempo, por lo tanto, abandonó el antiguo que es connectionpool('https://www.baidu.com') y mantuvo el nuevo que es connectionpool('https://www.zhihu.com') . El siguiente paso es el mismo. Es por esto que vemos tres Starting new HTTPS connection en el registro.

¿Qué pasa si ponemos pool_connections en 2:

 s = requests.Session() s.mount('https://', HTTPAdapter(pool_connections=2)) s.get('https://www.baidu.com') s.get('https://www.zhihu.com') s.get('https://www.baidu.com') """output INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.baidu.com DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 None INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2623 DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 None """ 

Genial, ahora solo creamos conexiones dos veces y guardamos una conexión estableciendo el tiempo.

Finalmente, pool_maxsize .

En primer lugar, debe preocuparse por pool_maxsize solo si utiliza Session en un entorno multiproceso , como realizar solicitudes simultáneas desde varios subprocesos que utilizan la misma Session .

En realidad, pool_maxsize es un argumento para inicializar HTTPConnectionPool de HTTPConnectionPool , que es exactamente el conjunto de conexiones que mencionamos anteriormente. HTTPConnectionPool es un contenedor para una colección de conexiones a un host específico, y pool_maxsize es el número de conexiones para guardar que se pueden reutilizar. Si está ejecutando su código en un hilo, no es posible ni necesario crear varias conexiones al mismo host, ya que la biblioteca de solicitudes está bloqueando, por lo que la solicitud HTTP siempre se envía una tras otra.

Las cosas son diferentes si hay varios hilos.

 def thread_get(url): s.get(url) s = requests.Session() s.mount('https://', HTTPAdapter(pool_connections=1, pool_maxsize=2)) t1 = Thread(target=thread_get, args=('https://www.zhihu.com',)) t2 = Thread(target=thread_get, args=('https://www.zhihu.com/question/36612174',)) t1.start();t2.start() t1.join();t2.join() t3 = Thread(target=thread_get, args=('https://www.zhihu.com/question/39420364',)) t4 = Thread(target=thread_get, args=('https://www.zhihu.com/question/21362402',)) t3.start();t4.start() """output INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (2): www.zhihu.com DEBUG:requests.packages.urllib3.connectionpool:"GET /question/36612174 HTTP/1.1" 200 21906 DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2606 DEBUG:requests.packages.urllib3.connectionpool:"GET /question/21362402 HTTP/1.1" 200 57556 DEBUG:requests.packages.urllib3.connectionpool:"GET /question/39420364 HTTP/1.1" 200 28739 """ 

¿Ver? Estableció dos conexiones para el mismo host www.zhihu.com , como dije, esto solo puede suceder en un entorno multiproceso. En este caso, creamos un pool_maxsize=2 conexiones con pool_maxsize=2 , y no hay más de dos conexiones al mismo tiempo, así que es suficiente. Podemos ver que las solicitudes de t3 y t4 no crearon nuevas conexiones, reutilizaron las antiguas.

¿Qué pasa si no hay suficiente tamaño?

 s = requests.Session() s.mount('https://', HTTPAdapter(pool_connections=1, pool_maxsize=1)) t1 = Thread(target=thread_get, args=('https://www.zhihu.com',)) t2 = Thread(target=thread_get, args=('https://www.zhihu.com/question/36612174',)) t1.start() t2.start() t1.join();t2.join() t3 = Thread(target=thread_get, args=('https://www.zhihu.com/question/39420364',)) t4 = Thread(target=thread_get, args=('https://www.zhihu.com/question/21362402',)) t3.start();t4.start() t3.join();t4.join() """output INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (2): www.zhihu.com DEBUG:requests.packages.urllib3.connectionpool:"GET /question/36612174 HTTP/1.1" 200 21906 DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2606 WARNING:requests.packages.urllib3.connectionpool:Connection pool is full, discarding connection: www.zhihu.com INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (3): www.zhihu.com DEBUG:requests.packages.urllib3.connectionpool:"GET /question/39420364 HTTP/1.1" 200 28739 DEBUG:requests.packages.urllib3.connectionpool:"GET /question/21362402 HTTP/1.1" 200 57556 WARNING:requests.packages.urllib3.connectionpool:Connection pool is full, discarding connection: www.zhihu.com """ 

Ahora, pool_maxsize=1 , la advertencia llegó como se esperaba:

 Connection pool is full, discarding connection: www.zhihu.com 

También podemos observar que dado que solo se puede guardar una conexión en este grupo, se crea una nueva conexión nuevamente para t3 o t4 . Obviamente esto es muy ineficiente. Por eso en la documentación de urllib3 dice:

Si planea utilizar un grupo de este tipo en un entorno de multiproceso, debe establecer el tamaño máximo del grupo en un número mayor, como el número de subprocesos.

Por último, pero no menos importante, HTTPAdapter instancias de HTTPAdapter montadas en diferentes prefijos son independientes .

 s = requests.Session() s.mount('https://', HTTPAdapter(pool_connections=1, pool_maxsize=2)) s.mount('https://baidu.com', HTTPAdapter(pool_connections=1, pool_maxsize=1)) t1 = Thread(target=thread_get, args=('https://www.zhihu.com',)) t2 =Thread(target=thread_get, args=('https://www.zhihu.com/question/36612174',)) t1.start();t2.start() t1.join();t2.join() t3 = Thread(target=thread_get, args=('https://www.zhihu.com/question/39420364',)) t4 = Thread(target=thread_get, args=('https://www.zhihu.com/question/21362402',)) t3.start();t4.start() t3.join();t4.join() """output INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (2): www.zhihu.com DEBUG:requests.packages.urllib3.connectionpool:"GET /question/36612174 HTTP/1.1" 200 21906 DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2623 DEBUG:requests.packages.urllib3.connectionpool:"GET /question/39420364 HTTP/1.1" 200 28739 DEBUG:requests.packages.urllib3.connectionpool:"GET /question/21362402 HTTP/1.1" 200 57669 """ 

El código anterior es fácil de entender, así que no lo explico.

Supongo que eso es todo. Espero que este artículo te ayude a entender mejor las solicitudes. Por cierto, he creado una idea que contiene todos los códigos de prueba utilizados en este artículo. Sólo descarga y juega con él 🙂

Apéndice

  1. Para https, las solicitudes utilizan HTTPSConnectionPool de urllib3 , pero es más o menos lo mismo que HTTPConnectionPool, así que no las diferencio en este artículo.
  2. El método de mount Session garantizará que el prefijo más largo coincida primero. Su implementación es bastante interesante, así que la publiqué aquí.

     def mount(self, prefix, adapter): """Registers a connection adapter to a prefix. Adapters are sorted in descending order by key length.""" self.adapters[prefix] = adapter keys_to_move = [k for k in self.adapters if len(k) < len(prefix)] for key in keys_to_move: self.adapters[key] = self.adapters.pop(key) 

    Tenga en cuenta que self.adapters es un OrderedDict .

Gracias a @ laike9m por las preguntas y respuestas existentes y el artículo, pero las respuestas existentes no mencionan las sutilezas de pool_maxsize y su relación con el código de multiproceso.

Resumen

  • pool_connections es el número de conexiones que se pueden mantener pool_connections en el pool en un momento dado desde un punto final (host, puerto, esquema). Si desea mantener un máximo de n conexiones TCP abiertas en un grupo para reutilizarse con una Session , desea pool_connections=n .
  • pool_maxsize es efectivamente irrelevante para los usuarios de requests debido a que el valor predeterminado para pool_block (en requests.adapters.HTTPAdapter ) es False lugar de True

Detalle

Como se señaló correctamente aquí, pool_connections es el número máximo de conexiones abiertas dado el prefijo del adaptador. Se ilustra mejor a través del ejemplo:

 >>> import requests >>> from requests.adapters import HTTPAdapter >>> >>> from urllib3 import add_stderr_logger >>> >>> add_stderr_logger() # Turn on requests.packages.urllib3 logging 2018-12-21 20:44:03,979 DEBUG Added a stderr logging handler to logger: urllib3  (NOTSET)> >>> >>> s = requests.Session() >>> s.mount('https://', HTTPAdapter(pool_connections=1)) >>> >>> # 4 consecutive requests to (github.com, 443, https) ... # A new HTTPS (TCP) connection will be established only on the first conn. ... s.get('https://github.com/requests/requests/blob/master/requests/adapters.py') 2018-12-21 20:44:03,982 DEBUG Starting new HTTPS connection (1): github.com:443 2018-12-21 20:44:04,381 DEBUG https://github.com:443 "GET /requests/requests/blob/master/requests/adapters.py HTTP/1.1" 200 None  >>> s.get('https://github.com/requests/requests/blob/master/requests/packages.py') 2018-12-21 20:44:04,548 DEBUG https://github.com:443 "GET /requests/requests/blob/master/requests/packages.py HTTP/1.1" 200 None  >>> s.get('https://github.com/urllib3/urllib3/blob/master/src/urllib3/__init__.py') 2018-12-21 20:44:04,881 DEBUG https://github.com:443 "GET /urllib3/urllib3/blob/master/src/urllib3/__init__.py HTTP/1.1" 200 None  >>> s.get('https://github.com/python/cpython/blob/master/Lib/logging/__init__.py') 2018-12-21 20:44:06,533 DEBUG https://github.com:443 "GET /python/cpython/blob/master/Lib/logging/__init__.py HTTP/1.1" 200 None  

Arriba, el número máximo de conexiones es 1; es (github.com, 443, https) . Si desea solicitar un recurso de un nuevo triple (host, puerto, esquema), la Session internamente volcará la conexión existente para dejar espacio para una nueva:

 >>> s.get('https://www.rfc-editor.org/info/rfc4045') 2018-12-21 20:46:11,340 DEBUG Starting new HTTPS connection (1): www.rfc-editor.org:443 2018-12-21 20:46:12,185 DEBUG https://www.rfc-editor.org:443 "GET /info/rfc4045 HTTP/1.1" 200 6707  >>> s.get('https://www.rfc-editor.org/info/rfc4046') 2018-12-21 20:46:12,667 DEBUG https://www.rfc-editor.org:443 "GET /info/rfc4046 HTTP/1.1" 200 6862  >>> s.get('https://www.rfc-editor.org/info/rfc4047') 2018-12-21 20:46:13,837 DEBUG https://www.rfc-editor.org:443 "GET /info/rfc4047 HTTP/1.1" 200 6762  

Puedes subir el número a pool_connections=2 , luego pool_connections=2 entre 3 combinaciones de hosts únicas y verás lo mismo en juego. (Otra cosa a tener en cuenta es que la sesión retendrá y enviará las cookies de la misma manera).

Ahora para pool_maxsize , que se pasa a urllib3.poolmanager.PoolManager y, en última instancia, a urllib3.connectionpool.HTTPSConnectionPool . La cadena de documentos para el tamaño máximo es:

Número de conexiones a guardar que se pueden reutilizar. Más de 1 es útil en situaciones multiproceso. Si el block se establece en Falso, se crearán más conexiones, pero no se guardarán una vez que se hayan utilizado.

Por cierto, block=False es el valor predeterminado para HTTPAdapter , aunque el valor predeterminado es True para HTTPConnectionPool . Esto implica que pool_maxsize tiene poco o ningún efecto para HTTPAdapter .

Además, requests.Session() no es seguro para subprocesos; no debe utilizar la misma instancia de session de varios subprocesos. (Consulte aquí y aquí ). Si realmente desea hacerlo, lo más seguro sería prestar a cada hilo su propia instancia de sesión localizada, luego usar esa sesión para realizar solicitudes a través de varias URL, a través de threading.local() :

 import threading import requests local = threading.local() # values will be different for separate threads. vars(local) # initially empty; a blank class with no attrs. def get_or_make_session(**adapter_kwargs): # `local` will effectively vary based on the thread that is calling it print('get_or_make_session() called from id:', threading.get_ident()) if not hasattr(local, 'session'): session = requests.Session() adapter = requests.adapters.HTTPAdapter(**kwargs) session.mount('http://', adapter) session.mount('https://', adapter) local.session = session return local.session