Desenvolvimento

14 out, 2014

Como fazer um tunning de aplicações web

Publicidade

A velocidade da aplicação em executar os processos está intimamente ligada com a experiência do usuário final. Na prática, vejo que os responsáveis pela criação de sistemas (gerentes, projetistas e programadores) não têm colocado isso como um fator de preocupação nos ciclos de desenvolvimento. O resultado é o mesmo de sempre, em ambiente de testes, homologação e durante algum tempo inicial de produção, o sistema acaba enganosamente funciona lindo e maravilhoso, mas depois de algum tempo, as reclamações relacionadas com a lentidão começam a aparecer. Diante desse contexto, segue abaixo as principais práticas que eu venho utilizando como sucesso como se fosse uma “receita de bolo” para resolver esse tipo de situação:

Acesso ao banco de dados

Reduzir ao máximo o número de vezes que a solução faz acesso ao banco de dados.

Problema: Muitos programadores têm a mania de ir desenvolvendo classes, componentes e módulos sem antes e/ou durante fazer uma análise organizada de como estas partes do sistema estão fazendo estes acessos. Com isso, vemos como resultado aplicações com baixa performance devido aos vários e desnecessários acessos ao banco “round-trips” que se multiplicam a medida do número de usuários simultaneamente conectados.

Solução: Analisar quantas vezes a aplicação está acessando o banco, o porquê do acesso e assim tentar uma forma de evitar este acesso. Neste momento, muitos profissionais pecam por não conhecerem recursos básicos de banco de dados e de SQL-ANSI, principalmente pelas propagações de frameworks ORM. Qualquer meio é válido, alguns recursos usados para alcançar isso são o uso de VIEWS, JOIN, SUBQUERYS e o Pattern Store Procedure Facade. Todos os programadores têm que possuir um lema em mente: “O acesso remoto é que mais degrada a performance de uma aplicação e o acesso ao banco de dados é um deles, então eu tenho que fazer de tudo para evitar ou minimizar ao máximo.“

Índices adequados

Criar os índices para as todas as tabelas que sofrem consultas na aplicação.

Problema: Muitos programadores não conhece o mínimo dos fundamentos de banco de dados, criando bancos sem nenhum índice de busca para as tabelas que sofrem alto número de SELECT + WHERE CAMPO. A questão problemática é que quando uma tabela sem índice sofre um SELECT +WHERE CAMPO, o registro é buscado, na maioria das vezes, da pior e mais demorada forma possível, que é o “sequencialmente”. A pior notícia é que isso é um problema cumulativo, ou seja, quanto mais registro existente, mais demorada fica a consulta. Já tive experiências de diminuir o tempo de um procedimento de fechamento mensal em 50% do tempo, pelo simples ato de criar os índices nas tabelas usados pelo processo.

Solução: Para cada SELECT que o programa faz, em cada tabela, verifique os campos de busca colocados no WHERE e, assim, crie um índice para cada um deles.

Consultas gigantescas

Sistemas que permitem o usuário consultar e trazer do banco de dados um alto número de registros.

Problema: Algumas situações que costumam ocorrer em sistemas no modelo desktop, ou mais conhecido como “FAT CLIENT”, não se encaixam em aplicativos web. Um destes casos é quando sistemas permitem aos usuários filtrar e trazer do banco de dados consultas com um alto número de registros complemente desnecessário. Em casos em que o modelo era desktop, isso não acarretava problemas devido à própria natureza da solução. Entretanto em sistemas web, onde recursos de execução são compartilhados e o numero de acesso simultâneos é ilimitado, o sistema estará gastando um alto e precioso número relevante de memória.

Explicando de forma prática, eu já peguei sistemas na redondezas onde os programadores estavam replicando a arquitetura que continha uma camada de persistência CRUD. O problema era que o framework replicava um método que sempre retornava todos os registros existente. Isto estava sendo propagado para todo o sistema e seus processos relacionados. Se paramos para analisar, mecanismos de busca são disponibilizados ao usuário para que ele tenha autonomia de buscar registros individuais ou grupos lógicos deles. Qual seria o motivo, ou o que poderia fazer um usuário com 500 linhas de resultados de uma consulta? Que ser humano na face da terra gostaria de visualizar 500 registros em uma olhada? Mesmo que a regra de negócio ainda apoiasse o caso, o usuário final, sendo um humano comum, não teria tamanha visibilidade para isso.

