Praticamente todos os aplicativos empresariais bem-sucedidos, cedo ou tarde, entram na fase em que é necessário escalonamento horizontal. Em muitos casos, você pode simplesmente iniciar uma nova instância e reduzir a carga média. Mas há casos menos triviais em que devemos garantir que diferentes nós saibam uns sobre os outros e distribuam a carga de trabalho de maneira adequada.

Foi uma feliz coincidência que erlang, que escolhemos pela sintaxe agradável e pelo hype em torno, tem suporte de primeira classe . Em teoria, isso soa completamente trivial:
A troca de mensagens entre processos em diferentes nós, bem como entre links e monitores é transparente […]
Na prática, tudo é um pouco mais complicado. O distribuído erlang foi desenvolvido quando "container" significava uma caixa de metal grande para transporte, e "docker" era apenas um sinônimo para estivador. No IP4 havia muitos endereços não utilizados, e nas falhas de rede, geralmente, as responsáveis eram as ratas que roeram os cabos, enquanto o tempo médio de tempo de atividade dos sistemas produtivos era medido em décadas.
Agora todos nós estamos inimaginavelmente autossuficientes, empacotados, e executamos o distribuído erlang em um ambiente onde IPs dinâmicos são atribuído aleatoriamente, e os nós podem aparecer e desaparecer à vontade do planejador. Para evitar um monte de código padrão em cada projeto que executa o distribuído erlang, em resposta a um ambiente hostil, é necessária ajuda.
Nota: eu sei que existe . É realmente legal, tem mais de mil estrelas, e o autor é bem conhecido na comunidade, e tudo mais. Se você estiver satisfeito com os métodos oferecidos por este pacote para criar e manter um cluster — fico feliz por você. Infelizmente, eu preciso de muito mais. Quero gerenciar a configuração em detalhes e não ser um espectador externo no teatro de reformação do cluster.
Requisitos
O que eu precisava pessoalmente era uma biblioteca que assumisse o gerenciamento do cluster e tivesse as seguintes propriedades:
- funcionamento transparente tanto com uma lista de nós codificada rigidamente quanto com descoberta dinâmica através de serviços erlang;
- callback totalmente funcional em cada alteração de topologia (nó para lá, nó para cá, instabilidade da rede, divisões);
- uma interface transparente para iniciar um cluster com nomes longos e curtos, assim como com
:nonode@nohost; - suporte a Docker de forma nativa, sem a necessidade de escrever código de infraestrutura.
Isso significa que, depois que eu testei o aplicativo localmente em :nonode@nohost, ou em um ambiente artificialmente distribuído usando , eu quero simplesmente executar docker-compose up --scale my_app=3 e ver como ele executa três instâncias no Docker sem quaisquer alterações de código. Eu também quero que os aplicativos dependentes, como mnesia — quando a topologia muda, o cluster seja reorganizado em tempo real sem qualquer empurrão adicional por parte do aplicativo.
Cloister não foi concebido como uma biblioteca capaz de fazer tudo: desde o suporte a clusters até fazer café. Não é uma bala de prata que visa cobrir todos os casos possíveis, ou ser uma solução academicamente completa no sentido que teóricos de CS atribui a esse termo. Esta biblioteca visa atender a um objetivo muito claro, mas executar seu trabalho não muito abrangente de forma perfeita. Esse objetivo será garantir total transparência entre um ambiente local de desenvolvimento e um ambiente distribuído flexível, cheio de contêineres hostis.
A abordagem escolhida
Cloister é prever a execução como um aplicativo, embora usuários experientes possam trabalhar com a construção e manutenção do cluster manualmente, iniciando Cloister.Manager na árvore de supervisores do aplicativo alvo.
Ao ser executada como aplicativo, a biblioteca depende de config, de onde lê os seguintes valores principais:
config :cloister,
otp_app: :my_app,
sentry: :"cloister.local", # ou ~w|n1@foo n2@bar|a
consensus: 3, # número de nós a serem considerados
# o cluster está ativo
listener: MyApp.Listener # listener a ser chamado quando
# o anel tiver mudadoOs parâmetros acima significam literalmente o seguinte: Cloister utilizado para um aplicativo OTP :my_app, utiliza descoberta de serviço Erlang para conectar nós, pelo menos três, e MyApp.Listener módulo (implementador de ) configurado para receber notificações sobre mudanças na topologia. Uma descrição detalhada da configuração completa pode ser encontrada em .
Com essa configuração, o aplicativo Cloister será , adiando o processo de inicialização do aplicativo principal até que um consenso seja alcançado (três nós estão conectados e interligados, como no exemplo acima). Isso permite que o aplicativo principal presuma que, quando iniciado, o cluster já está disponível. A cada mudança na topologia (que ocorrerá muitas vezes, pois os nós não são iniciados de forma totalmente sincronizada), um manipulador será chamado. . Na maioria dos casos, executamos uma ação quando recebemos uma mensagem com o estado %Cloister.Monitor{status: :up}, o que significa: “olá, o cluster foi montado”.
Na maioria das vezes, a configuração consensus: 3 é ideal, porque mesmo que esperemos que mais nós se conectem, o retorno de chamada passará por status: :rehashing → status: :up em qualquer novo nó adicionado ou removido.
Ao iniciar em modo de desenvolvimento, basta definir consensus: 1 e Cloister com alegria percebe a expectativa da montagem do cluster, vendo :nonode@nohost, ou :node@host, ou :node@host.domain — dependendo de como o nó foi configurado (:none | :shortnames | :longnames).
Gerenciamento de aplicativos distribuídos
Aplicativos distribuídos não existem em um vácuo, geralmente incluem dependências distribuídas, como mnesia. É fácil para nós gerenciar sua reconfiguração a partir do mesmo retorno de chamada on_state_change/2. Aqui, por exemplo, uma descrição detalhada de como reconfigurar mnesia em tempo real em .
A principal vantagem de usar Cloister é que ele executa todas as operações necessárias para reconstruir o cluster após mudanças na topologia por trás dos panos. O aplicativo simplesmente é iniciado em um ambiente distribuído já preparado, com todos os nós conectados, independentemente de sabermos os endereços IP e, portanto, os nomes dos nós com antecedência, ou se eles foram atribuídos/alterados dinamicamente. Isso não requer configurações especiais do Docker, e do ponto de vista do desenvolvedor de aplicativos, não há diferença entre iniciar em um ambiente distribuído ou local em :nonode@nohost. Mais detalhes sobre isso podem ser lidos em .
Embora o processamento complexo de mudanças na topologia seja possível por meio de uma implementação própria MyApp.Listener, sempre podem existir casos limite onde essas limitações da biblioteca e a abordagem tendenciosa à configuração se tornem um obstáculo para a implementação. Isso é normal, basta seguir o que foi mencionado acima. libcluster, que é mais versátil, ou até mesmo gerenciar um cluster de baixo nível sozinho. O objetivo desta biblioteca de código não é cobrir todos os cenários possíveis, mas usar o cenário mais comum sem dores desnecessárias e cópias coladas volumosas.
Nota: neste local havia a frase «Happy clustering!» na original, e o Yandex, que estou traduzindo (não vou pesquisar dicionários), me sugeriu a opção «Feliz agrupamento!». Um melhor tradução, talvez, especialmente à luz da atual situação geopolítica — é impossível imaginar.
Fonte: habr.com
