Escalando o jogo! a milhares de solicitações simultâneas
Publicados: 2022-03-11Os 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.
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.
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.

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.
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:
- Agende um serviço de e-mail assíncrono.
- 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.