Teorema CAP em Microservices

Sempre que se fala sobre o Teorema CAP vem em mente a teoria, porém gostaria de mostrar na prática como este conceito pode afetar a arquitetura e o desenvolvimento de aplicações distribuídas. Muito do que vai ser discutido aqui, trata-se de conhecimento adquirido na prática durante o trabalho, conversas e pesquisas.

Bom, vamos ao que interessa!

#Sabemos que o software perfeito não existe… Quem nunca achou aquele bug inofensivo em um aplicativo famoso, precisou limpar o cache do navegador ou até mesmo reiniciar o computador, eu poderia ficar horas aqui citando exemplos, porém todos nós sabemos muito bem disso. Pesquisando por referências a melhor metáfora que encontrei foi em um artigo da IBM

"Barato, Rápido e Bom: Escolha Dois"

Assim é o teorema CAP, também conhecido por Teorema de Brewer, criado pelo respeitado professor Eric Brewer em meados dos anos 2000, referenciando armazenamento de dados distribuídos. Portanto, assim como na metáfora, precisamos fazer trocas, priorizar "isto" à "aquilo", os famosos trade offs.

De acordo com o teorema temos três opções

Consistência(Consistency)

O quão consistente os dados estarão durante a execução da aplicação, ou seja, em um sistema consistente: No mesmo momento em que um dado é consultado no node1 e node2 terão o mesmo retorno. Pode parecer caótico um dado estar com um valor errado/desatualizado, porém em alguns casos não há problemas, então pode ser uma boa abrir mão da consistência.

Disponibilidade(Availability)

Pior do que não conseguir acessar um site que se DESEJA acessar é só não conseguir acessar um site em que se PRECISA acessar. É isso mesmo que a disponibilidade representa, o quão disponível está a aplicação para o usuário, ao priorizá-la toda request terá um response com valor correto, independente do tráfego ou outros fatores.

Tolerância de partição(Partition tolerance)

É a capacidade do sistema continuar funcional, mesmo se qualquer outro nó correlacionado venha a ficar inoperante. Afirmação inicialmente ligada a armazenamento de dados, porém que também podem ser trazidas para o contexto de outras aplicações, principalmente de micro serviços.

Exemplo de aplicação genérica:

https://cdn-images-1.medium.com/max/800/0*UKD-jL88J-9Mj48p

Conforme imagem acima, na situação hipotética existe um serviço de envio de email e um outro de autenticação. Sempre que um usuário loga em um dispositivo desconhecido, um email é disparado para o mesmo com um alerta. Se o serviço de envio de email estiver fora do ar não deve tornar inoperante o serviço de autenticação.

Priorizando escolhas

Letras miúdas…

O teorema 'permite' que duas opções sejam escolhidas, porém assim como entre "Barato, Rápido e Bom" todos querem o "Bom", em sistemas distribuidos a tolerância de partição, o famoso desacoplamento, é indispensável, um vez que um serviço inoperante não pode inutilizar outros serviços, gerando um efeito bola de neve. Imagine se o serviço de autenticação, citado anteriormente, sofresse interferência por conta da indisponibilidade do envio de email, simplesmente nenhum usuário conseguiria se autenticar e usufruir da aplicação(na prática não é bem assim, mas o exemplo serve).

Portanto nos resta escolher entre Disponibilidade(A) e Consistência(C), os famosos:

  • CP = Consistência(C) + Partição(P)
  • AP = Disponibilidade(A) + Partição(P)

https://cdn-images-1.medium.com/max/800/0*tJnwUxNjkA6RjYGn

Diagrama Teorema CAP: Disponibilidade, consistência e Tolerância de partição

Contextualizando

A consistência e a disponibilidade estão diretamente interligadas, lembre-se de que nosso sistema deve ser particionado. Para falar sobre disponibilidade e consistência vamos utilizar um exemplo: Novamente temos dois micro serviços genéricos, ServiceA e ServiceB. Porém em ambos temos a utilização de Produtos.

Prezando por Disponibilidade (Consistência Eventual)

Eventualmente os dados estarão consistentes. Para priorizar disponibilidade à consistência o seguinte cenário ocorre:

Os registros gerados em A e B são compartilhados entre eles assim que criados, portanto ficam duplicados. A comunicação entre A e B é assíncrona e possui um delay na criação dos registros entre os serviços.

Assim existem dois cenários:

Alteração - Em caso de alteração existirá um momento em que o dado em A será diferente do dado em B, durante o pretiodo de latência.

Exclusão/Inclusão - Em caso de exclusão e inclusão o dado irá existir em um serviço e em outro não, durante o período de latência.

Por conta da priorização da disponibilidade, eventuamente os dados estarão consistentes. Uma consulta pode retornar um valor desatualizado.

Os cenários permanecem assim até os dados serem atualizados pelo sistema, tempo que pode ser maior ou menor dependendo de sua capacidade em relação ao fluxo de demandas solicitadas. Normalmente ocorre por meio do envio de eventos assíncronos, em message-brokers como RabbitMQ e Apache Kafka.

Prezando por Consistência (Consistência Estrita)

Estritamente os dados estarão consistentes. Ao ser demandada consistência estrita, deve-se utilizar bancos relacionais com o princípio ACID, enquanto um registro estiver sendo manipulado o mesmo estará com uma espécie de lock impedindo de ser visualizado enquanto estiver inconsistente. Agora ficou bem melhor de ver a relação consistência x disponibilidade não é verdade? Bom… vamos ao exemplo:

Agora ao invés do sistema A e B possuírem o cadastro de produto, apenas um terceiro sistema C será responsável pelos cadastros, assim sempre saberemos que o registro estará atualizado, a comunicação será feita de uma forma síncrona por meio de Rest HTTP, porém se o serviço C sair do ar todos que dependem dele ficam inoperantes.

Problemas…

A comunicação entre serviços por requests http pode gerar alguns incômodos, como por exemplo: A url de acesso e suas portas. Também pode ocorrer em aplicações que prezam por disponibilidade(assíncronas) se houver a necessidade de escalar os serviços. Existem algumas formas de contornar estes problemas utilizando Service discovery como o Eureka da netflix e um API gateway por exemplo o padrão do spring Spring-cloud-starter-gateway, porém como não é o foco deste artigo não irei me aprofundar no tema.

Artigo postado lá no medium também, tenho alguns outros lá: https://medium.com/p/5fa1d23f7174/edit