Photo by NeONBRAND on Unsplash

Jogando xadrez com processamento distribuido

O seu número de requisições alcançou a estratosfera, e o seu pequeno monolito não da mais conta de processar todas as requisições em um tempo bom o suficiente para não frustrar o usuário (algo em torno de 2 segundos), o quê fazer?

Se formos consultar por opções, vamos ter uma enchurrada de conteúdo sobre RabbiqMQ, Kafka e Microservices. Mas calma, a sua aplicação já funciona bem, só precisa resolver um ponto dela, algum método que é responsável pelas suas dores de cabeça, só isso.

Como fugir das balas de canhão e conseguir escalar um pouco a sua aplicação?
Sem configurações desnecessárias, servidores exclusivos, ou 500 horas de curso online, apenas sockets!

Podemos utilizar a descrição de sockets da wikipédia para começar a entender do que se trata:

A network socket is an internal endpoint for sending or receiving data within a node on a computer network.

Ou seja, podemos entender sockets como um componente capaz de estabelecer canais de comunicação na rede para troca de dados.
Dessa forma, podemos delegar a um componente fora da aplicação a responsabilidade de processar algum dado, este processamento pode ser tanto CPU bound quanto I/O bound, não importa, o mérito está em conseguir justamente criar esse tunel de comunicação entre dois hosts, e assim distribuir o processamento da aplicação.

Nós já fazemos algo semelhante quando decidimos usar o RabbitMQ, ou seja, estabelecemos um canal de comunicação com o broker, que é responsável por gerenciar o channel e connection criadas para que clientes possam enviar mensagens para serem enfileiradas em alguma fila.

No caso do RabbitMQ, uma série de objetos é criada e mantida pelo broker, que tem o papel de gerenciar diversas entidades, entre elas:
- Exchanges
- Bindings
- Connections
- Channels
- Queues

Não é uma escolha arquitetural do time do RabbitMQ criar estes objetos, eles fazem parte do Advanced Message Queuing Protocol (AMQP), que se trata de um padrão para troca de mensagens. Não faz parte do escopo deste artigo se debruçar mais sobre o RabbitMQ e AMQP, mas no futuro vamos analisar melhor estes dois caras 😉

Configurar o RabbiqMQ pode então significar ter uma máquina a parte para ele, seja container ou máquina física. Precisamos configurar o servidor, criar nossas exchanges, bindings e queues. Obviamente tudo isso funciona muito bem, mas nós não queremos isso, nós temos apenas 1 funcionalidade nos dando problemas, nossa aplicação já provou o seu valor, mas não foi possível, por quaisquer motivos, prever a evolução dessa funcionalidade.

ZeroMQ

Podemos utilizar sockets como meio de comunicação entre diferentes hosts, e assim enviar nossas requisições de dentro da nossa aplicação para algum outro host, que será responsável por processar a requisição, e para essa missão lhes apresento o ZeroMQ!

ZeroMQ

Uma das descrições mais interessantes que encontrei sobre o ZeroMQ está no próprio site do projeto, que basicamente diz:

It's sockets on steroids

Ou seja, é uma biblioteca que provê comunicação entre diversos hosts de forma simples, leve e rápida. Trata-se de apenas um pacote a ser adicionado a sua aplicação, mas que vai fazer você conseguir enviar e receber mensagens, filtrar mensanges, disparar eventos, criar roteamentos entre multiplos pontos e etc.

Minha definição pessoal é que o ZeroMQ basicamente:

Provê infraestrutura como código, permitindo distribuir mensagens de forma inteligente em uma rede multi conectada.

Podemos utilizar o ZeroMQ como meio de enviar uma requisição de um host para o outro.
Para exemplificar isso vamos utilizar o NetMQ, que é o cliente .NET. Os exemplos exibidos abaixo são em c#, mas os exemplos podem fácilmente ser convertidos para java, ou outras linguagens. Atualmente o ZeroMQ já foi portado para 28 linguagens, então é bem capaz que a sua linguagem de preferência deve estar ai:)

Vamos começar pelo código do servidor, nossa intenção aqui é enviar uma mensagem para um servidor, que irá fazer algum processamento enquanto o cliente aguarda pela resposta.

