python python-3.x urllib python-multithreading httpserver

python - Excepciones misteriosas cuando se hacen muchas solicitudes concurrentes desde urllib.request a HTTPServer



python-3.x python-multithreading (3)

Está utilizando el valor predeterminado de acumulación de listen() , que probablemente sea la causa de muchos de esos errores. Este no es el número de clientes simultáneos con conexión ya establecida, sino la cantidad de clientes que esperan en la cola de escucha antes de que se establezca la conexión. Cambie su clase de servidor a:

class FancyHTTPServer(ThreadingMixIn, HTTPServer): def server_activate(self): self.socket.listen(128)

128 es un límite razonable. Es posible que desee comprobar socket.SOMAXCONN o su sistema operativo somaxconn si desea aumentarlo aún más. Si aún tiene errores aleatorios con cargas pesadas, debe verificar su configuración de ulimit y aumentarla si es necesario.

Lo hice con tu ejemplo y obtuve más de 1000 hilos funcionando bien, así que creo que eso debería resolver tu problema.

Actualizar

Si mejoró pero sigue fallando con 200 clientes simultáneos, entonces estoy bastante seguro de que su problema principal fue el tamaño de la acumulación. Tenga en cuenta que su problema no es la cantidad de clientes concurrentes, sino la cantidad de solicitudes de conexión simultáneas. Una breve explicación de lo que eso significa, sin profundizar en las partes internas de TCP.

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind((HOST, PORT)) s.listen(BACKLOG) while running: conn, addr = s.accept() do_something(conn, addr)

En este ejemplo, el socket ahora acepta conexiones en el puerto dado, y la llamada s.accept() se bloqueará hasta que un cliente se conecte. Puede hacer que muchos clientes intenten conectarse simultáneamente y, dependiendo de su aplicación, es posible que no pueda llamar a s.accept() y despachar la conexión del cliente tan rápido como los clientes intenten conectarse. Los clientes pendientes se ponen en cola, y el tamaño máximo de esa cola está determinado por el valor de BACKLOG. Si la cola está llena, los clientes fallarán con un error de conexión rechazada.

El subprocesamiento no ayuda, porque lo que hace la clase ThreadingMixIn es ejecutar la do_something(conn, addr) en un hilo separado, para que el servidor pueda regresar al mainloop y a la llamada s.accept() .

Puede intentar aumentar aún más la acumulación, pero habrá un punto en el que eso no ayudará porque si la cola crece demasiado, algunos clientes s.accept() el tiempo de espera antes de que el servidor realice la llamada s.accept() .

Entonces, como dije antes, su problema es el número de intentos de conexión simultáneos, no el número de clientes simultáneos. Tal vez 128 es suficiente para su aplicación real, pero está obteniendo un error en su prueba porque está tratando de conectarse con todos los 200 hilos a la vez e inundar la cola.

No se preocupe por ulimit menos que obtenga un error de Too many open files , pero si desea aumentar el retraso acumulado más allá de 128, investigue en socket.SOMAXCONN . Este es un buen comienzo: https://utcc.utoronto.ca/~cks/space/blog/python/AvoidSOMAXCONN

Estoy tratando de hacer este desafío criptográfico Matasano que implica hacer un ataque de tiempo contra un servidor con una función de comparación de cadenas artificialmente ralentizada. Dice usar "el marco web de su elección", pero no tenía ganas de instalar un marco web, así que decidí usar la clase HTTPServer integrada en el módulo http.server .

Se me ocurrió algo que funcionó, pero fue muy lento, así que traté de acelerarlo utilizando el grupo de subprocesos (mal documentado) integrado en multiprocessing.dummy . Fue mucho más rápido, pero noté algo extraño: si hago 8 o menos solicitudes al mismo tiempo, funciona bien. Si tengo más que eso, funciona por un tiempo y me da errores en momentos aparentemente aleatorios. Los errores parecen ser inconsistentes y no siempre son los mismos, pero generalmente tienen Connection refused, invalid argument , OSError: [Errno 22] Invalid argument , urllib.error.URLError: <urlopen error [Errno 22] Invalid argument> , BrokenPipeError: [Errno 32] Broken pipe , o urllib.error.URLError: <urlopen error [Errno 61] Connection refused> en ellos.

¿Hay algún límite en la cantidad de conexiones que el servidor puede manejar? No creo que el problema sea el número de subprocesos en sí, porque escribí una función simple que hacía la comparación de cadenas desaceleradas sin ejecutar el servidor web, y lo llamé con 500 subprocesos simultáneos, y funcionó bien. No creo que el problema sea simplemente hacer solicitudes de muchos subprocesos, porque he creado rastreadores que utilizan más de 100 subprocesos (todos realizan solicitudes simultáneas al mismo sitio web) y funcionan bien. Parece que el HTTPServer no está destinado a hospedar de manera confiable sitios web de producción que obtienen grandes cantidades de tráfico, pero me sorprende que sea tan fácil hacerlo colapsar.

