DEV Community

Cover image for Historia de la BeamVM
Camilo for JavaScript Chile

Posted on

Historia de la BeamVM

¿Qué es Erlang?

Erlang es un lenguaje de programación funcional. Esto quiere decir que se basa en los principios de funciones con transparencia referencial e inmutabilidad. La transparencia referencial quiere decir que una función con los mismos parámetros debería retornar el mismo resultado. La inmutabilidad quiere decir que no puedo alterar el valor de una variable una vez que se ha asignado, por ejemplo si digo que x es 5, no es lógico que también sea 6 (sería deshonesto). Sin embargo existen casos donde una función puede retornar otro resultado con los mismos parámetros (por ejemplo una función de la fecha actual). Erlang usa un enfoque pragmático: Obedecer los principios de la programación funcional (inmutabilidad, transparencia referencial) y romperlos cuando aparecen conflictos del mundo real.

Además del lenguaje de programación, Erlang tiene todo un ecosistema de herramientas. En primer lugar se encuentra su máquina virtual (BEAM VM), la cual ejecuta código compilado con un bytecode específico, muy similar a la JVM de Java. Por lo que el código puede ser ejecutado en cualquier sistema para la cual la BEAM VM sea compatible. También proporciona herramientas de desarrollo como compiladores, debuggers, herramientas para análisis de rendimiento y pruebas. El framework OTP, servidores web, generadores de analizadores de código fuente (parsers), una base de datos distribuida llamada mnesia, entre otras herramientas. La máquina virtual y sus bibliotecas permiten actualizar el código en caliente (hot reload), lo que significa cambiar el código durante la ejecución del programa sin interrupción del servicio y también permitir la ejecución del código de forma distribuida en varias computadoras, manejando los errores y fallos de una forma simple y poderosa.

Todas estas características permiten una habilidad de Erlang para la resiliencia frente a errores y organización del código en procesos y mensajes concurrentes.

Origen de Erlang

Erlang comenzó como una biblioteca de Prolog, luego como un dialecto de Prolog, para finalmente ser un lenguaje de programación por si mismo. El objetivo fue resolver el problema de construir sistemas distribuidos robustos y confiables. Desarrollado originalmente en la empresa de telecomunicaciones Ericsson para sus switch telefónicos. Desde un inicio se enfrentaron a desafíos de escala mundial como cientos de milllones de usuarios y condiciones de servicio muy estrictas. Fue liberado al público en 1998 como un proyecto Open Source. Hoy en día, según Cisco, el 90% del tráfico de internet es orquestado por nodos programados en Erlang.

Erlang y la BEAM VM han evolucionado a lo largo de más de 30 años para otros casos de uso como la robótica, machine learning, aplicaciones web, entre otros. Uno de sus creadores principales Joe Armstrong lo detalla de la siguiente forma:

Erlang fue diseñado para escribir programas concurrentes que se ejecutasen eternamente. Erlang usa procesos concurrentes para estructurar el programa. Estos procesos no tienen memoria compartida y se comunican por paso de mensajes asíncronos. Los procesos de Erlang son ligeros y pertenecen al lenguaje, no al sistema operativo. Erlang tiene mecanismos que permiten que los programas cambien on-the-fly (en vivo) así, esos programas pueden evolucionar y cambiar sin detener su ejecución. Estos mecanismos simplifican la construcción de software implementando sistemas non-stop (que no se detienen).

El desarrollo inicial de Erlang tuvo lugar en 1986 en el Laboratorio de Computación de Ericsson. Erlang fue diseñado con un objetivo específico en mente: proporcionar una mejor forma de programar aplicaciones de telefonía. En ese momento, las aplicaciones de telefonía eran atípicas del tipo de problemas que podían resolver los lenguajes de programación convencionales. Las aplicaciones de telefonía son, por su naturaleza, altamente concurrentes: un simple switch debe manejar decenas o cientos de miles de transacciones simultáneas. Tales transacciones son intrínsecamente distribuidas y el software se espera que sea altamente tolerante a fallos. Cuando el software que controla los teléfonos falla, sale en los periódicos, algo que no ocurre cuando fallan las aplicaciones de escritorio. El software de telefonía debe también cambiar on-the-fly, esto es, sin perder el servicio mientras se realiza una actualización del código. El software de telefonía debe también operar en tiempo real, con ajustados requisitos de tiempo para algunas operaciones, y más relajado tiempo en otras clases de operaciones.

¿Por qué Erlang es Bueno?

Joe Armstrong, fijó los requisitos de Erlang en solucionar los problemas de un entorno altamente concurrente, que no puede permitirse caer y que debe de actualizarse sin pérdida de servicio. Actualmente, esta definición calza con casi la mayor parte de servicios en Internet.

