¿Debo seguir o abandonar Python para lidiar con la concurrencia?

31

Tengo un proyecto LOC de 10K escrito en Django con una gran cantidad de Celery ( RabbitMQ ) para trabajos de asincronía y de fondo donde sea necesario, y he llegado a la conclusión de que partes del sistema se beneficiarían de ser reescrito en algo diferente a Django para una mejor concurrencia . Las razones incluyen:

  • Manejo de señales y objetos mutables. Especialmente cuando una señal activa otra, manejarlas en Django usando el ORM puede ser sorprendente cuando las instancias cambian o desaparecen. Quiero usar un enfoque de mensajería donde los datos pasados ​​no cambian en un controlador ( el enfoque de copia en escritura de Clojure parece bueno, si lo hago bien).
  • Algunas partes del sistema no están basadas en la web y necesitan un mejor soporte para realizar tareas simultáneamente. Por ejemplo, el sistema lee las etiquetas NFC y, cuando se lee una, se enciende un LED durante unos segundos (tarea de apio), se reproduce un sonido (otra tarea de apio) y se consulta la base de datos (otra tarea). Esto se implementa como un comando de administración de Django, pero Django y su ORM son sincrónicos por naturaleza y comparten memoria es limitante (estamos pensando en agregar más lectores NFC, y no creo que el enfoque Django + Celery funcione más, Me gustaría ver mejores capacidades para pasar mensajes).

¿Cuáles son las ventajas y desventajas de usar algo como Twisted o Tornado en comparación con utilizar un lenguaje como Erlang o Clojure ? Estoy interesado en beneficios prácticos y detrimentos.

¿Cómo llegó a la conclusión de que a algunas partes del sistema les iría mejor en otro idioma? ¿Estás sufriendo problemas de rendimiento? ¿Qué tan graves son esos problemas? Si puede ser más rápido, ¿es esencial que sea más rápido?

Ejemplo 1: Django en el trabajo fuera de una solicitud HTTP:

  1. Se lee una etiqueta NFC.
  2. Se consulta la base de datos (y posiblemente LDAP), y queremos hacer algo cuando los datos estén disponibles (luz roja o verde, reproducir un sonido). Esto bloquea el uso de Django ORM, pero mientras haya trabajadores de Apio disponibles, no importa. Puede ser un problema con más estaciones.

Ejemplo 2: "pasar mensajes" utilizando señales de Django:

  1. Se post_deletemaneja un evento, otros objetos pueden ser alterados o eliminados debido a esto.
  2. Al final, las notificaciones deben enviarse a los usuarios. Aquí, sería bueno si los argumentos pasados ​​al controlador de notificaciones fueran copias de objetos eliminados o que se eliminarán y se garantice que no cambien en el controlador. (Podría hacerse manualmente simplemente no pasando objetos gestionados por el ORM a los controladores, por supuesto).
Simon Pantzare
fuente
Creo que se obtendrán mejores respuestas si explicas más por qué has llegado a la conclusión
Winston Ewert
55
Antes de que alguien diga que las preguntas sobre la elección del idioma están fuera de tema, voy a decir que creo que esta está bien, ya que es un problema práctico con requisitos específicos. Espero que haga algunas comparaciones detalladas.
Adam Lear
¡Twisted es lo opuesto a concurrente! Es un servidor de un solo subproceso que se basa en eventos, no lo llevará a ninguna parte si necesita una verdadera concurrencia.

Respuestas:

35

Pensamientos de apertura

¿Cómo llegó a la conclusión de que a algunas partes del sistema les iría mejor en otro idioma? ¿Estás sufriendo problemas de rendimiento? ¿Qué tan graves son esos problemas? Si puede ser más rápido, ¿es esencial que sea más rápido?

Asincronía de un solo hilo

Hay varias preguntas y otros recursos web que ya abordan las diferencias, los pros y los contras de la asincronía de subproceso único frente a la concurrencia de subprocesos múltiples. Es interesante leer acerca de cómo funciona el modelo asincrónico de un solo hilo de Node.js cuando la E / S es el principal cuello de botella, y hay muchas solicitudes atendidas a la vez.

