Inicialmente, o nome desse post era pra ser: “otimizar ou escalar?”, mas acabei optando por este outro nome. Afinal, a postagem é sobre como foi otimizado, então…
Há algum tempo, aqui em meu trabalho, sofríamos com um sistema meio problemático: o sistema em questão é um sistema montado para que alunos possam escolher disciplinas, e é feito em duas fases. Nosso problema específico era com a segunda fase: um aluno só pode escolher uma disciplina desde que ela possua vagas, e como há certas disciplinas que muitas pessoas querem, isso vira uma corrida contra o tempo: praticamente metade da universidade acessando o sistema ao mesmo tempo, para conseguir sua vaga, e o sistema demorando muito tempo para completar cada requisição, enfileirando requisições, e um tempo absurdo para atender cada uma…
Enfim, foi jogado mais poder de processamento, mas para cada um processador que acrescentávamos, entravam mais quinheitos alunos, e cerca de cem disciplinas. Claramente, não haveria load-balancer que aguentasse, nem máquina que pudesse aceitar. Estudamos uma base não-relacional, diversas soluções, até chegar em uma: processar a página no cliente.
Antes disso, alguns dados: para cada aluno que se loga no sistema, os passos são: Identificar o aluno e seu curso, Montar a grade de seu curso, Separar todas as disciplinas (indicando o que ela é em seu curso), Identificar quais disciplinas o aluno está matriculado, quantas vagas há em cada uma e, finalmente, exibir a página personalizada.
Isso demorava cerca de quatro segundos. Por página.
Para piorar o cenário, havia situações em que a disciplina desejada não estava disponível, por ter esgotado o número de vagas, e os alunos repetidamente atualizavam a página, na esperança de alguém ter desistido. Somando tudo isso, era o suficiente para o sistema deixar de responder completamente, e embora a máquina não caísse, havia uma espera considerável até ele processar todas as requisições. Foram então feitas diversas modificações:
A primeira: o tempo de busca do banco de dados era um problema, então isso passou para um cache. Montar a página personalizada, por aluno, era outro problema tão grave quanto o primeiro (demorar dois segundos ao invés de quatro para processar a requisição não ia adiantar quase nada), então a idéia seria: montar uma página que tem poucas coisas dinâmicas, e delegar a montagem da página personalizada para o browser. Os processos foram:
- Buscar todas as disciplinas, todas as matrículas, e efetuar a contagem de vagas
- Converter tudo para JSON (Javascript Object Notation)
- Montar um cache disso tudo
- Entregar uma página para o browser com tudo isso, e deixar que ele se vire
Claro que o último pedaço foi o que mais complicou, e como já se sabia que a complexidade estava toda lá, estudei uma forma de fazer testes com Javascript. Optei por usar a biblioteca “Jasminne”, porém precisei adicionar suporte a páginas HTML estáticas, para testar o que acontecia no browser. Também fiz uns helpers, para puxar os dados das disciplinas na tela, usando “getElementById” e outros matchers do browser, para poder escrever testes tipo:
expect(disciplinas('obrigatorias')).toMatch(/Disciplina Obrigatória/); expect(labelDa('Disciplina Obrigatória').innerHTML).toMatch('10 requisições'); expect(disciplina('Disciplina Selecionada pelo Browser')).toBeChecked(); disciplina('Disciplina do Calendário').click(); expect(segunda('08:00').className).not.toEqual(classeUm);
E coisas semelhantes. Depois de montada essa base, foi finalmente feita a modificação, e o tempo das requisições baixou de 4 segundos para, em média, 40 milisegundos.
Uma bela melhora.
E aí, começaram as pequenas coisas. Quando alguém clicava em atualizar, no browser, todas as seleções que ele tinha feito desapareciam. Da mesma forma, se o aluno selecionava uma disciplina, e por algum motivo desse erro na hora de efetuar a matrícula (vagas preenchidas, etc), o browser também perdia a seleção. A solução foi salvar na URL do browser quais disciplinas foram selecionadas, com document.location.hash.
O outro problema foi com os caches.
Na hora que alguém fazia sua matrícula, o cache era invalidado, e no próximo acesso o primeiro aluno que entrasse na página revalidaria o cache. Porém, quando vários alunos entravam ao mesmo tempo, havia o mesmo problema dos quatro segundos… pior ainda, como o cache agora estava sendo invalidado e revalidado o tempo todo, e tudo estava sendo convertido para JSON, o tempo aumentava ainda mais…
A solução foi usar o DelayedJob, e revalidar o cache em background. Isso dá ao sistema um estado de “consistência eventual”, ou seja, por alguns instantes o que está sendo apresentado na tela não condiz com o que está no banco. Mas também, o sistema da forma como estava antes demorava tanto tempo para renderizar uma tela nos momentos de pico, que muitas vezes o dado estava ainda MAIS inconsistente, então foi uma melhoria. Claro que, na hora de salvar as matrículas, usa-se os dados do banco de dados, para evitar inconsistência, porém o que mais demorava no sistema era o momento de exibir a tela, portanto usar o banco nesse caso não causa nenhum peso adicional no banco.
O resultado? 500 conexões simultâneas, e a máquina nem sequer usa todo o processamento disponível… além disso, como todas as disciplinas estão disponíveis para o Javascript, dá para fazer coisas bem legais na tela, como identificar disciplinas conflitantes via client-side! How cool is that?
Mas, ainda há um segundo problema: Internet Explorer. Fui ver como ele se comportava, mas infelizmente só tenho a versão 8 dele instalada. A versão 8 desse browser consegue ser, em média, cinco vezes mais lento que o Firefox 3, e QUINZE vezes mais lento que o Chrome… Ou seja, ele é LENTO.
De certa forma, isso já resolve metade do problema. Identifiquei o browser, informei que ele é muito lento, e informei para instalar o Google Frame. Meio problema resolvido. Para quem optar por não instalar, dá para usar o IE8, porém com uma lentidão considerável, ou seja, espera-se que o próprio usuário opte por mudar de browser.
Enfim, bem por cima, todas essas mudanças permitiram atender todas as requisições corretamente, sem precisar se render à soluções de escalabilidade tais como “load-balancing” (ir adicionando mais máquinas quando estivesse no início da matrícula) ou separar o sistema em um servidor só para ele (e que não iriam auxiliar em quase nada, já que o número de usuários e disciplinas do sistema aumentam mais do que poderíamos adicionar de máquina).
3 Comments
Renato · 2011-04-04 at 12:16
É como eu li em algum lugar esses dias: Cada cliente da sua aplicação pode ser considerado como uma unidade de processamento. Cabe a nós escolher se encaramos eles como terminais burros ou como workers.
Além do relato de uma história de sucesso, você ainda ajudou a humanidade (ou pelo menos a parcela que mexe com web), “convertendo” alguns usuários do IE 😛
Dá pra ter bastante ideias!
PS: Não conhecia o Google Chrome Frame! Muito interessante…
PotHix · 2011-04-05 at 09:28
Æ!!
Interessante a solução!
Não tem nenhum problema de todos os usuários terem acesso ao Json com todas as disciplinas? Bastando olhar no developer tools?
Há braços
Maurício Szabo · 2011-04-06 at 10:10
Na verdade, não tem problema, porque todos os usuários, nesse sistema específico, podem escolher qualquer disciplina que eles queiram.
Como é validado no server-side, sempre consultando ao Banco de Dados, não há a possibilidade de um aluno escolher uma disciplina que ele não possa se matricular. Além disso, essas disciplinas sempre estão disponíveis em outros formatos, tipo PDF ou Excel, então se eles quiserem ver o JSON é mais uma questão de conveniência 🙂
Abraços!
Comments are closed.