Libertando-se do monólito

E como evoluímos com microsserviços

(19 de julho de 2018)

Um código de decisão Monolith monkeys

Em Conio, tudo começou com o que é comumente chamado de “Monólito”. Ou seja, uma única base de código contendo tudo do aplicativo completo: seus componentes de front-end, componentes de back-end, serviços de API, tarefas em segundo plano; inferno, até mesmo scripts de desenvolvedores. E funcionou muito bem no início. Apenas alguns engenheiros de software, trabalhando em áreas separadas da base de código (poucas chances de conflitos de alteração de código), são fáceis de implantar. Foco total em escrever funcionalidades de aplicativos, sem se preocupar com muito mais. Como abordamos a implantação? Com apenas alguns clientes beta cientes dos constantes progressos, não era um problema real encerrar os serviços por um tempo, lançar a base de código completa (não importa quão pequenas ou grandes fossem as mudanças gerais e se incluíam migrações de banco de dados), e, em seguida, apresentar os serviços novamente.

Foi definitivamente gratificante ver um produto tomando forma a partir do zero e receber elogios dos clientes finais. No entanto, sabíamos muito bem que essa abordagem não é adequada para uma empresa Fintech moderna.

E então?

Com a maioria dos aplicativos de software, os clientes são bastante tolerantes. Sim, o Whatsapp pode parar de funcionar e ter uma interrupção por algumas horas: definitivamente um incômodo, mas não um problema percebido. O mesmo vale para Pokémon Go ou seu aplicativo de jogo favorito. No entanto, esse não é o caso quando dinheiro está envolvido : mudanças de humor se você não conseguir fazer login em sua conta bancária ou fazer operações comerciais. Isso é ainda pior no caso de aplicativos de criptomoeda: a maioria das pessoas se lembra dos erros infames do passado e, sempre que não conseguem acessar seus fundos de criptomoeda, mesmo por um curto período de tempo, surgem especulações. Isso é justo. O dinheiro é seu e você deve ter pouco ou nenhum problema quando quiser usá-lo.

O Monolith acima não é adequado para esse cenário: qualquer alteração no código-base em produção exigiria uma implantação completa, com tempo de inatividade associado. Todos os dias trabalhamos para melhorar nossos serviços corrigindo bugs, tornando nossa interface ainda mais amigável, removendo funcionalidades antigas e adicionando novas que têm melhor aproveitamento. Freqüentemente, lançamos essas atualizações diariamente para que nossos clientes possam obter benefícios imediatos e nos esforçamos para não ter nenhum impacto na experiência do cliente. Ou seja, qualquer fórmula que inventemos nos bastidores, deve ser invisível para o mundo exterior (pelo menos, na maioria das vezes). Portanto, mudamos o Monolith e escolhemos o que é comumente chamado de “arquitetura de microsserviços”.

Evolução por meio de microsserviços

A base de código única e fortemente colada agora está decomposta em partes menores, cada uma representando um serviço específico. Sempre que executados, os serviços comunicam-se de forma síncrona por meio do protocolo HTTP padrão e de forma assíncrona por meio de filas (gerenciado por exemplo por RabbitMQ e Apache Kafka).

Interações em uma arquitetura de microsserviços

É bastante desafiador começar a quebrar o monólito em componentes menores, mas vale muito a pena o esforço. Em termos militares, é muito semelhante ao que Júlio César fez para governar firmemente o grande território da Gália: “ dividir e conquistar ”.

1) O produto pode ser implantado continuamente. Uma atualização de código agora se aplica apenas a um microsserviço: na maioria dos casos, ele pode ser imediatamente implantado na produção e lançado sem impacto para o cliente

2) O código é mais fácil de gerenciar. Do ponto de vista da organização da empresa, as coisas mudam quando uma equipe de 2 engenheiros de software se torna uma equipe de 10 engenheiros de software. É mais eficaz e com menos conflitos de código quando cada membro da equipe é responsável por seu próprio microsserviço.

3) O código é mais fácil de manter. Uma arquitetura de microsserviços requer, por natureza, a definição de uma interface para se comunicar com o mundo externo (seja o aplicativo de front-end ou outro serviço de back-end) e está completamente isolada de qualquer outro ponto de vista. Isso permite revisar, redesenhar ou até mesmo reescrever completamente do zero (mesmo em idiomas diferentes, se conveniente) componentes únicos do aplicativo sem afetar o resto.

