Construyendo aplicaciones distribuidas con Erlang/OTP

Cualquiera que se haya enfrentado a la construcción de un sistema distribuido, se va a haber dado cuenta que no es labor simple. Así sea pues estamos edificando un sistema a base de microservicios, por el hecho de que repartimos un inconveniente en partes para solventarlas de forma paralela, o bien pues nuestro sistema tiene una concurrencia altísima, nos enfrentaremos a una serie de inconvenientes que son de más conocidos. Y es que existen muchos factores a tomar en consideración, como el control de la concurrencia, la sincronización de los datos o bien la tolerancia a fallos. La buena nueva es que si somos programadores de Elixir o bien Erlang, lo vamos a tener considerablemente más simple merced a OTP.

OTP

OTP (Open Telecom Platform), es un conjunto de librerías, herramientas y patrones que nos dejan administrar procesos y concurrencia con considerablemente más sencillez. OTP fue creado pensando en centrales telefónicas, que por aquella temporada (charlamos de mediados de los noventa), eran de los pocos sistemas enormemente concurrentes que existían. Transcurrido un tiempo, fueron apareciendo más inconvenientes que OTP podía solucionar y es que sus autores lograron crear un modelo capaz de lidiar con conceptos como distribuido, tolerante a fallos, escalable, que marcha en tiempo real y enormemente libre. ¿Qué es lo que significan estos términos?

Escalable: en el momento en que un sistema puede amoldarse a cambios de carga o bien recursos libres.
Distribuido: se refiere a cuando podemos reunir sistemas y como interaccionan unos con otros. Podemos crear conjuntos de sistemas de forma horizontal, por poner un ejemplo agregando más máquinas hardware, para tener más recursos o bien incorporar capacidad de proceso de forma vertical haciendo más potentes nuestras máquinas hardware virtualizadas.
Tolerante a fallos: todo el sistema se comportará de manera previsible cuando se generen fallos. Si el sistema es tolerante a fallos, la latencia y la capacidad de contestación no se van a ver mermadas en demasía y el sistema va a poder proseguir marchando de forma normal.
Funcionamiento en tiempo real: el tiempo de contestación y la latencia van a ser incesantes, y vamos a ser capaces de devolver una contestación en un tiempo razonable y generalmente bajo. Con independencia de las solicitudes concurrentes que recibamos, habremos de ser capaces de contestar a todas y cada una .
Alta disponibilidad: da lo mismo que tengamos un bug en nuestro código, el sistema debe continuar marchando. O sea, que las actualizaciones del código, los parches o bien otras operaciones propias de mantenimiento, no deben parar el sistema, que debe proseguir marchando de forma continua.
Los autores de Erlang/OTP lograron crear un modelo capaz de lidiar con conceptos como distribuido, tolerante a fallos, escalable, que marcha en tiempo real y enormemente libre

Con OTP, y usando tanto Erlang, como Elixir, podemos lograr supervisar todas y cada una estas peculiaridades de los sistemas distribuidos de forma robusta. ¿Y de qué forma logra OTP hacer fácil (o bien accesible) lo que es complejo? Puesto que con una mezcla de las próximas peculiaridades.

Erlang/Elixir

Un lenguaje funcional es de ayuda en el momento de lograr cierta seguridad en el momento de crear software distribuido, mas más esencial es la imperturbabilidad del mismo. En otros lenguajes alterables, debemos recurrir a sistemas de sincronización de datos para eludir inconvenientes acceso concurrente. Semáforos, monitores, bloqueos etcétera son palabras conocidas entre todos los que nos hemos visto en la necesidad de programar alguna aplicación basada en hilos o bien procesos.

Con Erlang y Elixir es una cosa que tenemos solucionado desde la base, en tanto que siendo los datos inalterables, evitamos de un plumazo todos estos inconvenientes. Si las estructuras de datos de nuestros programas no pueden alterarse, no van a existir inconvenientes de concurrencia.

Además de esto, estos 2 lenguajes asimismo están diseñados para lanzar procesos de manera sencilla y su forma de administrarlos nos ayuda mucho en el momento de producir aplicaciones diseñadas para trabajar de forma distribuida.

La máquina virtual BEAM

