Entendendo MapReduce

Sabemos que ter um bom modelo de banco de dados relacionais é importante, porém com a necessidade de aplicações mais escaláveis nos dias atuais, talvez você precise “desnormalisar” o seu banco de dados. O que adiantaria uma foreign key se você tem tabelas espalhadas em diversos data sources? Por questões de performance, dados podem ser distribuídos em data centers distintos, então como buscar pelo id se você não sabe onde está esse dado? Por isso é importantíssimo que a aplicação controle essa integridade, com um bom design OO, para não depender de constraints e stored procedures do banco de dados.

MapReduce é um modelo de programação, e framework introduzido pelo Google para suportar computações paralelas em grandes coleções de dados em clusters de computadores. Agora MapReduce é considerado um novo modelo computacional distribuído, inspirado pelas funções map e reduce usadas comumente em programação funcional. MapReduce é um “Data-Oriented” que processa dados em duas frases primárias: Map e Reduce. A filosofia por trás do MapReduce é: Diferentemente de data-stores centrais, como um banco de dados, você não pode assumir que todos os dados residem em um lugar central portanto você não pode executar uma query e esperar obter os resultados em uma operação síncrona. Em vez disso, você precisa executar a query em cada fonte de dados simultaneamente. O processo de mapear a requisição do originador para o data source é chamado de ‘Map’, e o processo de agregação do resultado em um resultado consolidado é chamado de ‘Reduce’.

Hoje existem diversas implementações de MapReduce, como : Hadoop, Disco, Skynet, FileMap e Greenplum. Hadoop é a implementação mais famosa implementada em Java como um projeto open source.

Se você quer contribuir nesse projeto, assista esse screencast.

Anúncios

Web Sites de Alta Performance (Frontend)

Sabemos que bancos de dados relacionais costumam ser o gargalo das aplicações web, por isso quando falamos de performance é mais comum focar no processamento do servidor. Já escrevi um post sobre escalabilidade e performance. Porém, muitas vezes não nos preocupamos com o conteúdo que estamos escrevendo nas páginas e as vezes os dados estão prontos para ser exibidos ao usuário mas o browser está se matando para renderizar o conteúdo. Segundo Steve Souders, do Google e autor do livro “High Performance Web Sites“, na maioria das páginas web, o lado do servidor corresponde a menos que 10 a 20 % do tempo de resposta ao usuário. Portanto devemos nos focar nos outro 80 a 90 % de tempo de resposta, que é no lado do cliente (frontend).

Dentre os 80 a 90 % de tempo de resposta, o carregamento do HTML gasta somente 10% da demora. O restante do tempo fica a cargo de Imagens, CSS, Javascript e outros componentes. A maior parte do tempo é gasto fazendo download desses componentes quando ainda não estão cacheados no browser, e ainda tem um tempo para persear  HTML, scripts, e stylesheets.

O livro “High Performance Web Sitesimagem12” fala de 14 regras de performance a considerar ao desenvolver um web site. As regras são:

  • Faça pouco HTTP request
  • Use um CDN
  • Adicione um Expiramento no header
  • Compacte componentes
  • Coloque styles no topo
  • Mova scripts para o final
  • Evite Expressões CSS
  • Faça JS e CSS externos
  • Reduza DNS lookups
  • Minimize JavaScript
  • Evite redirects
  • Remova scripts duplicados
  • Configure ETags
  • Permitir que AJAX faça uso de cache

Configurações

Podem ser feitas configurações  no servidor passando parâmetros no header HTTP para fazer compressão, e manter conexões socket abertas, requisições condicionais e expiramento. Abaixo tem um exemplo de um request e response usando compactação e a mesma conexão para várias requisições (keep-alive)

Request

GET /us.js.yimg.com/lib/common/utils/2/yahoo_2.0.0-b2.js
HTTP/1.1
Host: us.js2.yimg.com
User-Agent:Mozilla/5.0(…)Gecko/20061206Firefox/1.5.0.9
Accept-Encoding: gzip,deflate
Connection: keep-alive

Response

HTTP/1.1 200 OK
Content-Type: application/x-javascript
Last-Modified: Wed, 22 Feb 2006 04:15:54 GMT
Connection: keep-alive

O Gzip reduz em aproximadamente 70% do tempo de resposta, porém evite compactar imagens e PDFs pois a compactação não será tão eficiente. O Apache 1.3 usa o mod_gzip. Já o Apache 2.X usa o mod_deflate. Essa compactação é muito boa, porém faça apenas para conteúdos estáticos, não para dinâmicos como por exemplo o JSP. Abaixo segue uma configuração no apache do mod_deflate para compactar tudo que é js e css.

imagem10

Faça Poucas Requisições HTTP

Cada imagem ou gif é uma requisição diferente ao servidor. Uma técnica interessante é chamada de Estampa CSS. Essa técnica consiste em combinar várias imagens em uma só e controlar a exibição por CSS. Considerando apenas 1 imagem contendo várias outras iamgens, será somente 1 requisição ao servidor.

