História da Arquitetura do LinkApi: Escalando para 1 milhão de integrações por minuto

LinkApi é uma plataforma no modelo SaaS de integração entre sistemas, isso significa que os nossos clientes podem criar conexões entre aplicações independentes, sem precisar desenvolver uma linha de código sequer ou manter uma infraestrutura própria para isto.

Existem desafios de escalabilidade, performance e principalmente de modelo de negócio que precisávamos solucionar antes de colocar o produto no mercado.

O LinkApi é constituído basicamente de um motor de integrações, conectores de entrada e conectores de saída, que internamente chamamos de triggers e actions. Ou seja, se quisermos fazer uma integração entre o Facebook e um banco de dados SQL Server, por exemplo, temos que ter um conector para buscar os dados do Facebook e outro conector para inserir as informações numa tabela do SQL Server.

1 milhão de integrações por minuto

Assim conseguimos criar conectores independentes do cliente para realizar integrações diferentes, reutilizando os conectores na maioria das integrações. Este é um aspecto crucial para escalar a operação do produto, pois conseguimos subir integrações complexas em no máximo quatro semanas usando este modelo.

Além do mais, precisamos garantir performance e escalabilidade no motor de integrações, pois vários clientes funcionam dentro da mesma infraestrutura. Para chegar no desempenho que temos hoje passamos por algumas transformações na arquitetura da aplicação.

Node.js com Straw

Quando construimos o LinkApi, a arquitetura do software era baseada em Node.js usando um framework chamado Straw. Decidimos por usar o Node por dois motivos:

  • Performance em aplicações que usa muito I/O. Graças a característica fundamental do Node de utilizar operações de I/O não bloqueantes;
  • Facilidade de criar novos conectores usando uma linguagem simples como o JavaScript.

Usamos o Straw para facilitar a distribuição dos processos, pois ele é um framework de processamento em tempo real que organiza os nós em uma topologia definida previamente. O Straw tem uma performance muito boa e é fácil de utilizar.

Conseguimos escalar bem até certo ponto utilizando esta arquitetura. No entanto, ao chegarmos em 20 mil transações simultâneas, começamos a experimentar um problema intermitente de queda de alguns nós de processamento.

O Straw usa o Redis para coordenar as tarefas entre os nós da topologia configurada, por algum motivo o Redis parava de responder. Entramos em contato com o criador da solução para tentar achar uma saída, mas não encontramos solução para este problema.

Tivemos que partir para uma outra solução. Foi aí que começamos a pesquisar sobre outras arquiteturas, padrões, frameworks e linguagens diferentes.

Python com Celery

Ao pesquisar mais sobre possíveis soluções, chegamos na possibilidade de usarmos Python com Celery, que é um framework bastante utilizado para realizar processamento assíncrono de forma distribuída. É bem mais maduro que o Straw mas tem uma arquitetura completamente diferente, além de ser obrigatório o uso de Python.

Fizemos inicialmente uma PoC (prova de conceito) para entender se esta solução atenderia nossas demandas, e o resultado foi muito bom! Conseguimos não só melhorar a performance do sistema, como também acabamos de vez com o problema de intermitência dos nós de processamento que caiam sem explicação.

Estávamos bastante animados com a solução se não fosse por um único problema: Se mudássemos para Python como solução principal do motor das integrações, teríamos que reescrever toda a aplicação, reestabilizar, testar novamente. Até aí tudo bem, era um preço que estávamos dispostos a pagar. No entanto, seria necessário também reescrever os conectores em Python, a esta altura da operação do LinkApi. já tínhamos dezenas de conectores criados, testados, homologados e vários clientes usando-os para integrar seus sistemas.

Não tínhamos tempo nem dinheiro para investir numa nova solução, seria como começar de novo o produto e teríamos um risco enorme de quebrar as integrações que já estavam funcionando. Isso tudo sem falar de que o time não tinha a expertise técnica necessária para suportar uma solução tão complexa usando uma linguagem nova, um framework novo, etc.

Então abandonamos a ideia e partimos para outra estratégia: adaptar o que já tínhamos trocando o Straw.

Node.js com Kue

Decidimos, então, por estratégia de negócio continuar com o Node.js. No entanto, nos livrando da parte que não funcionava: o Straw. Pesquisamos bastante para fazer essa mudança, pois não queríamos ter um retrabalho muito grande, nem errar como fizemos na escolha anterior.

Acabamos decidindo por testar o Kue, conforme sua própria documentação diz: “Kue is a priority job queue backed by redis.

Para adaptar a aplicação para o Kue tivemos que fazer algumas mudanças estruturais na aplicação como estruturar as etapas do processo de integração em módulos com divisões muito bem definidas, pois cada uma das etapas passou a ser um job dentro do Kue.