Twisted, Tornado y otros modelos asincrónicos hacen un excelente uso de un solo hilo. Dado que gran parte de la programación web tiene muchas E / S (red, base de datos, etc.), el tiempo que pasa esperando llamadas remotas se suma significativamente. Ese es el tiempo que podría dedicarse a hacer otras cosas, como iniciar otras llamadas a la base de datos, generar páginas y generar datos. La utilización de ese hilo único es extremadamente alta.

Uno de los mayores beneficios de la asincronía de un solo hilo es que usa mucha menos memoria. En la ejecución de subprocesos múltiples, cada subproceso requiere una cierta cantidad de memoria reservada. A medida que aumenta el número de subprocesos, también lo hace la cantidad de memoria necesaria solo para que existan los subprocesos. Como la memoria es finita, significa que hay límites en la cantidad de subprocesos que se pueden crear en cualquier momento.


Ejemplo

En el caso de un servidor web, imagine que cada solicitud recibe su propio hilo. Digamos que se requiere 1 MB de memoria para cada subproceso, y el servidor web tiene 2 GB de RAM. Este servidor web podría procesar (aproximadamente) 2000 solicitudes en cualquier momento antes de que simplemente no haya suficiente memoria para procesar más.

Si su carga es significativamente más alta que esto, las solicitudes tardarán mucho tiempo (cuando espere a que se completen las solicitudes más antiguas), o tendrá que lanzar más servidores al clúster para ampliar la cantidad de solicitudes simultáneas posibles .


Concurrencia multihilo

La concurrencia de subprocesos múltiples se basa en la ejecución de varias tareas al mismo tiempo. Eso significa que si un subproceso está bloqueado esperando que vuelva una llamada de la base de datos, se pueden procesar otras solicitudes al mismo tiempo. La utilización de subprocesos es menor, pero la cantidad de subprocesos que se ejecutan es mucho mayor.

El código multiproceso también es mucho más difícil de razonar. Hay problemas con el bloqueo, la sincronización y otros problemas divertidos de concurrencia. La asincronía de un solo hilo no sufre los mismos problemas.

Sin embargo, el código multiproceso es mucho más eficaz para tareas intensivas de CPU . Si no existen oportunidades para que un subproceso "ceda", como una llamada de red que normalmente se bloquearía, un modelo de un solo subproceso simplemente no tendrá ningún tipo de concurrencia.

Ambos pueden coexistir

Por supuesto, hay una superposición entre los dos; No son mutuamente exclusivos. Por ejemplo, el código de varios subprocesos se puede escribir de forma no bloqueante, para utilizar mejor cada subproceso.


La línea de fondo

Hay muchos otros problemas a considerar, pero me gusta pensar en los dos de esta manera:

  • Si su programa está vinculado a E / S , entonces la asincronía de un solo hilo probablemente funcionará bastante bien.
  • Si su programa está vinculado a la CPU , probablemente será mejor un sistema de subprocesos múltiples.

En su caso particular, debe determinar qué tipo de trabajo asincrónico se está completando y con qué frecuencia surgen esas tareas.

  • ¿Ocurren en cada solicitud? Si es así, la memoria probablemente se convertirá en un problema a medida que aumente el número de solicitudes.
  • ¿Se ordenan estas tareas? Si es así, tendrá que considerar la sincronización si usa múltiples hilos.
  • ¿Estas tareas son intensivas en CPU? Si es así, ¿un solo hilo puede mantenerse al día con la carga?

No hay una respuesta simple. Debe considerar cuáles son sus casos de uso y diseñar en consecuencia. A veces, un modelo asincrónico de un solo hilo es mejor. Otras veces, se requiere el uso de varios subprocesos para lograr un procesamiento paralelo masivo.

Otras Consideraciones

Hay otros problemas que debe considerar también, en lugar de solo el modelo de concurrencia que elija. ¿Conoces Erlang o Clojure? ¿Crees que serías capaz de escribir código seguro de subprocesos múltiples en uno de estos idiomas para mejorar el rendimiento de tu aplicación? ¿Va a tomar mucho tiempo ponerse al día en uno de estos idiomas, y el idioma que aprenda le beneficiará en el futuro?