imagerollover

<div style=”background-image: url(‘a_lot_of_sprites.gif’);

background-position: -260px -90px;

width: 26px; height: 24px;”>

</div>

Outro padrão é a combinação de Scripts e Estilos. Cada arquivo Javascript ou css chamado na página é uma requisição diferente. Combinar os arquivos js em apenas um, faz que haja apenas uma requisição no servidor.
imagem81
9 Requisições !
Porém, juntar tudo em um pode dificultar um pouco a manutenção desses arquivos, por isso existem frameworks que facilitam esse trabalho. No Rails, podemos usar:
<%= javascript_include_tag “prototype”, “cart”, “checkout”, :cache => “shop” %>
E fazer uma única camada usando:
<script type=”text/javascript” src=”/javascript/shop.js”></script>
Os estilos css devem ser chamados no início do arquivo para o browser já saber como renderizar a página. Essa forma não vai diminuir o tempo de carga dos arquivos nem a quantidade de requisições, porém diminui o tempo de resposta do usuário. Já os scripts devem ficar no final do arquivo pois o browser faz várias requisições em paralelo, porém os arquivos js não podem ser baixados simultaneamente. Se houver muitos js no início, o browser espera baixar esses arquivos e somente depois começa a renderizar outros componentes da tela, causando uma sensação de congelamento na página.
imagem11
Na imagem acima o browser começa a carregar simultaneamente um componente css e um componente js, porém o segundo componente js só vai ser carregado depois que o primeiro javascript terminar de carregar.
Faça Javascript e CSS arquivos externos, pois você pode aproveita-los em outras páginas, além de usufruir do recurso de cache do browser. Quando o javascript é carregado ele fica no que chamamos de Primed Cache, e se outra página fazer uso desse javascript ela vai pegar do cache e não do servidor.
AJAX

O AJAX é bastante usado nas páginas web hoje em dia, porém temos que nos preocupar em enviar as mesmas requisições repetidamente ao servidor. Podemos controlar essas requisições com parâmetros para verificar se a informação que estamos solicitando já não foi requisitada antes. Outro exemplo é o autocomplite. É recomendado que o usuário digite pelo menos umas 3 letras para fazer a requisição ajax, porém ele pode continuar digitando mais letras e para cada letra digitada será enviado uma requisição a mais ao servidor. Vai do bom senso alterar essa regra para evitar muitas requisições.
Tabelas
Muita gente gosta de usar tabelas, colocando borda invisível, para organizar os componentes na tela. Porém essa maneira gera elementos no DOM desnecessariamente. Tem um site que mostra uma maneira mais adequada para ter o mesmo efeito sem precisar carregar uma tabela inteira.
imagem7
ETag
Entity Tags (ETags) são um mecanismo que web servers e browsers usam para validar componentes cacheados. O Carlos Brando escreveu dois posts interessantes sobre o assunto, em Rails,  aqui e aqui.
Ferramentas
Existem ferramentas para compactação, como por exemplo: JSMin, ShrinkSafe, Packer, YUI Compressor. Sobre essa última o Paulo Siqueira escreveu um post muito interessante aqui.
Confiram uma palestra de Steve Souders na época que trabalhava no Yahoo.
Screencast
Para quem gostou do assunto tem um screencast muito interessante disponibilizado pela Treina Tom chamado “Otimização de Performance com uso de Padrões Web”
O Renato Elias escreveu um post muito interessante sobre Escalando para alguns milhares, páginas WEB.
Tem outra apresentação interessante aqui.  Apresentação é do pessoal de Stanford, descreve basicamente como medir o tempo de renderização das páginas web pelos browsers e mostra algumas estratégias para diminuir esse tempo.

Escalabilidade != Performance

A Escalabilidade é um requisito não funcional de arquitetura que deve ser considerado em todos os projetos porém, dependendo das necessidades do cliente (requisito funcional) e da quantidade de usuários, a escalabilidade pode um peso maior ou menor dentro do projeto.

Muita gente confunde escalabilidade com desempenho (performance) mas são duas coisas muito diferentes, apesar se um influenciar o outro. Performance é a capacidade de atender requisições dos usuários em tempo de processamento e consumo de memória aceitáveis conforme as necessidades do cliente. Já a escalabilidade é a capacidade de manter a disponibilidade e o desempenho a medida que a carga transacional aumenta. Você pode ter um sistema altamente performático em uma máquina mas que é impossível de escalar em uma arquitetura de cluster, por exemplo. Pensar em escalabilidade não é somente pensar em aumentar máquina. É pensar em IO, memória, requisição, banco de dados e assim por diante. Por exemplo, uma aplicação web que guarda muitos dados na sessão pode ser bem performática, mas se essa aplicação passar a ser usada como um web site, com milhares de usuários, a quantidade de informação na sessão (memória do servidor) tornará o sistema lento! Um HttpSession é muito difícil de escalar se for fazer uma estratégia na mão. Nesses casos uma estratégia de cache distribuído, como o memcached, poderia resolver o problema.

