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

Refatore seu tempo

Refatore o código e ganhe tempo e produtividade!

O Refactoring, basicamente, é melhorar o design do código sem alterar a funcionalidade nem quebrá-lo. 

Quando você se depara com um código muito ilegível e precisamos dar manutenção nele, o que você faz? Primeiro devemos verificar se existe testes automatizados para essa funcionalidade. Se houver testes pode pular para a última etapa que o trabalho vai ser mais simples. 🙂 Um código com muita infra-estrutura misturado com código de negócio, com variáveis abreviadas e métodos sem nenhum sentido, provavelmente te fará gastar um bom tempo para entender o objetivo dele.

Para entender como separar o domínio do que é infra-estrutura leiam o livro sobre DDD.

Se simplesmente fizermos mais um ‘if’ para atender a solicitação do usuário estaremos contribuindo com esse trabalho sujo. Um ‘if’ pode parecer ser uma solução (momentânea), porém a qualidade começa a cair nesse exato momento. Estaremos prejudicando nosso próprio trabalho e, pelo contrário que muitos possam pensar, prejudicando a produtividade.

Com um código complexo como esse é bastante provável que não se tenha testado todas possibilidades e provavelmente precisaremos enfrentá-lo novamente. ‘Enfrentá-lo novamente’ significa olhar para o mesmo código que já analisamos e tentar entender denovo o que ele está fazendo, ou seja, o cliente está pagando esse tempo de entendimento que ele já havia pago. 

É interessante que o cliente saiba dessa dificuldade, do time de desenvolvimento, de manter o código. Porém cuidado com o que vai falar para ele pois somos todos profissionais! Não adianta só falar mal do código pois agora a responsabilidade é nossa.

Devemos fazer um trabalho de conscientização para que o cliente entenda que será necessário fazer refatorações e essa funcionalidade pode demorar mais do que o cliente espera. Porém um grande débito técnico está sendo retirado do projeto. É claro que tudo tem que ser devidamente priorizado junto ao cliente, pois o objetivo maior é atendê-lo. Entretando, cuidado para que esse débito não caia em esquecimento!

Para nos tornarmos bons, devemos conhecer os os nomes de Gurus e saber o que eles fazem. Como por exemplo Martin Fowler, Kent Beck dentre tantos outros. Atualmente estou lendo o livro “Secrets of the Rockstars Programmers” em que Ed Burns entrevista Rod Johnson. Interessante que ambos dizem amar as variáveis dos códigos com nomes longos. Eles citam que já que IDE’s tem auto-completar, porque não usar nomes longos? Rod diz usar nomes com até 50 caracteres!

Se você precisar dar manutenção em um projeto que não tem testes automatizados, refatore (e teste muito)! Nesse caso é recomendado é ler o livro: 

Working Effectively with Legacy Code

Por outro lado, havendo testes, faça as devidas refatorações e dispare-os para ver se nada foi quebrado. 

Antes de qualquer alteração, seja correção ou algo novo, faça os testes automatizados (TDD). Não contribua para a incerteza desse projeto e invista na confiança no seu trabalho. Pelo menos o que você fizer tem que ter teste automatizado.

Independentemente do trabalho que estivermos fazendo, temos que nos responsabilizar por ele. Mesmo que não formos o autor do projeto, se o código não estiver claro, é nossa obrigação alterá-lo para que se torne mais legível. Fazendo isso, gastaremos um pouco mais de tempo fazendo as refatorações. Porém se precisarmos alterá-lo novamente é certo que a produtividade será absurdamente maior.

Uma frase muito interessante de Martin Fowler:

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

-Martin Fowler et al, Refactoring: Improving the Design of Existing Code, 1999″

Gosto muito, e recomendo a leitura do artigo “Machucando o código por diversão e lucro“.

O Urubatan escreveu um artigo muito interessante sobre Refactoring e outro sobre “Comentário no código é para os fracos”

O Papo também escreveu algo relacionado aqui.

Se você quer gosta de Ruby e quer se tornar um expert sobre testes, sigam os excelentes artigos da Improve It. Já tem esse e esse.

Publicado em arquitetura. 1 Comment »

Livre-se do seu Débito

Technical debt (Débito Técnico) é um problema muito comum nos times de desenvolvimento de Software. Geralmente é algo muito simples que você deixa pra resolver depois, porém pode cair no esquecimento e é aí que o débito pode se transformar em um grande problema.

Os débitos técnicos afetam tudo em nosso trabalho pois mata a produtividade, cria defeitos, prejudica a leitura do código e muito mais. Esses problemas são muitas vezes visíveis somente pelos desenvolvedores, portanto não devemos esperar ordens para tirá-los da prateleira. Débitos devem ser mensurados e priorizados junto às outras tarefas.

Um dos débitos mais comuns são as duplicações de código. Refatore o código o quanto antes!. Outra situação, se você cair na necessidade de duplicar o código, então está na hora de melhorar seu design. Devemos seguir sempre o DRY.

Os times ágeis costumam usar cartões nos quadros para que os débitos técnicos estejam sempre visíveis e não caiam no esquecimento. Aproveite a dica. 🙂

Arquitetura do LinkedIn

Esse post é baseado em um artigo do Hurvitz sobre a arquitetura do LinkedIn.

