Microsserviços são o estilo arquitetônico predominante da atualidade. Para o nosso propósito, não vale a pena descrever aqui o que significa e suas vantagens, pois há muito conteúdo excelente disponível por aí. Em vez disso, vamos ser mais específicos e revisar o que aprendemos criando um sistema complexo em uma fintech nos últimos seis anos. Esta é uma breve história dos microsserviços no Nubank.

O começo

Seis anos, de 2013 a 2019, é também toda a vida útil da empresa, o que significa que começamos com microsserviços desde o primeiro dia, desafiando o conselho padrão de começar com um monólito. A justificativa geralmente dada é que é melhor otimizar para uma rápida centralização no início – enquanto a startup ainda está em busca de adequação ao mercado – para posteriormente refatorar para uma estrutura mais estável. Descobrimos que, de fato, começar com microsserviços, especialmente em 2013, nos tornou mais lentos no início. Montar uma infraestrutura de provisionamento complexa enquanto construímos o produto do zero foi muito para uma equipe pequena e levou algum tempo até sentirmos que o trabalho fluía sem problemas. Por outro lado, o nosso negócio não estava muito inclinado a mudanças rápidas, e aqueles primeiros meses foram provavelmente o melhor momento para investir numa base sólida, em vez de mais tarde, quando tivemos que enfrentar as pressões crescentes de ampliação e desenvolvimento do conjunto de recursos.

Isso não quer dizer que acertamos tudo desde o início. Pelo contrário, tivemos que mudar muitas das nossas abstrações centrais à medida que entendíamos o domínio com mais profundidade, o que às vezes significava que tínhamos que redesenhar os limites de serviço. Este processo de aprendizagem continua até hoje. A qualquer momento, um esquadrão ou outro trabalhará na divisão de um serviço ou na fusão de dois serviços.

Confira nossas oportunidades de trabalho 

Conheça nossas oportunidades

Provisionamento e implantação

Nossa infraestrutura de produção passou por diversas iterações. Começamos com um serviço gerenciado do Chef, experimentamos com o Fleet e ECS do CoreOS, construímos uma infraestrutura DIY simples de um contêiner por máquina virtual usando as abstrações principais do AWS EC2 e CloudFormation, para finalmente convergir na implantação de nossos próprios clusters Kubernetes. Foi uma longa jornada, mas muito menos árdua devido à automação pervasiva. Todos os nossos recursos em nuvem, desde o início, são provisionados por meio de algum tipo de processo automatizado. Mesmo coisas de uma vez só, como aquela instância de livegrep que um esquadrão queria colocar em execução rapidamente, precisam ser automatizadas. Como a maioria das coisas na empresa, a nossa obsessão pela automação é muito mais uma norma cultural do que uma regra imposta.

A mais recente encarnação da nossa infraestrutura de automação assume a forma de uma base de código Clojure que orquestra a criação de recursos AWS ou Kubernetes. Uma entrada crítica para esse processo vem de um repositório git onde os engenheiros colaboram para declarar metadados para cada microsserviço: que tipo de banco de dados ele requer, quão pesada é a carga de trabalho, que tipo de construção e ferramentas de teste ele deve passar durante a construção etc. Com base nesses dados, nossa automação pode fazer mágica, como provisionar bancos de dados, configurar a descoberta de serviços e até criar pipelines de construção inteiros para todos os serviços.

Infraestrutura imutável

Grande parte da nossa cultura de engenharia está conectada à comunidade e às ideias de programação funcional. Uma ideia central é o conceito de imutabilidade: é sempre mais seguro construir cópias atualizadas dos dados do que alterá-los. Quando transportamos esta visão para o campo da infraestrutura, isso se traduz na construção de cópias atualizadas dos recursos, em vez de transformá-los no local.

Uma aplicação simples desse princípio se aplica à implantação: primeiro criamos novos contêineres para depois desmontarmos as instâncias antigas. Nada particularmente interessante aí: um exemplo bastante padrão de técnicas azul-verde.