Tive uma experiência recente com uma aplicação web que estava apresentando problema de performance, então precisei fazer uma análise para descobrir a causa. Infelizmente essa aplicação também tem problema de manutenabilidade e nessas horas os autores nunca trabalham mais no projeto e não deixaram o endereço da casa deles para quem for dar manutenção! 🙂

Analisando o código, de cara já vi que teria problemas pois o sistema não tem testes unitários. Com certeza haveria a necessidade de fazer refatorações e nem ao menos posso garantir que não vou “quebrar” o software. Os testes de unidade e/ou de integração são tão importantes quanto o projeto em si pois são eles que garantem a qualidade do software. Um programador profissional deve escrever testes, como já disse Phillip Calçado aqui! Como não há testes, a refatoração foi dividida em sprints bem curtos de no máximo uma semana. Já que o projeto está em produção, é interessante construir testes de falha e depois testes de sucesso, pelo menos para as funcionalidades mais importantes. O ideal seria que a equipe tivesse trabalhado com TDD.

Essa aplicação, escrita em Java, apresentava problemas arquiteturais de performance mesmo com uma infra poderosa com vários clusters e load balancing, e consequentemente afetou a escalabilidade pois o aumento transacional causou indisponibilidade do sistema. Portanto, com o aumento de usuários, entrada de novas funcionalidades e crescimento de registros no banco de dados, a aplicação começou a travar e apresentar muitos problema de lentidão.

Antes de começar a fazer qualquer alteração precipitada eu baixei o JProfiler para tirar uma primeira análise. A ferramenta é boa mesmo! Monitorar consumo de memória, tempo de processamento, hotspot, trabalho do garbage collection, consultas demoradas no banco de dados etc. É uma boa prática monitorar frequentemente as aplicações com um profiler para saber como está indo o desenvolvimento, evitando gargalos desnecessários. Essa prática também evitaria otimizações prematuras, como disse Joshua Bloch aqui. Por exemplo, muita gente altera concatenação de String para StringBuffer, mas nem sabe que internamente a concatenação de string seria transformada em StringBuilder pelo java.

Voltando à aplicação, de nada adiantaria sair alterando o código se o problema estiver no banco de dados. Precisei criar índices das tabelas e com a ajuda do JProfiler isso ficou bem fácil. Já na parte da aplicação, a ferramenta também me ajudou a encontrar problemas como:

  • Excesso de instanciação de objetos dentro de loops bastante grandes. Muitos desses objetos são apenas para trafegar dados dentro da aplicação. Eles não tem inteligência nenhuma, são como fantoches. Mas o que o JProfiler aponta é que o garbage collector está precisando fazer Full GC toda hora, e isso gasta tempo!
  • Muito uso de Reflection do java dentro de loops bastante grandes. Esse recurso é muito poderoso, mas deve-se tomar algumas precauções. O desempenho da aplicação é melhor quando a chamada de um método é feito da maneira convencional. Se for para dar produtividade e o overhead não for tão grande, vale a pena. Por outro lado o uso de reflection pode deixar o código difícil de entender. No caso da aplicação em análise uma solução mais Orientada a Objetos eliminaria esses impulsos por querer dificultar problemas que seriam simples. Só para uma curiosidade, o Reflection foi rescrito no java 1.4 para melhorias de performance.
  • Comparação de strings para buscar um objeto em uma lista. Essa busca de objetos pode ser otimizada pela própria linguagem utilizando, por exemplo, o método contains de um collection. Um loop utilizando Java.util.Vector pode ser transformado em um Java.util.HashSet para buscar o objeto mais rapidamente se no caso não importar a ordem dos objetos. Porém se a intenção for fazer cálculos com atributos de uma classe, como ocorre em algums pontos da aplicação, a idéia seria que a própria classe (entidade) faça o cálculo a partir de seus atributos. Isso, além de ser mais Orientado a Objetos, eliminaria a necessidade de fazer loops para buscar o objeto e setar um valor nele.
  • Loops desnecessários. Alguns loops podem ser eliminados apenas com uma solução mais orientada a objetos. Outros podem ser eliminados apenas alterando a consulta no banco de dados para já trazer um resultado calculado, por exemplo.
  • Problemas em aberturas e tempo de vida de transações de banco de dados. Esse com certeza é um dos piores mas não vale a pena eu comentar pois a solução depende de cada aplicação.
Aplicações em Ruby on Rails sofrem com a falta de thread safety, o que pode prejudicar a escalabilidade. Mais o pessoal está escalando uma grande aplicação em Rails fazendo um uso extensivo de cache de fragmentos. Que tal o GUJ rodar em Rails 7 vezes mais rápido do que roda em java? Segue o link do bate-papo.
Conclusão:

Tem muita gente dizendo que Ruby on Rails não escala por causa do case do twitter mas tá aí uma prova que mesmo uma aplicação em Java, com recursos como GC, JIT e hotspot pode não escalar, pois não é a linguagem que não escala, e sim o modelo que não foi pensado para escalar.