Escalando o jogo! a milhares de solicitações simultâneas

Publicados: 2022-03-11

Os desenvolvedores da Web Scala geralmente não consideram as consequências de milhares de usuários acessando nossos aplicativos ao mesmo tempo. Talvez seja porque adoramos prototipar rapidamente; talvez seja porque testar esses cenários é simplesmente difícil .

Independentemente disso, vou argumentar que ignorar a escalabilidade não é tão ruim quanto parece — se você usar o conjunto adequado de ferramentas e seguir boas práticas de desenvolvimento.

Ignorar a escalabilidade não é tão ruim quanto parece – se você usar as ferramentas adequadas.

Lojinha e a Brincadeira! Estrutura

Há algum tempo, iniciei um projeto chamado Lojinha, minha tentativa de construir um site de leilões. (A propósito, este projeto é de código aberto). Minhas motivações foram as seguintes:

  • Eu realmente queria vender algumas coisas antigas que não uso mais.
  • Não gosto de sites de leilões tradicionais, principalmente os que temos aqui no Brasil.
  • Eu queria “brincar” com o Play! Quadro 2 (trocadilho intencional).

Então, obviamente, como mencionado acima, decidi usar o Play! Estrutura. Não tenho uma contagem exata de quanto tempo levou para construir, mas certamente não demorou muito para eu ter meu site funcionando com o sistema simples implantado em http://lojinha.jcranky.com. Na verdade, gastei pelo menos metade do tempo de desenvolvimento no design, que usa o Twitter Bootstrap (lembre-se: não sou designer…).

O parágrafo acima deve deixar pelo menos uma coisa clara: não me preocupei muito com o desempenho, se é que me preocupei, ao criar a Lojinha.

E esse é exatamente o meu ponto: há poder em usar as ferramentas certas – ferramentas que o mantêm no caminho certo, ferramentas que o incentivam a seguir as melhores práticas de desenvolvimento por sua própria construção.

Nesse caso, essas ferramentas são o Play! Framework e a linguagem Scala, com Akka fazendo algumas “aparições de convidados”.

Deixe-me mostrar o que quero dizer.

Imutabilidade e cache

É geralmente aceito que minimizar a mutabilidade é uma boa prática. Resumidamente, a mutabilidade torna mais difícil raciocinar sobre seu código, especialmente quando você tenta introduzir qualquer paralelismo ou simultaneidade.

O jogo! A estrutura Scala faz com que você use imutabilidade uma boa parte do tempo, assim como a própria linguagem Scala. Por exemplo, o resultado gerado por um controlador é imutável. Às vezes você pode considerar essa imutabilidade “incômoda” ou “irritante”, mas essas “boas práticas” são “boas” por um motivo.

Nesse caso, a imutabilidade do controlador foi absolutamente crucial quando finalmente decidi executar alguns testes de desempenho: descobri um gargalo e, para corrigi-lo, simplesmente armazenei em cache essa resposta imutável.

Por cache , quero dizer salvar o objeto de resposta e servir uma instância idêntica, como está, para quaisquer novos clientes. Isso libera o servidor de ter que recalcular o resultado novamente. Não seria possível fornecer a mesma resposta para vários clientes se esse resultado fosse mutável.

A desvantagem: por um breve período (tempo de expiração do cache), os clientes podem receber informações desatualizadas. Isso é um problema apenas em cenários em que você precisa absolutamente que o cliente acesse os dados mais recentes, sem tolerância a atrasos.

Para referência, aqui está o código Scala para carregar a página inicial com uma lista de produtos, sem cache:

 def index = Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) }

Agora, adicionando o cache:

 def index = Cached("index", 5) { Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) } }

Bem simples, não é? Aqui, “índice” é a chave a ser utilizada no sistema de cache e 5 é o tempo de expiração, em segundos.

Após o armazenamento em cache, a taxa de transferência subiu para 800 solicitações por segundo. Isso é uma melhoria de mais de 4x para menos de duas linhas de código.