Solução: Os processos do sistema em questão devem ser analisados e devem ser implementadas validações lógicas corretas, que não permitam  executar consultas que retornem uma alto número de registros no banco de dados. Duas práticas neste tópico são bem comuns – o uso sistemático de paginação, e a implementação de filtros inteligentes, baseados em regra do próprio negócio, que não deixassem a ocorrência de grandes intervalos. Como tudo na vida, existem exceções e podemos, sim, encontrar situações em sistemas que teriam a necessidade de consultar grandes volumes de registros, mas isso já está mais que comprovado que é uma porcentagem pequena e restrita do total da automação.

Ordenação de dados

Ordenar dados em memória usando java ao invés de usar ORDER BY.

Problema: Uma funcionalidade muito comum é disponibilizar a ordenação das tabelas apresentadas na aplicação pelas suas próprias colunas. A questão problemática é quando a aplicação efetua mais um acesso ao banco a cada ordenação requisitada. Ou seja, o programador usa o recurso de SELECT ORDER BY para fazer a ordenação.

Solução: Transformar as linhas da tabela em objetos Java e, assim, ordená-los usando recursos do JSE, evitando gasto com tempo, acesso ao banco e recursos de memória. Esta solução pode ser facilmente implementando com a interface Comparable.

Pool de conexões

Use indiscutivelmente a abordagem de pool como paradigma de acesso ao banco de dados.

Problema: Eu realmente não sei o motivo, mas já peguei alguns aplicativos web por aí que abrem e fecham objetos de conexão com o banco de dados a cada requisição. Ou seja, a cada pedido enviado ao container Java, no mínimo duas chamadas remotas são efetuadas, uma para autenticar o usuário/senha e outra para efetuar a comando SQL desejado. Esta opção é uma das piores gafes que um desenvolvedor web pode fazer para deixar o sistema com a pior performance possível, sem falar que o sistema pode “baleiar” o banco quando o número de acesso simultâneos exceder a capacidade de resposta do determinado banco de dados.

Solução: Na web existe uma única solução comprovada que é o uso efetivo da abordagem de Pool de conexões. No momento da disponibilização – deploy da aplicação, o sistema deve abrir um número X de conexões com o banco de dados que será posteriormente usado em toda a aplicação. Esta abordagem mistura o conceito de compartilhamento e concorrência, sendo que pedidos em tempos diferentes reutilização a mesma conexão e pedidos simultâneos usarão diferentes conexões. Este número X deve ser levantando e configurado de forma parametrizada, de acordo com o perfil da aplicação e do modo/quantidades que os usuários estarão gastando conexões durante utilização do sistema.

Cache

Cachear informações que sofrem alto índice de acesso e baixa ocorrência de alteração.

Problema: Sistemas em geral implementam administração de informações na qual poderíamos classificar em dois tipos: dados de manutenção/parâmetrose de processos:

  • Manutenção/Parâmetros – informações que os sistemas têm que guardar, usadas como parte do processo, que não possuem um fim nelas mesmas. Este tipo de formação frequentemente sofre um baixo índice de manutenção e um alto número de acessos. Ou seja, no escopo da aplicação, estes dados raramente são alterados e muitos usados.
  • Processos – informações resultantes de processos com regras de negócio do escopo da aplicação. Estes podem ou não sofrer alterações e podem ou não ser altamente acessados. Tudo depende da natureza do negócio da aplicação.

A situação complicada seria o sistema fazer um acesso ao banco de dados a cada momento que diferentes usuários (concorrentes ou não) necessitassem usar informações de manutenção – que na grande maioria dos casos são iguais. Ou seja, teríamos vários usuários acessando o banco de dados repetidas vezes para pegar as mesmas informações, gastando assim tempo e memória de forma desnecessária.

