Microsserviços com Elixir + Erlang/OTP — BEAM

Marcelo M. Gonçalves
11 min readSep 16, 2021

--

Se pudéssemos descrever brevemente tecnologias como Elixir e Erlang, diríamos que Elixir trata-se de uma eficiente e moderna linguagem de programação funcional, de propósito geral, criada por José Valim, influenciada por Erlang, Clojure, Haskell e Ruby, sendo lançada oficialmente em 2014. Por outro lado, Erlang/OTP trata-se de uma plataforma de desenvolvimento completa contendo a linguagem, a Erlang VM (BEAM), o framework OTP e tools.

Elixir não se trata apenas de uma FPL (Functional Programming Language), sendo também uma CPL (Concurrent Programming Language)

De maneira geral, ambas as tecnologias propondo-se a facilitar a construção de sistemas escaláveis, concorrentes, responsivos e distribuídos. Aplicações Elixir são construídas para rodar em cima do Erlang VM (BEAM), criando-se uma dependência do ambiente de execução (runtime). Porém, não necessariamente precisamos aprender a plataforma nem a linguagem Erlang como pré-requisitos para trabalharmos com Elixir, ao passo em que conhecimentos anteriores adquiridos envolvendo Erlang (Ericsson Language) oferecem benefícios, facilitando a adoção do Elixir como linguagem de programação em seu projeto.

A plataforma Erlang foi idealizada para entregarmos aplicações com características como baixa latência, altamente concorrentes e tolerante a falhas. Concebida em meados de 1980 pela Ericsson, inicialmente voltada para a área de telecomunicações. Naturalmente por envolver operações críticas (ligações telefônicas) necessitava permanecer sempre em operação mesmo sob alta demanda. Desta forma, fatores relacionados à computação paralela e alta concorrência foram determinantes para seu sucesso desde sua criação, primando por componentes sendo entregues com maior responsividade, confiança e escalabilidade.

Embora criada inicialmente para a área de telecom, a plataforma não se destina a este único domínio, podendo ser estendido e largamente utilizado em qualquer aplicação que necessite entregar alta-concorrência, tolerância a falhas, disponibilidade e com natureza distribuída. De forma geral, caso o sistema não alcance estes objetivos, eventualmente irá falhar em entregar seus objetivos gerais.

Diferente dos anos 1990 (desktop-based), as aplicações atuais rodam na nuvem (in-cloud) com foco na web, devendo ser estruturadas para atender diversas requisições simultâneas, operar com baixa latência, recuperar de falhas ocorridas sem expor erros ao usuário final, com downtime próximo de zero.

Idealmente, deveria ser possível upgrade do sistema com ele rodando, sem afetar seu funcionamento. Determinadas características são difíceis de se atingir sem a ajuda de uma plataforma e linguagem adequadas e direcionadas para este contexto, pois determinados conceitos foram a principal motivação por trás do desenvolvimento do Erlang pelo time da Ericsson, liderado por Joe Armstrong.

Recentemente, Erlang/OTP tem ganhado mais atenção por conta de grandes plataformas, como WhatsApp, Riak distributed database, RabbitMQ, Heroku cloud, etc, terem implantado de forma bem sucedida seu funcionamento há quase duas décadas, provando ser uma tecnologia aprovada e battle-tested. Erlang foi desenhado para suportar a criação de sistemas altamente disponíveis, devendo manter-se em operação mesmo em circunstâncias inesperadas.

A possibilidade de entregarmos alta disponibilidade é abordada pelo Erlang através de conceitos como: fault-tolerance, scalability, distribution, responsiveness, live update. Determinados conceitos podem parecer superficialmente simples, porém sabemos que quando estamos em produção as coisas podem não sair da forma como esperávamos. Também sabemos que para atender os cenários que Erlang se propõe a abordar, estamos nos referindo a tarefas de extrema complexidade.

Por este motivo, Erlang oferece ferramentas para que possamos encarar estes desafios, pois foi criado para isso, possibilitando a ele entregar toda esta eficiência a um custo menor para os profissionais e empresas que o adotarem se comparado a utilização de outros mecanismos ou abordagens disponíveis no mercado.

A linguagem Erlang possui sua sintaxe baseada na linguagem de programação PROLOG. Elixir oferece uma oportunidade a uma sintaxe mais simples e moderna. “What Elixir brings to the table is a complete different surface syntax, inspired by Ruby. What you might call a “non-scary” syntax, and a load of extra goodies — Joe Armstrong

