fbpx
RabbitMQ e Kubernetes | Rivais ou Aliados?
Publicado em: segunda-feira, 9 de ago de 2021

Se, ambos, RabbitMQ e Kubernetes promovem um melhor uso de sua infraestrutura, trazem resiliência, escalabilidade, performance, será que eles não seriam concorrentes? Como eles concorrem? Eles podem colaborar?

Entendendo o papel de cada um

RabbitMQ

RabbitMQ é um message broker. O papel dele é receber uma mensagem, armazená-la de forma segura, para que imediatamente ou tempos depois, um consumidor receba-a para processá-la.

O RabbitMQ faz o controle de qual mensagem está indo para qual consumidor, de forma a não produzir processamento desnecessário no consumo de uma mesma fila. Ou seja, cada consumidor recebe uma mensagem diferente, sem risco de colisão.

Kubernetes

Kubernetes é um orquestrador de containers, e seu papel é lidar com toda a complexidade de subida de diversos containers em diversos hosts e entregar para quem gerencia, uma gestão unificada desse cluster. Entre seus recursos avançados está a capacidade de autoscalling com base em métricas padrão e métricas customizadas, entre outros.

O que eles fazem da mesma forma?

Absolutamente nada. Não há similaridade entre os 2. Mas eles colaboram na empreitada de entregar alguns benefícios. Vamos detalhar cada um e ver como cada um deles funciona?

Eu estou listando os 5 principais: disponibilidade, eficiência, resiliência, confiabilidade e escalabilidade. Vou detalhar cada um mostrando o que RabbitMQ faz, o que Kubernetes faz e tentar tirar a ambiguidade.

A grosso modo, a sensação que se tem quando se depara com essa pergunta é que ambos fazem tudo e ao mesmo tempo não fazem nada.

Disponibilidade

estado ou qualidade do que é ou está disponível.

O papel do Kubernetes aqui é provisionar instâncias e garantir que haja o que houver, você terá instâncias da sua aplicação online para responder pela requisição. Mas é aí que acaba o papel dele. Se alguma coisa ocorrer que faça sua aplicação estar online, mas não ser capaz de processar um request, por alguma incompatibilidade com alguma outra camada, serviço dependente ou até mesmo versão. O usuário ou serviço que fez a requisição receberá um erro.

Aqui o RabbitMQ consegue colaborar com o Kubernetes. Em vez de você devolver um erro para seu cliente, você recebe a requisição, envia para o RabbitMQ. Enquanto sua aplicação estourar exceptions, a mensagem continua no RabbitMQ. Até que a situação se normalize.

Eficiência

Capacidade de realizar tarefas ou trabalhos de modo eficaz e com o mínimo de desperdício; produtividade.

Com Kubernetes nós temos a habilidade de subir instâncias da nossa aplicação dinamicamente de acordo com a demanda. Precisamos adicionar alguns componentes para avaliar as métricas e no final das contas configuramos o HPA (Horizontal Pod Autoscaler) para que com base no consumo da nossa API, comecemos a subir novas instâncias da nossa aplicação.

Até aqui tudo bem?

Mas aí que mora a pegadinha do consumo excessivo de infra. A natureza síncrona desse processamento faz com que, não seja só uma questão de subir mais instâncias da minha API.

Em um desenho síncrono, se eu tenho 10 instâncias da minha API, recebendo pressão (alta demanda por processamento), também vamos pressionar tudo que essa API depende: Banco, outras API’s, cache, banco das outras API’s etc.

Tudo depende de quais recursos são consumidos nos fluxos de negócio que estão recebendo essa pressão.

Na prática, essas dependências não fogem da necessidade de escalar também, embora tenha potencial para que seja em uma proporção menor. Ou seja, se eu tenho demanda para 10 instâncias da minha API, eu preciso ter um banco que atenda essas 10 instâncias, api’s secundárias etc.

Pelo fato de você ter de processar toda a tarefa durante o processamento do request, você não só tem um request mais lento, porque ele precisa realizar todas as operações, mas também precisa de mais instâncias, por conta do tempo de processamento de cada request. Então precisamos alocar tantas instâncias quando forem necessárias para atender o workload.

Ok, parece óbvio. Mas as implicações disso são enormes.

Se temos 10 instâncias da nossa API em um cenário em que normalmente teríamos 2, ou 3, mas agora precisamos de 10 instâncias por conta de por um pico de demanda, então vamos propagando a pressão produzindo um efeito dominó.

Pesquisa do google relacionada tem alguns posts interessantes