Código do server

    static void Main(string[] args)
    {
        using (var replySocket = new ResponseSocket("@tcp://*:5555"))
        {
            while (true)
            {
                var message = replySocket.ReceiveFrameString();
                replySocket.SendFrame($"Olá, {message}");
            }
        }
    }

Iniciamos criando um socket do tipo ResponseSocket e fazemos um bind no localhost, fazendo com que o servidor comece a ouvir por conexões na porta 5555.
Fazer um "bind" significa que o socket irá permitir que outros peers se conectem a ele. Na sequência, o servidor aguarda a chegada de alguma requisição

var message = replySocket.ReceiveFrameString();

Assim que o servidor recebe uma mensagem, ele envia um resposte com uma mensagem

replySocket.SendFrame($"Olá, {message}");

E para o nosso servidor, isso é tudo 😉
Agora vamos criar nosso cliente, o código é levemente parecido.

static void Main(string[] args)
{
    using (var reqSocket = new RequestSocket(">tcp://localhost:5555"))
    {
        Console.WriteLine("Please, enter your name");

        while (true)
        {
            var name = Console.ReadLine();

            if (!string.IsNullOrEmpty(name))
            {
                reqSocket.SendFrame(name);
                var response = reqSocket.ReceiveFrameString();
                Console.WriteLine($"Response from server: {response}");
            }
            else
            {
                Console.WriteLine("Please, enter your name");
            }
        }
    }
}

Iniciamos criando um socket do tipo RequestSocket que realiza um connect no endereço tcp localhost:5555, fazer um "connect" significa que haverá pelo menos um peer aguardando pela conexão. A diferença básica entre um bind e connect é que fazemos o bind na parte que não varia com frequência, enquanto que o connect fica por conta do lado que varia com frequencia. Por exemplo, em uma conexão cliente-servidor sabemos que sempre haverá pelo menos um servidor, mas não controlamos o número de clientes. Em seguida a aplicação cliente armazena o nome inserido pelo usuário e envia para o servidor:

reqSocket.SendFrame(name);

Após enviar, o client aguarda pela resposta do servidor:

var response = reqSocket.ReceiveFrameString();

E é isso pessoal! temos agora um client que faz requisições a um server, que por sua vez responde assim que a mensagem for processada.
Com poucas linhas de código conseguimos criar um par de sockets (Request-Reply) em que é possível enviar requisições de um lado para o outro.
Entretando, o par de sockets request-reply é síncrono, o que significa que um request sempre irá esperar por um reply, da mesma forma que o reply não consegue processar mais nenhum cliente enquanto não devolver a resposta da requisição anterior.

Existem cenários em que um par request-reply resolve o nosso problema, mas podemos evoluir um pouco mais o nosso exemplo.
Digamos que temos uma aplicação web que envia mensagens para serem processadas pelo servidor, e que começa a ouvir pela resposta em alguma fila de resposta.

Vamos começar pelo nosso client, ele deve publicar alguma mensagem para ser processada pelo servidor, e aguarda a resposta em alguma fila. No nosso exemplo, estabelecemos que a resposta deverá ser publicada em uma fica específica, utilizando um GUID como identificador.

Este "identificador" também é conhecido como topic ou filter.

Código do Client

static void Main(string[] args)
{
    using (var pubSocket = new PublisherSocket("@tcp://*:10000"))
    using(var responseSub = new SubscriberSocket(">tcp://localhost:11000"))
    {
        while (true)
        {
            Console.WriteLine("Please, enter your name");
            var name = Console.ReadLine();

            if (!string.IsNullOrEmpty(name))
            {
                var message = new Message(name, Guid.NewGuid().ToString());
                pubSocket.SendMoreFrame("getHello").SendFrame(Newtonsoft.Json.JsonConvert.SerializeObject(message));
                responseSub.Subscribe(message.Id);
                var topic = responseSub.ReceiveFrameString();
                var messageFromTopic = responseSub.ReceiveFrameString();

                Console.WriteLine($"Response: {messageFromTopic}");
            }

        }
    }
}