Concorrência e Tolerância a Falhas com BEAM

Sistemas construídos utilizando a plataforma Erlang/OTP, mesmo os mais triviais, são altamente concorrentes. Uma vez que não dependem de heavyweight-threads (OS threads) e processos do sistema operacional, sendo sua unidade primitiva de concorrência chamada Erlang-process. A quantidade de processos que executam na máquina virtual do Erlang (BEAM — Bogdan/Björn’s Erlang Abstract Machine) podem chegar aos milhões executando em um único processo do sistema operacional (OS Proccess), paralelamente distribuídos considerando o número de núcleos do CPU da máquina.

Schedulers são gerenciados pelo BEAM para cada núcleo do CPU, encarregando-se de abstrair e mapear threads para lightweight-threads, beneficiando-se da otimização durante a execução dos processos no BEAM.

Lightweight-threads (green threads) funcionam como threads virtuais em memória, cujo ciclo de vida é de responsabilidade da VM. Isso porque o BEAM utiliza seus próprios schedulers para distribuir a execução dos processos de acordo com os recursos de hardware disponíveis, paralelizando a execução e sempre buscando o máximo de otimização possível, beneficiando-se de estar posicionado em uma camada abaixo da aplicação, praticamente a nível de sistema operacional.

Processos no Erlang VM são isolados: Não compartilham memória nem impactam outros processos em execução em caso de falhas. BEAM permite criarmos supervisores para detecção de erros nos processos, e em caso de situações inesperadas são criados novos processos automaticamente em seu lugar, sem impactar outros processos rodando.

Este nível de isolamento permite que aplicações rodando no BEAM sejam tolerantes a falhas (fault-tolerant). A comunicação entre os processos ocorre de forma assíncrona por troca de mensagens, dispensando mecanismos de sincronização complexos como locks, mutex, tornando a interação entre os componentes mais fácil de entender e consequentemente de desenvolver.

Aplicações executadas dentro do BEAM são compostas por diversos pequenos processos concorrentes e escaláveis, colaborativamente entregando valor ao negócio.

A natureza distribuída das aplicações BEAM permite que a comunicação ocorra transparentemente tanto quando estão rodando na mesma instância quanto em instâncias fisicamente separadas em múltiplas máquinas remotas. Sendo facilmente executado em cluster e na nuvem de forma escalável, tornando-o mais resiliente em caso de desastres.

Aplicações Responsivas com Elixir

Para uma aplicação ser responsiva, precisa continuar processando novas requisições mesmo sob alto volume de carga e ainda assim retornar com baixa latência ao cliente/usuário do serviço. Para isso, o ambiente de execução (runtime) desempenha um papel crucial nos tempos de resposta das aplicações rodando em ambientes virtuais (VMs), como no caso do BEAM.

Com a ajuda das lightweight-threads (processos), alocadas pelos schedulers através de pequenas janelas de execução, o tempo é fatiado e gerenciado, podendo ocorrer pausas entre a janela de um processo e de outro, porém sem que haja bloqueio entre eles nem o resto do sistema. Mesmo para operações custosas, como I/O, processos aguardam sua vez, sem interferir ou bloquear os demais.

Mesmo as pausas do garbage collector (coletor de lixo) ocorrem isoladamente e rapidamente para cada processo quando necessário, evitando pausas do sistema inteiro para que a coleta seja realizada.

Desta forma, características de alta concorrência e paralelismo entregues por aplicações rodando no BEAM promovem a tolerância a falhas, responsividade e distribuição dos componentes de forma natural construídos a partir do Elixir.

Joe Armstrong, em sua tese de PhD (2003): Making reliable distributed systems in the presence of software errors, descreve conceitos concretos (teor técnico) relacionados a sistemas distribuídos, demonstrando como construir e rodar aplicações altamente concorrentes e tolerantes a falha. Alguns destes conceitos, incluem: Everything is a process, Processes are strongly isolated, Processes have unique names, Processes share no resources, Error handling is non-local.

Uma das maiores vantagens do Elixir está na simplificação de código, tornando as aplicações mais reduzidas e assertivas, fáceis de entender com isso facilitando sua manutenção pois as intenções são claramente reveladas. Os códigos fonte em Elixir são significativamente mais inteligentes e flexíveis se comparados ao Erlang, e após compilados, geram bytecodes compatíveis e rodando com exato comportamento dentro do ambiente de execução do BEAM se comparados a aplicações Erlang.

