Caelum | Ensino e Inovação - Cursos de Java, Scrum, Ruby on Rails


Um produto para muitos clientes: implementando multitenancy

Por Guilherme Silveira em 23/08/10

São diversos as aplicações web disponíveis, como quadros eletrônicos, sistemas de tracking, email e aplicações para empresas, ou até mesmo controle de clientes e vendas. Eles até ganharam um pomposo nome dentro do cloud computing: Software as a Service (SAAS).

O que essas aplicações possuem em comum? Todas elas atendem diversos clientes sem que um tenha conhecimento da existência dos outros.

Em um post recente no Tectura.com.br foram discutidas vantagens e desvantagens de diversas abordagens para produtos com a necessidade de suportar mais de um tenant.

A Microsoft categoriza três tipos de abordagens dependendo do nível de compartilhamento de recursos entre os clientes e apresente um relatório onde analisa os custos contra a segurança.

Em um extremo, nada é compartilhado entre cada cliente: para cada nova conta criada dentro de seu serviço, é criada uma nova máquina na cloud e uma instalação limpa é executada, com seu próprio banco.

Dois servidores, dois bancos

Nessa abordagem p processo de autorização é automático, um cliente é criado através da instalação automática de uma nova máquina virtual e os recursos são compartilhados se o ambiente for uma cloud.

Na outra ponta, tudo é compartilhado: novos clientes são inseridos no mesmo conjunto de máquinas e a instalação é feita através de um simples insert no banco, adicionando um novo cliente. Qual abordagem escolher?

Um servidor, um banco

A autorização é feita programaticamente na aplicação, os recursos são compartilhados entre todas elas, escala-se tipicamente através do uso de load balancers e replicação master/slave e a cada novo cliente basta executar um insert no banco.

Em qualquer abordagem onde o banco seja compartilhado por diversas empresas, precisamos garantir a segurança dos dados de cada uma, para que nenhum acesso indevido ocorra, trazendo preocupações de autorização para dentro do código de nossa aplicação. Caso seja criado para cada cliente um banco em uma máquina no cloud, essa questão fica concentrada em um único ponto da arquitetura, a segurança está implícita.

Dois servidores, um banco

Diversos servidores e um banco implica em clientes não afetarem uns aos outros, um controle programático de autorização e novos clientes são criados a partir da instalação de um novo contexto web.

Ao mesmo tempo, o processo de customização de seu serviço por cliente também é afetada de acordo com o nível de compartilhamento de dados entre eles. Na abordagem onde os clientes compartilham o mesmo servidor web, a customização é uma preocupação de nossa aplicação, enquanto ao utilizarmos aplicações web distintas para cada cliente, podemos facilmente separar customizações por instância, por deploy efetuado.

A escalabilidade é afetada pois compartilhar recursos em memória na camada web, entre eles dados cacheados do banco ou filas, permitem diminuir o tempo de processamento ou de latência, aumentando o número de requisições suportados.

O processo de escalar também está diretamente ligado: quando uma máquina não aguentar mais as requisições, será levantada uma outra máquina que funcionará em cluster para um, n ou todos os clientes?

Outro fator importante é como limitar o uso do serviço de acordo com as regras contratadas pelo cliente e controlar o dano que um pode causar a outros. Em um pico de uso por parte dos usuários de um cliente, ativamos o chargeback, dependendo do tipo de serviço que é prestado. Implementações comuns de chargeback em cloud (baseados em virtualização) permitem um pico de consumo temporário ou até mesmo mover a aplicação de uma máquina para outra sem que cliente algum perçeba o que está acontecendo.

Dois servidores, um banco

Diversos servidores e um banco implica em clientes não afetarem uns aos outros, um controle programático de autorização e novos clientes são criados a partir da instalação de um novo contexto web.

Ao invés daqueles usuários atrapalharem a performance de outros clientes, é alocado memória e processador distintos dos atuais para ele.