Traté de quitar gradualmente cosas de mi código que parecían no estar relacionadas con el problema, como suelo hacer cuando diagnostico errores misteriosos como este, pero eso no fue muy útil en este caso. Parecía que a medida que eliminaba el código aparentemente no relacionado, la cantidad de conexiones que el servidor podía manejar aumentaba gradualmente, pero no había una causa clara de los fallos.

¿Alguien sabe cómo aumentar el número de solicitudes que puedo hacer a la vez, o al menos por qué está sucediendo esto?

Mi código es complicado, pero se me ocurrió este sencillo programa que demuestra el problema:

#!/usr/bin/env python3 import os import random from http.server import BaseHTTPRequestHandler, HTTPServer from multiprocessing.dummy import Pool as ThreadPool from socketserver import ForkingMixIn, ThreadingMixIn from threading import Thread from time import sleep from urllib.error import HTTPError from urllib.request import urlopen class FancyHTTPServer(ThreadingMixIn, HTTPServer): pass class MyRequestHandler(BaseHTTPRequestHandler): def do_GET(self): sleep(random.uniform(0, 2)) self.send_response(200) self.end_headers() self.wfile.write(b"foo") def log_request(self, code=None, size=None): pass def request_is_ok(number): try: urlopen("http://localhost:31415/test" + str(number)) except HTTPError: return False else: return True server = FancyHTTPServer(("localhost", 31415), MyRequestHandler) try: Thread(target=server.serve_forever).start() with ThreadPool(200) as pool: for i in range(10): numbers = [random.randint(0, 99999) for j in range(20000)] for j, result in enumerate(pool.imap(request_is_ok, numbers)): if j % 20 == 0: print(i, j) finally: server.shutdown() server.server_close() print("done testing server")

Por alguna razón, el programa anterior funciona bien a menos que tenga más de 100 hilos, pero mi código real para el desafío solo puede manejar 8 hilos. Si lo ejecuto con 9, generalmente obtengo errores de conexión, y con 10, siempre obtengo errores de conexión. Intenté usar concurrent.futures.ThreadPoolExecutor , concurrent.futures.ProcessPoolExecutor y multiprocessing.pool lugar de multiprocessing.dummy.pool y ninguno de esos parecía ayudar. Intenté usar un objeto HTTPServer simple (sin ThreadingMixIn ) y eso simplemente hizo que las cosas funcionaran muy lentamente y no solucionó el problema. Intenté usar ForkingMixIn y eso tampoco lo solucionó.

¿Qué se supone que debo hacer al respecto? Estoy ejecutando Python 3.5.1 en una MacBook Pro tardía de 2013 que ejecuta OS X 10.11.3.

EDITAR: Probé algunas cosas más, incluido ejecutar el servidor en un proceso en lugar de un hilo, como un simple HTTPServer , con ForkingMixIn y con ThreadingMixIn . Ninguno de esos ayudó.

EDITAR: Este problema es más extraño de lo que pensaba. Traté de hacer una secuencia de comandos con el servidor y otra con muchos hilos haciendo solicitudes y ejecutándolas en diferentes pestañas en mi terminal. El proceso con el servidor funcionó bien, pero el que hacía las solicitudes colapsó. Las excepciones fueron una combinación de ConnectionResetError: [Errno 54] Connection reset by peer , urllib.error.URLError: <urlopen error [Errno 54] Connection reset by peer> , OSError: [Errno 41] Protocol wrong type for socket , urllib.error.URLError: <urlopen error [Errno 41] Protocol wrong type for socket> , urllib.error.URLError: <urlopen error [Errno 22] Invalid argument> .

Lo intenté con un servidor ficticio como el anterior, y si limité el número de solicitudes concurrentes a 5 o menos, funcionó bien, pero con 6 solicitudes, el proceso del cliente se bloqueó. Hubo algunos errores del servidor, pero continuaron. El cliente se bloqueó independientemente de si estaba usando subprocesos o procesos para realizar las solicitudes. Luego intenté poner la función ralentizada en el servidor y fue capaz de manejar 60 solicitudes concurrentes, pero se bloqueó con 70. Esto parece que puede contradecir la evidencia de que el problema está con el servidor.

EDITAR: urllib.request mayoría de las cosas que describí usando requests lugar de urllib.request y encontré problemas similares.

EDITAR: ahora estoy ejecutando OS X 10.11.4 y corriendo en los mismos problemas.