Otra de las patas esenciales en OTP es la máquinva virtual. Erlang y Elixir corren sobre una máquina virtual famosa como BEAM, que curiosamente son las iniciales de Bogdan/Björn's Erlang Abstract Machine, nombres de 2 programadores que trabajaban en Ericsson por la temporada.

En palabras de Joe Armstrong, uno de los coautores de Erlang Puedes imitar la lógica de Erlang, mas si no corre sobre la máquina virtual de Erlang no puedes imitar su semántica. Conque, por realmente bonitos que sean los lenguajes de programación, sin una máquina virtual bien desarrollada, no tendríamos muchas de las funcionalidades cubiertas.

Puedes imitar la lógica de Erlang, mas si no corre sobre la máquina virtual de Erlang no puedes imitar su semántica. Joe Armstrong

El código que producimos con Erlan o bien Elixir (y algún lenguaje más) hay que compilarlo, para crear un fichero con extensión .beam. Ese fichero es al final el que se ejecuta sobre BEAM.

BEAM está optimada para administrar concurrencia, tiene un colector de basura por cada proceso (haciendo que la recolección sea más fácil y veloz) y que marcha de forma muy predecible y consistente en todos y cada uno de los casos.

Herramientas y librerías

Aparte de Erlang y Elixir como lenguajes, y aparte de BEAM como máquina virtual, OTP incluye otra serie de añadidos que hacen toda la magia posible. Ciertas de estas peculiaridades son el Erlang runtime system (ERS), ciertas librerías estándar (stdlib), bases de datos distribuidas como MNESIA, una compilación de protocolos y también interfaces para comunicarse con otros lenguajes de programación, como C o bien Java, herramientas de seguridad como SSL, sistemas de acceso a LDAP y un largo etcétera como un debugger gráfico y Observer para controlar procesos.

Nodos

Los nodos son un conjunto de las herramientas previamente descritas, como de herramientas de terceros, que marchan sobre el sistema operativo. Cada nodo, puede marchar de forma independiente, mas se comunica con el resto de nodos de la red, dejando hacer nuestro sistema escalable de forma horizontal.

Cada nodo puede conectarse a uno o bien múltiples nodos, de forma transitiva. Esto es, que si tenemos un nodo A, conectado a B, y conectamos B a C, C asimismo va a estar conectado con A. Para administrar la seguridad de los nodos se usa lo que es conocido como una magic cookie. Cuando se procura una conexión entre nodos, se verifica esta cookie y si coincide los nodos pueden conectarse. En otro caso se rechaza la conexión.

Procesos

Si bien OTP está compuesta de muchas partes distintas, podríamos decir que la parte primordial son los procesos. Al final son los procesos los encargados de efectuar las operaciones demandadas, y la administración que hace OTP de ellos es parte esencial en todo el sistema.

No debemos meditar en los procesos tal y como si estuviésemos hablando de procesos del Sistema Operativo. En un caso así los procesos son considerablemente más ligeros, lo que nos deja ejecutar muchos de forma concurrente sin que nuestro sistema se resienta. En verdad un nodo puede ejecutar centenares de miles de procesos (aun millones en dependencia de la potencia del hardware), sin afectar al desempeño.

Un proceso en Erlang/Elixir está compuesto por su buzón de mensajes, su colector de basura, un stack con la información precisa y una zona para administrar los links a otros procesos. En conjunto, probablemente el tamaño no sea más que de 1Kb (2Kb en sistemas de sesenta y cuatro bits). Como veis los procesos son pequeñísimos, lo que hace que el cambio de contexto que debe efectuar el procesador sea muy rápido.

Mas la parte más esencial es indudablemente la comunicación entre procesos. Los procesos se comunican basándonos en un modelo de actores, o bien lo que es exactamente lo mismo, los procesos no comparten memoria, y solo se comunican unos con otros a través del buzón de mensajes. De nuevo esto nos evita muchos inconvenientes de concurrencia.

Si un proceso desea comunicarse con otro, va a dejar un mensaje en el buzón del proceso receptor, que el proceso receptor procesará cuando le resulte posible. Merced a este modelo de actores, evitamos los inconvenientes relacionados con compartir memoria y hacemos considerablemente más fácil el trabajo del colector de basura.

