python - mac - django mysql windows
Conexión de base de datos persistente en la aplicación Django/WSGI (4)
Quiero mantener una conexión persistente abierta a una base de datos heredada de terceros en una aplicación web con tecnología Django.
Quiero mantener abierta la conexión entre la aplicación web y la base de datos heredada, ya que la creación de una nueva conexión es muy lenta para esta base de datos especial.
No es como la agrupación de conexiones habitual, ya que necesito almacenar la conexión por usuario web. El usuario "Foo" necesita su propia conexión entre el servidor web y la base de datos heredada.
Hasta ahora uso Apache y wsgi, pero podría cambiar si otra solución encaja mejor.
Hasta ahora uso django. Aquí también podría cambiar. Pero el dolor sería mayor, ya que ya hay una gran cantidad de código que debe integrarse nuevamente.
Hasta ahora uso Python. Supongo que Node.js encajaría mejor aquí, pero el dolor de cambiar es demasiado alto.
Por supuesto, se necesitaría algún tipo de tiempo fuera. Si no hay una solicitud de http del usuario "Foo" durante N minutos, entonces la conexión persistente deberá cerrarse.
¿Cómo podría resolverse esto?
Actualizar
Lo llamo DB
pero no es un DB que se configura mediante la configuración. BASES DE DATOS. Es un sistema extraño, legado, no muy extendido, como DB que necesito integrar.
Si hay 50 personas en línea utilizando la aplicación web en este momento, entonces necesito tener 50 conexiones persistentes. Una para cada usuario.
Código para conectarse a DB
Podría ejecutar esta línea en cada solicitud:
strangedb_connection = strangedb.connect(request.user.username)
Pero esta operación es lenta. Usar la conexión es rápido.
Por supuesto, strangedb_connection
no se puede serializar y no se puede almacenar en una sesión :-)
demonio trabajador gestionando la conexión
Tu foto se ve como:
user -----------> webserver <--------[1]--> 3rd party DB
connection [1] is expensive.
Podrías resolver esto con:
user ----> webserver <---> task queue[1] <---> worker daemon <--[2]-> 3rd party DB
[1] task queue can be redis, celery or rabbitmq.
[2] worker daemon keeps connection open.
Un demonio trabajador haría la conexión a la base de datos de terceros y mantendría la conexión abierta. Esto significaría que cada solicitud no tendría que pagar los costos de conexión. La cola de tareas sería la comunicación entre procesos, el trabajo de envío al daemon y las consultas en la base de datos de terceros. El servidor web debe ser lo más liviano posible en términos de procesamiento y permitir que los trabajadores realicen tareas costosas.
precarga con apache + modwsgi
Puede preload
y hacer que la conexión costosa se realice antes de la primera solicitud. Esto se hace con la directiva de configuración WSGIImportScript
. No recuerdo en la parte superior de mi cabeza si tener una configuración de carga previa a la carga significa que cada solicitud ya tendrá la conexión abierta y la compartirá; pero como tiene la mayoría del código, este podría ser un experimento fácil.
precarga con uwsgi
uwsgi
soporta precarga también. Esto se hace con la directiva de import
.
En lugar de tener varios procesos de trabajo, puede usar la directiva WSGIDaemonProcess
para tener varios subprocesos de trabajo que se ejecutan en un solo proceso. De esa manera, todos los hilos pueden compartir la misma asignación de conexión de base de datos.
Con algo así en tu configuración de apache ...
# mydomain.com.conf
<VirtualHost *:80>
ServerName mydomain.com
ServerAdmin [email protected]
<Directory />
Require all granted
</Directory>
WSGIDaemonProcess myapp processes=1 threads=50 python-path=/path/to/django/root display-name=%{GROUP}
WSGIProcessGroup myapp
WSGIScriptAlias / /path/to/django/root/myapp/wsgi.py
</VirtualHost>
... luego puedes usar algo tan simple como esto en tu aplicación Django ...
# views.py
import thread
from django.http import HttpResponse
# A global variable to hold the connection mappings
DB_CONNECTIONS = {}
# Fake up this "strangedb" module
class strangedb(object):
class connection(object):
def query(self, *args):
return ''Query results for %r'' % args
@classmethod
def connect(cls, *args):
return cls.connection()
# View for homepage
def home(request, username=''bob''):
# Remember thread ID
thread_info = ''Thread ID = %r'' % thread.get_ident()
# Connect only if we''re not already connected
if username in DB_CONNECTIONS:
strangedb_connection = DB_CONNECTIONS[username]
db_info = ''We reused an existing connection for %r'' % username
else:
strangedb_connection = strangedb.connect(username)
DB_CONNECTIONS[username] = strangedb_connection
db_info = ''We made a connection for %r'' % username
# Fake up some query
results = strangedb_connection.query(''SELECT * FROM my_table'')
# Fake up an HTTP response
text = ''%s/n%s/n%s/n'' % (thread_info, db_info, results)
return HttpResponse(text, content_type=''text/plain'')
... que, en el primer golpe, produce ...
Thread ID = 140597557241600
We made a connection for ''bob''
Query results for ''SELECT * FROM my_table''
... y, en el segundo ...
Thread ID = 140597145999104
We reused an existing connection for ''bob''
Query results for ''SELECT * FROM my_table''
Obviamente, deberá agregar algo para eliminar las conexiones de la base de datos cuando ya no sean necesarias, pero es difícil saber la mejor manera de hacerlo sin más información sobre cómo se supone que funciona su aplicación.
Actualización # 1: Respecto a la multiplexación de E / S vs multiproceso
Trabajé con hilos dos veces en mi vida y cada vez fue una pesadilla. Se perdió mucho tiempo en la depuración de problemas no reproducibles. Creo que una arquitectura de E / S controlada por eventos y sin bloqueo podría ser más sólida.
Una solución que use multiplexación de E / S podría ser mejor, pero sería más compleja y también requeriría que su biblioteca "strangedb" la soporte, es decir, tendría que ser capaz de manejar EAGAIN
/ EWOULDBLOCK
y tener la capacidad de reintentar el sistema llamar cuando sea necesario.
El subprocesamiento múltiple en Python es mucho menos peligroso que en la mayoría de los otros idiomas, debido al GIL de Python, que, en esencia, hace que todos los códigos de bytes de Python sean seguros para subprocesos.
En la práctica, los subprocesos solo se ejecutan al mismo tiempo cuando el código C subyacente utiliza la macro Py_BEGIN_ALLOW_THREADS
, que, con su homólogo, Py_END_ALLOW_THREADS
, generalmente se envuelve alrededor de las llamadas al sistema y las operaciones intensivas de CPU.
La ventaja de esto es que es casi imposible tener una colisión de hilos en el código Python, aunque la desventaja es que no siempre hará un uso óptimo de múltiples núcleos de CPU en una sola máquina.
La razón por la que sugiero la solución anterior es que es relativamente simple y requeriría cambios mínimos en el código, pero puede haber una mejor opción si pudiera desarrollar más en su biblioteca "strangedb". Parece bastante extraño tener una base de datos que requiera una conexión de red separada por usuario concurrente.
Actualización # 2: sobre multiprocesamiento vs multiproceso
... las limitaciones de GIL en cuanto a los hilos parecen ser un problema. ¿No es esta una de las razones por las que la tendencia es usar procesos separados?
Esta es posiblemente la razón principal por la que existe el módulo de multiprocessing
de Python, es decir, para proporcionar la ejecución concurrente del ThreadPool
de Python en múltiples núcleos de CPU, aunque hay una clase de ThreadPool
undocumented ThreadPool
en ese módulo, que utiliza subprocesos en lugar de procesos.
Las "limitaciones de GIL" sin duda serían problemáticas en los casos en que realmente necesitas explotar cada ciclo de CPU en cada núcleo de CPU, por ejemplo, si estuvieras escribiendo un juego de computadora que tenía que procesar 60 cuadros por segundo en alta definición.
Sin embargo, es probable que la mayoría de los servicios basados en la web pasen la mayor parte del tiempo esperando que algo suceda, por ejemplo, la E / S de la red o la E / S del disco, que los hilos de Python permitirán que se produzcan simultáneamente.
En última instancia, se trata de un equilibrio entre el rendimiento y la capacidad de mantenimiento, y dado que el hardware suele ser mucho más barato que el tiempo de un desarrollador, favorecer la capacidad de mantenimiento sobre el rendimiento suele ser más rentable.
Francamente, en el momento en que decides utilizar un lenguaje de máquina virtual, como Python, en lugar de un lenguaje que se compila en código de máquina real, como C, ya estás diciendo que estás preparado para sacrificar algo de rendimiento a cambio de conveniencia .
Vea también El problema C10K para una comparación de técnicas para escalar servicios basados en web.
Por lo que sé, ha descartado la mayoría (¿todas?) De las soluciones comunes a este tipo de problema:
- Almacenar la conexión en un diccionario ... necesita N trabajadores y no puede garantizar qué solicitud va a qué trabajador
- Almacenar datos en caché ... demasiados datos
- Almacenar información de conexión en caché ... la conexión no es serializable
Por lo que puedo ver, realmente hay solo una solución ''meta'' para esto, use la sugerencia de un diccionario de @ Gahbu y garantice que las solicitudes de un user
determinado vayan al mismo trabajador. Es decir, encontrar una forma de asignar desde el objeto User
a un trabajador determinado de la misma manera cada vez (¿tal vez hash su nombre y MOD por el número de trabajadores?).
Esta solución no aprovecharía al máximo a sus N trabajadores si todos los Usuarios activos actualmente se asignaran al mismo trabajador, pero si todos los Usuarios tienen la misma probabilidad de estar activos al mismo tiempo, entonces el trabajo debería distribuirse por igual. (Si no son todos igualmente probables, entonces el mapeo puede dar cuenta de eso).
Las dos formas posibles en las que puedo pensar hacer esto serían:
1. Escribe un asignador de solicitud personalizado
No estoy realmente familiarizado con la tierra de interconexión apache / wsgi pero ... podría ser posible reemplazar el componente dentro de su servidor Apache que despacha las solicitudes HTTP a los trabajadores con alguna lógica personalizada, de manera que siempre se envíe al mismo proceso.
2. Ejecute un equilibrador de carga / proxy delante de N trabajadores de subproceso único
No estoy seguro de si puedes usar un paquete de Ready to Go aquí o no, pero el concepto sería:
- Ejecute un proxy que implemente esta lógica ''vincular al usuario a un índice''
- Haga que el proxy luego envíe las solicitudes a una de N copias de su servidor web Apache / wsgi, cada una de las cuales tiene un solo trabajador.
NB: Esta segunda idea la encontré aquí: https://github.com/benoitc/gunicorn/issues/183
Resumen
Para ambas opciones, la implementación en su aplicación existente es bastante simple. Su aplicación simplemente cambia para usar un diccionario para almacenar la conexión persistente (creando uno si ya no lo hay). La prueba de una sola instancia es la misma en el desarrollo que en la producción. En producción, las instancias en sí mismas no son lo más sabias que siempre se les pregunta sobre los mismos usuarios.
Me gusta la Opción 2 aquí por las siguientes razones:
- Tal vez exista un paquete de servidor que le permita definir este problema de proxy
- De lo contrario, crear una aplicación proxy personalizada para sentarse frente a su aplicación actual podría no ser demasiado difícil (especialmente considerando las restricciones en las que está (ya) cuando la solicitud llega al servicio
strangedb
)
Una forma sencilla de hacer esto sería tener otro proceso de Python que administre el grupo de conexiones persistentes (una para cada usuario y puede agotarse cuando sea necesario). Y luego, el otro proceso de python y django pueden comunicarse con algo rápido como zeromq. comunicación entre procesos en python