No JavaOne 2008, funcionários do LinkedIn apresentaram duas sessões falando sobre a arquitetura desse site de relacionamento. Podemos ver os vídeos nos links abaixo:

 

O LinkedIn é um dos maiores sites de relacionamento na Internet. Trata-se de uma aplicação Web 2.0 em Java.

Algumas Estatisticas:

 

  • 22 milhões de membros
  • 4+ milhões de únicas visitas por mês.
  • 40 milhões de visualizações por dia.
  • 2 milhões de buscas por dia.
  • 250K de convites enviados por dia.
  • 1 milhão de respostas postadas.
  • 2 milhões de mensagens de e-mail por dia.
Software:
  • Solaris (rodando em plataforma Sun x86 e Sparc)
  • Tomcat e Jetty como servidores de aplicação.
  • Oracle e MySQL como DBs
  • Não usam ORM (como o Hibernate); they use puro JDBC
  • ActiveMQ para JMS. (Particionado pelo tipo de mensagem. Usando MySQL.)
  • Lucene como uma engine de buscas.
  • Spring entre as camadas.
Arquitetura do Servidor
(2003 a 2005)
  • Uma aplicação monolítica.
  • Um único banco de dados (Core Database)
  • O gráfico de rede cacheada em memória no The Cloud
  • Busca de membros usando Lucene. Rodando na mesma máquina que o The Cloud, porque a busca de membros deve ser filtrada de acordo com a busca do usuário de rede.
  • Aplicação Web atualizando o banco de dados principal (Core Database) diretamente. O Core Database atualiza o The Cloud.
( 2006 )
  • Adicionado réplicas de banco (Replica DB’s) para reduzir a leitura no Core Database. As réplicas contém dados read only. Um servidor RepDB gerencia atualizações das réplicas.
  • Movido a engine de Buscas para fora do The Cloud, passando a ser contido em seu próprio servidor.
  • Mudou o lugar onde as atualizações eram feitas, adicionando o Databus. Este é um componente central que distribui atualizações na base. O novo fluxo de atualizações seria:
  1. Alterações originadas da Aplicação Web.
  2. A Aplicação Web atualiza o Core Database
  3. O Core Database envia atualizações para o Databus
  4. O Databus envia as atualizações para: O Replica DB’s, para o The Cloud, e para a engine de Busca
( 2008 )
  • A Aplicação Web não faz mais nada por si só. Ela divide parte das lógicas de negócio em Serviços.
  • A Aplicação Web se torna apenas uma GUI (interface) para o usuário, mas agora ela chama serviços para manipular Perfil, Grupos, etc.
  • Cada Serviço tem seu próprio banco de dados de domínio, ou seja, são partições verticais.
  • A arquitetura permite outras aplicações (através da aplicação Web) acessar o LinledIn. Permite adicionar aplicaçnoes para Recrutar pessoas, adicionar pessoas, etc.
Mais detalhes sobre o The Cloud pode ser encontrado no blog citado no início. O Cache foi implementado em C++ em vez de Java por duas razões.
  • Usar o mínimo de RAM possível.
  • Pausas de Garbage Collection estavam matando eles. [Porém o GC está sendo melhorado desde 2003, isso ainda seria um problema hoje?]
Aparte eles usam o Ehcache para cachear perfis de membros (cerca de 22 milhões de membros). Eles tentaram cachear usando o algoritmo LFU (Least Frequently Used), mas consideraram que o EHcache as vezes bloqueava por 30 segundos para recalcular o LFU, então eles mudaram para o LRU (Least Recently Used)
Arquitetura de Comunicação:
Esse serviço é referente a mensagens permanentes, por exemplo mensagens e e-mail.
  • O sistema inteiro é assíncrono e usa JMS pesadamente.
  • Clientes postam mensagens via JMS
  • Mensagens são depois roteadas via um roteador de serviços para o mailbox apropriado ou diretamente para o processo de email.
  • Entrega de mensagens> tanto Pull ( clientes solicitam suas mensagens), ou Push (envio de email)
  • Eles usam o Spring, com o proprietário LinkedIn Extenção Spring. Usam HTTP-RPC.
Serviço de Actualização na Rede:
Esse serviço é responsável por notificações de vida curta, por exemplo atualização de status de seus contatos. O LinkedIn trabalhou em três grandes interações, detalhadas no blog no início.
Alguns aspectos interessantes da aplicação:
  • Mais de 6500 testes de unidade e de integração.
  • 500 Html Unit testes.
  • Uso de integração continua (Hudson).
  • Uso de EasyMock.
A apresentação termina com algumas dicas sobre escalabilidade. São velhas mas boas:
  • Não pode usar somente um banco de dados. Use muitos banco de dados, particionados horizontalmente e verticalmente.
  • Por causa da partição, esqueça sobre integridade referencial ou cruzamento de domínios com JOINs.
  • Esqueça sobre 100% de integridade de daos.
  • Em larga escala, custo é um problema: hardware, bases de dados, licenças, armazenamento, poder.
  • Depois que você for grande, spammers e scrapers vêm atrapalhar.
  • Cache!
  • Usar fluxos assíncronos.
  • Relatórios e Análises são um desafio, considere eles quando desenhar o sistema.
  • Espere que o sistema falhe.
  • Não subestime a sua trajetória de crescimento.

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.