Sin embargo pensar de que Erlang solamente es para casos de uso de procesos y mensajes livianos y concurrentes es insuficiente para describirlo. En su tésis de doctorado Joe Armstrong detalla componentes genéricos llamados "behaviours" (comportamientos) en Erlang. Estos "behaviours" son similares a las interfaces en otros lenguajes de programación y permiten el polimorfismo, es decir, que los programas puedan trabajar con múltiples formas.

Joe Armstrong detalló seis distintos behaviours gen_server, gen_event, gen_fsm (gen_statem), supervisor, application y release. Definió que estos seis behaviours eran suficientes para crear sistemas distribuidos confiables y robustos.

Los behaviours son escritos por expertos y están basados en años de experiencia y representan las "mejores prácticas". Permiten que los programadores de la aplicación se enfoquen en la "lógica de negocios", mientras que la infraestructura es proporcionada automáticamente por el behaviour. El código es escrito de forma secuencial y toda la concurrencia es realizada por el behaviour "por debajo". Esto facilita que nuevos miembros del equipo aprendan la lógica de negocios, ya que es secuencial y es similar a cómo operan otros lenguajes de programación.

La siguiente lista es una breve descripción de cada behaviour.

  • gen_server: Un servidor genérico. Permite crear un servicio que puede recibir llamadas.
  • gen_event: Un gestor de eventos. Permite enviar mensajes cuando ocurren eventos definidos.
  • gen_statem: Una máquina de estados, anteriormente conocida como gen_fsm. Permite validar estados de los datos.
  • supervisor: Un supervisor es un proceso cuya tarea es que otros procesos (hijos) esten vivos y realizando su labor. Con múltiples estrategias para reiniciarlos si estos fallan. Un supervisor puede ser padre de otros supervisores.
  • application: Una aplicación es un conjunto de componentes gen_server, gen_event, gen_statem y supervisor utilizados para un fin. Se le llama "árbol de supervisión" (supervision tree).
  • release: Un sistema puede contener una o múltiples application, lo que se considera un release. Además proporciona herramientas para actualizar el código y volver a un estado anterior (rollback) si la actualización falla.

Si se tiene en cuenta que un supervisor puede supervisar a otros supervisores (los cuales pueden estar ejecutándose en otro computador), nos da una idea de lo poderoso que pueden ser los behaviour. Se podría hacer un paralelo con Kubernetes, pero la principal diferencia es que estos behaviours son ejecutados a nivel del proceso/hilo a diferencia de Kubernetes que se ejecuta a nivel del contenedor Docker.

Las ideas de los supervisores y las estrategias de reinicio vienen de la observación de que usualmente es más simple reiniciar un servicio para solucionar un problema. ¿Has probado apagar y prender un equipo para solucionar un problema?. Esto se puede explicar con un ejemplo: Si estoy siguiendo la ruta de un mapa y me pierdo, es más simple partir del punto inicial que desde donde me perdí para llegar a la meta, es decir, en vez de encontrar el error y repararlo sobre la marcha, es más rápido y correcto registrar el error y volver al inicio para intentarlo de nuevo.

Saber que los procesos pueden fallar y serán reiniciados por un supervisor nos permite fallar temprano y rápido (Siguiendo las recomendaciones de Jim Gray). Un proceso produce un resultado correcto según la especificación o envía una señal de fallo y se detiene su operación. "Let it crash!" (Déjalo caer) es una frase acuñada por Joe Armstrong para denominar este comportamiento de "camino feliz", donde si ocurre algo fuera del "camino feliz", el proceso debe detenerse y no tratar de solucionar el problema sobre la marcha (potencialmente empeorando la situación), dejando que otro componente dentro del árbol de supervisión maneje el error.

Los supervisores y la filosofía de "Let it Crash!" de Erlang le permiten producir sistemas robustos y confiables. Se puede ejemplificar con la máquina Ericsson AXD301, la cual alcanzó nueve nueves (99.9999999%) de fiabilidad en sus sistemas. Para poner en perspectiva la fiabilidad de cinco nueves (99.999%) se considera bueno (5.26 minutos de servicio caido por año). En grandes compañias se estima que existe 1.6 horas de servicio caido por semana. Nueve nueves de fiabilidad es como un parpadeo al año de servicio caido (31.56 milisegundos al año). Si bien los nueve nueves fueron alcanzados en una situación específica y no hay claridad total de cómo se obtuvieron dichos datos, se puede afirmar que la tecnología de Erlang da una fiabilidad y robustes muy grande.

