quinta-feira, 17 de dezembro de 2009

Práticas de aplicativos web com Java

Amigos,
Achei um artigo sobre desenvolvimento Java muito legal, escrito pelo
Fernando Franzini.

No mundo conceitual da engenharia de software vemos inúmeras teorias, diretrizes, abordagens, idéias, teoremas etc... tudo isso relacionado com todas as partes envolvidas na produção de um sistema. Entretanto, na realidade nua e crua, vemos que a prática pode se distanciar um pouco desta almejada "Realidade Conceitual Ideal". Neste artigo, quero falar um pouco sobre práticas reais relacionadas a situações que eu tenho vivido na construção, teste e homologação de sistemas web usando java. Então vamos lá =).

Independente de qual filosofia, metodologia, abordagens ou frameworks que se possa usar no desenvolvimento do aplicativo web em java, nos últimos anos tenho sentido que não consigo fugir de dois problemas: consumo de memória e performance.

Consumo de Memória
Não é novidade para ninguém que, em java, a memória é automaticamente gerenciada através de um recurso chamado Garbage Collection, que a cada versão da JVM tem sido melhorado e incrementado. Este fato traz, para qualquer desenvolvedor, a falsa idéia de que ele pode deitar e rolar na implementação do sistema que o "Sr. pancudão coletor de lixo" vai limpar toda a sujeira que ele deixar para traz.

Na prática, percebo que os responsáveis pelos sistemas em geral - gerentes, projetistas, programadores etc., não tem colocado isso como um fator de preocupação nos ciclos de desenvolvimento, deixando "o pau torar" na programação. O resultado é que em ambiente de testes e homologação o sistema funciona lindo e maravilhoso, mas depois que entra em produção, mesmo após apenas alguns dias em que o serviço está no ar, começam as runtime exceptions apontando a falta de memória. E agora? O que fazer se o sistema está gastando mais memória do que a suportada pela configuração da JVM?

"O aumento de memória das máquinas e JVM,
ou mesmo o aumento de máquinas nos clusters,
não é a solução, e sim uma prorrogação do problema!"

Qualquer pessoa poderia facilmente sugerir que aumentássemos a memória da máquina e JVM. Entretanto, vemos que isso não é uma solução, e sim uma prorrogação do problema, porque quanto mais pessoas usando, ou quando novos processos forem acrescentados no sistema, maior será o gasto. Solucionar o problema seria perguntar para o sistema se ele não está gastando memória mais do que o necessário!!! Depois dessa certeza é que se delega mais memória para JVM, sendo que a aplicação realmente está precisando. Não quero entrar em detalhes sobre regiões de memória da JVM e nem sobre escalabilidade vertical e horizontal, pois fogem do escopo do artigo. A questão é que vejo muita gente tentando escalar a aplicação sem nem ao menos avaliar o seu consumo real.

Performance
A velocidade da aplicação em executar os processos e apresentar algum resultado está intimamente ligada com a experiência do usuário final, que vai passar horas e horas do seu dia ali na frente do sistema que foi maravilhosamente escrito para ele. Na prática, vejo que, novamente, os responsáveis pelos sistemas em geral - gerentes, projetistas e programadores, também 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 funciona lindo e maravilhoso, mas depois de algum tempo começam as reclamações relacionadas com a lentidão crescente.

Seguem abaixo algumas das práticas que venho utilizando como se fosse uma "receita de bolo", mais ou menos um "pente fino" que é passado nas aplicações para corrigir as duas situações problemáticas acima citadas.

1. Acesso ao banco de dados - reduzir ao máximo o número de vezes que a aplicaçã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 à base que se multiplicam na aplicação a medida do número de usuários simultaneamente conectados.

Solução: Analisar quantas vezes a aplicação está acesssndo 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 e SUBQUERYS. 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."

2. Índices adequados - Criar í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 bases 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.

3. 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 limitados 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 nas redondezas onde os programadores juniores 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.

4. Ordenação de Tabelas - Ordenar as tabelas 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 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.

5. Usar Pool de conexões - usar 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 web container, no mínimo 2 chamadas remotas são efetuadas, uma autenticar o usuário/senha e outra para efetuar a comando SQL desejado. Esta opção é uma das piores gafes que um programador 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.

6. Usar 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:

a. 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.

b. 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.
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é 40% o acesso ao banco de dados. Uma sugestão bem simples e que deu bastante resultado é simplesmente implementar um mini cache usando o "application context" da aplicação que, com algumas classes estáticas, se consegue cachear/alterar estes dados, reduzindo em um porcentagem considerável o acesso ao banco de dados.

7. Controlar a criação 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: 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!

8. Controlar 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:

- 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.

- usar StringBuffer/StringBuilder para as String que sofrem alterações constantes ou para situações de manipulação de arquivos.

9. Aumentar as 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.

Fonte:
IMaster

Nenhum comentário:

Postar um comentário