¿Qué hay de las dificultades asociadas con la comunicación entre estos dos sistemas? ¿Será demasiado complejo mantener dos sistemas separados en paralelo? ¿Cómo recibirá el sistema Erlang tareas de Django? ¿Cómo Erlang comunicará esos resultados a Django? ¿Es el rendimiento suficientemente significativo un problema que valga la pena la complejidad adicional?


Pensamientos finales

Siempre he encontrado que Django es lo suficientemente rápido, y lo usan algunos sitios con mucho tráfico. Hay varias optimizaciones de rendimiento que puede hacer para aumentar la cantidad de solicitudes concurrentes y el tiempo de respuesta. Es cierto que hasta ahora no he hecho nada con Celery, por lo que las optimizaciones de rendimiento habituales probablemente no resolverán los problemas que pueda tener con estas tareas asincrónicas.

Por supuesto, siempre existe la sugerencia de lanzar más hardware al problema. ¿Es el costo de aprovisionar un nuevo servidor más barato que el costo de desarrollo y mantenimiento de un subsistema completamente nuevo?

He hecho demasiadas preguntas en este momento, pero esa era mi intención. La respuesta no será fácil sin análisis y más detalles. Sin embargo, poder analizar los problemas se reduce a conocer las preguntas que se deben hacer ... así que espero haber ayudado en ese frente.

Mi instinto dice que una reescritura en otro idioma es innecesaria. La complejidad y el costo probablemente serán demasiado grandes.


Editar

Respuesta al seguimiento

Su seguimiento presenta algunos casos de uso muy interesantes.


1. Django trabajando fuera de las solicitudes HTTP

Su primer ejemplo consistió en leer etiquetas NFC y luego consultar la base de datos. No creo que escribir esta parte en otro idioma sea tan útil para usted, simplemente porque consultar la base de datos o un servidor LDAP estará sujeto a la E / S de la red (y posiblemente al rendimiento de la base de datos). Por otro lado, el número de solicitudes simultáneas estará vinculado por el propio servidor, ya que cada comando de administración se ejecutará como su propio proceso. Habrá tiempo de configuración y desmontaje que afectará el rendimiento, ya que no está enviando mensajes a un proceso que ya se está ejecutando. Sin embargo, podrá enviar múltiples solicitudes al mismo tiempo, ya que cada una será un proceso aislado.

Para este caso, veo dos caminos que puedes investigar:

  1. Asegúrese de que su base de datos sea capaz de manejar múltiples consultas a la vez con la agrupación de conexiones. (Oracle, por ejemplo, requiere que configure Django en consecuencia 'OPTIONS': {'threaded':True}.) Puede haber opciones de configuración similares a nivel de base de datos o nivel de Django que puede ajustar para su propia base de datos. No importa en qué idioma escriba las consultas de su base de datos, tendrá que esperar a que estos datos vuelvan antes de poder encender los LED. Sin embargo, el rendimiento del código de consulta puede marcar la diferencia, y el Django ORM no es tan rápido ( pero , por lo general, lo suficientemente rápido).
  2. Minimice el tiempo de configuración / desmontaje. Tenga un proceso en ejecución constante y envíele mensajes. (Corríjame si me equivoco, pero esto es en lo que realmente se está enfocando su pregunta original). Si este proceso está escrito en Python / Django u otro lenguaje / marco está cubierto anteriormente. No me gusta la idea de usar comandos de administración con tanta frecuencia. ¿Es posible tener un pequeño fragmento de código ejecutándose constantemente, que empuja los mensajes de los lectores NFC a la cola de mensajes, que Celery luego lee y reenvía a Django? La configuración y desmontaje de un pequeño programa, incluso si está escrito en Python (¡pero no en Django!), Debería ser mejor que iniciar y detener un programa Django (con todos sus subsistemas).

No estoy seguro de qué servidor web está utilizando para Django. mod_wsgipara Apache le permite configurar la cantidad de procesos y subprocesos dentro de los procesos que el servicio solicita. Asegúrese de ajustar la configuración relevante de su servidor web para optimizar la cantidad de solicitudes que se pueden atender.


2. "Pasar mensajes" con señales de Django

