Vestibular Premiado: como escalar uma API

Participando ativamente da campanha do Vestibular Premiado, compartilhamos os aprendizados do squad Matrícula Direta

Denis Motta Urbanavicius
Tech at Quero
7 min readNov 27, 2020

--

Quando falamos em escalabilidade, a primeira coisa que nos vem à cabeça é o auto-scaling, mas, na verdade, isso é apenas a cereja do bolo. Existem muitos fatores que devemos ficar atentos antes de configurarmos um auto-scaling. Usando como base o Vestibular Premiado, evento da Quero Educação com 57 mil inscritos, como estudo de caso, compartilhamos alguns aprendizados.

Métricas importantes

Antes de colocar a mão na massa, precisamos seguir esses 3 passos:

  1. encontrar e entender quais problemas temos;
  2. medir o tamanho desses problemas;
  3. e por fim criar/usar uma estratégia de correção. Ou seja, neste bloco, vamos falar de como encontrar, entender e medir os problemas.

Uso de CPU da aplicação:

Essa métrica serve para entendermos o quanto nossa aplicação está otimizada em relação aos outros itens. É muitas vezes desejado que o gargalo principal seja o uso de CPU da aplicação por ser mais fácil de controlar o auto-scaling, mas isso não quer dizer que não devemos otimizá-la, muito pelo contrário. Comentarei sobre isso em “Código pouco eficiente”, abaixo.

Uso de Memória da aplicação:

Aqui, olhamos se a aplicação tem problemas de uso de memória conforme o volume de acessos aumenta e, via de regra, devemos sempre ter uma sobra de mais ou menos 20% do uso de pico para evitar travamentos repentinos (out of memory).

Reservar um espaço de SWAP é desejável e pode evitar um travamento, mas deve ser raramente usado.

Uso de CPU do banco de dados:

Aqui entendemos se nossas queries estão otimizadas e/ou faltam índices. É normal o uso de CPU do banco de dados subir com o volume de acessos, mas, se o uso subir de forma exponencial, pode ser um indício de falta de índices/otimizações.

Uso de memória do banco de dados:

A memória do banco de dados é usada para cache de consultas (leituras apenas) e a quantidade desejada depende do uso da aplicação e/ou quantidade de registros. Os bancos de dados normalmente vêm configurados de fábrica para usar entre 80% e 90% da memória total da máquina e isso é perfeitamente normal. Deve-se aumentar apenas se observar ganho de velocidade em consultas.

Tempo de resposta da aplicação:

Essa métrica é muitas vezes ignorada, mas é provavelmente a mais importante. O que medimos aqui é quanto tempo a aplicação demora para responder cada request. Ela é importante, pois, quando temos situações de lock ou concorrência o problema, pode não aparecer em outras métricas, mas será facilmente encontrado e medido pelo tempo de resposta.

Como testar

Agora que sabemos quais métricas devemos ficar de olho, devemos testar nossa aplicação para entender onde está o gargalo. Eu disse no singular pois esse é um processo minucioso no qual removemos um problema por vez e, depois, reiniciamos o ciclo de testes.

Para testar, basta simular uma alta carga de acessos, começando pelas principais rotas do sistema (as que os usuários acessam com mais frequência) e aumentando a cobertura de rotas depois de algumas rodadas. Existem diversas aplicações para criar testes de carga e relatórios, como jMeter e Cypress.

Outro ponto de atenção é criar uma bateria de testes mais realista possível para evitar de testar situações que não refletem o cenário real. Em um dos testes que fizemos no Vestibular Premiado, nós reutilizamos o mesmo usuário em todas as requisições e acabamos criando uma concorrência irreal pelo mesmo registro no banco de dados (ops!). Isso mascarou os resultados dos testes e tivemos que refazê-los para poder procurar os problemas.

Código pouco eficiente

Nesse momento, suponho que você rodou os testes e descobriu que o uso de CPU da aplicação ficou cravado em 100% ou pior: 100% de apenas um dos núcleos.

100% de um núcleo só:

Isso acontece porque sua aplicação está rodando com uma thread só (óbvio, né?) e é mais frequente em linguagens interpretadas, mas também é comum em outras linguagens. Felizmente existem bibliotecas que resolvem isso para você, como a cluster (NodeJs) e gUnicorn (Python).

100% de todos núcleos:

