Coordenação ou Orquestração em Microsserviços

2022-01-14

Em arquiteturas de software baseadas em microsserviços, é comum que existam vários serviços fazendo seus trabalhos de forma isolada. Em sistemas bem divididos baseados em bounded contexts, um ou um grupo de serviços podem colaborar em conjunto para fornecer funcionalidades para um dado contexto.

Nesse caso, estes serviços possuem seu próprio banco de dados. Eles são os donos daquele contexto. Eles escrevem no banco de dados, eles são responsáveis pela consistência de dados nos seus próprios banco de dados.

Nesse molho de serviços e microsserviços existem duas grandes formas de organizar o sistema e o fluxo de dados. Essas duas grandes formas são em sistemas orquestrados e sistemas coreografados.

##O que são sistemas orquestrados?

Sistemas orquestrados são sistemas que possuem um nó (leia, serviço, microsserviço, nanosservico, etc.) que é responsável por consultar N outros serviços com o objetivo de realizar um processamento.

Pense num problema do tipo: precisamos pegar dados de N serviços para mostrar em um dashboard. Como você resolveria o problema? Pense um pouco a respeito.

Não podemos simplesmente acessar o banco de dados do serviço amiguinho, isso quebraria a barreira contextual que o serviço que precisa dos dados tem acesso. Na verdade você pode acessar o banco de dados do serviço amiguinho, mas isso não é legal. Confia em mim que em breve você vai descobrir que o seu "eu do passado" cometeu um tremendo erro e você vai se culpar muito por isso.

Hmm, bom... seguindo no nosso problema! Vamos desenhar um usuário mexendo na sua interface de usuário que usa o serviço A como backend (ou porta, se você for fã de arquitetura hexagonal).

Yey! Agora temos um usuário master-blaster tirando altos relatórios consistentes ao longo de N serviços (no nosso caso são 4, o orquestrador e os 3 serviços que ele depende)... Você já conseguiu encontrar o problema? Ainda não? Vamos seguir investigando mais um pouco esse esquema.

Lembra do problema do acoplamento? Temos um acoplamento distribuído aqui. É tipo um 💩, só que multiplicado por 3, logo 3x💩. Isso não está cheirando muito bem.

Digamos que, por algum motivo o serviço B cai. Ou simplesmente começa a responder de alguma forma indevida. Por algum motivo, B, resolveu violar o seu contrato com o mundo externo. B pode fazer isso, nada impede que B simplesmente pare de funcionar. Muitas coisas podem fazer ele parar de funcionar. Quer alguns exemplos?

  1. Fim do espaço em disco disponível para o seu banco de dados realizar escritas
  2. Falha na rede que conecta B com o resto da malha de serviços
  3. Bugs que passaram pela suíte de testes

E a lista vai longe.

Nosso sistema até então é bem simples de implementar. Precisa de algum dado do serviço B, C ou D? Só buscar lá. O dado está a uma requisição HTTP de distância. Só não esqueça do que você está abrindo mão. Pesquise sobre CAP Theorem para ter mais contexto.

Além disso, imagine colocar um novo serviço na jogada. Imagine precisar quebrar o contrato com o mundo externo de algum dos serviços que A depende, devido a alguma nova implementação de alguma regra de negócio maluca. Imagine ter que cumprir requisitos de tempo de resposta em um universo onde A depende de outros 3 serviços. Qualquer um dos serviços B, C ou D pode acabar demorando um pouco mais para responder por praticamente qualquer motivo. Imagine que A começa, de repente, a receber muita carga onde é necessário realizar autoscaling para aguentar o tranco. Nesse caso A (e todas as outras cópias de A) vão causar sobrecarga nos serviços B, C e D e consequentemente estes serviços poderão ser duplicados para aguentar a carga. (Dica: autoscaling pode sair caro.)

Esse esquema é simples o suficiente para ser implementado como uma Prova de Conceito ou até mesmo como um MVP, mas a quantidade de contras, dependendo do seu caso, acabam inviabilizando que esse esquema evolua de forma sustentável dentro da sua malha de microsserviços.

##O que são sistemas coreografados?

A coreografia entre serviços está diretamente relacionada a sistemas assíncronos regidos por sequência de eventos e consistência eventual.

Imagine que agora B, C e D não conhecem A e A não conhece eles também. Como que a pessoa que está tentando pegar seus relatórios vai conseguir os relatórios? Como que o serviço A vai encontrar os dados que ele precisa para retornar à aplicação de front-end?

Vamos desenhar novamente o nosso esquema, mas agora isolando B, C e D em seus próprios mundinhos.

Tá, agora talvez não faça sentido. Mas vamos por partes.

Digamos que B, C e D recebem eventos e produzem eventos. Esses ventos são transmitidos via um canal de comunicação, geralmente chamado de Message Bus. Se você não conhece esse conceito ainda, imagine como se fosse um canal, um duto, um cano, conectado no serviço B onde ele recebe dados de N outros locais que ele não conhece. B só conhece o formato das mensagens que ele recebe e outros serviços só precisam saber o formato das mensagens que devem publicar em um determinado canal. (Isso também é conhecido como compartilhamento de DTOs, geramente essas coisas viram bibliotecas dentro da empresa).

Agora B recebe essas mensagens, realiza o seu processamento, salva dados no banco de dados e publica novos eventos em algum outro canal. Nesse caso B sabe que tipo de mensagem deve publicar nesse outro canal, o shape da mensagem. Outro serviço, como o serviço X ou Y (que nós nem conhecemos) podem consumir mensagens desse canal.