Su segundo caso de uso también es bastante interesante; No estoy seguro si tengo las respuestas para eso. Si está eliminando instancias de modelo y desea operar en ellas más tarde, podría ser posible serializarlas JSON.dumpsy luego deserializarlas JSON.loads. Será imposible recrear completamente el gráfico de objetos más adelante (consultar modelos relacionados), ya que los campos relacionados se cargan lentamente desde la base de datos y ese enlace ya no existirá.

La otra opción sería marcar de alguna manera un objeto para su eliminación, y solo eliminarlo al final del ciclo de solicitud / respuesta (después de que todas las señales hayan sido atendidas). Puede requerir una señal personalizada para implementar esto, en lugar de confiar en ello post_delete.

Josh Smeaton
fuente
1
muchos FUD y dudas sobre el bloqueo y otras cosas que no son problemas con Erlang, ninguno de los problemas de estado compartido tradicionales que enumera son consideraciones con un idioma y tiempo de ejecución específicamente diseñado para no compartir el estado. Erlang puede manejar decenas de miles de procesos discretos en muy poco ram, la presión de la memoria tampoco es un problema.
@Jarrod, personalmente no conozco a Erlang, así que aceptaré lo que digas al respecto. De lo contrario, casi todo lo demás que mencioné es relevante. Costo, complejidad y si las herramientas actuales se están utilizando correctamente o no.
Josh Smeaton el
Este es el tipo de respuesta épica que realmente me encanta leer ^^. +1, buen trabajo!
Laurent Bourgault-Roy
Además, si tiene plantillas de DJango, pueden usarse en erlang con Erlydtl
Zachary K
8

Hice un desarrollo muy sofisticado y altamente escalable para un importante ISP de EE . UU . Hicimos algunos números de acción serios usando un servidor Twisted , y fue una pesadilla de complejidad hacer que Python / Twisted escale en todo lo que estaba vinculado a la CPU . El enlace de E / S no es un problema, pero el enlace de la CPU era imposible. Podríamos juntar los sistemas rápidamente, pero lograr que escalen a millones de usuarios concurrentes fue una pesadilla de configuración y complejidad si estuvieran atados por la CPU.

Escribí una publicación de blog al respecto, Python / Twisted VS Erlang / OTP .

TLDR; Erlang ganó.


fuente
4

Problemas prácticos con Twisted (que amo y he usado durante unos cinco años):

  1. La documentación deja algo que desear, y el modelo es bastante complejo de aprender de todos modos. Me resulta difícil lograr que otros programadores de Python trabajen en código Twisted.
  2. Terminé usando el bloqueo de E / S de archivos y el acceso a la base de datos por falta de buenas API de bloqueo. Esto realmente puede dañar el rendimiento.
  3. No parece haber una gran comunidad y una comunidad saludable usando Twisted; por ejemplo, Node.js tiene un desarrollo mucho más activo, especialmente para la programación de back-end web.
  4. Todavía es Python, y al menos CPython no es lo más rápido.

He trabajado un poco usando Node.js con CoffeeScript y si el rendimiento concurrente es su preocupación, entonces puede valer la pena el salto.

¿Ha considerado ejecutar varias instancias de Django con algún arreglo para distribuir clientes entre instancias?

Dickon Reed
fuente
1
La documentación de Python en general deja algo que desear: / (No digo que sea tan malo, pero para un lenguaje que uno popular esperaría que fuera mucho mejor).
Torre
3
Creo que la documentación de Python y, en particular, la documentación de Django, son algunos de los mejores documentos para cualquier idioma. Sin embargo, muchas bibliotecas de terceros dejan algo que desear.
Josh Smeaton
1

Sugeriré lo siguiente antes de considerar cambiar a otro idioma.

  1. Use LTTng para registrar eventos del sistema como fallas de página, cambios de contexto y esperas de llamadas del sistema.
  2. Convierta donde sea que esté tomando demasiado tiempo usar la biblioteca C, y use cualquier patrón de diseño que desee (multihilo, basado en eventos de señal, devolución de llamada asíncrona o Unix tradicional select) que es bueno para E / S allí.

No usaría threading en Python una vez que la aplicación tenga prioridad en el rendimiento. Tomaría la opción anterior, que puede resolver muchos problemas como la reutilización de software, la conectividad con Django , el rendimiento, la facilidad de desarrollo, etc.

Holmes
fuente