A simplicidade do código é um dos fatores mais afetados pela escolha feita: um código que sabe da existência de múltiplos clientes está diretamente ligado a uma chave que identifica o cliente atual. E essa ligação se reflete por todos os lados do código, uma vez que o comportamento da aplicação em geral depende dele: uma chave estrangeira, um relacionamento, que vai permear toda a aplicação.

Caso a aplicação implemente o suporte a multi tenant através de instalções web distintas, com configurações para cada cliente apontando para um bancos distintos, o código fica mais simples e fácil de manter, uma vez que não é necessário se preocupar com a existência de outros clientes.

Por fim, uma preocupação que surge para a empresa provedora dos serviços é de como agrupar os dados existentes em todos os clientes e gerar relatórios de administração e estatísticas que permitam a melhora do serviço prestado. Quando possuimos um único banco, escalado através de master slave ou múltiplos nós, basta executarmos queries longas (sql ou não), mas se o sistema estiver distribuido entre diversos bancos sem ligação entre si, um processo de batch deve rodar para agrupar os dados e permitir o consumo posterior pelas ferramentas de BI.

É importante analisar todos esses pontos antes de tomar a decisão se o controle de espaço de aplicação por cliente será feito no nosso código, na camada web, no banco ou em algum outro ponto. Como discutimos bastante no curso de arquitetura, toda e qualquer decisão implica em um tradeoff: o importante é saber o que está sendo trocado e qual é o impacto disso.

  • Share/Bookmark

O cloud computing é inevitável?

Por Paulo Silveira em 28/10/09

A Caelum.com.br roda agora no cloud. Por que? Vale a pena? Temos tantos acessos assim?

Entre os exemplos clássicos do bom uso do cloud temos o sucesso do New York Times: eles conseguiram digitalizar em PDF mais de 4 terabytes de edições do jornal muito antigas (1851-1922) usando 100 máquinas e gastando apenas 240 dólares! O cloud é perfeito para situações em que você precisa de muito desempenho e recursos por um curto período de tempo.

Essa definitivamente não é a necessidade da Caelum em rodar em um cloud. Qual seria então?

Temos várias opções para você ter seu próprio cloud: através do TerraCotta, pelo VMWare ou pelo Grid Gain. Usando esses softwares você mesmo precisa prover sua infraestrutura de máquinas, o que pode ser trabalhoso. Um nível mais abstrato e encapsulado seria usar o EC2 da Amazon: ele já fornece as máquinas, facilitando muito a manutenção, e cobra por tempo de processamento (eliminando o gasto inerente ao tempo ocioso que suas máquinas poderiam ficar). No Brasil, a Locaweb foi a pioneira ao oferecer esse serviço.


cloud locaweb

Por último, temos hosts que já provêm tudo para você: a infraestrutura e o software para permitir a escalabilidade. O Google App Engine é um desses hosts.

Aqui, sim, entra a Caelum e entra também o interesse das empresas de hosting: se o cloud é mais barato para os fornecedores por compartilhar recursos ociosos, e ainda oferece altíssima escalabilidade e disponibilidade para os clientes, temos então um casamento de interesses. Essa relação ganha-ganha fortalece muito a tendência da adoção de uma arquitetura mais elástica. Os clientes também ganham uma enorme independência de hardware, terceirizando os grandes problemas que acabam acontecendo quando temos servidores dedicados: esses servidores não são à prova de bala. Para a Caelum, mesmo sem necessidade de rodar em 100 servidores, é interessante ter o poder de fazer o deploy de nossas aplicações Web sem ter de nos preocupar se “é suficiente X micros e Y de RAM para esta aplicação?”, por menor que ela seja.

O site da Caelum usava VRaptor 3 como controlador, JSP e taglibs na visualização e Hibernate para persistência. Para colocá-lo no cloud do Google App Engine, a única grande mudança que fizemos foi migrar a camada de persistência para o BigTable. Trata-se de um banco de dados não relacional proprietário, que possui uma API específica, mas também há uma implementação (não completa) de JPA para ele, o que facilita bastante a migração.