O resto da nossa infraestrutura vai, em algum nível, sofrendo com essa carga de trabalho, e precisa escalar de acordo com o workload. Toda a cadeia de trabalho da sua API precisa em algum nível ter também elasticidade para que você consiga de fato usar as novas instâncias da sua API que subiram.

Subir novas instâncias da nossa API é tarefa fácil para o Kubernetes. Mas e o resto da infra?

Vamos supor que nossa demanda normal seja de 3 instâncias da tal API, o máximo que o resto da nossa infra acomoda sem precisar escalar banco, outras API’s, etc são 6 instâncias, mas nossa demanda (ou carga de trabalho) é compatível com 10 instâncias.

Como RabbitMQ soluciona esse problema?

Agora com RabbitMQ e Kubernetes você sobe um worker que processe o mesmo que sua API processava. Só que em vez de receber a mensagem via HTTP, o worker vai consumir o RabbitMQ usando AMQP. Na prática, movemos código de um lugar para o outro.

Para esse Worker, no Kubernetes, teríamos um autoscalling configurado para nosso worker de 1 a 6.

Já que 6 é o nosso limite. A partir daí, precisaríamos escalar o resto da infra.

Esse movimento imediatamente faz com que nossa API responda muito mais rápido. O único trabalho da API é fazer uma validação bem simples e enviar a mensagem para o RabbitMQ. Estou falando de talvez até 50 vezes, 100 vezes, 300 vezes mais rápido. Depende de quão lento era o processamento.

Mas não tem mágica, se o código antigo era lento, o código movido para o worker continua lento.

Se levava 1 segundo para processar na API, continuará levando o mesmo 1 segundo no worker. Nós só movemos o código de lugar.

Essa mudança faz com que a demanda por 10 instâncias de API’s, vire algo em torno de 1, 2, talvez 3 instâncias, só pela redução de escopo de processamento da API. O ganho de velocidade é proporcional à redução da quantidade de instâncias.

Outra característica é que se 10 instâncias da API representavam 200 requests por segundo, com essa mudança, 10 instâncias representariam talvez 20k requests por segundo. Estou jogando números, mas o importante é a ordem de grandeza.

Parece perfeito, mas não é. Ainda temos nosso worker, que só movemos o código que ele “é lento”.

O RabbitMQ é o mediador e a única comunicação entre sua API e quem processa as mensagens, nosso worker. Na medida que a API publica mensagens no RabbitMQ, elas vão sendo enfileiradas. Se a fila estiver beirando zero, a mensagem será processada imediatamente pelo worker.

Demonstrei em uma demo operações com menos de 2 milissegundos fazendo:

  • Publisher
    • Insert no SQL Server antes da publicação da mensagem no RabbitMQ
    • Envio para o RabbitMQ
    • Confirmação de envio
  • Consumer
    • Consumo da mensagem do RabbitMQ
    • Update na mesma mensagem no SQL Server
    • Confirmação de processamento

Enquanto a API está recebendo uma pressão maior do a capacidade dos workers de processar mensagens, então o RabbitMQ vai acumular mensagens de forma resiliente em suas filas.

Quando essa proporção for invertida, ou seja, a pressão é reduzida, e a capacidade de processamento for maior que o volume de publicação de mensagens. Então nosso worker vai consumir todo backlog até que chegue a zero.

Perto de zero é quando estamos processando mensagem ao vivo: Mensagem chega, é enviada para o RabbitMQ e é consumida quase que instantaneamente. Quando não estamos com essa sobra, tiramos proveito da natureza assíncrona para consumir somente o quanto aguentamos. Diluindo no tempo toda a pressão sob nossa infra.

Para o Kubernetes, a demanda de escala horizontal fica menor, produzindo menos efeitos colaterais. Saber o limite do worker ajuda para determinar o máximo de instâncias. Na prática conseguimos lidar com cenários bem complexos e de bastante carga com esse desenho. Também conseguimos ter um consumo de infra menos agressivo, aproveitando melhor a ociosidade nos tempos de baixa pressão.

Resiliência

Capacidade natural para se recuperar de uma situação adversa, problemática; superação.

Aqui o Kubernetes te ajuda gerenciando as instâncias dos seus containers, matando instâncias ruins, subindo instâncias de reposição. Mas, se uma mensagem chega na sua API e ela tem de processar, e ela por algum motivo não consegue. O usuário toma uma exception. E isso não necessariamente diz respeito à uma queda da sua aplicação pode ser um serviço secundário importante que está falhando.

Nesse caso, o RabbitMQ te ajuda garantindo que mesmo que haja falha, a mensagem está sendo mantida no RabbitMQ. Somente quando o processamento for reestabelecido e puder ocorrer com sucesso, a mensagem será deletada dele. Isso evita que pedidos sejam perdidos, que problemas ocorram.