La norma es usar solo tantos hilos como núcleos, de ahí el requisito de 8 hilos (incluidos los núcleos virtuales). El modelo de subprocesamiento es el más fácil de poner en funcionamiento, pero en realidad es una manera de hacerlo. Una mejor forma de manejar conexiones múltiples es usar un enfoque asincrónico. Aunque es más difícil.

Con su método de enhebrado, podría comenzar investigando si el proceso permanece abierto después de salir del programa. Esto significaría que tus hilos no se están cerrando, y obviamente causarán problemas.

Prueba esto...

class FancyHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True

Eso asegurará que tus hilos se cierren apropiadamente. Puede suceder automáticamente en el grupo de subprocesos, pero probablemente valga la pena intentarlo de todos modos.


Diría que su problema está relacionado con algunos bloqueos de IO, ya que he ejecutado con éxito su código en NodeJs. También noté que tanto el servidor como el cliente tienen problemas para trabajar individualmente.

Pero es posible aumentar el número de solicitudes con algunas modificaciones:

  • Defina el número de conexiones concurrentes:

    http.server.HTTPServer.request_queue_size = 500

  • Ejecute el servidor en un proceso diferente:

    server = multiprocessing.Process (target = RunHTTPServer) server.start ()

  • Use un grupo de conexiones en el lado del cliente para ejecutar las solicitudes

  • Use un grupo de subprocesos en el lado del servidor para manejar las solicitudes

  • Permita la reutilización de la conexión en el lado del cliente configurando el esquema y usando el encabezado "keep-alive"

Con todas estas modificaciones, logré ejecutar el código con 500 hilos sin ningún problema. Entonces, si quieres probarlo, aquí tienes el código completo:

import random from time import sleep, clock from http.server import BaseHTTPRequestHandler, HTTPServer from multiprocessing import Process from multiprocessing.pool import ThreadPool from socketserver import ThreadingMixIn from concurrent.futures import ThreadPoolExecutor from urllib3 import HTTPConnectionPool from urllib.error import HTTPError class HTTPServerThreaded(HTTPServer): request_queue_size = 500 allow_reuse_address = True def serve_forever(self): executor = ThreadPoolExecutor(max_workers=self.request_queue_size) while True: try: request, client_address = self.get_request() executor.submit(ThreadingMixIn.process_request_thread, self, request, client_address) except OSError: break self.server_close() class MyRequestHandler(BaseHTTPRequestHandler): default_request_version = ''HTTP/1.1'' def do_GET(self): sleep(random.uniform(0, 1) / 100.0) data = b"abcdef" self.send_response(200) self.send_header("Content-type", ''text/html'') self.send_header("Content-length", len(data)) self.end_headers() self.wfile.write(data) def log_request(self, code=None, size=None): pass def RunHTTPServer(): server = HTTPServerThreaded((''127.0.0.1'', 5674), MyRequestHandler) server.serve_forever() client_headers = { ''User-Agent'' : ''Mozilla/5.0 (Windows NT 6.1; Win64; x64)'', ''Content-Type'': ''text/plain'', ''Connection'': ''keep-alive'' } client_pool = None def request_is_ok(number): response = client_pool.request(''GET'', "/test" + str(number), headers=client_headers) return response.status == 200 and response.data == b"abcdef" if __name__ == ''__main__'': # start the server in another process server = Process(target=RunHTTPServer) server.start() # start a connection pool for the clients client_pool = HTTPConnectionPool(''127.0.0.1'', 5674) # execute the requests with ThreadPool(500) as thread_pool: start = clock() for i in range(5): numbers = [random.randint(0, 99999) for j in range(20000)] for j, result in enumerate(thread_pool.imap(request_is_ok, numbers)): if j % 1000 == 0: print(i, j, result) end = clock() print("execution time: %s" % (end-start,))

Actualización 1:

Aumentar el request_queue_size simplemente le da más espacio para almacenar las solicitudes que no se pueden ejecutar en el momento para que puedan ejecutarse más tarde. Por lo tanto, cuanto más larga sea la cola, mayor será la dispersión para el tiempo de respuesta, que creo que es lo contrario de su objetivo aquí. En cuanto a ThreadingMixIn, no es ideal, ya que crea y destruye un hilo para cada solicitud y es caro. Una mejor opción para reducir la cola de espera es usar un grupo de subprocesos reutilizables para manejar las solicitudes.

La razón para ejecutar el servidor en otro proceso es aprovechar otra CPU para reducir el tiempo de ejecución.

Para el lado del cliente, usar HTTPConnectionPool fue la única forma que encontré para mantener un flujo constante de solicitudes, ya que tenía un comportamiento extraño con urlopen al analizar las conexiones.