Aplicações Elixir podem comunicar-se com componentes em execução escritos em Erlang. Podemos utilizar bibliotecas Elixir dentro do Erlang e vice-versa, tanto padrão como de terceiros (third-party) garantindo interoperabilidade, não existindo nenhuma tarefa realizada no Erlang que não possa ser devidamente efetivada no Elixir, conferindo a ambos o mesmo nível de performance.

Macros e Composição de Funções no Elixir

Elixir possui macros, possibilitando que determinado código em tempo de compilação adquira como entrada uma representação interna do nosso código, gerando uma saída alternativa. Macros são inspiradas no Lisp, possuindo uma estrutura abstrata em árvore possibilitando executarmos manipulações complexas de código de entrada visando obter saídas diferentes, contando com ferramentas auxiliares facilitando determinadas transformações. Utilizando macros podemos estender o Elixir customizando as nossas necessidades de forma poderosa e flexível, atuando como um dos facilitadores oferecidos pela linguagem.

Elixir trata-se de uma linguagem com paradigma funcional e concorrente, confiando na imutabilidade dos dados e nas funções que os transformam.

Um dos benefícios de ser funcional possibilita desenvolvermos componentes compostos por pequenas funções, reusáveis e modulares permitindo agrupamentos. Novas funções podem ser compostas baseadas em funções menores existentes, muitas vezes de forma que sequer os autores originais imaginaram ser possível. Elixir permite a execução de pipelines encadeados de forma elegante e limpa, tratando funções como pontos de transformação no fluxo, sendo combinadas de diferentes formas para alcançarmos os efeitos desejados durante a execução do nosso processo.

APIs (REST) e Desenvolvimento Web

Quando nos referimos a construção de aplicações Elixir voltadas ao ambiente Web, incluindo exposição de APIs, não podemos deixar de mencionar o Phoenix como ferramenta mais popular. Phoenix trata-se de um Framework Web MVC, escrito em Elixir, focado na produtividade e fácil manutenção dos componentes, possibilitando a construção rápida de aplicações web ricas e interativas, exposição de REST APIs, os quais beneficiam-se do paradigma funcional e do OTP para entregarmos aplicações altamente escaláveis.

Phoenix possui similaridades com outros Frameworks MVC, como Rails e Django, provendo a maior parte das features necessárias para sua aplicação Web de forma implícita (out-of-the-box). Atualmente, Phoenix é utilizado em conjunto com o Phoenix.LiveView, uma biblioteca construída em cima do Phoenix possibilitando a criação de aplicações em tempo real (real-time) sem necessidade de escrevermos JavaScript, submetendo atualizações de página via WebSocket.

Permitindo-nos prover componentes interativos através da stack P.E.T.A.L, convertido para tecnologias populares como Phoenix, Elixir, Tailwind CSS, Alpine.js, LiveView. Aplicações construídas utilizando Phoenix, possuem características como produtividade, real-time, e funcionais no topo da lista. Em contrapartida, tanto a comunidade quanto o ecossistema ainda são pequenos.

Kubernetes e Erlang VM (BEAM)

Aplicações construídas em Elixir e rodando no BEAM possuem características em comum como self-healing (tolerância a falhas), horizontal scaling (horizontalmente escaláveis), e distribution (componentes distribuídos) aparentemente sobrepostos, provocando confusão quanto a distinção de comportamento.

Ao realizarmos o deploy dos nossos serviços no Kubernetes teremos o benefício do isolamento na execução de nossos containers da aplicação, adquirindo todas as vantagens deste tipo de ambiente de execução. Reforçando que uma coisa não elimina nem substitui a outra, sendo importante salientar a importância deste nível de controle tanto na camada de cluster quanto na camada de aplicação do software.

Outras funcionalidades, aparente conflitantes entre Elixir/BEAM e Kubernetes, como gerenciamento de configuração, Service-discovery, Roll Outs automatizados (Kubernetes) e Hot code swapping (Erlang VM) encontram-se em níveis diferentes a partir do cluster e da aplicação e possuem suas configurações aplicadas apenas a camada onde está situada.

Kubernetes gerencia o ciclo de vida dos containers em execução, destruindo e startando em caso de falhas, ao mesmo tempo em que no Elixir/BEAM podemos criar supervisores que ficariam encarregados de tarefas similares para recriação de processos com falha, porém em camadas de execução distintas.

Dependendo do tipo de falha, Elixir provê este comportamento a nível de aplicação (falhas parciais), deixando que o Kubernetes atue a nível de cluster para erros menos granulares. Tendo esta distinção em mente, podemos preparar nossas aplicações para reagir a todo o tipo de falha, configurando comportamentos pré-estabelecidos flexibilizando ao máximo a tolerância a falhas para cenários inesperados.