Outro ponto é que na subida de novas versões de uma API ou serviço, que produza alterações de banco. Por alguns momentos, o banco estará incompatível com instâncias da versão antiga da aplicação que estão no ar recebendo requisições. Se o recurso for compartilhado, entre a versão antiga e a nova, você só escapa disso aumentando o grau de engenharia de software para garantir que as mudanças em banco sejam permissivas. Ou seja, não produzam nenhum efeito na versão antiga que está em pleno phaseout com a versão nova. Transformando seu deployment em um deployment de 2 fases.

  1. Transição – Mudanças acontecem de forma permissiva.
  2. Finalização – Conclusão das mudanças que atendem somente a versão mais recente, ignorando versões anteriores.

Com RabbitMQ enquanto houvesse essa falha no processamento, as mensagens não sofreriam confirmação de processamento, por óbvio, estar falhando. E é aqui que novamente a mágica acontece. Quando o rollout for completo, o banco e aplicação serão compatíveis e então o processamento acontecerá.

Confiabilidade

É a extensão em que medidas repetidas de um fenômeno relativamente estável situam-se próximas umas das outras; é o grau de confiança de uma proposta; capacidade de um instrumento não variar em seus resultados, sendo utilizado por diferentes operadores ou.

Sobre confiabilidade, o Kubernetes não tem muito a ajudar, mas o RabbitMQ te dá tempo, e desde que a mensagem enviada para a fila seja coerente, você tem tempo para lidar com ela e tratá-la.

Isso é possível na medida que você pode produzir políticas de estratégias de duplicação de mensagem com janela de retenção. Vamos supor que você crie uma rota, para que toda mensagem que chegue no message broker vá para uma Streams (feature nova no RabbitMQ). Essa stream tem política de retenção de 1 mês.

Com uma estratégia simples como essa você é capaz de reprocessar a mensagem em 1 mês.

Agora vamos supor que você não esteja com a versão 3.9.x que traz Streams.

Sempre que a mensagem não for processada, se você adotar um consumo resiliente, terá a segurança de que a mensagem voltará a estar disponível na fila para outro consumidor, mesmo que a instancia em execução agora exploda.

Escalabilidade

Qualidade do sistema que consegue suportar um aumento relativamente elevado de carga sem que isso afete negativamente o seu desempenho.

Esse é um espaço super divertido. Enquanto o Kubernetes sobe mais instâncias da sua aplicação, podendo inclusive usar a quantidade de mensagens em fila como métrica para a decisão, o RabbitMQ lida com a distribuição das mensagens entre os diversos consumidores.

Um ponto importante é que, como vimos no tópico Eficiência, os consumidores de filas escalam somente o quanto puderem, não necessariamente na mesma proporção ou com o mesmo limite da escala de sua API. Dessa forma a demanda por criação de novos containers e nós do seu cluster kubernetes é muito menor.

Esse desenho evita que a escala seja um ofensor para os demais componentes da sua arquitetura. Então você escala uma parte que vai “agredir” apenas o RabbitMQ, enquanto todo o resto permanece abaixo ou no máximo no limite do resto da infra. Dessa forma você garante o atendimento do workload e também garante que sua infra não precisará crescer tanto quanto poderia sem essa estratégia.

Rivais ou Aliados?

A intenção desse post foi demonstrar como o RabbitMQ não é capaz de subir instâncias da sua aplicação, da mesma forma como o Kubernetes não é capaz de mudar a forma como você processa mensagens.

Os impactos do efeito dominó na escala de aplicações é algo a ser considerado para evitar aumento excessivo de gastos com infra.

Se cada um fizer o seu papel, você consegue atender muito mais, com muito menos.

Mas se não tomar os devidos cuidados com a forma e com o desenho da solução, empregando corretamente cada componente, pagará um preço. E pode ser bem caro para o seu projeto, para sua empresa e até para você.

Mas não há motivo para medo, tudo é aprendizado.

Luiz Carlos Faria

Mensagem do Autor

Espero que goste desse post. Não deixe de comentar e falar o que achou. 

Se acha que esse post pode ajudar alguém que você conheça, compartilhe!

 

Categorias

Assine

0 comentários

Enviar um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

Lives

Fique de olho nas lives

Fique de olho nas lives no meu canal do Youtube, no Canal .NET e nos Grupos do Facebook e Instagram.

Aceleradores

Existem diversas formas de viabilizar o suporte ao teu projeto. Seja com os treinamentos, consultoria, mentorias em grupo.