Assim passamos a ter a seguinte estrutura de jobs dentro do queue do Kue (fica estranho falar isso mesmo haha):

  • GetIntegrations: Obtém todas as integrações que devem ser executadas naquele momento;
  • GetTrigger: Baseado nas integrações a serem executadas busca dinamicamente qual o conector (trigger) deve ser executado para trazer os dados da integração;
  • TriggerExec: Executa de fato o primeiro passo da integração, obtendo os dados da origem usando o conector selecionado na fase anterior;
  • CreateTask: Uma task dentro do Linkapi representa uma integração sendo executada, ou seja, agrega as informações do conector de origem, conector de destino, dados integrados e o status da integração executada;
  • GetAction: Busca dinamicamente qual o conector (action) deve ser executado para inserir os dados da integração no destino configurado;
  • ActionExec: Executa de fato a integração, enviando os dados para o destino usando o conector selecionado na fase anterior.

Implementamos e fizemos um teste de carga simples validando a ideia em ambiente de homologação, a solução se comportou como esperado.

Hora de colocar em algum cliente real!

Elegemos um cliente piloto para fazer as integrações com esta nova arquitetura. Em uma semana da nova arquitetura no ar sentimos um problema básico, o Redis começou a virar gargalo!

Subimos o Redis no Azure Redis Cache para entender se era um problema de escalabilidade, no entanto o problema continuou. E notamos uma coisa interessante, as conexões não estavam sendo fechadas! O que ocasionava em um limite que era alcançado mais rapidamente a medida do volume das integrações.

Muito provavelmente, isso era um bug do Kue ou uma limitação técnica dado o volume de tasks que tínhamos simultaneamente. O fato é que não encontramos saída razoável para o problema e tivemos que pivotar mais uma vez!

Node.js + Docker + RabbitMQ

Decidimos abandonar soluções baseadas em Redis, visto que em duas situações diferentes tivemos problemas parecidos de escala. Não, o Redis não é um problema. Mas o volume de transações simultâneas pedia algo preparado para isto: uma solução de fila, por exemplo.

Estudamos mais algumas soluções possíveis para resolver o problema, chegamos a testar algumas possibilidades e acabamos por eleger o RabbitMQ como message queue.

O RabbitMQ é um excelente message broker, muito utilizado no mundo inteiro e tem mais de 10 anos de mercado. Isso contou bastante para nosso critério de decisão, uma ferramenta robusta e com performance excelente!

Na época ficamos muito em dúvida entre o Kafka e o RabbitMQ — no artigo inclusive a recomendação é o Kafka, por questões de disponibilidade e performance. Analisando nosso cenário notamos que as mensagens tem uma variação grande de tamanho algumas maiores que 1 MB, e o Kafka trabalha bem com mensagens até 1 KB. Atualmente as mensagens trocadas entre os nós do LinkApi, tem em média 2 KB.

Assim o RabbitMQ passou a intermediar todas as trocas de mensagens entre os nós da aplicação, refatoramos mais uma vez a arquitetura com o foco em simplificar os processos e deixar a aplicação mais eficiente.

Nova arquitetura do LinkApi usando RabbitMQ e Docker:
  • Agenda: Consiste num processo que prepara a aplicação para executar novas integrações a cada 30 segundos;
  • Integration: Busca quais integrações deve ser executadas no momento;
  • Trigger: Executa as triggers para cada integração, obtendo os dados do destino para iniciar o processo;
  • Action: Executa as actions para cada integração, enviando os dados para o destino e finalizado o processo de integração;
  • RabbitMQ: Centraliza a troca de mensagens entre os containers, com 3 filas distintas para cada função: integração, trigger e action.

Neste novo modelo, o motor de integrações passou a ser separado em processos distintos, cada um com seu próprio contêiner.

Esta arquitetura nos deu mais escalabilidade, pois com Docker Swarm temos mais facilidade em criar novos nós, aumentando ou diminuindo a capacidade de processamento.

De modo tão simples quanto, isto:

docker service scale linkapi_trigger=3 linkapi_action=10

Conseguimos atender a demanda atual do LinkApi com folga depois destas mudanças e está longe de chegar ao limite do RabbitMQ.

Fiz uma apresentação sobre a história da arquitetura do LinkApi no evento DevXperience 2017, publiquei os slides no SlideShare:

Entre as evoluções previstas no roadmap do produto, estamos pensando em algumas melhorias neste modelo arquitetural para suportar algumas demandas de clientes, como fazer integrações acionando WebHooks e integrações em novos formatos.

 

Inscreva-se na nossa Newsletter

Fique por dentro das novidades e melhores práticas sobre Integrações do mercado!

Deixe uma resposta

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

Você também vai se interessar