Para controle de tentativas de falhas generalizadas, a nível de cluster, sempre teremos a opção de deixar ações a cargo do Kubernetes, como exemplo após limite de tentativas terem sido esgotadas. Erlang/OTP e Kubernetes atuam em camadas distintas. Na maior parte dos casos as tecnologias se complementam. O OTP possibilita controle de falhas parciais internamente na aplicação onde o Kubernetes não consegue nos ajudar.

Desvantagens do Elixir/Erlang VM

Não existem balas de prata quando nos referimos a arquitetura ou tecnologias e Elixir não é exceção. Do ponto de vista de velocidade de execução, como o código compilado Elixir roda em uma máquina virtual intermediária (BEAM) ao sistema operacional adjacente, não podemos comparar com linguagens que compilam diretamente para código nativo, como C/C++. A plataforma Erlang/OTP oferece performance dentro dos limites possíveis de recursos físicos à disposição, entregando diversas vantagens se comparado a outras máquinas virtuais.

O objetivo do BEAM não seria enfileirar e atender o máximo de requisições no menor espaço de tempo possível de forma desordenada e sem propósitos, porém estruturas internas de implementação da plataforma, pensadas desde o primeiro dia de seu desenvolvimento, foram focadas em concorrência, alta disponibilidade e performance, paralelismo e tolerância a falhas.

Estruturas diferenciadas como garbage-collector (coletor de lixo) executa em formato diferente, sendo mais assertivo e isolado, processos de longa duração executados no BEAM não bloqueia nem impactam outros processos em execução, alta carga BEAM consegue otimizar a utilização de recursos de hardware, possibilidade de usar lightweight-threads sendo menos custosos em comparação a threads, demonstram esta preocupação que acompanha a plataforma desde sua concepção.

Certamente, aplicações que demandam a execução de grandes cargas computacionais não são indicadas serem executadas no BEAM, nem utilizando Elixir, devendo considerar outras tecnologias como C/C++/Rust sendo adequadas para endereçar desafios que exijam mais do CPU. Ainda é possível executar tarefas desenvolvidas em outras tecnologias, como C++, dentro do BEAM em formato de chamada nativa (native-calls).

Precisamos estar cientes que a quantidade de ferramentas e bibliotecas Elixir não é tão abundante quanto em outras linguagens se comparado com Java, JavaScript, Python, Ruby, C#, culminando em mais tempo e esforço dependendo do tipo de demanda do projeto.

Ao mesmo tempo não podemos esquecer o propósito geral do Erlang/OTP e onde ele se destaca, sendo apropriado para criação de aplicações sem downtime, tolerante a falhas, resiliente, com alta disponibilidade e com baixa latência.

Precisamos avaliar em detalhes cada caso e cada projeto separadamente antes de tomarmos alguma decisão, existindo casos onde a aplicação não necessita abraçar as características citadas, sendo indicado considerar outras tecnologias mais apropriadas a cada cenário como forma de promover um trade-off mais assertivo.

Considerações Finais

A arquitetura de microsserviços aumenta a velocidade de adoção de novas tecnologias. Desta forma, podemos experimentar novas tecnologias como Elixir sem impactar o ecossistema como um todo. Para isto, desenvolvemos componentes isolados utilizando a stack tecnológica necessária para executar aplicações com Elixir e consequentemente o deploy da aplicação de forma isolada e autônoma. Um conjunto completo de vantagens na utilização da arquitetura de microsserviços pode ser encontrado aqui.

Precisamos estar atentos às tendências das aplicações modernas, rodando na nuvem (in-cloud) e naturalmente distribuídas. Necessitamos de ferramentas para nos auxiliar na difícil tarefa de construir sistemas cada vez mais tolerantes a falhas, resilientes, responsivos e altamente disponíveis.

Aplicações que sejam concorrentes e implicitamente concebidas inteiramente neste contexto de complexidade, pensadas desde o primeiro dia com estas características em mente, terão mais chances de prosperar. Em um universo de tecnologia cada vez mais dinâmico e disputado, nosso papel será encontrar plataformas, serviços e mecanismos que vão de encontro às necessidades de infraestrutura (runtime) destas aplicações, ajudando a abraçar os desafios que virão pela frente com mais naturalidade e facilidade, possibilitando a implantação bem sucedida destas tecnologias em direção ao sucesso.

--

--