El modelo de Actores

Se ha definido Erlang y su ecosistema, pero para tener una noción más íntegra se debe explicar lo que es concurrencia. En muchos lugares se puede considerar concurrencia y paralelismo como el mismo concepto. En Erlang son dos ideas separadas. La concurrencia se refiere a la idea de actores ejecutandose de forma independiente, pero no necesariamente al mismo tiempo. El paralelismo es tener actores independientes ejecutandose al mismo tiempo. Si lo vemos a nivel de procesador, concurrencia es que cada proceso tiene su tiempo de ejecución en un único procesador, similar a como funcionaban los sistemas antes de la existencia de múltiples núcleos en los procesadores. El paralelismo ha estado disponible desde el inicio de Erlang, simplemente era necesario un computador anexo y conectado al computador principal. En la actualidad los procesadores con múltiples núcleos permiten paralelismo en un único computador (en contextos industriales llegando a docenas de núcleos por procesador) y Erlang permite aprovechar estas características completamente (desde aproximadamente el año 2009 con la implementación del multiprocesamiento simétrico).

Para lograr la concurrencia Erlang utiliza el modelo de actores. Cada actor es un proceso separado (función) y aislado en la máquina virtual y se comunican utilizando mensajes. Cada proceso (actor) es totalmente independiente y no comparte ninguna información con otros procesos, solamente utilizan mensajes entre ellos para comunicar datos. Son livianos (no son procesos del sistema operativo). Toda la comunicación es explícita, segura y con alta trazabilidad. Si un proceso falla, no afectará a los otros procesos, ya que son totalmente independientes entre si.

Una cosa importante a considerar en relación a las habilidades de escalamiento de Erlang y sus procesos ligeros. Es cierto que pueden tener cientos de miles de procesos existentes al mismo tiempo, pero no significa abusar de ellos. Por ejemplo crear un videojuego de disparos en los que cada bala sea su propio actor es algo excesivo. Ya que hay un pequeño costo en enviar mensajes entre actores, si se dividen las tareas demasiado puede incluso perjudicar el rendimiento.

Se puede pensar de que la programación paralela es directamente proporcional a la cantidad de núcleos de un procesador, lo que se conoce como escalamiento lineal. Pero es importante recordar de que no existe algo perfecto y libre de costos asociados. El paralelismo no es la respuesta a cada problema, en algunos casos incluso puede afectar la velocidad de la aplicación, por ejemplo cuando existen tareas redundantes o el código es 100% secuencial pero intenta utilizar procesos paralelos. Un programa en paralelo va tan rápido como su parte secuencial más lenta. Esto significa que usar paralelismo para todos los problemas no garantiza que sea más rápido. Simplemente es una herramienta que puede ser muy útil, pero no siempre es la adecuada.

Conclusión

Quizás se ha encontrado con una situación similar a la siguiente: Mi sistema es local y no necesito de las capacidades de concurrencia que Erlang proporciona. Jamás llegaré a los niveles de exigencia en mis sistemas que justifiquen utilizar dichas capacidades.

Se podría pensar de que al tener sistemas de nicho nunca tendremos la necesidad de usar las bondades de la BEAM VM. ¿Pero qué pasa si tenemos una cantidad de datos enorme que debe ser procesada?.

Esto sucedió en una empresa donde debían migrar una base de datos de usuarios a un nuevo sistema. Se debían procesar las tablas y ajustarlas al formato del nuevo sistema, es decir, se debía crear un pequeño script ETL (Extract Transform Load) para extraer, transformar y cargar datos. En un inicio un programador que solamente conocía Python intentó realizar la migración, pero debido a la cantidad de datos la operación tomaba más de una semana en completar. Luego la tarea pasó a un equipo que conocía Elixir y la operación tardó menos de un día en ser completada, gracias a las bondades de concurrencia y paralelismo de la Beam VM.
Se puede argumentar de que tal ves era inexperiencia en Python o no se usaron las herramientas adecuadas en dicho lenguaje. Pero eso nos habla de que realizar concurrencia y paralelismo en otros lenguajes no es tan simple como en el ecosistema de Erlang.

Si bien todas estas herramientas e ideas podrían ser implementadas y desarrolladas en otros lenguajes de programación y ecosistemas (y viceversa). No es una tarea sencilla y tampoco se contaría con más de 30 años de evolución funcionando en sistemas productivos de clase mundial. Por lo que la máquina virtual de Erlang (BEAM VM) es una tecnología robusta, confiable y acertada para ser usada en las soluciones de software actuales. Se debe evaluar cada problema y seleccionar las herramientas adecuadas según su contexto.

Referencias

Top comments (0)