Objetivo
Durante minhas pesquisas, tomei conhecimento do Rabbit MQ (http://www.rabbitmq.com/) que trata-se de uma plataforma open-source para mensageria, disponível em diversos sistemas operacionais (Windows, Linux/Unix, Mac OS X e Amazon EC2).
Resolvi conhecê-lo e aproveitei para também incluí-lo junto aos demais benchmarks.
Instalação
Utilizei o Rabbit MQ em plataforma Windows. O processo de instalação dele não tem grande segredo. A única curiosidade é que por ele ter sido escrito em Erlang (http://erlang.org/), o runtime do Erlang precisa ser instalado.
Outro componente interessante de ser instalado (você vai precisar dele para rodar os exemplos) é o cliente .NET para o RabbitMQ. Você o encontra na página de downloads do Rabbit MQ. A documentação da API pode ser encontrada na página de documentação do Rabbit MQ. As vezes eu me surpreendo com a simplicidade e a organização de projetos open source.
Operação
O Rabbit MQ é relativamente simples. Não explorei todas as suas features, apenas o necessário para executar este teste/exemplo.
Existe um “Command Prompt” do Rabbit MQ (instalado junto com o servidor).
Alguns comandos úteis:
rabbitmqctl status
Mostra uma série de indicadores sobre o status do servidor.
rabbitmqctl environment
Mostra uma série de informações sobre o ambiente do servidor, entre elas uma das mais úteis que é a porta tcp que está sendo ouvida (default 5672).
rabbitmqctl list_queues
Lista as filas criadas.
rabbitmq-plugins enable rabbitmq_management
Habilita o plugin de gerenciamento do RabbitMQ. Uma vez que a instalação do plugin é realizada, basta reiniciar o servidor e acessá-lo através da URL http://localhost:15672/. Não esqueça de trocar localhost pelo IP ou hostname do seu servidor. O usuário default é guest/guest.
Cliente Rabbit MQ
A única coisa que fiz aqui foi pegar minha implementação de Websphere MQ e trocar as chamadas para o Rabbit MQ. Sim, eu poderia ter criado uma abstração mas como meu objetivo aqui é apenas entender como funciona e realizar alguns benchmarks, resolvi ser objetivo.
A implementação do cliente é muito simples. O método principal que executa a Task MSBuild segue:
public override bool Execute() { Stopwatch watch = new Stopwatch(); watch.Start(); Log.LogMessage("Starting Rabbit MQ transfer with " + TotalBatches.ToString() + " batchs with " + BatchSize.ToString() + " items each"); ConnectionFactory factory = new ConnectionFactory(); factory.HostName = HostName; IConnection conn = factory.CreateConnection(); IModel channel = conn.CreateModel(); channel.QueueDeclare(OutputQueueName, false, false, false, null); IBasicProperties props = channel.CreateBasicProperties(); props.ContentType = "text/xml"; props.DeliveryMode = 2; //persistent inputChannel = conn.CreateModel(); inputChannel.QueueDeclare(InputQueueName, false, false, false, null); messageCount = TotalBatches; System.Threading.Tasks.Task.Factory.StartNew(ProcessInputQueue); int count = 1; for (int i = 0; i < TotalBatches; i++) { MemoryStream ms = new MemoryStream(); StreamUtil.GenerateBigRequest(ms, false, count, count + (BatchSize - 1)); channel.BasicPublish("", OutputQueueName, props, ms.GetBuffer()); count += BatchSize; Log.LogMessage("Sent " + count.ToString()); } finishedEvent.WaitOne(); watch.Stop(); Log.LogMessage("Total processing time: " + watch.Elapsed.TotalSeconds.ToString("0.00") + " seconds"); conn.Close(); return true; }
ConnectionFactory é o objeto da API que encapsula uma conexão com uma fila. A criação do channel, QueueDeclare seguem o modelinho da API do Rabbit MQ.
O método que efetivamente faz a publicação na fila é o channel.BasicPublish. Existem algumas regras para publicações com tópicos, publish/subscribe, mas para o nosso exemplo, somente o BasicPublish resolve o problema.
Este método também inicia uma Task (que internamente inicia uma segunda thread) para processar a fila de resposta para o cliente, no método ProcessInputQueue. O código dele segue:
private void ProcessInputQueue() { while (true) { BasicGetResult result = inputChannel.BasicGet(InputQueueName, true); if (result == null) { Thread.Sleep(250); continue; } MemoryStream ms = new MemoryStream(result.Body); messageCount--; Log.LogMessage("Waiting for more " + messageCount.ToString()); StreamUtil.ImportarStream(ConnString, ms); if (messageCount <= 0) break; } finishedEvent.Set(); }
Mesma dinâmica do Websphere MQ. Aqui fico num loop infinito, pegando as mensagens. Sempre que não tem nada a processar (result == null), dou uma pequena dormidinha na thread somente para não matar a CPU da máquina. Pega a resposta e processa. Sem grandes segredos.
Servidor RabbitMQ
O servidor segue a mesma dinâmica:
public override bool Execute() { Log.LogMessage("Starting RabbitMQ Server"); ConnectionFactory factory = new ConnectionFactory(); factory.HostName = "localhost"; IConnection conn = factory.CreateConnection(); IModel inputChannel = conn.CreateModel(); inputChannel.QueueDeclare(InputQueueName, false, false, false, null); IConnection conn2 = factory.CreateConnection(); IBasicProperties props = inputChannel.CreateBasicProperties(); props.ContentType = "text/xml"; props.DeliveryMode = 2; //persistent IModel outputChannel = conn2.CreateModel(); outputChannel.QueueDeclare(OutputQueueName, false, false, false, null); int count = 0; while (true) { BasicGetResult result = inputChannel.BasicGet(InputQueueName, true); if (result == null) { Thread.Sleep(250); continue; } MemoryStream ms = new MemoryStream(result.Body); result = null; // let GC work. MemoryStream respStream = new MemoryStream(); StreamUtil.ProcessClientBigRequest(ConnString, ms, respStream, false, null); outputChannel.BasicPublish("", OutputQueueName, props, respStream.GetBuffer()); respStream = null; //let GC work count++; Log.LogMessage("Processed " + count.ToString()); }
Comparativo dos resultados
Como nem tudo são rosas, a máquina que estava usando como servidor resolveu morrer e tive que reinstalar tudo novamente. Por tabela, perdi a confiança nos números que tinha executado anteriormente e acabei reexecutando todos os testes no novo ambiente, com o objetivo de manter a premissa de “mesmo payload, mesmo ambiente” que utilizei desde o início dessa série.
Mesmo assim, continuei executando 10 vezes cada teste com cada um dos volumes (20k, 50k, 500k, 1M) e o número apresentado aqui é a média dos 10, para evitar diferenças que ocorrem numa ou outra execução devido a cache de banco, paginação de memória, garbage collector, etc.
Os números são:
Como os números mostram, o desempenho do RabbitMQ não foi satisfatório. Fiquei bastante frustrado com esse resultado. Apesar de toda a simplicidade da implementação, da instalação, etc., o número ficou praticamente a mesma coisa do que usar Web Services em SOAP.
Além disso, apesar de todo o trabalho para montar um novo ambiente (que mudou de 32 para 64 bits na máquina servidora), houve uma piora significativa dos números do File Transfer. Vou investigar mais a fundo as razões desta piora dado o novo ambiente instalado.
Código fonte
O código fonte das implementações acima foi atualizado no Git Hub: https://github.com/ericlemes/IntegrationTests
Conclusão
Eu vi várias pessoas recomendando o RabbitMQ, o que me motivou a realizar esta pesquisa comparando também com o MSMQ e Websphere MQ. Acho que ainda vale a pena uma investigação mais aprofundada de como otimizar o RabbitMQ antes de simplesmente descartá-lo. Este trabalho é bastante superficial.
Mas acho que a principal conclusão que eu chego é que antes de sair utilizando uma nova tecnologia, vale a pena buscar informações sobre ela, testar, brincar, comparar. Baseado nos números aqui obtidos, numa implementação entre plataformas distintas (java/.NET por exemplo), ficaria com SOAP e numa implementação apenas em plataforma Windows, com o MSMQ.