Solução: Analisar cuidadosamente e cachear as determinadas informações que se encaixam de alguma maneria no perfil de dados de “Manutenção/Parâmetros”. Conceitualmente, é fácil visualizar o mecanismos de cache: o primeiro usuário que necessitar da determinada informação efetuará um acesso ao banco e cacheará os dados em algum lugar na memória, fazendo com que os próximos usuários não precisem gastar tempo e memória repetindo o ciclo. O cache é atualizado quando estes dados forem atualizados de alguma maneira no sistema, bem como o perfil deles já mostrou que seria um caso difícil de acontecer.

Segue um resumo das estratégias gerais:

  • Use cache de global para evitar consultas repetitivas dentro da solução global para dados que não sofram alteração de nível global;
  • Use cache de sessão para evitar consultas repetitivas dentro da mesma sessão para casos no qual dados estes dados não sofram alteração durante a o tempo de duração da sessão;
  • Use cache de thread local para evitar consultas repetitivas dentro da mesma requisição para casos no qual estes dados os sofram alteração durantes as requisições.

A prática do cache, entretanto, não é algo simples ou trivial; demandando tempo e esforço para ser implementado. Eu poderia sugerir implementações prontas como o EhCache, ou serviços de cache disponibilizados pelos frameworks ORM. Veja que a utilização só vale a pena se os dados realmente possuírem um alto índice de acesso. Com esta abordagem, a aplicação consegue reduzir em média até 60% o acesso ao banco de dados.

Ciclo de vida de objetos

Controle efetivo da criação de objetos durante a execução do programa.

Problema: Algo que precisamos sempre lembrar durante a programação é que o operador new aloca fisicamente o objeto da memória, gastando espaço no HEAP da aplicação. A primeira questão problemática é que percebo que os programadores usam o new de forma displicente, sem nem ao menos parar para pensar em que contexto da aplicação está usando. Tudo é motivo para fazer um new! Eu já vi casos em que para reiniciar o estado do objeto, o programador dava um new na referência.

Solução: O programador tem que sair desse comodismo e começar a analisar todos os seus new! Duas perguntas resolvem o problema: por que estou alocando esse objeto? Quantas vezes esse código vai ser executado?

Estas duas perguntas vão consciencializar o programador daquela situação, levando-o no mínimo a tomar uma das duas decisões abaixo:

  • Reutilizar Objetos: ao invés de ficar sempre fazendo new, ele pode aumentar a visibilidade do escopo daquele objeto e assim reusá-lo, restartando seu estado;
  • Reduzir o Escopo: reduzir o escopo dos objetos vai deixá-los disponíveis mais rápido para o coletor de lixo. Se for preciso, use escopos lógicos menores com { }.

Esta solução é uma das mais difíceis a serem implementadas, porque invadem a “cultura” do programador de se autocriticar. Mas quero animá-los dizendo que a batalha da economia de memória e performance se ganha na somatória de detalhes.

Objetos string

Controlar o gasto com os objetos Strings.

Problema: Outra coisa que sempre precisamos lembrar é que os objetos String chamados de “wrappers” são imutáveis, ou seja, uma vez instanciados eles nunca mudam. Qualquer operação com a String gerará uma terceira String. O problema aqui é o uso abusivo, desnecessário e inconsciente das String durante a execução do programa.

Solução: Segue a mesma ideia do tópico 5. O programador deve analisar a questão da necessidade e entender o por quê daquele uso. Duas opções surgem para contornar a situação:

  1. Usar final static para as Strings que se encaixam no contexto de estáticas (SQLs, mensagens). Ou seja, somente será gasto um objeto para todas as execuções do programa;
  2. Usar StringBuffer/StringBuilder para as String que sofrem alterações constantes ou para situações de manipulação de arquivos.

Configurações de memória da JVM

Problema: Mesmo depois de todas as precauções tomadas, a aplicação ainda pode gastar mais memória do valor default previamente configurado na JVM.

Solução: Neste casos, é preciso fazer um estudo, apurando a média de memória gasta pela aplicação, usando alguma ferramenta de profile e, assim, configurar um adequado número razoável de memória.

É isso, pessoal! O artigo fica aberto para sugestões e novas ideias.