Tunning de Aplicações Web

Postado em Atualizado em

salon-tuning-bA 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 têm o mínimo de 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 “seqüencialmente”. 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 comumente ocorrentes 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 numero 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. A questão problemática 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 2 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 numero X de conexões com o banco de dados que sera 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 numero X deve ser levantando e configurada 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 2 tipos: dados de manutenção/parâmetros e 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. Estes tipo de formação frequentemente sofre um baixo índice de manutenção e um alto número de acesso. 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) necessitam 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 para 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:

  1. Por que estou alocando esse objeto?
  2. 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 idéia do tópico 5. O programador deve analisar a questão da necessidade e entender o porquê 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 memoria.

É isso, pessoal. O artigo fica aberto para sugestões e novas idéias.

“De fato, sem fé é impossível agradar a Deus, porquanto é necessário que aquele que se aproxima de Deus creia que ele existe e que se torna galardoador dos que o buscam.” Hebreus 11:6

Anúncios

2 comentários em “Tunning de Aplicações Web

    […] os interessados nesse tipo de assunto, veja minha regrinha de bolo de otimização que qualquer um pode reusar como ponto inicial para esse tipo de atividade. Até a próxima ;D […]

    Tunning de solução Java Web - disse:
    23/10/2014 às 14:01

    […] os interessados nesse tipo de assunto, veja minha regrinha de bolo de otimização que qualquer um pode reusar como ponto inicial para esse tipo de […]

Os comentários estão encerrados.