Para testar o efeito dessa alteração, executei alguns testes JMeter (incluídos no repositório GitHub) localmente. Antes de adicionar o cache, obtive uma taxa de transferência de aproximadamente 180 solicitações por segundo. Após o armazenamento em cache, a taxa de transferência subiu para 800 solicitações por segundo. Isso é uma melhoria de mais de 4x para menos de duas linhas de código.

Foi assim que usei o Play! cache para melhorar o desempenho no meu site de leilões Scala.

Consumo de memória

Outra área onde as ferramentas Scala certas podem fazer uma grande diferença é no consumo de memória. Aqui, novamente, Jogue! empurra você na direção certa (escalável). No mundo Java, para uma aplicação web “normal” escrita com a API de servlet (ou seja, quase qualquer framework Java ou Scala disponível), é muito tentador colocar muito lixo na sessão do usuário porque a API oferece métodos de chamada que permitem que você faça isso:

 session.setAttribute("attrName", attrValue);

Por ser tão fácil adicionar informações à sessão do usuário, muitas vezes há abuso. Como consequência, o risco de usar muita memória possivelmente sem um bom motivo é igualmente alto.

Com o jogo! framework, isso não é uma opção—o framework simplesmente não tem um espaço de sessão do lado do servidor. O jogo! A sessão do usuário do framework é mantida em um cookie do navegador e você precisa conviver com isso. Isso significa que o espaço da sessão é limitado em tamanho e tipo: você só pode armazenar strings. Se você precisar armazenar objetos, terá que usar o mecanismo de cache que discutimos anteriormente. Por exemplo, você pode querer armazenar o endereço de e-mail ou nome de usuário do usuário atual na sessão, mas terá que usar o cache se precisar armazenar um objeto de usuário inteiro de seu modelo de domínio.

Jogar! mantém você no caminho certo, forçando você a considerar cuidadosamente seu uso de memória, o que produz código de primeira passagem praticamente pronto para cluster.

Novamente, isso pode parecer uma dor no início, mas na verdade, Play! mantém você no caminho certo, forçando-o a considerar cuidadosamente o uso de memória, que produz código de primeira passagem praticamente pronto para cluster - especialmente porque não há nenhuma sessão do lado do servidor que precisaria ser propagada em todo o cluster, tornando a vida infinitamente mais fácil.

Suporte assíncrono

Próximo neste jogo! revisão da estrutura, examinaremos como o Play! também brilha no suporte assíncrono (hronous). E além de seus recursos nativos, o Play! permite incorporar Akka, uma ferramenta poderosa para processamento assíncrono.

Embora o Lojinha ainda não aproveite ao máximo o Akka, sua simples integração com o Play! facilitou muito:

  1. Agende um serviço de e-mail assíncrono.
  2. Processe ofertas para vários produtos simultaneamente.

Resumidamente, Akka é uma implementação do Actor Model que ficou famoso por Erlang. Se você não está familiarizado com o Akka Actor Model, imagine-o como uma pequena unidade que só se comunica por meio de mensagens.

Para enviar um e-mail de forma assíncrona, primeiro crio a mensagem e o ator apropriados. Então, tudo que eu preciso fazer é algo como:

 EMail.actor ! BidToppedMessage(item.name, itemUrl, bidderEmail)

A lógica de envio de e-mail é implementada dentro do ator, e a mensagem informa ao ator qual e-mail gostaríamos de enviar. Isso é feito em um esquema de fogo e esquecimento, o que significa que a linha acima envia a solicitação e continua a executar o que tivermos depois disso (ou seja, ela não bloqueia).

Para mais informações sobre o Async nativo do Play!, dê uma olhada na documentação oficial.

Conclusão

Resumindo: desenvolvi rapidamente um pequeno aplicativo, o Lojinha, capaz de escalar e expandir muito bem. Quando tive problemas ou descobri gargalos, as correções foram rápidas e fáceis, com muito crédito devido às ferramentas que usei (Play!, Scala, Akka e assim por diante), o que me levou a seguir as melhores práticas em termos de eficiência e escalabilidade. Com pouca preocupação com o desempenho, consegui escalar para milhares de solicitações simultâneas.

Ao desenvolver seu próximo aplicativo, considere cuidadosamente suas ferramentas.

Relacionado: Reduzir o código do Boilerplate com macros Scala e quase aspas