Desvantagens? O Google App Engine ainda está em beta, e já ficou fora do ar alguns momentos, em especial o serviço do DataStore (o BigTable). Há também o problema do cold start e do plugin para o Eclipse, que é um pouco lento para iniciar o servidor de testes: falaremos com detalhes sobre ambos em um outro post. Outra desvantagem é a dependência ao BigTable: como é um serviço proprietário, migrar para qualquer outro cloud vai necessitar de ajustes, mesmo que usando a JPA.

Respondendo o título do post: independente se o seu cloud é Amazon EC2, Google App Engine ou GridGain, algo é certo: ele traz vantagens tanto para os desenvolvedores quanto para o host, mostrando que a estratégia do cloud está cada vez mais solidificada.

Agradecimentos ao Pedro Matiello, Guilherme Silveira e Sérgio Lopes pela migração do site. Em breve, postaremos detalhes de como trabalhar na Google App Engine. Teremos uma palestra sobre Cloud computing no CaelumDay no Rio de Janeiro, esse 7 de novembro!

  • Share/Bookmark

Fractais e Caos em cloud computing com o Google App Engine

Por Guilherme Silveira em 03/09/09

Estudando métodos matemáticos no curso de matemática aplicada, fiz minha iniciação científica sob supervisão do professor Eduardo Colli, em um trabalho de conclusão de curso sobre sistema dinâmicos discretos, que implementei em Java. É na área de sistemas dinâmicos – em geral não lineares – que começaram a falar sobre o comportamento caótico de determinadas partículas (uma área próxima a teoria do caos). Um dos sistemas dinâmicos mais estudados é o atrator de Lorenz.

atrator de Lorenz

Aproveitando que esses algoritmos são paralelizaveis, resolvemos colocar o Google App Engine em uma prova de fogo, e tirar proveito da plataforma em nuvem para gerar os gráficos mais rapidamente, como num cluster: não só usar o Google App Engine para escalabilidade e disponibilidade, mas sim também para performance. O principal procedimento do algoritmo consiste em iterar uma mesma função na ordem de bilhões de vezes. Isto é, na versão tradicional em uma única thread, existe um ou mais loops parecidos com esse pseudo código:


for(int tempo = 0; tempo < 60*60; tempo++) {
  for(int inicial=0;inicial < 1000; inicial++) {
    x0, x1 = condicaoInicial[inicial];
    for(int iteracao = 0; iteracao <= 100000; iteracao++) {
      x0, x1 = recalcula(x0, x1);
    }
  }
}

O código acima está bem mais simples do que o código real, mas ilustra o quão lento seria a execução do mesmo. Um exemplo comum de execução deste software envolve encontrar a bacia de atração de um atrator, tarefa que demorava entre 2 a 5 minutos para o conhecido mapa de Henon.

Em nossas máquinas, um diagrama de bifurcação de Henon, como mostrado a seguir, demorava cerca de 5 minutos para ser computado:

diagrama de bifurcação do mapa de henon

Cada um desses desenhos pode parecer rodar em um período razoavelmente curto de tempo, mas uma vez que o mapa de Henon depende de 2 parâmetros, se eu desejar varrer um desses parâmetros (considerando ele o tempo) e criar um vídeo do mesmo com duração de 1 minuto e 24 frames por segundo, o tempo médio seria de 3 horas. Em situações reais, com outros atratores, chegamos a criar filmes que demoraram mais de 7 horas para serem criados.

Para tornar esse software paralelizável, a nova aplicação possui três partes: um cliente que vai iniciar o pedido, um servidor que recebe esse pedido e dispara diversas requisições para o cloud, onde cada máquina vai gerar um pouco do gráfico.

O cliente pede para o servidor as cores que deve usar para pintar uma imagem via ajax/jsonp. A aplicação do servidor foi implementada em rails, esta dispara diversas requisições em paralelo para os servidores do Google App Engine utilizando uma API de IO não blocante, similar ao NIO no Java. Já os nós do cloud rodam Java no Google App Engine, basicamente com o código do software original. Após a execução cada nó devolve os números para o servidor ruby, que vai agregando os resultados conforme eles vão sendo processados, para gerar a imagem final.

Os testes iniciais mostraram redução de até 51 vezes no tempo de execução por executar o programa em 100 requisições paralelas ao Google App Engine:

100 iterações por requisição

Requisições Tempo total (segundos) Tempo por iteração (milisegundo)
local 6 16.6
1 3 33
10 6.4 156.25
50 14 357
100 17 588
200 23.55 849

Usando o memcached, que é o cache fornecido pelas APIs do Google App Engine, temos um ganho ainda maior:


CacheFactory cacheFactory =
       CacheManager.getInstance().getCacheFactory();
cache = cacheFactory.createCache(Collections.emptyMap());
String result = (String) cache.get(script);
if (result != null) { ... }

Após essas modificações, o nosso ganho é de um desempenho 93 vezes superior por rodar a aplicação em ~100 máquinas!

Podemos ainda melhorar esses nuúmeros se tirarmos ainda mais proveito da infraestrutura que o HTTP e a rede nos fornece: proxies. Configurando adequadamente o proxy de sua empresa o conteúdo cacheado de diversas requisições não expira até a requisição seguinte ocorrer, obtendo resultados assustadoramente rápidos. Não é a toa que os gurus dos webservices RESTFul consideram o Squid uma excelente opção no lugar de um gigante ESB.

hiperboloide

Para quem se interessar, existem diversos outros exemplos de mapas 2d de sistemas dinâmicos de relativa fácil implementação.

Processos em batch e relatórios são exemplos de tarefas que costumam demorar bastante no mundo corporativo, e muitos deles poderiam ser quebrados em tarefas paralelas. Nós programadores estaremos em breve rodando muitas das nossas aplicações no cloud: não só porque é uma maneira de ter mais escalabilidade e disponibilidade, mas também porque esse tipo de infraestrutra elástica diminui muito os custos operacionais das grandes empresas de hosting. Saber tirar proveito disso vai ser papel fundamental de todo programador. Bancos de dados não relacionais e linguagens que facilitam o trabalho da programação concorrente tornam-se excelentes nesse cenário.

  • Share/Bookmark

OndeTrabalhar.com – os bastidores desse projeto Rails

Por Cauê Guerra em 05/08/09

Desde que lançamos o OndeTrabalhar.com muitas pessoas tem nos perguntado sobre quais tecnologias foram utilizadas para construí-lo, e aqui vamos falar um pouco sobre o projeto, seu ambiente e suas gems.

O OndeTrabalhar.com está implantado em um servidor rodando Passenger com Ruby Enterprise Edition, utilizando o httpd. Ainda utilizamos o plugin ExceptionNotifier, para sermos avisados por email sempre que um erro 500 ocorre, e a gem request-log-analyzer, que analiza o arquivo de log e consegue mostrar estatísticas bem interessantes sobre o uso do sistema.

Utilizamos também as bibliotecas javascript prototype e script.aculo.us, criamos muitas rake tasks para automatizar diversas tarefas e criamos algumas regex bem malucas (com a ajuda da ferramenta Rubular, que permite que testemos uma regex muito facilmente).

Abaixo, segue a lista das principais gems e suas funcionalidades:

Além deles, ainda utilizamos algumas gems para conseguirmos fazer nossos testes, que cobrem exatos 95% do nosso código, uma excelente medida:

Como todo projeto, aprendemos novos detalhes e problemas do dia a dia que enfrentamos com Rails. Adicionamos o resultado dessa experiência no nosso curso de rails, e os detalhes mais avançados estão em um novo, que será lançado em breve.

  • Share/Bookmark

Pequenos objetos imutáveis e Tiny Types

Por Jonas Abreu em 20/07/09

Uma das grandes preocupações que temos quando estamos desenvolvendo aqui na Caelum e nas nossas consultorias é como manter o código o mais expressivo possível. Expressividade está muito ligada a uma manutenabilidade maior do código, porque código mais fácil de entender costuma ter menos bugs. Uma das técnicas que usamos pra atingir esse objetivo são os Tiny Types.

Dêem uma olhada no código do seguinte método:


public Projeto criaProjeto(String nomeProjeto,
                           String descricaoProjeto) {
       // Executa a lógica de criação do projeto
}