Lembra que eu disse lá no começo que isso era desejável, mas temos de ter cuidado? Aqui vale uma análise quantitativa e de custos. Se precisamos de 2 a 8 núcleos para atender a quantidade de usuários simultâneos que desejamos com tempo de resposta baixo, pode valer mais a pena investir seus esforços de engenharia em outros lugares do que tentar economizar alguns dólares com infraestrutura.

No entanto, se você precisa de muito mais poder de processamento para atender sua demanda (aqui, falo de uma ordem de grandeza ou mais de diferença), é hora de procurar o que está errado com o seu código. Existem ferramentas nativas de profiling que podem te ajudar a encontrar gargalos e momentos que aplicação fica esperando, mas também é importante termos um desenvolvedor capaz de entender os pormenores da linguagem e identificar práticas que causam problemas (como criar uma estrutura massivamente OO em Python, que não é otimizado para isso).

Não existe uma saída fácil nesse momento, você sempre deve tomar o cuidado de avaliar o custo (tempo + quantidade de devs) para resolvê-los e ponderar se vale a pena fazer intervenções grandes.

Banco de dados

Chegamos no ponto mais delicado, principalmente, quando usamos bancos relacionais. Não me entenda errado, bancos relacionais funcionam muito bem e muitas vezes ficam com a culpa por conta de devs preguiçosos e não por suas limitações. Aqui, precisamos falar de métricas novamente, pois a análise é mais complexa.

Uso de CPU alto, pouco uso de disco (iops baixo):

Indício de falta de índices ou de concorrência entre registros! Aqui, devemos olhar o slow query log ou, no nosso caso,que usamos RDS Postgres, a página “Performance Insights” do console da AWS. Essa página usa um código de cores que indica qual tipo de ação está bloqueando o comando, exatamente o que estamos procurando.

Histórico de uso de CPU por tipo de comando

O mais importante é o uso de CPU (barra verde), mas vale a pena estudar as outras métricas para entender situações diferentes. Na imagem abaixo, conseguimos entender quais comandos estão causando problemas.

Tipos de await por query. Medido em quantidade de CPUs — 3.09 = 3 núcleos e 9% do quarto

E o próximo gráfico mostra o que aconteceu quando criamos os índices necessários. A seta vermelha é o momento que criamos os índices — o volume de acessos continuou o mesmo:

Uso de CPU do banco de dados no momento que adicionamos índices

Mesmo depois dessas correções, nosso tempo de resposta continuava alto. O RDS reportava ocupação alta de ClientRead e baixa quantidade de iops. Lembra que eu falei que testamos as rotas com um usuário só? Foi neste momento que descobrimos que criamos concorrência em um único registro.

Tempo de resposta da aplicação durante a crise e, na seta vermelha, após as correções

Uso de CPU baixo, IOPS alto:

Indica que muitos dados estão sendo puxados em cada consulta. Não é necessariamente um problema, mas você deve procurar entender se está puxando campos demais nas suas queries (nunca use select * se não precisa de todos os campos).

Cereja do bolo:

Depois de fazer todas essa otimizações, é hora de escalar verticalmente e/ou horizontalmente. Aqui as possibilidades são imensas (sharding, replicas, máquinas com burst, etc.) e não é o foco desse artigo.

Dependência de terceiros

Aqui, foi o nosso maior aprendizado com o Vestibular Premiado. Temos algumas integrações com parceiros que funcionavam em tempo real e qualquer falha poderia causar impacto na nossa plataforma.

Por uma série de motivos que não valem a pena estender aqui, decidimos usar SQS para organizar e transformar essas integrações em eventos assíncronos. Criamos uma estrutura onde a aplicação guarda os dados necessários no banco de dados e envia o ID para a fila SQS, retornando um feedback de sucesso pro aluno imediatamente.

De forma assíncrona, temos um job que busca esses IDs da fila, carrega os dados e envia para o terceiro. Se der erro ou o terceiro não responder, o ID volta para a fila. Abordagem simples, direta e resiliente.

Foi assim que conseguimos atender mais de 20 mil provas (entre redações e múltipla escolha) e 3800 acessos simultâneos com apenas um mês de esforço de um squad de engenharia e custo mínimo de infraestrutura (uma única t3.medium deu conta da aplicação).

Requests por minuto do último dia de Vestibular Premiado

Leia também:

--

--