Começamos criando dois sockets, um do tipo PublisherSocket que faz um bind no na porta 10000, iremos utilizar esse socket para enviar request para serem processados.
Em seguida criamos um socket do tipo SubscriberSocket, e nos conectamos ao endereço tcp://localhost:11000, vamos utilizar este socket para receber o resultado do processamento, que esperamos ser publicado em uma fila particular daquela mensagem.

Para publicamos uma mensagem fazemos:

pubSocket.SendMoreFrame("getHello").SendFrame(Newtonsoft.Json.JsonConvert.SerializeObject(message));

No código acima utilizamos o pubSocket para publicar a mensagem, primeiro identificamos a fila a qual desejamos publicar a mensagem (getHello no nosso caso) e com o SendFrame nós efetivamentes enviamos a mensagem.

Em seguida o cliente se inscreve na fila com o ID gerado, e aguarda por alguma mensagem.
O nosso servidor novamente tem um código semelhante, ele vai criar dois sockets, um deles vai ter a função de se inscrever no topic "getHello" e aguardar por requisições, o outro socket do tipo SubscriberSocket vai ter a função de publicar as mensagens que foram processadas em uma fila exclusiva.

Código do Server

static void Main(string[] args)
{
    using (var pubSocket = new PublisherSocket("@tcp://*:11000"))
    using(var responseSub = new SubscriberSocket(">tcp://localhost:10000"))
    {
        responseSub.Subscribe("getHello");

        while (true)
        {
            var topic = responseSub.ReceiveFrameString();
            Console.WriteLine($"Topic: {topic}");

            var messageJson = responseSub.ReceiveFrameString();
            var message = Newtonsoft.Json.JsonConvert.DeserializeObject<Message>(messageJson);

            pubSocket.SendMoreFrame(message.Id).SendFrame($"Hello, {message.Name}");
        }
    }
}

Nosso servidor cria dois sockets, o PublisherSocket que faz um bind na porta 11000 e aguarda por conexões, e um SubscriberSocket faz um connect no endereço tcp://localhost:10000. O nosso server fica escutando as mensagens na fila "getHello", e para cada request vai responder em uma fila específica utilizando o ID da mensagem.

It's DONE! Temos agora o processamento de requisições em filas estáticas e dinâmicas, sem servidores gerenciando conexões e diversos outros objetos, com configurações e páginas admin.

Algumas coisas a se considerar do ZeroMQ:

  • ZeroMQ garante a entrega de todas as partes de uma mensagem, ou nenhuma delas;
  • Uma mensagem deve caber na memória, ou seja, para fazer envio de mensagens com tamanhos enormes, devemos dividi-la em partes menores;

O que fizemos basicamente foi "eliminar" o broker da jogada, utilizando para isso uma ferramenta que nos garante a entrega da mensagem e criando uma conexão direta entre o cliente e o servidor responsável por processar a mensagem.

Implementamos dois padrões de troca de mensagens suportadas pelo ZeroMQ, que são:

  • Request-Response
  • Pub/Sub

Existem ainda diversos outros padrões, como Push-Pull, Router-Dealer e etc. Mas calma, iremos abordar todos eles no futuro!
Ainda existe muito sobre o que falar, como padrões avançados de distribuição de mensagens, razão de performance entre aplicações com broker e brokerless, garantia de entrega e garantia de recuperação de falhas e etc. Iremos abordar tudo isso num futuro próximo.

Você pode fazer um clone do projeto criado no Github do road, aqui.

Chess: Photo by NeONBRAND on Unsplash

Tags: | | | | | | | | | | | | | | |

Sobre o Autor

Douglas Ramalho
Douglas Ramalho

Pesquisador apaixonado por filmes, música e bits, muitos bits. Sou um profissional viciado em tecnologia, com 11 anos de experiência em TI e um caminho repleto de suor, sangue e bugs. Gosto não apenas de estudar, mas também de promover o conhecimento, dessa forma meu objetivo é compartilhar toda a minha experiência para que possamos evoluir juntos, e também ajudar outras pessoas neste processo, para que em um grande grupo de apaixonados possamos elevar o nível de excelência a um patamar ainda maior.

0 Comentários

Deixe um comentário

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

Solicitar exportação de dados

Utilize este formulário para solicitar uma cópia dos seus dados neste site.

Solicitar remoção de dados

Utilize este formulário para solicitar a remoção dos seus dados neste site.