4) O desempenho pode ser aprimorado. Cada microsserviço pode agora usar sua linguagem mais apropriada. Componentes de computação criptográfica pesada podem, por exemplo, ser otimizados em C, enquanto os serviços de API em Python e tarefas de longa execução em Go.

5) Isolamento e segurança aprimorados do código. Cada microsserviço pode ser executado em seu próprio contêiner Docker, fornecendo isolamento de privilégios, rede e segregação de dados e, de suma importância para uma fase de crescimento, enorme potencial de escalabilidade.

Os microsserviços são a resposta, então?

Obviamente, almoços grátis não existem. Uma arquitetura de microsserviços também vem com seu próprio conjunto de desafios difíceis:

1) Complexidade operacional. Os engenheiros de DevOps são definitivamente necessários para suavizar as complexidades do novo processo de implantação.

2) Hardware inchado. Os microsserviços costumam ser executados em contêineres Docker; assim que o número de microsserviços prolifera, torna-se cada vez mais desafiador executar o aplicativo completo no mesmo hardware de antes.

3) Sobrecarga de intercomunicação: cada solicitação pode precisar interagir com um ou mais diferentes microsserviços através da rede. Isso pode causar aumento da latência e pode estar sujeito a falhas temporárias. A fim de implementar serviços resilientes, bem como melhorar a escalabilidade de todo o sistema, é necessário mover as interações para mensagens assíncronas (por exemplo, usando Apache Kafka e / ou RabbitMQ)

4) Consistência eventual. Este é provavelmente o desafio mais difícil de uma arquitetura de microsserviços. Dado um único microsserviço, é possível criar transações RDBMS dentro de seus limites . Infelizmente, porém, um problema comum em arquiteturas distribuídas é lidar com várias transações que não estão dentro dos mesmos limites. Como resultado, o sistema pode terminar em um estado ilegal e irrecuperável. Para mitigar tais problemas, Conio adota estratégias diferentes:

  1. Seguindo as práticas de Domain Driven Design, decompor os domínios de nível superior em subdomínios e confiná-los em contextos delimitados individualmente ; cada contexto limitado é implementado como um microsserviço, onde os limites da transação são aplicados. Isso resolve a possibilidade de haver inconsistências para subdomínios específicos.
  2. Implemente interações assíncronas idempotentes, que mais cedo ou mais tarde resolvem as inconsistências.
  3. Sempre que possível, evite qualquer ação que possa envolver vários subdomínios.

5) Relatórios complexos. Como cada subdomínio reside em um contexto limitado específico, relatórios complexos que envolvem vários subdomínios podem exigir a consulta de dados de várias fontes de dados: isso pode ter um impacto negativo na expressividade dos domínios e na escalabilidade do sistema. Aqui em Conio , adotamos uma arquitetura CQRS para oferecer suporte a atividades de backoffice e relatórios de análise de negócios.

6 ) Sistema de registro. Cada elemento em um sistema distribuído contribui para a criação do log de todo o sistema. Porém, é necessário implementar ferramentas que possam criar as conexões necessárias entre todos esses logs separados para ter um log unificado para cada interação. Aqui no Conio, usamos a pilha ELK (ElasticSearch, Logstash, Kibana) para armazenar e consultar os dados do log: cada log é enriquecido com os ids de correlação necessários que permitem o log unificado mencionado acima.

Nunca pare a evolução

Nossa opinião? A decomposição da base de código única inicial deve ser vista como uma tarefa de longo prazo , com refinamentos constantes. Em Conio, levamos alguns anos, onde, passo a passo, mudamos de uma enorme base de código para mais de 100 microsserviços. Chegamos a um ponto em que nos sentimos orgulhosos dos resultados, mas ao mesmo tempo continuamos explorando. Existem várias novas otimizações possíveis: mover do Docker Swarm para o Kubernetes? Migrando serviços de backend para frontend para funções lambda sem servidor? Mudando para um fluxo de operação de implantação contínua completo? As possibilidades são infinitas.

Nós tocamos em vários tópicos e tecnologias aqui. Nos próximos artigos, compartilharemos mais detalhes sobre nossas descobertas e progresso. Se desejar, sinta-se à vontade para comentar e nos contar sua experiência.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *