architecture erlang otp

architecture - ¿Cómo se diseña la arquitectura de un sistema multinúcleo tolerante a fallas distribuido basado en Erlang/OTP?



(1)

Me gustaría construir un sistema basado en Erlang / OTP que resuelva un problema "embarazosamente paralelo".

Ya leí / revisé:

  • Aprende algo de Erlang;
  • Programación de Erlang (Armstrong);
  • Programación de Erlang (Cesarini);
  • Erlang / OTP en acción.

Tengo la esencia de Procesos, Mensajes, Supervisores, gen_servers, Logging, etc.

Entiendo que ciertas opciones de arquitectura dependen de la aplicación en cuestión, pero aún así me gustaría conocer algunos principios generales del diseño del sistema ERlang / OTP.

¿Debería comenzar con algunos gen_servers con un supervisor y construir incrementalmente sobre eso?

¿Cuántos supervisores debería tener? ¿Cómo decido qué partes del sistema deberían estar basadas en procesos? ¿Cómo debo evitar los cuellos de botella?

¿Debo agregar el registro más tarde?

¿Cuál es el enfoque general de la arquitectura de sistemas multiprocesadores tolerante a fallas distribuida Erlang / OTP?


¿Debo comenzar con unos gen_servers con un supervisor y construir incrementalmente sobre eso?

Le falta un componente clave en las arquitecturas de Erlang aquí: ¡aplicaciones! (Es decir, el concepto de aplicaciones OTP, no aplicaciones de software).

Piensa en las aplicaciones como componentes. Un componente en su sistema resuelve un problema particular, es responsable de un conjunto coherente de recursos o abstrae algo importante o complejo del sistema.

El primer paso al diseñar un sistema Erlang es decidir qué aplicaciones se necesitan. Algunos se pueden extraer de la web tal como están, a los que podemos hacer referencia como bibliotecas. Otros necesitarás escribirte a ti mismo (de lo contrario no necesitarías este sistema en particular). Estas aplicaciones a las que generalmente nos referimos como la lógica de negocios (a menudo también necesita escribir algunas bibliotecas, pero es útil mantener la distinción entre las bibliotecas y las aplicaciones de negocio principales que unen todo).

¿Cuántos supervisores debería tener?

Debe tener un supervisor para cada tipo de proceso que desee supervisar.

¿Un grupo de trabajadores temporales idénticos? Un supervisor para gobernarlos a todos.

¿Proceso diferente con diferentes responsabilidades y estrategias de reinicio? Un supervisor para cada tipo diferente de proceso, en una jerarquía correcta (dependiendo de cuándo deben reiniciarse las cosas y qué otro proceso debe realizarse con ellas).

Algunas veces está bien colocar un montón de diferentes tipos de procesos bajo el mismo supervisor. Este suele ser el caso cuando tiene algunos procesos únicos (por ejemplo, un supervisor de servidor HTTP, un proceso de propietario de tabla ETS, un recopilador de estadísticas) que siempre se ejecutarán. En ese caso, puede ser demasiado complicado tener un supervisor para cada uno, por lo que es común agregar el supervisor bajo un mismo. Solo tenga en cuenta las implicaciones de usar una estrategia de reinicio particular al hacer esto, para que no one_for_one el proceso estadístico, por ejemplo, en caso de que su servidor web one_for_one ( one_for_one es la estrategia más común para usar en casos como este). Tenga cuidado de no tener ninguna dependencia entre procesos en un supervisor one_for_one . Si un proceso depende de otro proceso bloqueado, también puede bloquearse, lo que activa la intensidad de reinicio de los supervisores con demasiada frecuencia y hace que el supervisor se bloquee demasiado pronto. Esto se puede evitar teniendo dos supervisores diferentes, que controlarían completamente los reinicios según la intensidad y el período configurados ( explicación más larga ).

¿Cómo decido qué partes del sistema deberían estar basadas en procesos?

Cada actividad concurrente en su sistema debe estar en su propio proceso. Tener la abstracción equivocada de la concurrencia es el error más común por los diseñadores del sistema Erlang al principio.

Algunas personas no están acostumbradas a lidiar con la concurrencia; sus sistemas tienden a tener muy poco de eso. Un proceso, o unos gigantescos, que ejecuta todo en secuencia. Estos sistemas suelen estar llenos de código malicioso y el código es muy rígido y difícil de refactorizar. También los hace más lentos, ya que es posible que no utilicen todos los núcleos disponibles para Erlang.