Digamos que esse seja um factory method para criação de Projeto. Ele seria usado da seguinte forma:


new FabricaDeProjeto().criaProjeto("nome", "descrição");

Mas nada impede que esse método seja chamado dessa forma:


new FabricaDeProjeto().criaProjeto("descrição", "nome");

Notaram a diferença sutil na ordem dos parâmetros? Isso vai acontecer? Provável. Imaginem o tempo gasto com debug para corrigir esse problema? Não seria melhor aproveitarmos a tipagem explícita do Java para pegarmos esse problema em tempo de compilação? Ficaria assim:


public Projeto criaProjeto(Nome nomeProjeto,
                           Descricao descricaoProjeto) {
       // executa a lógica de criação do projeto
}

Ganhamos checagem em tempo de compilação. Mas apenas isso? Vamos olhar como esse método seria usado:


new FabricaDeProjeto()
     .criaProjeto(new Nome("nome"), new Descricao("descrição"));

O código ficou mais expressivo. Fica bem claro que estamos passando o Nome do projeto e não algo genérico. Com isso, fica muito mais difícil confundir o que passar em cada parâmetro. Estamos fazendo uso do sistema de tipos explicitos do java para evitar problemas. Além disso existem mais vantagens, embora um pouco mais sutis.

Com esse novo design do código, temos uma melhor divisão de responsabilidade. Por exemplo, não queremos permitir que o nome do projeto possua números. Como faríamos isso na primeira forma? Colocaríamos a lógica de validação dentro do método criaProjeto. Agora não precisamos fazer isso. Podemos colocar a lógica de validação dentro do objeto Nome, que é o objeto responsável por tudo relativo à Nome. Colocamos a funcionalidade no lugar onde ela deve ficar. Dividimos melhor a responsabilidade porque podemos adicionar métodos em Nome.

Mas temos um pouco mais de trabalho pra fazer isso, correto? Será que o código não vai ficar lento porque estamos criando vários objetos apenas para encapsular Strings? Não. A forma como o Garbage Colector trabalha, coletando os objetos que devem ser mantidos em memória (e não o contrário) simplesmente não vai ser afetada pela criação dos novos objetos, que possuem vida bem curta.

Podemos ir ainda mais longe. Podemos fazer com que esses pequenos objetos sejam imutáveis. Se eles forem imutáveis, teremos diversos ganhos. Por exemplo, quem estiver manipulando esses pequenos objetos não precisará se preocupar com concorrência. Para objetos imutáveis,
não faz diferença se eles estão em um ambiente concorrente ou não: são thread safe por natureza. Esse ganho é tão importante, que existem linguagens de programação onde você não pode criar “objetos” mutáveis, como Erlang. Além da thread safety, a outra principal vantagem é não termos de nos preocupar com que código de outras pessoas modifiquem nossos objetos, sofrendo efeitos colaterais por causa da invocação de um método e passagem deste objeto como parâmetro. Também não há como nossos objetos ficarem fora de um estado consistente.

Esses pequenos objetos imutáveis podem facilitar muito o desenvolvimento, prevenindo problemas e evitando que por preguiça separemos de forma errada as responsabilidades dos objetos. No começo pode parecer que estamos criando um monte de objetos a mais, mas na verdade não é bem assim. Como esses objetos pequenos possuem responsabilidades bem definidas, fica muito fácil reaproveitar. Imaginem quantos lugares precisam ter descrição? Usaremos o mesmo pequeno tipo. O gasto inicial que temos é bem recompensante conforme o tempo passa.

  • Share/Bookmark



Caelum | Ensino e Inovação
São Paulo: Rua Vergueiro, 3185, cj. 87, próximo ao Metrô Vila Mariana   |   Tel. (11) 5571-2751
Rio de Janeiro: Rua Senador Dantas, 80, cj. 307/308 - Centro   |   Tel. (21) 2220-4156 ou 2297-0033
Brasília: SCS Qd. 8 Bl. B-50, Sala 521 - Ed. Venâncio 2000   |   Tel. (61) 3039-4222