B não conhece nada além de seus canais de comunicação e o trabalho que ele deve fazer. O trabalho que ele deve fazer que é o que é conhecido como coreografia. Cada serviço sabe como realizar o seu trabalho e somente o seu trabalho. Cada serviço pertencente a esse esquema opera de forma única, desacoplado dos demais serviços.

Essa forma de organização de microsserviços é muito usada em sistemas de grande porte que precisam lidar com vazão bem grande de dados, onde cada serviço consegue escalar individualmente já que agora não depende de nenhum outro serviço.

Serviço C está recebendo muita carga? Sem problema, a infraestrutura pode duplicar ou triplicar a quantidade de instâncias do serviço C temporariamente, sem afetar os serviços B e D.

Tá Gustavo, mas como que a gente resolve aquele problema lá? O problema dos relatórios...

Oops, bem lembrado!

Problemas envolvendo relatórios são um tanto quanto desafiadores, mas geralmente você consegue resolver com um esquema de agregação de eventos e tabelas analíticas.

Eu não sou um cientista de dados, beeem longe disso. Mas sei que tabelas transacionais com muitas relações causa problemas ao banco para pegar relatórios complexos. (Agora imagina como o seu eu do passado iria resolver isso com aquele esquema orquestrado precisando fazer consultas distribuídas entre N serviços usando HTTP)

A solução aqui é criar tabelas para cada relatório. Flat. Sem relações. Quer criar um filtro? Todos os dados estão na tabela. Quer pegar dados de vai saber quando?, os dados estão em uma mesma tabela.

O esquema pode ser o seguinte: dado uma sequência de eventos, o serviço A agrega cada evento nas respectivas tabelas analíticas que dependem do evento.

Vamos desenhar!

No esquema acima, B, C e D recebem eventos e publicam eventos nos canais b_updated, c_updated e d_updated, respectivamente. O serviço A, por outro lado, escuta por eventos publicados nos canais b_updated, c_updated e d_updated. Baseado em cada evento, A agrega nas tabelas analíticas de acordo com a necessidade de cada relatório.

Dessa forma, agora a pessoa pode pegar seus relatórios, fazer filtros e muito mais coisas baseado nas tabelas analíticas. Precisa adicionar mais colunas nas tabelas analíticas? Basta alterar o mecanismo de agregação, talvez escutar eventos de um novo canal, talvez acessar dados que antes eram ignorados em cada payload que representa um evento.

Então,

  • ✅ Escalabilidade
  • ✅ Baixo ou nenhum acoplamento
  • ✅ Facilidade em escalar times dentro de contextos delimitados
  • ✅ Tolerância a falhas de rede

Parece perfeito, não?

##Consistência e integridade de dados

Bom, o esquema anterior é bom mas não é perfeito. Na computação, infelizmente, não existe bala de prata.

É difícil garantir a consistência de esquemas dessa forma. O serviço A geralmente vai estar defasado de acordo com a capacidade da infraestrutura em propagar e gerenciar eventos.

Outro problema é abraçar o mecanismo de gerenciamento de mensagens. Esse mecanismo não pode falhar e se falhar, deve ser resiliente o suficiente para não perder mensagens.

Sobre o problema de relatórios em específico, adicionar uma nova coluna em uma tabela analítica pode ser doloroso se os requisitos de negócio forem de "não aceitamos que essa coluna seja nula, queremos o dado que deveria estar lá, esteja lá" (imagine o caso em que você adicionou uma nova coluna na tabela analítica e agora precisa processar retroativamente os eventos que resultaram naquela tabela). Dependendo do nível de complexidade do sistema, isso pode ser quase que impossível de ser feito ou vai necessitar de muito, muito, muito, muito esforço.

O uso de dados vai ser grande também. Hoje em dia é "barato" armazenar dados. Porém saiba que se o serviço A produz dados derivados de algo que já é salvo no serviço B, A vai, de certa forma, duplicar informação. Essa informação "duplicada" vai precisar de espaço para ser salva em disco.

##Resumo

Sistemas distribuídos são difíceis. São difíceis de criar, são difíceis de evoluir e são difíceis de manter. Além disso, são caros. Sua empresa precisa ter bala na agulha para manter um sistema desse porte e que seja seguro o suficiente para ser operado de forma distribuída. Você não vai querer perder dados, também não vai querer corromper dados, por isso não vai querer N serviços acessando o mesmo banco de dados.

Não permitir que N serviços acessem o mesmo banco de dados traz um leque bem grande problemas que precisam ser resolvidos. Isso custa tempo e dinheiro. Dinheiro que talvez uma empresa com um produto que ainda não tem mercado, possa não existir.

Particularmente acredito que nenhum produto deve ser feito em microsserviços se ainda não tem um mercado. Você vai começar com zero clientes, zero dinheiro e um problema bem complicado para resolver e manter. Monolitos escalam (veja o case do Shopify), monolitos são mais fáceis de manter, monolitos são mais baratos e monolitos vão acelerar o desenvolvimento do seu produto e diminuir muito seu time to market.

Se der certo e o seu monolito não conseguir aguentar o tranco, bom... agora você tem a grana, mercado e clientes para desenvolver um sistema distribuído da forma correta.

##Referências

#arquitetura

📝 Edite esta página