Otras personas captan de inmediato los conceptos de concurrencia pero no los aplican de manera óptima; sus sistemas tienden a abusar del concepto de proceso, lo que hace que muchos procesos permanezcan inactivos esperando a otros que están trabajando. Estos sistemas tienden a ser innecesariamente complejos y difíciles de depurar.

En esencia, en ambas variantes obtienes el mismo problema, no usas toda la concurrencia disponible para ti y no obtienes el máximo rendimiento fuera del sistema.

Si se apega al principio de responsabilidad única y cumple con la regla de tener un proceso para cada actividad realmente concurrente en su sistema, debería estar bien.

Hay razones válidas para tener procesos inactivos. A veces mantienen un estado importante, a veces desea guardar algunos datos temporalmente y luego descartar el proceso, a veces esperan eventos externos. El mayor escollo es pasar mensajes importantes a través de una larga cadena de procesos mayormente inactivos, ya que ralentizará su sistema con mucha copia y usará más memoria.

¿Cómo debo evitar los cuellos de botella?

Difícil de decir, depende mucho de su sistema y de lo que está haciendo. Sin embargo, en general, si tiene una buena división de responsabilidades entre aplicaciones, debería poder escalar la aplicación que parece ser el cuello de botella por separado del resto del sistema.

¡La regla de oro aquí es medir, medir, medir ! No piense que tiene algo que mejorar hasta que haya medido.

Erlang es excelente ya que le permite ocultar la concurrencia detrás de las interfaces (conocida como concurrencia implícita). Por ejemplo, utiliza una API de módulo funcional, un module:function(Arguments) normal module:function(Arguments) interfaz de module:function(Arguments) , que a su vez podría generar miles de procesos sin que la persona que llama tenga que saberlo. Si tiene sus abstracciones y su API correcta, siempre puede paralelizar u optimizar una biblioteca después de que haya comenzado a usarla.

Dicho esto, aquí hay algunas pautas generales:

  • Intente enviar mensajes directamente al destinatario, evite canalizar o enrutar mensajes a través de procesos intermediarios. De lo contrario, el sistema simplemente pasa tiempo moviendo mensajes (datos) sin funcionar realmente.
  • No abuse de los patrones de diseño de OTP, como gen_servers. En muchos casos, solo necesita iniciar un proceso, ejecutar un fragmento de código y luego salir. Para esto, un gen_server es excesivo.

Y un consejo extra: no reutilizar procesos. Engendrar un proceso en Erlang es tan barato y rápido que no tiene sentido reutilizar un proceso una vez que se acaba su vida útil. En algunos casos, puede tener sentido reutilizar el estado (por ejemplo, análisis complejo de un archivo), pero es mejor almacenarlo canónicamente en otro lugar (en una tabla ETS, una base de datos, etc.).

¿Debo agregar el registro más tarde?

Ya existe una funcionalidad básica de registro en Erlang / OTP, el registrador de errores . Junto con SASL (Bibliotecas de soporte de arquitectura de sistema), puede iniciar y ejecutar con el inicio de sesión en tiempo no oportuno.

Cuando llegue el momento (y si ha abstraído la API de inicio de sesión desde el principio) puede cambiar esto por algo que mejor se adapte a sus necesidades. La biblioteca de registro de facto de terceros actualmente es Basho''s Lager .

¿Cuál es el enfoque general de la arquitectura de sistemas multiprocesadores tolerante a fallas distribuida Erlang / OTP?

Para resumir lo que se ha dicho anteriormente:

  • Divida su sistema en aplicaciones
  • Coloque sus procesos en la jerarquía de supervisión correcta, según sus necesidades y dependencias
  • Ten un proceso para cada actividad realmente concurrente en tu sistema
  • Mantenga una API funcional para los otros componentes en el sistema. Esto te permite:
    • Refactorice su código sin cambiar el código que lo está usando
    • Optimizar el código después
    • Distribuya su sistema cuando sea necesario (¡simplemente haga una llamada a otro nodo detrás de la API! ¡La persona que llama no se dará cuenta!)
    • Pruebe el código más fácilmente (menos trabajo configurando arneses de prueba, más fácil de entender cómo usarlo)
  • Comience a usar las bibliotecas disponibles para usted en OTP hasta que necesite algo diferente (lo sabrá, cuando llegue el momento)

Errores comunes:

  • Demasiados procesos
  • Muy pocos procesos
  • Demasiado enrutamiento (mensajes reenviados, procesos encadenados)
  • Muy pocas aplicaciones (nunca he visto el caso opuesto, en realidad)
  • No hay suficiente abstracción (hace que sea difícil refactorizar y razonar. ¡También hace que sea difícil de probar!)