Uma aplicação em maior escala é criar novas pilhas de produção. De vez em quando, temos que fazer alterações maiores do que uma simples implantação. Poderíamos reforçar a segurança, melhorando a descoberta de serviços ou redimensionando elementos de infraestrutura. Independentemente das especificidades, a abordagem geral é a mesma. Inovamos uma pilha de produção inteira — incluindo todos os serviços, clusters Kubernetes, clusters Kafka etc. — testamos se ela está funcionando bem e, em seguida, apontamos o tráfego para a nova pilha atualizando os aliases de DNS de todos os pontos de entrada. Desnecessário dizer que todo esse processo é altamente automatizado, a ponto de uma pequena equipe poder executar todas as etapas a cada dois meses.

Comunicação de serviço e mensagens assíncronas

Cuidar das finanças das pessoas é uma tarefa que levamos muito a sério. Consequentemente, a integridade dos dados é de extrema importância. Executar uma malha de microsserviços e manter esses padrões elevados traz novos desafios: como garantir que os dados nunca sejam perdidos em um mundo onde falhas parciais e partições de rede são a norma? Nossa resposta é confiar fortemente em mensagens assíncronas.

Falhas parciais e partições de rede devem ser aceitas para garantir a confiabilidade. 

A maioria das interações entre serviços é intermediada por um broker de mensagens replicado confiável. Em vez de fazer com que o serviço cliente espere que o servidor termine o processamento e responda — sujeito a todos os tipos de falhas devido a desequilíbrios de carga, falhas de rede e assim por diante — o serviço iniciador publicará de forma confiável uma mensagem para ser posteriormente consumida pelo próximo serviço no fluxo.

Falhas ainda podem ocorrer. Imagine que, para processar uma mensagem, o atendimento ao cliente precise fazer uma ligação para um serviço terceirizado que está com problemas de estabilidade. Mesmo assim, podemos recuperar e evitar a perda de dados detectando o erro em uma camada inferior e redirecionando automaticamente a mensagem para um tópico de mensagens não entregues.

Extração de dados e tomada de decisão

Além dos aspectos operacionais que abordamos até agora, devemos considerar as necessidades de dados para a tomada de decisões na empresa. Os tomadores de decisão, incluindo analistas de negócios e cientistas de dados, dependem de dados antes escritos por microsserviços para seus modelos. Nosso objetivo como engenheiros é oferecer a eles uma interface estável para esses dados. Isto é um desafio, visto que os modelos e limites de dados estão sempre mudando. Além disso, nossos dados são fragmentados para lidar com a escalabilidade — em suma, muito brutos e desagradáveis para nossos colegas analíticos trabalharem.

Há alguns anos, implantamos uma camada chamada “contratos” para resolver o problema. Os contratos servem como a interface estável mencionada anteriormente. Usando contratos, podemos expor com segurança os dados de microsserviços para o lado analítico da empresa e reduzir o risco de quebrar modelos (silenciosamente). Contratos são objetos Scala gerados automaticamente a partir do modelo de dados de serviço. Com os contratos, nossos trabalhos em lote do Spark transformam dados em tabelas destinadas a cumprir qualquer finalidade analítica em estágios posteriores. Para garantir que essa camada reflita consistentemente a realidade (o formato dos dados), dependemos fortemente de testes automatizados — tanto para os serviços quanto para os trabalhos do Spark.

Por causa dos testes, raramente encontramos problemas devido a alterações no esquema de dados iniciais que interrompem a análise posterior. No entanto, ainda temos dificuldade em reagir adequadamente às mudanças nos “valores dos dados” em estágios iniciais. Isso pode acontecer quando a definição de um dado muda (às vezes de forma perigosa e sutil) enquanto a análise posterior não está ciente disso. Atualmente, estamos lidando com esse problema por vários ângulos, como detecção de anomalias em estatísticas de tabelas (contagem, cardinalidade etc.), melhorando a comunicação entre engenharia e partes interessadas analíticas, e desvinculando contratos e casos de uso analíticos (implementando um armazém de dados). No entanto, ainda está longe de ser um problema resolvido.

Concluindo

Há muito mais sobre o que podemos conversar. Desde outros padrões de confiabilidade que aplicamos ao longo dos anos até os detalhes de nossa jornada de orquestração de contêineres. Desde a maneira como planejamos a escalabilidade com fragmentação até a cultura de aprender com interrupções por meio de análises post-mortem sem culpa. Desde nossas experiências na introdução de back-ends para nossos front-ends até… bem, você entendeu. Isso é demais para uma única postagem no blog, mas fique atento às notas futuras da nossa equipe de engenharia.

Confira nossas oportunidades de trabalho 

Conheça nossas oportunidades