Capítulo 1. Introducción Introducción
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Tienes que implantar un sistema en tiempo real suave, escalable y tolerante a fallos, con requisitos de alta disponibilidad. Tiene que estar dirigido por eventos y reaccionar ante estímulos externos, carga y fallos. Debe responder siempre. Has oído hablar, con razón, de muchas historias de éxito que te dicen que Erlang es la herramienta adecuada para el trabajo. Y de hecho lo es, pero aunque Erlang es un potente lenguaje de programación, no basta por sí solo para agrupar todas estas características y construir sistemas reactivos complejos. Para hacer el trabajo de forma correcta, rápida y eficaz, también necesitas middleware, bibliotecas reutilizables, herramientas, principios de diseño y un modelo de programación que te diga cómo diseñar y distribuir tu sistema.
Nuestro objetivo con este libro es explorar múltiples facetas de la disponibilidad y la escalabilidad, así como temas relacionados como la concurrencia, la distribución y la tolerancia a fallos, en el contexto del lenguaje de programación Erlang y su marco OTP. Erlang/OTP se creó cuando el equipo del Laboratorio de Informática de Ericsson (CS Lab) se propuso investigar cómo podían desarrollar eficazmente la próxima generación de sistemas de telecomunicaciones en un sector en el que el tiempo de comercialización se estaba volviendo crítico. Esto era antes de la Web, antes de las tabletas y los teléfonos inteligentes, de los juegos multiusuario masivos en línea, de la mensajería y del Internet de las Cosas.
En aquella época, los únicos sistemas que requerían los niveles de escalabilidad y tolerancia a fallos que hoy damos por descontados eran los aburridos conmutadores telefónicos. Tenían que gestionar picos masivos de tráfico en Nochevieja, cumplir las obligaciones reglamentarias de disponibilidad de las llamadas a los servicios de emergencia y evitar las penosamente costosas penalizaciones contractuales impuestas a los proveedores de infraestructuras cuyos equipos causaran interrupciones. En términos sencillos, si descolgabas el teléfono y no oías el tono de llamada al otro lado, podías estar seguro de dos cosas: los altos cargos se meterían en serios problemas y la interrupción sería noticia de portada en los periódicos. Pasara lo que pasara, esos conmutadores no podían fallar. Incluso cuando fallaban los componentes y la infraestructura que los rodeaba, había que atender las peticiones. Hoy en día, los reguladores y las multas han sido sustituidos por usuarios impacientes y sin lealtad que no dudarán en cambiar de proveedor, y los artículos de portada de los periódicos han sido sustituidos por la histeria colectiva en las redes sociales. Pero los problemas fundamentales de disponibilidad y escalabilidad persisten.
Como resultado, tanto los conmutadores de telecomunicaciones como los sistemas modernos tienen que reaccionar a los fallos tanto como a la carga y a los acontecimientos internos. Así que, aunque la gente del Laboratorio de Informática de Ericsson no se propuso inventar un lenguaje de programación, la solución al problema que querían resolver resultó ser uno. Es un gran ejemplo de inventar un lenguaje y un modelo de programación que facilite la tarea de resolver un problema concreto y bien definido.
Definir el problema
Como mostramos a lo largo de este libro , Erlang/OTP es único entre los lenguajes y marcos de programación por la amplitud, profundidad y coherencia de las funciones que ofrece para sistemas escalables y tolerantes a fallos con requisitos de alta disponibilidad. Diseñar, implantar, operar y mantener estos sistemas es todo un reto. Los equipos que consiguen construirlos y ponerlos en marcha lo hacen iterando continuamente a través de esas cuatro fases, utilizando constantemente los comentarios de las métricas de producción y el monitoreo para ayudar a encontrar áreas que pueden mejorar no sólo en su código, sino también en sus procesos de desarrollo y funcionamiento. Los equipos de éxito también aprenden a mejorar la escalabilidad por otros medios, como las pruebas, la experimentación y la evaluación comparativa, y se mantienen al día de la investigación y el desarrollo relevantes para las características de su sistema. Las cuestiones no técnicas, como los valores y la cultura de la organización, también pueden desempeñar un papel importante a la hora de determinar si los equipos pueden cumplir o superar los requisitos de su sistema.
Utilizamos los términos distribuido, tolerante a fallos, escalable, tiempo real suave y alta disponibilidad para describir los sistemas que pensamos construir con OTP. Pero, ¿qué significan realmente estas palabras?
Escalable se refiere a lo bien que puede adaptarse un sistema informático a los cambios de carga o de recursos disponibles. Los sitios web escalables, por ejemplo, son capaces de manejar sin problemas los picos de tráfico sin que se caiga ninguna petición de los clientes, incluso cuando falla el hardware. Un sistema de chat escalable puede ser capaz de dar cabida a miles de nuevos usuarios al día sin que se interrumpa el servicio que presta a sus usuarios actuales.
Distribuido se refiere a cómo se agrupan los sistemas e interactúan entre sí. Los clústeres pueden diseñarse para escalar horizontalmente añadiendo hardware básico (o normal), o en una sola máquina, donde se despliegan instancias adicionales de nodos independientes para utilizar mejor los núcleos disponibles. Las máquinas individuales también pueden virtualizarse, de modo que las instancias de un sistema operativo se ejecuten en otros sistemas operativos o compartan los recursos bare-metal. Añadir más potencia de procesamiento a un clúster de base de datos podría permitirle escalar en cuanto a la cantidad de datos que puede almacenar o el número de peticiones por segundo que puede gestionar. El escalado descendente suele ser igual de importante; por ejemplo, una aplicación web construida sobre servicios en la nube puede querer desplegar capacidad extra en momentos de pico y liberar las instancias informáticas no utilizadas en cuanto disminuya el uso.
Los sistemas tolerantes afallos siguen funcionando de forma predecible cuando fallan cosas en su entorno. La tolerancia a fallos debe diseñarse en un sistema desde el principio; ni se te ocurra añadirla a posteriori. ¿Qué pasa si hay un error en tu código o se corrompe tu estado? ¿O qué pasa si se produce un corte en la red o un fallo de hardware? Si un usuario que envía un mensaje provoca la caída de un proceso, el usuario recibe una notificación sobre si el mensaje se ha entregado o no, y puede estar seguro de que la notificación recibida es correcta.
Por tiempo real suave entendemos en la previsibilidad de la respuesta y la latencia, manejando un rendimiento constante y garantizando una respuesta en un plazo aceptable. Este rendimiento debe permanecer constante independientemente de los picos de tráfico y del número de solicitudes simultáneas. No importa cuántas solicitudes simultáneas pasen por el sistema, el rendimiento no debe degradarse bajo cargas pesadas. El tiempo de respuesta, también conocido como latencia, tiene que ser relativo al número de solicitudes simultáneas, evitando grandes variaciones en las solicitudes causadas por los recolectores de basura "para el mundo" u otros cuellos de botella secuenciales. Si el rendimiento de tu sistema es de un millón de mensajes por segundo y resulta que se procesan un millón de solicitudes simultáneas, debería tardar 1 segundo procesar y entregar una solicitud a su destinatario. Pero si durante un pico se envían dos millones de solicitudes, no debería haber degradación en el rendimiento; no algunas, sino todas las solicitudes deberían gestionarse en 2 segundos.
La alta disponibilidad minimiza o elimina por completo el tiempo de inactividad como consecuencia de fallos, interrupciones, actualizaciones u otras actividades operativas. ¿Qué pasa si se bloquea un proceso? ¿Y si se corta el suministro eléctrico a tu centro de datos? ¿Tienes un suministro redundante o una batería de reserva que te dé tiempo suficiente para migrar tu clúster y apagar limpiamente los servidores afectados? ¿O redundancia de red y hardware? ¿Has dimensionado tu sistema asegurándote de que, incluso después de perder parte de tu clúster, el hardware restante tenga suficiente capacidad de CPU para manejar los picos de carga? No importa si pierdes parte de tu infraestructura, si tu proveedor de la nube experimenta una avería embarazosa o si estás realizando tareas de mantenimiento; un usuario que envía un mensaje de chat quiere tener la seguridad de que llega a su destinatario. Los usuarios del sistema esperan que simplemente funcione. Esto contrasta con la tolerancia a fallos, en la que se informa al usuario de que no ha funcionado, pero el sistema en sí no se ve afectado y sigue funcionando. La capacidad de Erlang para realizar actualizaciones de software durante el tiempo de ejecución ayuda. Pero si empiezas a pensar en lo que implica tratar con cambios en el esquema de la base de datos, o actualizaciones a protocolos no compatibles con versiones anteriores en entornos potencialmente distribuidos que manejan peticiones durante la actualización, la simplicidad se desvanece muy rápidamente. Cuando realizas tus operaciones bancarias en línea los fines de semana o por la noche, quieres estar seguro de que no te encontrarás con un embarazoso cartel de "cerrado por mantenimiento rutinario" en el sitio web.
En efecto, Erlang facilita la resolución de muchos de estos problemas. Pero al fin y al cabo, sigue siendo sólo un lenguaje de programación. Para los sistemas complejos que vas a implantar, necesitas aplicaciones ya creadas y bibliotecas que puedas utilizar listas para usar. También necesitas principios y patrones de diseño que informen la arquitectura de tu sistema con el objetivo de crear clusters distribuidos y fiables. Necesitas directrices sobre cómo diseñar tu sistema, junto con herramientas para implementarlo, desplegarlo, monitorizarlo, hacerlo funcionar y mantenerlo. En este libro cubrimos las bibliotecas y herramientas que te permiten aislar los fallos a nivel de nodo, y crear y distribuir múltiples nodos para conseguir escalabilidad y disponibilidad.
Tienes que pensar mucho en tus requisitos y propiedades, asegurándote de elegir las bibliotecas y patrones de diseño adecuados que garanticen que el sistema final se comporta como tú quieres y hace lo que pretendías en un principio. En tu búsqueda, tendrás que hacer concesiones que dependen unas de otras: concesiones en tiempo, recursos y características, y concesiones en disponibilidad, escalabilidad y fiabilidad. Ninguna biblioteca prefabricada puede ayudarte si no sabes lo que quieres obtener de tu sistema. En este libro, te guiamos por los pasos necesarios para comprender estos requisitos, y te guiamos por los pasos necesarios para tomar decisiones de diseño y las compensaciones necesarias para conseguirlas .
OTP
OTP es un conjunto de marcos, principios y patrones independientes del dominio que guían y apoyan la estructura, diseño, implementación y despliegue de los sistemas Erlang. Utilizar OTP en tus proyectos te ayudará a evitar la complejidad accidental: cosas que son difíciles porque elegiste herramientas inadecuadas. Pero otros problemas siguen siendo difíciles, independientemente de las herramientas de programación y el middleware que elijas.
Ericsson se dio cuenta de ello muy pronto . En 1993, junto con el desarrollo del primer producto Erlang, Ericsson inició un proyecto para abordar las herramientas, el middleware y los principios de diseño. Los desarrolladores querían evitar dificultades accidentales que ya se habían resuelto, y en su lugar centrar su energía en los problemas difíciles. El resultado fue BOS, el Sistema Operativo Básico. En 1995, BOS se fusionó con el desarrollo de Erlang, reuniéndolo todo bajo un mismo techo para formar Erlang/OTP tal y como lo conocemos hoy. Puede que hayas oído referirse al dream team que da soporte a Erlang como el equipo OTP. Este grupo fue una consecuencia de esta fusión, cuando Erlang dejó de ser una organización de investigación y se formó un grupo de producto para seguir desarrollándolo y manteniéndolo.
Difundir el conocimiento de OTP puede promover la adopción de Erlang en entornos informáticos corporativos más "probados". El mero hecho de saber que existe una plataforma estable y madura para el desarrollo de aplicaciones ayuda a los tecnólogos a vender Erlang a los directivos, un paso crucial para que su adopción industrial sea más generalizada. Las startups, por su parte, simplemente se ponen manos a la obra, ya que Erlang/OTP les permite alcanzar la velocidad de comercialización y reducir sus costes de desarrollo y operaciones.
Se dice que OTP consta de tres bloques de construcción(Figura 1-1) que, utilizados conjuntamente, proporcionan un enfoque sólido para diseñar y desarrollar sistemas en el dominio del problema que acabamos de describir. Son el propio Erlang, herramientas y bibliotecas, y un conjunto de principios de diseño. Veremos cada uno por separado.
Erlang
El primer bloque de construcción es el propio Erlang, que incluye la semántica del lenguaje y su máquina virtual subyacente. Las características clave del lenguaje, como los procesos ligeros, la ausencia de memoria compartida y el paso asíncrono de mensajes, te acercarán un paso más a tu objetivo. Igual de importantes son los enlaces y monitores entre procesos, y los canales dedicados para la propagación de las señales de error. Los monitores y la notificación de errores te permiten construir, con relativa facilidad, complejas jerarquías de supervisión con recuperación de fallos incorporada. Como el paso de mensajes y la propagación de errores son asíncronos, la semántica y la lógica de un sistema desarrollado para ejecutarse en un único nodo Erlang pueden distribuirse fácilmente sin tener que cambiar nada del código base.
Una diferencia significativa entre la ejecución en un único nodo y la ejecución en un entorno distribuido es la latencia con la que se entregan los mensajes y los errores. Pero en los sistemas de tiempo real suave, tienes que tener en cuenta la latencia independientemente de si el sistema está distribuido o sometido a una gran carga. Así que si has resuelto una faceta del problema, habrás resuelto las dos.
Erlang te permite ejecutar todo tu código sobre una máquina virtual altamente optimizada para la concurrencia, con un recolector de basura por proceso, lo que produce un comportamiento del sistema predecible y sencillo. Otros entornos de programación no pueden permitirse este lujo porque necesitan una capa adicional para emular el modelo de concurrencia y la semántica de error de Erlang. Citando a Joe Armstrong, coinventor de Erlang, "Puedes emular la lógica de Erlang, pero si no se ejecuta en la máquina virtual de Erlang, no puedes emular la semántica". Los únicos lenguajes que hoy en día se libran de esto están construidos sobre el emulador BEAM de , la máquina virtual de Erlang predominante. Existe todo un ecosistema de ellos, siendo los lenguajes Elixir y Lisp Flavored Erlang los que más tracción están ganando en el momento de escribir este libro. Lo que escribimos en este libro sobre Erlang también se aplica a ellos.
Herramientas y bibliotecas
El segundo bloque de construcción , que surgió antes de que el código abierto se convirtiera en la norma generalizada para los proyectos de software, incluye aplicaciones que se entregan como parte de la distribución estándar Erlang/OTP. Puedes ver cada aplicación como una forma de empaquetar recursos en OTP, donde las aplicaciones pueden tener dependencias de otras aplicaciones. Las aplicaciones incluyen herramientas, bibliotecas, interfaces hacia otros lenguajes y entornos de programación, bases de datos y controladores de bases de datos, componentes estándar y pilas de protocolos. La documentación de OTP hace un buen trabajo separándolas en los siguientes subconjuntos:
Las aplicaciones básicas de son las siguientes:
Proporcionan las herramientas y los componentes básicos necesarios para diseñar, crear, poner en marcha y actualizar tu sistema. Cubrimos las aplicaciones básicas en detalle a lo largo de este libro. Junto con el compilador, son el subconjunto mínimo de aplicaciones necesarias en cualquier sistema escrito en Erlang/OTP para hacer algo significativo.
Las aplicaciones de bases de datos incluyen mnesia, la base de datos distribuida de Erlang, y odbc, una interfaz utilizada para comunicarse con bases de datos relacionales SQL. Mnesia es una opción popular porque es rápida, se ejecuta y almacena sus datos en el mismo espacio de memoria que tus aplicaciones, y es fácil de usar, ya que se accede a ella a través de una API de Erlang.
Las aplicaciones de operaciones y mantenimiento incluyen os_mon, una aplicación que te permite monitorizar el sistema operativo subyacente; snmp, un agente y cliente del Protocolo simple de gestión de redes ; y otp_mibs, bases de información de gestión que te permiten gestionar sistemas Erlang mediante SNMP.
La colección de aplicaciones de interfaz y comunicación proporciona pilas de protocolos e interfaces para trabajar con otros lenguajes de programación, incluyendo un compilador ASN.1(asn1) y soporte de tiempo de ejecución, ganchos directos en programas C(ei y erl_interface) y Java(jinterface), junto con un analizador XML(xmerl) . Hay aplicaciones de seguridad para SSL/TLS, SSH, criptografía e infraestructura de clave pública. Los paquetes gráficos incluyen un port de wxWidgets(wx), junto con una interfaz fácil de usar. La aplicación eldap proporciona una interfaz cliente hacia el Protocolo Ligero de Acceso a Directorios (LDAP). Y para los aficionados a las telecomunicaciones, existe una pila Diameter (definida en el RFC 6733), utilizada para el control de políticas y la autorización, junto con la autenticación y la contabilidad. Profundiza aún más y encontrarás la pila Megaco. Megaco/H.248 es un protocolo para controlar elementos de una pasarela multimedia físicamente descompuesta, separando la conversión de medios del control de llamadas. Si alguna vez has utilizado un smartphone, es muy probable que indirectamente hayas dado una vuelta por el diámetro Erlang y las aplicaciones megaco.
La colección de aplicaciones de herramientas facilitan el desarrollo, la implementación y la gestión de tu sistema Erlang. En este libro sólo cubrimos las más relevantes, pero las reseñamos todas aquí para que conozcas su existencia:
El depurador es una herramienta gráfica de que te permite recorrer tu código mientras influyes en el estado de las funciones.
El observadorintegra el monitor de aplicaciones y el gestor de procesos, junto con herramientas básicas para monitorizar tus sistemas Erlang mientras se desarrollan y en producción.
El dializador es una herramienta de análisis estático de que encuentra discrepancias de tipo, código muerto y otros problemas.
El rastreador de eventos(et) utiliza puertos para recoger eventos de rastreo en entornos distribuidos, y perceptte permite localizar cuellos de botella en tu sistema rastreando y visualizando actividades relacionadas con la concurrencia.
Erlang Syntax Tools(syntax_tools) contiene módulos para manejar árboles sintácticos de Erlang de forma compatible con otras herramientas relacionadas con el lenguaje. También incluye un fusionador de módulos que te permite fusionar módulos Erlang, junto con un renombrador, resolviendo el problema de los choques en un espacio de módulos no jerárquico.
La aplicación parsetools contiene el generador de análisis sintáctico (yecc) y un generador de análisis léxico para Erlang(leex).
Reltool es una herramienta de gestión de versiones que proporciona un front-end gráfico junto con ganchos back-end que pueden ser utilizados por sistemas de compilación más genéricos.
Runtime_tools es una colección de utilidades que incluye las sondas DTrace y SystemTap, y dbg, una envoltura fácil de usar alrededor de las funciones integradas (BIF) de rastreo .
Por último, la aplicación de herramientas es una colección de perfiladores, herramientas de cobertura de código y herramientas de análisis de referencias cruzadas de módulos, así como el modo Erlang para el editor emacs.
Las aplicaciones de prueba proporcionan herramientas para pruebas unitarias(eunit), pruebas del sistema y pruebas de caja negra. El Servidor de Pruebas (empaquetado en la aplicación test_server ) es un marco que puede utilizarse como motor de una aplicación de herramientas de pruebas de nivel superior. Lo más probable es que no lo utilices, porque OTP proporciona una de estas herramientas de prueba de nivel superior en la forma de common_test, una aplicación adecuada para pruebas de caja negra. Common_testadmite la ejecución automatizada de casos de prueba basados en Erlang para la mayoría de los sistemas de destino, independientemente del lenguaje de programación.
Tenemos que mencionar los Object Request Brokers (ORBs) y las aplicaciones del lenguaje de definición de interfaces (IDL) por razones nostálgicas, que recuerdan a uno de los coautores sus pecados pasados. Incluyen un broker llamado orber, un compilador IDL llamado ic, y algunos otros Servicios de Objetos Comunes CORBA que ya nadie utiliza.
En este libro cubrimos y hacemos referencia a algunas de estas aplicaciones y herramientas. Algunas de las herramientas que no cubrimos se describen en Erlang Programming (O'Reilly), y las que no, están cubiertas por el conjunto de páginas del manual de referencia y la guía del usuario que forman parte de la documentación estándar de Erlang/OTP.
Estas aplicaciones no constituyen la totalidad del soporte de herramientas para Erlang; se ven reforzadas por miles de otras aplicaciones implementadas y soportadas por la comunidad y disponibles como código abierto. Cubrimos algunas de las aplicaciones predominantes en la segunda mitad del libro, donde nos centramos en las arquitecturas distribuidas, la disponibilidad, la escalabilidad y el monitoreo. Incluyen los marcos Riak Core y Scalable Distributed (SD) Erlang; aplicaciones de regulación de carga como jobs y safetyvalve; y aplicaciones de monitoreo y registro como como elarm, folsom, exometer y lager. Una vez que hayas leído este libro y antes de empezar tu proyecto, revisa los manuales de referencia y guías de usuario de Erlang/OTP estándar y de código abierto, porque nunca sabes cuándo te resultarán útiles .
Principios de diseño del sistema
El tercer bloque de construcción de OTP consiste en un conjunto de principios abstractos, reglas de diseño y comportamientos genéricos. Los principios abstractos describen la arquitectura de software de un sistema Erlang, utilizando procesos en forma de comportamientos genéricos como ingredientes básicos. Las reglas de diseño mantienen la compatibilidad de las herramientas que utilizas con el sistema que estás desarrollando. Utilizar este enfoque proporciona una forma estándar de resolver los problemas, haciendo que el código sea más fácil de entender y mantener, además de proporcionar un lenguaje y un vocabulario comunes entre los equipos.
Los comportamientos genéricos de OTP pueden verse como formalizaciones de patrones de diseño concurrentes. Los comportamientos se empaquetan en módulos de biblioteca que contienen código genérico que resuelve un problema común. Tienen soporte incorporado para depuración, actualización de software, gestión genérica de errores y funcionalidad incorporada para actualizaciones.
Los comportamientos pueden ser procesos trabajadores, que hacen todo el trabajo duro, y procesos supervisores, cuyas únicas tareas son iniciar, detener y monitorizar a los trabajadores o a otros supervisores. Como los supervisores pueden monitorizar a otros supervisores, la funcionalidad de una aplicación puede encadenarse para que sea más fácil desarrollarla de forma modular. Los procesos monitorizados por un supervisor se denominan sus hijos.
OTP proporciona bibliotecas predefinidas para trabajadores y supervisores, lo que te permite centrarte en la lógica empresarial del sistema. Estructuramos los procesos enárboles de supervisión jerárquicos , dando lugar a estructuras tolerantes a fallos que aíslan los fallos y facilitan la recuperación. OTP te permite empaquetar un árbol de supervisión en una aplicación, como se ve en la Figura 1-2, donde los círculos con anillos dobles son supervisores y los demás procesos son trabajadores.
Entre los comportamientos genéricos que forman parte del middleware OTP se incluyen:
Servidores genéricos, proporcionando un patrón de diseño cliente-servidor
Máquinas de estados finitos genéricas , que te permiten implementar FSMs
Manejadores y gestores de eventos, que te permiten tratar genéricamente con flujos de eventos
Supervisores, monitoreo otro trabajador y procesos de supervisión
Aplicaciones, que te permiten empaquetar recursos, incluidos árboles de supervisión
Los cubrimos todos en detalle en este libro, además de explicar cómo implementar los tuyos propios. Utilizamos comportamientos para crear árboles de supervisión, que se empaquetan en aplicaciones. A continuación, agrupamos las aplicaciones para formar una versión. Una versión describe lo que se ejecuta en un nodo .
Nodos Erlang
Un nodo Erlang consta de varias aplicaciones débilmente acopladas, que podrían estar compuestas por algunas de las aplicaciones descritas en "Herramientas y bibliotecas", combinadas con otras aplicaciones de terceros y aplicaciones que escribas específicamente para el sistema que intentas implementar. Estas aplicaciones podrían ser independientes entre sí o depender de los servicios y API de otras aplicaciones. La Figura 1-3 ilustra una versión típica de un nodo Erlang con la máquina virtual (VM) dependiente del hardware y del sistema operativo, y aplicaciones Erlang que se ejecutan sobre la VM y que interactúan con componentes no Erlang dependientes del sistema operativo y del hardware.
Agrupa un clúster de nodos Erlang -posiblemente emparejándolos con nodos escritos en otros lenguajes de programación- y tendrás un sistema distribuido. Ahora puedes escalar tu sistema añadiendo nodos hasta que llegues a ciertos límites físicos. Éstos pueden venir dictados por cómo compartiste tus datos, por restricciones de hardware o de red, o por dependencias externas que actúen como cuellos de botella.
Distribución, infraestructura y multinúcleo
La tolerancia a los fallos -uno de los requisitos fundamentales de Erlang desde sus raíces en las telecomunicaciones- tiene como resorte principal la distribución. Sin distribución, la fiabilidad y disponibilidad de una aplicación que se ejecute en un único host dependería en gran medida de la fiabilidad del hardware y el software que componen ese host. Cualquier problema con la CPU, la memoria, el almacenamiento persistente, los periféricos, la fuente de alimentación o la placa base del host podría hacer caer fácilmente toda la máquina y, con ella, la aplicación. Del mismo modo, los problemas en el sistema operativo del host o en las bibliotecas de soporte podrían hacer caer la aplicación o hacer que no estuviera disponible. Lograr la tolerancia a fallos requiere varios ordenadores con cierto grado de coordinación entre ellos, y la distribución proporciona la vía para esa coordinación.
Durante décadas, la industria informática ha explorado cómo los lenguajes de programación pueden soportar la distribución. Diseñar lenguajes de propósito general ya es difícil de por sí; diseñarlos para que admitan la distribución aumenta considerablemente esa dificultad. Por eso, un enfoque habitual es añadir soporte de distribución a los lenguajes de programación no distribuidos mediante bibliotecas opcionales. Este enfoque tiene la ventaja de permitir que la compatibilidad con la distribución evolucione separadamente del propio lenguaje, pero a menudo sufre un desajuste de impedancia con el lenguaje, y los desarrolladores lo perciben como si estuviera "atornillado". Dado que la mayoría de los lenguajes utilizan llamadas a funciones como medio principal de transferir control y datos de una parte de una aplicación a otra, las bibliotecas de distribución añadidas suelen modelar los intercambios entre las partes distribuidas de una aplicación también como llamadas a funciones. Aunque conveniente, este enfoque está fundamentalmente roto porque la semántica de las llamadas a funciones locales y remotas, especialmente sus modos de fallo, son marcadamente diferentes.
En Erlang, los procesos se comunican mediante el paso de mensajes asíncronos . Esto funciona incluso si un proceso está en un nodo remoto, porque la máquina virtual de Erlang admite el paso de mensajes de un nodo a otro. Cuando un nodo se une a otro, también se entera de los nodos que ya conoce el otro. De este modo, todos los nodos de un clúster forman una malla, que permite a cualquier proceso enviar un mensaje a otro proceso de cualquier otro nodo del clúster. Cada nodo del clúster también rastrea automáticamente la capacidad de respuesta de otros nodos para conocer los nodos que no responden. Las ventajas del paso asíncrono de mensajes en los sistemas que se ejecutan en un nodo se extienden a los sistemas que se ejecutan en clústeres, ya que las respuestas pueden recibirse junto con los errores y los tiempos de espera.
Las primitivas de paso de mensajes y agrupación de Erlang pueden servir de base para una amplia variedad de arquitecturas de sistemas distribuidos. Por ejemplo, la arquitectura orientada a servicios (SOA), especialmente en su variante más moderna, los microservicios, encaja de forma natural en Erlang, dada la facilidad de desarrollo e implementación de procesos similares a servidores. Los clientes tratan dichos procesos como servicios, comunicándose con ellos mediante el intercambio de mensajes. Como otro ejemplo, considera que los clústeres Erlang no requieren nodos maestros o líderes, lo que significa que su uso para sistemas de réplicas entre pares funciona bien. Los clientes pueden enviar mensajes de solicitud de servicio a cualquier nodo par del clúster, y el par puede gestionar la solicitud por sí mismo o dirigirla a otro par. El concepto de clústeres autónomos, conocidos como grupos que se comunican entre sí a través de nodos pasarela que pueden subir y bajar o perder conectividad, existe en un framework llamado SD Erlang. Otro marco distribuido popular, inspirado en el documento Amazon Dynamo publicado en 2007, es Riak Core, que ofrece hashing consistente para programar trabajos, recuperación de redes particionadas y nodos fallidos mediante hashing consistente, consistencia eventual, y nodos virtuales que dividen el estado y los datos en entidades pequeñas y manejables que pueden replicarse y moverse entre nodos.
Con los sistemas distribuidos, también puedes conseguir escalabilidad . De hecho, la disponibilidad, la coherencia y la escalabilidad van de la mano, y cada una afecta a las demás. Comienza con el modelo de concurrencia y el concepto de paso de mensajes dentro del nodo, que extendemos a través de la red para utilizarlo en la agrupación de nodos. La máquina virtual de Erlang aprovecha los actuales sistemas multinúcleo permitiendo que los procesos se ejecuten con verdadera concurrencia, ejecutándose simultáneamente en distintos núcleos. Gracias a las capacidades de multiprocesamiento simétrico (SMP) de la máquina virtual de Erlang, éste ya está preparado para ayudar a las aplicaciones a escalar verticalmente a medida que siga aumentando el número de núcleos por CPU. Y como añadir nuevos nodos a un clúster es fácil -basta con que ese nodo se ponga en contacto con otro nodo para unirse a la malla-, el escalado horizontal también está al alcance de la mano. Esto, a su vez, te permite centrarte en el verdadero reto al tratar con sistemas distribuidos: a saber, distribuir tus datos y tu estado a través de hosts y redes que son poco fiables.
Resumen
Para que el diseño, la implementación, el funcionamiento y el mantenimiento sean más fáciles y robustos, tu lenguaje de programación y middleware tienen que ser compactos, su comportamiento en tiempo de ejecución predecible y la base de código resultante mantenible. Seguimos hablando de sistemas en tiempo real tolerantes a fallos, escalables y blandos, con requisitos de alta disponibilidad. Los problemas que tienes que resolver no tienen por qué ser complicados para beneficiarte de las ventajas que Erlang/OTP pone sobre la mesa. Las ventajas serán evidentes si desarrollas soluciones dirigidas a plataformas de hardware embebido como la placa Parallela, la BeagleBoard o la Raspberry Pi. Encontrarás que Erlang/OTP es ideal para el código de orquestación en dispositivos integrados, para el desarrollo del lado del servidor donde la concurrencia aparece de forma natural, y para todo el camino hasta las arquitecturas multinúcleo escalables y distribuidas y los superordenadores. Facilita el desarrollo de los problemas de software más difíciles, al tiempo que hace que los programas más sencillos sean aún más fáciles de implementar.
Lo que aprenderás en este libro
Este libro está dividido en dos secciones. La primera parte, del Capítulo 3 al Capítulo 10, trata del diseño e implementación de un único nodo. Debes leer estos capítulos secuencialmente, porque sus ejemplos y explicaciones se basan en los anteriores. La segunda mitad del libro, del Capítulo 11 al Capítulo 16, se centra en las herramientas, técnicas y arquitecturas utilizadas para la implementación, el monitoreo y las operaciones, al tiempo que explica los enfoques teóricos necesarios para abordar cuestiones como la fiabilidad, la escalabilidad y la alta disponibilidad. La segunda mitad se basa en parte en los ejemplos tratados en la primera mitad del libro, pero puede leerse independientemente de ella.
Comenzamos con una visión general de Erlang en el Capítulo 2, pensada no para enseñarte el lenguaje, sino más bien como curso de repaso. Si aún no conoces Erlang, te recomendamos que primero consultes uno o varios de los excelentes libros diseñados para ayudarte a aprender el lenguaje, como Simon St. Laurent's Introducing Erlang, Erlang Programmingde Francesco Cesarini y Simon Thompson, o cualquiera de los otros libros que mencionamos en el Capítulo 2. Nuestra visión general aborda los principales elementos del lenguaje, como las listas, las funciones, los procesos y mensajes, y el intérprete de comandos Erlang, así como aquellas características que hacen que Erlang sea único entre los lenguajes, como la vinculación y el monitoreo de procesos, las actualizaciones en vivo y la distribución.
Tras la visión general de Erlang, el Capítulo 3se sumerge en las estructuras de procesos. Los procesos Erlang pueden manejar una amplia variedad de tareas, pero independientemente de las tareas concretas o de sus dominios de problemas, surgen estructuras de código y ciclos de vida de procesos similares, parecidos a los patrones de diseño comunes que se han observado y documentado para lenguajes orientados a objetos populares como Java y C++. OTP captura y formaliza estas estructuras y ciclos de vida comunes orientados a procesos en comportamientos, que sirven como elementos base de los marcos reutilizables de OTP.
En el Capítulo 4 exploramos en detalle nuestro primer proceso trabajador. Se trata del comportamiento OTP más popular y utilizado, el gen_server
. Como su nombre indica , admite estructuras cliente-servidor genéricas, en las que el servidor gobierna determinados recursos informáticos -quizá una simple instancia de Almacenamiento de Términos Erlang (ETS), o un conjunto de conexiones de red a un servidor remoto no Erlang- y concede a los clientes acceso a ellos. Los clientes se comunican con los servidores genéricos de forma síncrona mediante una llamada-respuesta, de forma asíncrona mediante un mensaje unidireccional llamado "cast", o mediante primitivas de mensajería Erlang normales. La consideración completa de estos modos de comunicación nos obliga a escudriñar diversos aspectos de los procesos implicados, como qué ocurre si el cliente o el servidor mueren en medio de un intercambio de mensajes, cómo se aplican los tiempos de espera y qué puede ocurrir si un servidor recibe un mensaje que no entiende. Al abordar éstas y otras cuestiones comunes, el gen_server
maneja muchos detalles independientemente del dominio del problema, lo que permite a los desarrolladores centrar más su tiempo y energía en sus aplicaciones. El comportamiento gen_server
es tan útil que no sólo aparece en la mayoría de las aplicaciones Erlang no triviales, sino que también se utiliza en la propia OTP.
Antes de examinar más comportamientos OTP, seguimos nuestra discusión sobre gen_server
con un vistazo a algunos de los puntos de control y observación que proporcionan los comportamientos OTP(Capítulo 5). Estas características reflejan otro aspecto de Erlang/OTP que lo diferencia de otros lenguajes y marcos de trabajo: la observabilidad incorporada. Si quieres saber qué está haciendo tu proceso gen_server
, sólo tienes que activar el rastreo de depuración para ese proceso, ya sea en tiempo de compilación o en tiempo de ejecución desde un intérprete de comandos Erlang. Activar el rastreo hace que emita información que indica qué mensajes está recibiendo y qué acciones está realizando para gestionarlos. Erlang/OTP también proporciona funciones para asomarse a los procesos en ejecución y ver sus rastreos, diccionarios de procesos, procesos padre, procesos enlazados y otros detalles. También hay funciones OTP para examinar el estado y el estado interno específicamente de los comportamientos y otros procesos del sistema. Debido a estas funciones orientadas a la depuración, los programadores de Erlang suelen renunciar al uso de depuradores tradicionales y, en su lugar, confían en el rastreo para ayudarles a diagnosticar programas erróneos, ya que suele ser más rápido de configurar y más informativo.
A continuación examinaremos otro comportamiento OTP, gen_fsm
(Capítulo 6), que admite un patrón FSM genérico. Como ya sabrás en , un FSM es un sistema que tiene un número finito de estados, y los mensajes entrantes pueden hacer avanzar el sistema de un estado a otro, con posibles efectos secundarios como parte de las transiciones. Por ejemplo, puedes considerar que tu descodificador de televisión es un FSM en el que el estado actual representa el canal seleccionado y si se muestra algo en pantalla. Pulsar los botones del mando a distancia hace que el descodificador cambie de estado, tal vez seleccionando un canal diferente, o cambiando la visualización en pantalla para mostrar la guía de canales o enumerar los programas a la carta que puedan estar disponibles para su compra. Los FSM son aplicables a una amplia variedad de dominios de problemas porque permiten a los desarrolladores razonar e implementar más fácilmente los posibles estados y transiciones de estado de sus aplicaciones. Saber cuándo y cómo utilizar gen_fsm
puede evitarte intentar implementar tus propias máquinas de estados ad hoc, que a menudo se convierten rápidamente en código espagueti difícil de mantener y ampliar.
El registro y el monitoreo son partes críticas de cualquier historia de éxito de escalabilidad, ya que te permiten obtener información importante sobre tus sistemas en funcionamiento que puede ayudar a localizar cuellos de botella y áreas problemáticas que requieren una mayor investigación. El comportamiento Erlang/OTP gen_event
(Capítulo 7) proporciona soporte para subsistemas que emiten y gestionan flujos de eventos que reflejan cambios en el estado del sistema que pueden afectar a las características operativas, como aumentos sostenidos de la carga de la CPU, colas que parecen crecer sin límite o la incapacidad de un nodo de un clúster distribuido para llegar a otro. Estos flujos no tienen por qué detenerse en los eventos de tu sistema. Podrían gestionar los eventos específicos de tu aplicación originados por la interacción del usuario, las redes de sensores o las aplicaciones de terceros. Además de explorar el comportamiento de gen_event
, también echamos un vistazo a los gestores de eventos de registro de errores de las bibliotecas de apoyo a la arquitectura del sistema (SASL) de OTP, que proporcionan flexibilidad para gestionar informes de supervisor, informes de fallos e informes de progreso.
Los manejadores de eventos y los manejadores de errores son elementos básicos de numerosos lenguajes de programación, y también son increíblemente útiles en Erlang/OTP, pero no dejes que su presencia aquí te engañe: tratar los errores en Erlang/OTP es sorprendentemente diferente de los enfoques a los que están acostumbrados la mayoría de los programadores.
Después de gen_event
, el siguiente comportamiento que estudiamos es el supervisor(Capítulo 8), que gestiona los procesos de los trabajadores. En Erlang/OTP, los procesos supervisores inician los trabajadores y luego los vigilan mientras realizan las tareas de la aplicación. Si uno o varios trabajadores mueren inesperadamente, el supervisor puede tratar el problema de una de las varias formas que explicaremos más adelante en el libro. Esta forma de gestionar los errores, conocida como "dejar que se cuelgue", difiere significativamente de las tácticas de programación defensiva que emplean la mayoría de los programadores. "Dejar que se estrelle" y la supervisión, juntos una piedra angular de Erlang/OTP, son muy eficaces en la práctica.
A continuación, examinaremos el último comportamiento fundamental de OTP, la aplicación(Capítulo 9), que sirve como punto principal de integración entre el tiempo de ejecución Erlang/OTP y tu código. Las aplicaciones OTP tienen archivos de configuración que especifican sus nombres, versiones, módulos, las aplicaciones de las que dependen y otros detalles. Al ser iniciada por el tiempo de ejecución Erlang/OTP, tu instancia de aplicación inicia a su vez un supervisor de nivel superior que hace aparecer el resto de la aplicación. Estructurar módulos de código en aplicaciones también te permite realizar actualizaciones de código en sistemas vivos. Una versión de un paquete Erlang/OTP suele constar de varias aplicaciones, algunas de las cuales forman parte de la distribución de código abierto de Erlang/OTP y otras las proporcionas tú.
Una vez examinados los comportamientos estándar, pasamos a explicar cómo escribir tus propios comportamientos y procesos especiales(Capítulo 10). Los procesos especiales son procesos de que siguen ciertas reglas de diseño, lo que permite añadirlos a los árboles de supervisión de OTP. Conocer estas reglas de diseño no sólo puede ayudarte a comprender los detalles de implementación de los comportamientos estándar, sino que también te informará de sus ventajas y desventajas y te permitirá decidir mejor cuándo utilizarlos y cuándo escribir los tuyos propios en su lugar.
El Capítulo 11 describe cómo las aplicaciones OTP de un nodo se acoplan entre sí y se inician como un todo. Tendrás que crear tus propios archivos de lanzamiento, denominados en el mundo Erlang archivos rel. El archivo rel enumera las versiones de las aplicaciones y del sistema de tiempo de ejecución que utiliza el módulo systools
para agrupar el software en un directorio de lanzamiento independiente que incluye la máquina virtual. Este directorio de lanzamiento, una vez configurado y empaquetado, está listo para su implementación y ejecución en los hosts de destino. Cubrimos las herramientas contribuidas por la comunidad rebar3 y relx, la mejor forma de construir tu código y tus versiones.
La máquina virtual Erlang tiene límites y ajustes configurables del sistema que debes conocer al implementar tus sistemas. Hay muchos, desde límites que regulan el número máximo de tablas o procesos ETS hasta rutas de búsqueda de código incluidas y modos utilizados para cargar módulos. Los módulos en Erlang pueden cargarse al inicio, o cuando se llaman por primera vez. En sistemas con un estricto control de revisiones, tendrás que ejecutarlos en modo embebido, cargando los módulos al iniciarse y bloqueándose si no existen, o en modo interactivo, donde si un módulo no está disponible, se intenta cargarlo antes de terminar el proceso. Un proceso cardíaco de monitoreo externo monitorea la máquina virtual Erlang enviando latidos e invocando un script que te permite reaccionar cuando estos latidos no son reconocidos. Tú mismo implementas el script, lo que te permite decidir si reiniciar el nodo es suficiente o si -basándote en un historial de reinicios anteriores- quieres escalar el fallo y terminar la instancia virtual o reiniciar toda la máquina.
Aunque la tipificación dinámica de Erlang te permite actualizar tu módulo en tiempo de ejecución conservando el estado del proceso, no coordina las dependencias entre módulos, los cambios en el estado del proceso ni los protocolos no compatibles con versiones anteriores. OTP dispone de las herramientas para soportar las actualizaciones del sistema a nivel de sistema, incluyendo no sólo las aplicaciones, sino también el sistema en tiempo de ejecución. En el Capítulo 12 se presentan los principios y las bibliotecas de apoyo, desde la definición de tus propios scripts de actualización de aplicaciones hasta la escritura de scripts que soporten actualizaciones de versiones. Se proporcionan enfoques y estrategias para gestionar los cambios en el esquema de tu base de datos, así como directrices para las actualizaciones en entornos distribuidos y protocolos no compatibles con versiones anteriores. Para actualizaciones importantes en entornos distribuidos en los que se corrigen errores, se mejoran protocolos y se cambia el esquema de la base de datos, las actualizaciones en tiempo de ejecución no son para los débiles de corazón, pero son increíblemente potentes, ya que permiten actualizaciones automatizadas y operaciones sin interrupción. Descubrir que tu banca online no está disponible por mantenimiento debería ser cosa del pasado. Si no es así, envía una copia de este libro al departamento de informática de tu banco.
El funcionamiento y mantenimiento de cualquier sistema requiere visibilidad de lo que ocurre. Escalar clusters requiere estrategias sobre cómo compartes sus datos y su estado. Y la tolerancia a fallos requiere un enfoque sobre cómo replicarlos y persistir en ellos. Al hacerlo, tienes que lidiar con redes poco fiables, fallos y estrategias de recuperación. Aunque cada uno de estos temas merece un libro propio, los últimos capítulos de este libro te proporcionarán la base teórica necesaria a la hora de distribuir tus sistemas y hacerlos fiables y escalables. Proporcionamos esta teoría describiendo los pasos necesarios para diseñar una arquitectura escalable y de alta disponibilidad en Erlang/OTP.
El Capítulo 13 te dará una visión general de los enfoques necesarios a la hora de diseñar tu arquitectura distribuida, dividiendo tu funcionalidad en nodos independientes. Al hacerlo, a cada tipo de nodo independiente se le asignará un propósito específico, como actuar como pasarela de cliente gestionando grupos de conexiones TCP/IP o proporcionando un servicio como la autenticación o los pagos. Para cada tipo de nodo, definimos un enfoque para especificar interfaces y definir el estado y los datos que necesita cada nodo. Concluimos el capítulo describiendo los patrones arquitectónicos distribuidos más comunes y los distintos protocolos de red que pueden utilizarse para conectarlos.
Cuando ya tienes tu arquitectura distribuida, tienes que tomar decisiones de diseño que afectarán a la tolerancia a fallos, la resistencia, la fiabilidad y la disponibilidad. Sabes qué datos y qué estado necesitas en tus tipos de nodos, pero ¿cómo vas a distribuirlos y mantener su coherencia? ¿Vas a optar por el enfoque de compartirlo todo, compartir algo o no compartir nada, y cuáles son las compensaciones que tienes que hacer al elegir la coherencia fuerte, causal o eventual? En el Capítulo 14, describimos los distintos enfoques que puedes adoptar, introduciendo las estrategias de reintento que debes tener en cuenta en caso de que una solicitud se agote como resultado de un fallo de proceso, nodo o red, o por el mero hecho de que la red o tus servidores estén funcionando por encima de su capacidad.
Es fácil decir que vas a añadir hardware para que tu sistema escale horizontalmente, pero, por desgracia, las opciones de diseño introducidas en el Capítulo 14 tendrán un impacto en la escalabilidad de tu sistema . En el Capítulo 15, describimos los impactos derivados de tu estrategia de compartición de datos, modelo de consistencia y estrategia de reintentos. Cubrimos la planificación de la capacidad, incluidas las pruebas de carga, picos y estrés a las que debes someter a tu sistema para garantizar que se comporta de forma predecible bajo cargas pesadas, incluso cuando el hardware, el software y la infraestructura que lo rodean están fallando.
Una vez que hayas diseñado tus estrategias de escalabilidad y disponibilidad en , tienes que ocuparte del monitoreo. Si quieres conseguir un tiempo de actividad de cinco nueves, no sólo tienes que saber qué está pasando, sino también ser capaz de determinar rápidamente qué ha ocurrido y por qué. Concluimos el libro con el Capítulo 16, que examina cómo se utiliza el monitoreo para el soporte preventivo y la depuración postmortem.
El monitoreo se centra en las métricas, las alarmas y los registros. Este capítulo trata de la importancia de las métricas del sistema y de negocio de . Algunos ejemplos de métricas del sistema son la cantidad de memoria que utiliza tu nodo, la longitud de la cola de mensajes del proceso y la utilización del disco duro. Combinándolas con las métricas de negocio, como el número de intentos de inicio de sesión fallidos y con éxito, el rendimiento de mensajes por segundo y la duración de la sesión, se obtiene una visibilidad completa de cómo está afectando tu lógica de negocio a los recursos de tu sistema.
Como complemento de las métricas están las alarmas, en las que detectas e informas de anomalías, lo que permite al sistema tomar medidas para intentar resolverlas o alertar a un operador cuando es necesaria la intervención humana. Las alarmas podrían incluir un sistema que se queda sin espacio en disco (lo que da lugar a la invocación automática de scripts para comprimir o borrar registros) o un gran número de envíos de mensajes fallidos (que requieren la intervención humana para solucionar problemas de conectividad). El soporte preventivo en su máxima expresión, detectando y resolviendo los problemas antes de que se agraven, es imprescindible cuando se trata de alta disponibilidad. Si no tienes una visión en tiempo real de lo que está ocurriendo, resolver los problemas antes de que se agraven resulta extremadamente difícil y engorroso.
Y, por último, el registro de eventos importantes en el sistema te ayuda a solucionar los problemas de tu sistema tras una caída en la que hayas perdido su estado, de modo que puedes recuperar el flujo de llamadas de una solicitud concreta entre millones de otras para gestionar una consulta de atención al cliente, o simplemente proporcionar registros de datos con fines de facturación.
Con el monitoreo en marcha, estarás preparado para diseñar sistemas que no sólo sean escalables, sino también resistentes y de alta disponibilidad. ¡Feliz lectura! Esperamos que disfrutes del libro tanto como nosotros hemos disfrutado escribiéndolo.
Get Diseñar para la escalabilidad con Erlang/OTP now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.