Al tener la posibilidad de administrar los procesos de forma independiente, se nos presentan interesantes opciones para crear estructuras jerárquicas de procesos de manera que sea considerablemente más fácil administrar los procesos. Es acá donde entran en juego los conceptos de aplicación, supervisor o bien los más básicos como los GenServer.

Supervisores

Los supervisores son procesos que tienen el único objetivo de lanzar y controlar procesos hijos. Son capaces de advertir en el momento en que un proceso que depende de él se ha detenido (por un fallo o bien por una ejecución normal), y en dependencia de su configuración, emplear diferentes estrategias para su reinicio. Son las siguientes:

One for one: si un proceso falla, se vuelve a reiniciar ese y solo ese proceso.

One for all: si un proceso falla, se detienen todos y cada uno de los procesos de ese supervisor y se vuelven a empezar.

Rest for one: si un proceso falla, además de esto de él, se detienen todos y cada uno de los procesos que se hayan comenzado después y se vuelven a empezar.

Por ende la clave en el momento de emplear supervisores, es cerciorarse de que el orden de comienzo está adecuadamente designado y la estrategia de reinicio escogida es la adecuada.

Con todo esto podemos crear estructuras de supervisión aproximadamente complejas. Los fallos se pueden ir extendiendo cara arriba en la jerarquía de supervisión. Si un proceso falla, su supervisor va a decidir reiniciarlo. Si el inconveniente se solventa con ese reinicio, la ejecución proseguirá de forma normal. Mas si el proceso reiniciado vuelve a fallar, se proseguirá procurando, hasta el momento en que se alcance un límite de intentos preconfigurado. Es ahí cuando el supervisor se detendrá y va a pasar el fallo a su supervisor. Si ningún reinicio solventa el inconveniente, posiblemente se tomen medidas radicales como reiniciar la máquina virtual, o bien aun reiniciar la máquina.

Aplicaciones

Las aplicaciones no tienen una definición simple, mas son algo como conjuntos de módulos, supervisores, configuraciones y otros recursos. Estos conjuntos son independientes unos de otros y es una forma de reunir código para poder desplegarlo en cualquier parte. Por poner un ejemplo podemos desplegar una aplicación en un nodo de Erlang y dicha aplicación va a poder ser arrancada y detenida como un todo. Las aplicaciones pueden ser de tipo normal o bien de tipo librería. Las primeras arrancan un supervisor para poder administrar los procesos dependientes, al paso que las de tipo librería no lo hacen, puesto que no lo precisan.

GenServer

Un GenServer, incorpora la habitual estructura cliente del servicio servidor. Si bien con Erlang y Elixir pueden lanzarse procesos de forma manual, es considerablemente más fácil crearlos por medio de un GenServer. Los GenServer se fundamentan en comportamientos (behaviours en inglés), que definen una interfaz común para la comunicación entre procesos. Esta interfaz emplea llamadas handle_call (síncronas) y handle_cast (asíncronas), para efectuar todas y cada una de las operaciones requeridas. Los GenServer se pueden comenzar desde una función start_link, que acostumbra a ser empleada, entre otras muchas cosas, por los supervisores en el momento de arrancar el proceso.

Como hemos comentado ya antes, con OTP usamos un modelo de actores, y solo podemos comunicarnos con un proceso por medio de su buzón de mensajes. Si empleamos los call, nuestro proceso va a quedar a la espera de una contestación del proceso recóndito, al paso que si usamos cast, seguiremos la aplicación sin aguardar ninguna contestación.

Actualización en caliente

Como afirmábamos, si un sistema que debe tener alta disponibilidad no puede detenerse para ser actualizado. Debemos asegurar que el sistema es capaz de marchar aun cuando debemos aplicar parches para corregir bugs o bien para agregar nueva funcionalidad.

Por fortuna, con OTP, tenemos la posibilidad de usar la actualización de código en caliente. Para esto los módulos deben cargarse anteriormente, de lo que se encarga un componente de OTP conocido como servidor de código.

En el sistema puede haber hasta 2 versiones de un mismo módulo, si bien en un inicio solo va a haber una versión. Si efectuamos algún cambio en el código, la versión existente va a pasar a ser la versión vieja, y la versión nueva