Pesquisar este blog

domingo, 6 de novembro de 2016

Twitter Stream com Akka 2.5 PlayFramework 2.6 no Java

Esta postagem é uma atualização do que foi explicado neste post.

O Twitter é uma rede social que provê um canal de comunicação, por onde podemos nos conectar para acessar os tweets, através de stream. Existem várias aplicações interessantes para esta plataforma, muitas delas envolvendo o uso de inteligência artificial.

Todo o código usado para explicar este tópico está no seguinte repositório.

Para acessar o stream do twitter, vamos utilizar uma lib chamada twitter4j-stream, esta api que irá converter as chamadas do java para a api REST do twitter, e vice e versa. Para adicioná-la ao nosso projeto, basta colocar ela como dependência do build.sbt:

"org.twitter4j" % "twitter4j-stream" % "4.0.2"
Uma outra dependência deste projeto será o playframework que atualmente está na versão 2.5.9.
Como vimos anteriormente, aqui, aqui e aqui, no play, há um arquivo de configuração das rotas, que é responsável por direcionar cada tipo de chamada, em cada caminho, a um método específico de um Controller.
Nosso arquivo de rotas terá o seguinte conteúdo:
# Home page
GET     /               @controllers.ApplicationController.index
GET     /stream    @controllers.ApplicationController.stream(query: String)

# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file)
Ou seja, teremos dois métodos get, um stream, que recebe um parâmetro chamado query, que será responsável por filtrar os tweets, e um método chamado index, que na verdade só fará um redirecionamento pro método stream, passando os atributos ruby e java.
A última dependência que teremos neste projeto, é o Akka, que é uma implementação do modelo de atores em Java e Scala.

Acessando o stream do twitter.

O primeiro passo para conseguir processar um stream do twitter, é criar uma app nele, e isto pode ser feito na página de dev do twitter. Lá existem instruções de como criar a sua app.
Depois de criada, você terá disponível 4 chaves, 2 de Consumer (Key e Secret) e 2 de Acesso ( AccessToken e AccessTokenSecret). Este tipo de autenticação obedece ao padrão OAuth.
Estas configurações serão feitas usando os seguintes parâmetros:
private Configuration config;
private ConfigurationBuilder configBuilder = new ConfigurationBuilder();
public StatusListener statusListener;
public TwitterStream twitterStream;
FilterQuery query;
Config é onde iremos colocar as configurações, o configBuilder é quem irá criar as configurações e o twitterStream é o objeto que se conecta ao twitter e traz os tweets.
A configuração será feita, desta maneira:
configBuilder.setOAuthConsumerKey("consumer_key");
configBuilder.setOAuthConsumerSecret("consumer_secret");
configBuilder.setOAuthAccessToken("access_token");
configBuilder.setOAuthAccessTokenSecret("token_secret");
config = configBuilder.build(); 
twitterStream = new TwitterStreamFactory(config).getInstance();
 Agora temos uma instância do twitterStream e é ele que iremos usar para se conectar à api do twitter, aliás, isto já foi feito, quando usamos o método getInstance();
O próximo objeto que iremos usar aqui é o SearchQuery, este objeto recebe os valores que queremos usar para pesquisar os tweets, no nosso exemplo será usado apenas a pesquisa por palavra, ou seja, uma palavra que queremos que esteja presente em todos os tweets. Existem outras formas de se pesquisar os tweets, como a localização (através da latitude e da longitude), a língua do tweet e os usuários que queremos seguir.
A construção deste objeto é  feita da seguinte maneira:
FilterQuery query = new FilterQuery().track(new String[]{searchQuery});
Depois é só adicionar o objeto de query ao twitterStream:
twitterStream.filter(query);
Agora todos os tweets serão filtrados de acordo com a nossa query, que no caso será por palavras. O último objeto que será utilizado é o StatusListener, este objeto escuta os eventos do stream, e quando um tweet chega, um método dele será chamado. Este objeto segue o padrão de listener, que é o mesmo utilizado pela api Swing do Java, e ele funciona exatamente da mesma maneira. Este objeto foi criado da seguinte maneira:
           public void onStatus(Status status) {
                Logger.info(status.getUser().getName());
                StringBuilder mensagem = new StringBuilder();
                try {
                    mensagem.append(status.getUser().getName());
                    mensagem.append(":");
                    mensagem.append(new String(status.getText().getBytes(), "utf-8"));
                    mensagem.append("\n");
                } catch (Exception e) {
                    e.printStackTrace();

                }
                socketActor.tell(ByteString.fromString(mensagem.toString()),
                        ActorRef.noSender());
            }
Aqui é a prirmeira vez que temos contato com um ator, o socketActor, mas o seu papel no sistema será explicado mais tarde.
Repare que o StatusListener possui um método chamado onStatus, que está sendo sobrescrito aqui, na verdade existem outros métodos do StatusListener que deverão ser sobrescritos, mas neste caso de uso, somente o onStatus é que interefere no comportamento do aplicativo e por isto, somente ele será descrito.
Logger.info é uma forma de escrevermos no log da aplicação. Repare que estamos escrevendo lá o nome dos usuários dos tweets recebidos.
O próximo passo agora é pegar os dados do tweet, que neste caso são o texto e o nome do usuário. Todos estes dados serão enviados para o ator. Repare que estamos usanso um cast (Object) para objeto, logo logo isto será explicado.
Depois de configurar o Listener, deve-se adicioná-lo ao twitterStream:
twitterStream.addListener(statusListener);

Akka e os atores

O modelo de atores é um modelo que podemos chamar de "bem simples". Trata-se de uma estrutura, onde criamos os atores, e estes atores são programados para processar um tipo de mensagem. As mensagens, como padrão, são classes static, que podem, ou não, possuir atributos. Os atores, não tem estado, ou seja, não possuem um parametro dentro dele que pode ser alterado pelo processamento de uma mensagem. 

No Akka, existe uma classe chamada ActorSystem, e é esta classe que controla quais e quantas instâncias de atores estão disponíveis para serem utilizados. Quando um ator recebe uma mensagem, ele irá alocar uma instância de um ator daquele tipo, para processar aquela mensagem. Enquanto isto, se outras mensagens chegarem para serem processadas, elas ficarão armazenadas em uma fila, e é o sistema de atores que controla isto, até que haja um ator disponível para processar aquela mensagem. 

Existem duas formas de se mandar uma mensagem para um ator, o tell e o ask. Ao chamar o método tell, devemos possuir uma lógica de chame e esqueça, o ator deverá conter toda a lógica, responsável por processar a mensagem, e nenhuma resposta será esperada deste método. Já ao chamar o método ask, a resposta que o ator devolve, é uma classe chamada Future, esta classe não contem em si o resultado, mas sim um link para quando ele estiver disponível. 

Ao chamarmos o método ask estamos usando um método assíncrono, por isso, o código continua a rodar, e quando houver uma resposta, ou seja, quando o ator tiver processado a mensagem, podemos acessá-la através de alguns métodos como o map, o foreach, o onSucess, etc.

A única chamada que fazemos ao nosso ator é do tipo tell, já que só devemos enviar os tweets recebidos, para que o ator repasse ele para a interface UI. Esta chamada é feita dentro do método onStatus no StatusListener. Para criar o sistema de atores e o nosso ator, usamos o seguinte código:

private static ActorSystem system = ActorSystem.create("mixedTweets");
    private static Materializer mat = ActorMaterializer.create(system);
    static final Pair<ActorRef, Publisher<Object>> ti =
            Source.actorRef(100, OverflowStrategy.fail()).
                    toMat(Sink.asPublisher(AsPublisher.WITHOUT_FANOUT),
                            Keep.both()).run(mat);
    static ActorRef socketActor = ti.first();
    static Publisher<Object> publisher = ti.second();

A criação do ActorSystem é bem simples, basta chamar o método create e dar um nome ao sistema.

Repare que todos os parametros daqui são static, isto é feito para evitar que tenhamos mais de uma instância destes parametros rodando na jvm, já que podemos ter várias instâncias de um controller rodando em um sistema. Caso haja mais de um sistema de atores, vai ser impossível garantir quem enviou e quem recebeu as mensagens.

O Materializer é uma fábrica que faz com que os streams do Akka rodem, e ele é utilizado ao final da linha que cria o Ator e o publisher. Para criar esta dupla, usa-se um Source, que é a fonte. Para criar ela, dizemos que a fila máxima de mensagens na fila é 100 e que o sistema irá falhar, caso este número exceda 100 (overflowstrategy.fail()). 

Depois esta fonte é convertida em um Materializer, e nesta conversão fazemos uma sincronização com o Publisher, que é o objeto que irá publicar os dados das mensagens recebidas pelo ator. Finalmente chama-se o método run, que vai rodar o sistema pelo Materializer.

Repare que o Objeto retornado é um ActorRef, que é a instância do nosso ator, e um Publisher<Object>, é por isto que estamos fazendo o cast da mensagem para Objeto, para que o sistema entenda que a mensagem é um Object, e ela será publicada em um publisher de Objeto. Se não fizer esta conversão, o sistema não funciona devido aos tipos diferentes.

O que fizemos aqui, foi dizer que as mensagens recebidas pelo ator, serão publicadas para a nossa interface, mas antes disto devemos ter uma resposta do método que le os streams para a interface, usando o publisher.

Controller

O método que le os streams, e retorna os valores para a interface é:
public play.mvc.Result stream(String queries) {
        Source<Object, ?> source = Source.fromPublisher(publisher);
        final Source<ByteString, ?> eventSource = source.map(ev ->
        {
            return ((ByteString) ev);
        });
        list.listenAndStream(queries, socketActor);
        list.twitterStream.addListener(list.statusListener);

        return ok().chunked(eventSource);
    }

O método listenAndStream, retorna um Source do publisher e este Source (fonte) será utilizado para repassar os eventos para a interface.

Ao criar o objeto eventSource, estamos convertendo as mensagens recebidas em eventos, eve é uma String. Isto está sendo feito utilizando o lambda do Java 8, que é uma forma de programação funcional e já foi explicada aqui.

O último passo deste método é retornar uma resposta ok, chunked, que nada mais é do que em partes, já que o fluxo do stream é contínuo, devemos manter aberta a conexão entre o browser e o controller, para que seja possível continuar a enviar os dados para o usuário. Esta resposta chunked vai ser realizada através de um Fluxo de Eventos EventSource.flow() e ela vai retornar uma mensagem Http.


sábado, 10 de setembro de 2016

Exemplo prático de banco de dados NOSql MongoDb no Java

Há alguns anos vem ganhando força, no mundo dos sistemas distribuídos, bancos de dados alternativos ao SQL, com isso surgiu o movimento de NoSQL( Not only SQL, ou não apenas SQL) que, ao contrário do que parece, não é um movimento de abolição do SQL, mas sim um movimento de utilização de alternativas, no armazenamento de dados. Os bancos de dados relacionais SQL são muito bons para armazenar alguns tipos de informação, como dados de estoque, cadastro de usuários, transações bancárias, porém com outros tipos de dados ele não performa muito bem, como armazenamento de imagens, páginas html e sistemas que possuem um fluxo de entrada de dados muito grande.

Classificação

Os bancos NoSQL podem ser classificados em:

Colunar

Estes tipos de bancos armazenam os dados de cada coluna separadamente, diferente do tipo de armazenamento padrão, onde cada linha é armazenada em um mesmo arquivo. Assim ao se fazer uma query selecionando algumas das colunas da sua tabela, o banco vai ter que ler somente aqueles arquivos que contém os dados das colunas e não precisará aplicar nenhum filtro.
Já em um banco comum ele deverá ler o arquivo com os dados de todas as colunas e depois aplicar um filtro para excluir os dados das colunas que não estão na query.
Performa muito bem em casos de queries que selecionam poucas colunas de tabelas grandes ou com muitas colunas e eles geralmente são usados em sistemas de BI.

Documental

Bancos de dados documentais são aqueles que armazenam documentos, ao invés de linhas em tabelas.
Estes bancos geralmente são schemaless, ou seja, não possuem esquema. Assim é possível inserir diversos tipos de dados diferentes, por exemplo, usuários, endereços, etc, em um mesmo local, ou com uma mesma definição de dados. Podemos ter um local de armazenamento chamado usuario, que recebe valores dos usuários, assim como valores do endereço.
Neste tipo de banco os dados armazenados pordem ser javascripts, JSONs, XML, etc e eles possuem diversas apis para se conectar com eles, REST, HTTP, Java, PHP, Python, C, etc.

Chave Valor ou Tuplas

Chave Valor é um tipo de armazenamento que funciona de maneira parecida com os mapas das linguagens de programação. Cada valor armazenado sempre terá uma chave, que será utilizada para recuperar aquele valor, e os dados.
Existem uma série de exemplos destes tipos de bancos, inclusive alguns que armazenam os dados apenas na memória das máquinas servindo como caches das aplicações.

Bancos de Grafos

Bancos de dados de grafos, armazenam os dados na forma de grafos e são muito úteis para armazenar a ligação entre os dados.
Este tipo de banco é muito utilizado para armazenar dados de redes sociais, por exemplo, um usuário X tem amizade com Y, Z e K, em um banco de dados de grafo, os dados dos usuários ficam armazenados, assim como a ligação entre eles e percorrer a distância entre um usuário e outro pode ser feita de uma maneira bem fácil.

Exemplo de NoSQL

Este post traz um exemplo de como usar um banco de dados NOSql em um sistema distribuído, usando como exemplo o MongoDb em um projeto Java.
O MongoDb é um banco de dados opensource que está classificado como um banco de documentos, já que ele armazena JSONs.

Databases

Neste banco temos databases, que são os container físicos dos dados. Cada database contém seus próprios arquivos no sistema der arquivos. Cada servidor pode possuir uma ou mais databases.

Collections

Uma collection é uma coleção de documentos do MongoDb, é o equivalente a uma tabela de um banco de dados SQL. Cada collection existe dentro de uma database e elas não possuem um esquema definido. Os documentos dentro de uma collection podem ter campos diferentes, mas cada um destes elementos geralmente são relativos a um mesmo propósito ou algo similar.

Documentos

Um documento é um par de chave valor, os quais não possuem um esquema, conforme explicado anteriormente, o que significa que documentos diferentes dentro de uma mesma collection podem ter campos diferentes e mesmo os campos iguais podem ser de tipos diferentes de dados.
Um exemplo de objeto armazenado no mongodb é mostrado abaixo:
{
   _id: ObjectId(57d0849edab45f72a3994895)
   titulo: 'Exemplo de MongoDB', 
   descricao: 'MongoDB é uma base NoSQL',
   por: 'Professor Dirceu Semighini Filho',
   site: 'http://www.dirceu.professor.blogspot.com.br',
   tags: ['mongodb', 'banco de dados', 'NoSQL'],
   likes: 100, 
   comentarios: [ 
      {
         usuario:'aluno1',
         mensagem: 'Comentário de exemplo',
         dataCriacao: new Date(2016,8,20,2,15),
         likes: 0 
      },
      {
         usuario:'aluno2',
         mensagem: 'Outro comentário de exemplo',
         dataCriacao: new Date(2016,8,25,7,45),
         likes: 5
      }
   ]
}
_id é um campo hexadecimal de 12 bytes, que deve ser único para cada documento, ele atua como se fosse uma chave primária destes objetos. Caso você não forneça um id o Mongo irá providenciar um, sendo que nesca caso os 4 primeiros bytes serão o timestamp atual, os 3 próximos bytes serão um id da máquina, os próximos 2 bytes serão o id do processo servidor do mongodb e os últimos 3 bytes é um número incremental.

Instalando o MongoDb no Linux

Para instalar o MongoDb no Linux, que seja derivado do Debian, digite no terminal:
 sudo apt-get install mongodb

Depois de instalado, para inicializar o serviço do mongo execute:
 sudo service mongodb start
Agora para executar o mongo basta digitar:
mongo
 Se a instalação ocorreu com sucesso, você irá ver o seguinte no terminal:
MongoDB shell version: 2.6.10
connecting to: test
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
    http://docs.mongodb.org/
Questions? Try the support group
    http://groups.google.com/group/mongodb-user

Comandos úteis do MongoDb

db.help()
Mostra os comandos disponíveis na versão instalada do mongo.
db.stats()
Mostra estatísticas dos dados armazenados no banco.

Considerações sobre como desenvolver um banco no Mongo

Todos objetos que forem utilizados juntos, devem ser armazenados juntos, assim serão executadas menos requisições ao mongo para extrair os dados. Tenha certeza que não serão necessários executar joins para ler objetos do banco.
Dados podem ser duplicados, já que o custo do armazenamento é bem menor do que o custo do processamento dos dados. Porém, não se deve duplicar dados que são constantemente atualizados, já que isso implicaria em múltiplas atualizações para atualizar um mesmo objeto.
Joins devem ser feitos na escrita e não na leitura.
Faça otimizações no seu esquema, para os casos de uso mais frequentes.

Usando o Mongo

Para saber quais são as bases disponíveis na sua instância do Mongo use o comando:
show dbs
Para criar uma base de dados basta usar o comando use, como mostrado a seguir:
use dsd
Repare que caso você liste novamente as bases de dados, esta base dsd ainda não estará na lista de bases, para que isto aconteça você deve ter pelo menos um documento inserido nela, para fazer isto execute:
db.professor.insert({"nome":"Dirceu"})
Com isto criamos uma collection, que é equivalente a uma tabela no mongo, e inserimos dados nesta collection.
O MongoDb suporta os seguintes tipos de dados:

  • String : Este é o tipo de dados mais comum e no Mongo ela dever ser do tipo UTF-8 .
  • Integer : Usado para armazenar números no Mongo, um Integer pode ser 32 bit ou 64 bit depende do seu servidor.
  • Boolean : Armazena valores (true/ false).
  • Double : Usado para armazenar valores decimais.
  • Min/ Max keys : Usado para comparar valores entre o maior e o menor dos elementos BSON.
  • Arrays : Valor utilizado para armazenar mais de um valor, ou uma lista, dentro de uma mesma chave.
  • Timestamp : ctimestamp. Valor muito usado para armazenar quando o dado foi inserido, ou alterado.
  • Object : Usado para armazenar documentos embutidos.
  • Null : Valor de nulidade.
  • Symbol : Semelhante a String, porem é utilizado em linguas que possuem caracteres não UTF-8.
  • Date : Usado para armazenar a data local, no formato UNIX Time. Você pode criar um objeto Date passando pra ele o dia, o mês e o ano.
  • Object ID : Usado para armazenar o ID daquele valor.
  • Binary data : Armazena dados binários.
  • Code : Usado para armazenar código Javascript.
  • Regular expression : Usado para armazenar expressões regulares no banco.
Para listar as collections da base de dados que estamos usando faça:
db.getCollectionNames()
[ "professor", "system.indexes" ]
 Para apagar a sua base de dados, digite:
db.dropDatabase()
Após este comando, se digitarmos show dbs, ele não mostrará mais a base dsd na lista de bases disponíveis naquela instância do Mongo.
Para listar os dados de uma collection use o domando find, da seguinte maneira:
 db.professor.find()
{ "_id" : ObjectId("57d0849edab45f72a3994895"), "nome" : "Dirceu" }
Para apagar uma collection execute o comando drop:
db.professor.drop()
Com isto todos os dados presentes naquela collection serão eliminados do Mongo.

Usando o Mongo com o Java

Até agora foi mostrado como manipular os dados no Mongo na unha, agora serão mostrados exemplos com o Java. Os exemplos usados neste post estão no seguinte repositório.
Existem alguns objetos que serão usados para manipular os dados do Mongo no Java, mas antes de tudo deve-se adicionar o driver do Mongo como dependência do seu projeto, para fazer isto, adicione a dependência no seu build.sbt:
"org.mongodb" % "mongo-java-driver" % "2.10.1"
Dentre as classes que utilizaremos no nosso projeto, a primeira a ser estudada é o cliente do mongo, MongoClient, para criar uma instância dela fazermos:
MongoClient mongo = new MongoClient( host , port);
Por padrão a porta utilizada pelo mongo é a 27017, e o host será o localhost, isto para instâncias do mongo que estão rodando na sua máquina, caso você esteja usando um mongo de uma outra máquina, altere o localhost para o nome da máquina onde está rodando o MongoDb.
Com a instância do cliente do Mongo é possível pegar uma instância de uma base, também chamada de DB, para isso inclua no seu código a linha abaixo:
DB mongoDB = mongo.getDB(dbName);
No DB é possível acessarmos as collections daquele banco, para fazer isto executamos:
DBCollection collection = mongoDB.getCollection(collectionName);
Para acessar as collections usamos o nome da collection.
O objeto collection possui métodos para inserir (insert), remover (remove), achar (find) e atualizar (update), objetos daquela collection. Todos estes métodos recebem um BasicDBObject como argumento. Este BasicDBObject nada mais é do que um mapa, onde vamos inserindo os atributos do nosso objeto, comoMongoClient mongo = new MongoClient( host , port); mostrado abaixo:
BasicDBObject inserir = new BasicDBObject();
inserir.put("nomeDoCampo", valorDoCampo);
Para cada atributo do objeto, iremos executar uma chamada ao método put passando o nome do campo e o seu valor. Ao final basta chamar um:
collection.insert(inserir);
que o objeto será gravado na collection do Mongo.

sexta-feira, 2 de setembro de 2016

Integrando SBT no Eclipse

Para integrar o SBT com o Eclipse, deve-se instalar um plugin do SBT. Para instalar um plugin no SBT deve-se editar o arquivo plugins.sbt, localizado em:
~/.sbt/0.13/plugins/plugins.sbt
Neste arquivo será adicionado a seguinte configuração:

addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0")
Com o plugins instalado, basta ir até a pasta onde o projeto será criado e digitar
sbt eclipse
Pronto agora os arquivos do eclipse foram criados nesta pasta e já pode-se importar este projeto no eclipse.
Antes de importar vamos criar o diretório que será utilizado para colocar nossas classes nele.
mkdir -p src/main/java
Este é o diretório padrão de fonte dos arquivos java no SBT.
Depois de adicionar as suas dependencias no arquivo build.sbt, conforme descrito aqui, dentro do console do sbt, digite o seguinte comando:
 eclipse with-source=true
Este comando será responsável por carregar as dependências adicionadas ao projeto na sua configuração do eclipse.

terça-feira, 23 de agosto de 2016

HDFS Hadoop Distributed File System o sistema de arquivos do Hadoop

O HDFS é um tipo de sistema de arquivos distribuídos que foi criado para ser usado em hardware comodity, ou seja máquinas dentro da rede da sua empresa.
Este sistema é altamente tolerante a falhas, uma vez que todo arquivo que é escrito nele, será replicado em pelo menos 3 discos diferentes, lembrando que este número de réplicas é um parâmetro de configuração e pode ser alterado. Ele possui uma velocidade de leitura e escrita muito grande, já que os arquivos podem ser distribuídos e escritos parcialmente em cada máquina, cada parte em uma máquina, para depois ser replicado.
Este sistema de arquivos pode ser utilizado em máquinas de baixo custo e por um sistema que possui uma quantidade de dados bem grande.
O HDFS pode ser acessado através de uma conexão de stream, que enviará para o cliente todas as alterações realizadas no sistema.
Para instalar o HDFS precisa-se de no mínimo 3 máquinas e como este sistema é altamente escalável, pode-se ter uma instância do hdfs com milhares de máquinas.

Falha de Hardware

Em um sistema com muitas máquinas uma falha de hardware certamente acontecerá, o HDFS foi desenvolvido contando com isto. Cada hd armazena uma parte do sistema de arquivos, como são vários componentes, principalmente em um sistema com milhares de máquinas, certamente uma delas irá falhar. Um dos pontos cruciais do HDFS é a rápida detecção de falha e a correção automática delas é um dos pontos principais deste sistema.

Acesso de dados via Streaming

Os aplicativos que rodam no HDFS precisam acessar os dados via streaming, isto permite que você plugue um sistema que fica ouvindo os dados chegarem, ao sistema de arquivos e leia os dados assim que eles forem gravados. Este sistema é focado em uma alta taxa de transferência de arquivos, ao invés de ser um sistema de baixa latência.

Grande quantidade de dados

Um arquivo típico de um sistema de HDFS tem de Gigabytes a Terabytes. O sistema do HDFS foi desenvolvido para suportar grandes arquivos, e foi configurado para performar bem com estes tipos de arquivos. Nele é possível agregar dados a estes arquivos com altas taxas de transferência e ele pode suportar milhões de arquivos em um único sistema.

Modelo de Coerência Simples

Os aplicativos do HDFS trabalham no sistema de escreva uma vez e leia diversas vezes. Depois que um arquivo foi criado, escrito e fechado, ele não mais necessitará ser alterado. Este tipo de restrição simplifica bastante as restrições de coerência dos dados e habilita uma alta taxa de transferência de dados. Aplicativos como crawlers da internet e MapReduce casam bem com este tipo de sistema de arquivos.

Mover a Computação é mais Simples que mover os dados

Em se tratando de uma aplicação onde a quantidade de dados acessados são gigantes, é mais fácil executar o processamento destes dados perto de onde eles estão armazenados, do que transferir grandes quantidades de dados para uma máquina processá-los. Isto minimiza o tráfego na rede e aumenta o desempenho do sistema.

NameNode e DataNode

O HDFS foi desenvolvido em uma arquitetura de mestre/escravo, onde o mestre, também conhecido como NameNode, é quem regula o acesso dos arquivos e a localização dos arquivos. Os DataNodes, que são os clientes, que é quem administra os dados e onde eles estão armazenados dentro das máquinas onde eles estão rodando, geralmente cada instância de DataNode roda em uma máquina.
Quando acessamos um sistema de dados HDFS, vemos uma lista de diretórios, como se fosse um sistema de arquivos comum. Ao escrevermos um arquivo, ele será divido em blocos e cada um destes blocos será armazenado, e replicado, em um DataNode, com isso consegue-se uma alta taxa de transferência de dados, pois ao ler um arquivo, ele estará armazenado em várias máquinas diferentes e a sua leitura será realizada em paralelo.
Os NameNodes são responsáveis por prover as operações de abertura, fechamento e renomeação dos arquivos e diretórios. Já os DataNodes são responsáveis por criar, apagar e replicar os blocos, lembrando que estas operações serão orquestradas pelo NameNode.

Funcionamento do HDFS

O HDFS foi planejado para rodar em sistemas com uma grande quantidade de dados, por isso os seus arquivos são divididos em blocos, os quais são replicados em várias máquinas.
É possível configurar a quantidade de réplicas que terá cada arquivo, este é um dos parâmetros de criação do arquivo.
Ao pensar em um sistema de arquivo que estará presente em milhares de máquinas, deve-se levar em conta como serão alocados cada um dos blocos de cada arquivo.
Nos datacenters as máquinas estão alocadas em Racks, sendo que cada rack contém uma determinada quantidade de máquinas. A comunicação das máquinas entre racks deve passar por um Switch e com isso o HDFS consegue detectar onde está cada DataNode.
Com estes dados, ao gravar um arquivo, geralmente todos os dados de um mesmo arquivo fica armazenado dentro de um mesmo Rack, afim de evitar muita transferência de dados pela rede ao lermos um arquivo. 
Com a informação da localização dos DataNodes, o HDFS também se preocupa em replicar os dados de um mesmo arquivo em diversos Racks diferentes, para evitar que dados sejam perdidos se um rack falhar por completo e também conseguir usar de diversos pontos de rede diferentes ao executar a leitura de um arquivo, fazendo com que a leitura dos dados seja bem rápida.

Apresentação


terça-feira, 10 de maio de 2016

Cluster de Computadores e Computação em Grade/Grid

Cluster de Computadores e Computação em Grade/Grid

Neste post será mostrada a teoria dos sistemas de cluster de computadores, assim como um exemplo dele, o cluster Beowulf, além da teoria da computação em Grade ou Grid.

Cluster de Computadores

Um cluster de computadores é um sistema que consiste em vários computadores conectados de forma que eles trabalhem como se fossem apenas um sistema.

Existem dois tipos de conexões entre os componentes de um sistema de cluster, as do tipo altamente acopladas, quando há um alto conhecimento entre cada um dos componentes deste sistema e as conexões abstratas, quando os componentes não sabem quase nada um do outro.

Em um Cluster os computadores são usualmente conectados por uma rede local (LAN Local Area Network) e cada nó (servidor) possui o seu hardware próprio, assim como uma instância do Sistema Operacional. Geralmente os nós possuem o mesmo sistema operacional (versão e tipo), assim como o mesmo hardware, porém em alguns casos, como o OSCAR (Open Source Cluster Application Resources) eles podem variar.

Os clusters ficaram populares quando os preços dos microcomputadores caíram e a performance dos seus processadores aumentaram. Em um certo ponto da história ficou mais barato criar um cluster local, conectando-se várias máquinas comuns em uma rede de alta velocidade, do que comprar um supercomputador. Os clusters são utilizados para rodar um programa de alta intensidade computacional em paralelo usando várias máquinas.

Pode-se criar um cluster usando apenas dois computadores, assim como através da utilização de um supercomputador. Um dos primeiros clusters de baixo custo, também conhecido como Stone Supercomputer foi realizado na década de 90. Este cluster foi implementado utilizando-se um toolkit do Linux chamado Parallel Virtual Machines e a Message Passing Interface provando que era possível construir uma máquina de alta performance a um baixo custo.

Enquanto a arquitetura de cluster pode ser utilizada para construir um cluster com poucas máquinas simples, ela também pode ser utilizada para se construir computadores de alta performance. A TOP500 é uma organização que divulga a cada 6 meses um ranking com os 500 computadores mais poderosos do mundo, e alguns destes sistemas são construídos através da utilização da arquitetura de cluster. As grandes empresas fabricantes de hardware estão sempre em uma disputa constante para ficar e manter-se em primeiro lugar desta lista. Esta lista sempre é divulgada em Junho e Novembro.

História

Os clusters não foram inventados por nenhuma empresa, mas sim pelos clientes que não conseguiam mais executar as suas tarefas em apenas uma máquina, ou precisavam de um backup. A data estimada da criação deste tipo de arquitetura é em 1960.

Atributos de um Cluster

Um cluster pode ser construído para diversas funcionalidades, desde para um sistema de negócio como um Web-Service, quanto para sistemas científicos de cálculo intensivo.

Um dos exemplos de cluster para sistemas científicos foi o utilizado no Projeto Genoma Humano, o qual foi um esforço internacional para se mapear o Genoma Humano e a identificação dos nucleotídeos que o compõem. Este projeto iniciou-se em 1990 e foi comunicado como encerrado em Abril de 2003, ele foi feito em 250 laboratórios, usando 5000 cientistas de 17 países diferentes. Com a evolução das máquinas e dos sistemas distribuídos, hoje ele poderia ser feito em 1 semana com apenas 2 pessoas.

Configurações de load balance são realizadas nos clusters para fazer com ele aproveite melhor o hardware disponível. O load balance é realizado pelo Master de forma a distribuir melhor as mensagens a serem processadas entre os nós do cluster.

Os clusters de computador são utilizados para tarefas de alta complexidade, ao invés de tarefas com altas taxas de leitura e escrita, como um banco de dados, ou um webservice. Estes clusters são utilizados por exemplo, para simular batidas de carros, analisar dados de prospecção de petróleo, processamento de imagens, processamento de alto volume de dados Big Data, para cálculos de predição. Um cluster que utiliza máquinas altamente acopladas se aproxima de um supercomputador.

Clusters de alta disponibilidade são aqueles onde existem nós redundantes e eles serão utilizados para entrar em operação quando alguma coisa falha. Estes clusters evitam o ponto simples de falha (single point of failure) e o Linux-HA é o exemplo mais comum deste tipo de cluster em software livre.

Cluster Load Balance

Load balance é uma maneira de distribuir os processos pelo cluster, de forma que eles sejam realizados da forma mais rápida possível, existem muitas formas de distribuir os processos entre os componentes do cluster.

A maneira mais simples de fazer um load balance é a chamada Round-Robin, onde os nós do cluster serão ordenados e cada processo que chega será direcionada ao próximo nó, quando o número de processos for maior que o número de nós, os nós receberão mais de um processo.

Uma outra forma de se distribuir os processos é escolher randomicamente um nó e direcionar o processo para ele, ou também conhecido como distribuição randômica.

Cada processo que será processado por um nó, é armazenado em uma fila, e existe uma distribuição que direciona os processos para o nó que contém a menor fila, assumindo que a menor fila é quem irá processar mais rápido.

Existem sistemas de load balance que calculam o tempo médio de processamento de cada nó, e sempre enviam os processos para as máquinas que possuem o menor tempo médio de processamento. Neste caso, para o cálculo, conta-se o tempo de envio da mensagem até a chegada da resposta.

Existem várias formas de se distribuir as tarefas em um sistema de cluster, mas é impossível determinar qual será a melhor maneira, sem saber pelo menos, como serão as mensagens que o sistema irá receber.

Clusters também são conhecidos por serem homogêneos, ou seja, as máquinas que fazem parte dele contém o mesmo sistema operacional e muitas vezes o mesmo hardware.

Configuração e Design

Um dos problemas de desenvolver um cluster é determinar o quão acoplado estarão os nós deste cluster. Por exemplo, um único processo poderá necessitar de uma frequente comunicação entre os nós, o que implica que este cluster deverá estar inserido em uma rede dedicada. Uma outra possível configuração extrema é quando os nós não necessitam de nenhuma comunicação entre si, o que ocorre nos Clusters Beowulf onde os slaves não se conhecem, o que é bem próximo a um sistema de GRID.

Em um cluster Beowulf tipicamente os nós tem sua própria versão do sistema operacional instalada, sua memória e discos locais. Porém em alguns casos, quando há uma rede dedicada, pode-se instalar um sistema de arquivos distribuído entre os nós, onde todos podem ler ou escrever nele.


Cluster Middleware

O cluster middleware é uma camada de software que orquestra a alocação de tarefas aos nós e torna possível com que o usuário veja um cluster como se fosse apenas uma máquina.

O middleware é responsável pela distribuição das tarefas no cluster e pelo controle do que já foi executado e o que ainda deverá ser executado.

Esta camada de software provê ao usuário uma interface, onde ele poderá se conectar tanto para submeter tarefas, quanto para analisar o que foi e  está sendo executado no cluster.

Cluster Beowulf

Um exemplo bem conhecido de cluster de computadores, são os clusters baseados em máquinas Linux chamado de Beowulf. Este cluster consiste em uma rede de computadores que são controlados e acessados por uma máquina conhecida como master simples. O master é quem possui o middleware instalado e no caso do Cluster Beowulf ele possui uma interface para que o cluster possa ser acessado pelos seus usuários. Os nós deste sistema não precisam de nenhum software, somente o sistema operacional.

Em um cluster Beowulf os nós não se conhecem, apenas o master conhece os nós, assim como cada nó só conhece o master. Neste tipo de cluster, as tarefas processadas por eles, não podem necessitar de nenhuma comunicação entre si, ou seja cada nó deverá ser capaz de processar uma tarefa por completo.

Computação em Grid

A computação em Grid difere de um cluster, pois no grid as máquinas estão em localidades diferentes, redes diferentes. Em um grid temos diversas máquinas diferentes atuando como se fossem um único supercomputador.

Nos grids, cada nó irá executar uma função do início ao fim, sem precisar se comunicar com um outro ponto, com isso ele pode ser geograficamente distribuído. Os grids tendem a ser mais dispersos e mais, geograficamente espalhados, do que os clusters. Isso faz com que os grids sejam bem maiores que os clusters, e eles até podem ter controladores, porém caso tenham estes controladores serão também distribuídos. Grids também são conhecidos por serem heterogêneos, ou seja, as máquinas que eles usam para processar os dados, podem conter diferentes sistemas operacionais, assim como diferentes hardwares.

Um grid pode ser utilizado por apenas uma aplicação, porém geralmente eles são desenvolvidos para serem usados por várias aplicações diferentes ao mesmo tempo. Eles geralmente são construídos para ocupar o tempo de processamento ocioso de máquinas. Imagina que uma empresa tenha milhares de computadores, e sabemos que muitas vezes o potencial do hardware não será utilizado por completo o dia todo, neste caso, um grid pode ser instalado para que, quando as máquinas estiverem ociosas, eles assumam o lugar e comecem a processar tarefas.

A coordenação de tarefas entre os nós de um grid não é uma tarefa simples, pois estes nós podem estar espalhados em diversos pontos, por isso esta coordenação utiliza alguns dos exemplos vistos no post de sincronização, assim como alguns softwares específicos tiveram que ser desenvolvidos para controlar este fluxo de informações.

Exemplo de Grid

Um dos exemplos mais conhecidos de GRID é o chamado World Community Grid. Este Grid fica em um site, e vários projetos podem se submeter a utilizar o seu poder de processamento. Nele as pessoas podem baixar um software que ao ser instalado em sua máquina irá utilizar os momentos de baixo processamento dela, por exemplo quando você deixa o computador ligado e vai tomar um café, para processar pedaços de projetos que estão disponíveis no site.

Quando este post foi escrito, dentre os projetos disponíveis no World Community Grid, eram:

  • Ajuda a lutar contra a tuberculose
  • Luta contra a AIDS
  • Melhorar formas de se encontrar água limpa para milhões de pessoas
  • Melhorar o desempenho de células solares
  • Ajuda a melhorar remédios contra câncer
  • Luta contra o EBOLA

Cada um destes projetos possuem milhares de subtarefas, ao se conectar e escolher alguns deles, o seu sistema baixa uma subtarefa, que será processada enquanto sua máquina estiver ociosa, por exemplo, quando o protetor de tela assumir. Esta subtarefa, pode ser processada de uma vez só, ou pode ser processada em vários eventos, imagine que você vá ao banheiro, ou tomar café várias vezes em um dia e em todos estes momentos, a tarefa será processada e ao terminá-la, o resultado será enviado para o coordenador e novas tarefas serão baixadas.

No começo parece ser algo bem simples, mas imagine que é bem complicado adivinhar o quanto de tempo de processamento, uma máquina terá disponível, ao longo do dia, isso varia de máquina para máquina e de pessoa para pessoa. Com isso, no início recebe-se tarefas mais simples, e depois de um tempo, estima-se o quanto em média, sua máquina contribuirá, e isso ajudará a escolher quais tarefas enviar pro seu nó.

Um outro detalhe é, não dá pra confiar que toda tarefa enviada, será processada, imagine que o usuário pode entrar de férias, sua máquina pode ter algum problema físico, ou de software e com isso algumas tarefas serão perdidas. Então, toda tarefa tem uma data de validade, ao recebê-la, sua máquina vai processá-la, mas a sua resposta só será aceita até uma data limite. Caso o coordenador tenha enviado uma tarefa, para ser processada, se ele não receber uma resposta até a data limite, ele reenvia aquela tarefa pra um outro componente processar. Aqui temos um outro problema, caso este reenvio não seja processado novamente, esta tarefa pode ficar atrasada e travar um processo, então é hora de decidir se ela será enviada para mais de um componente processá-la em paralelo.

O controle dos componentes de um Grid parecia ser simples inicialmente, mas pode se mostrar bem complicado, existem várias outras possibilidades de problemas acontecerem e decisões serem tomadas para contornar eles, deixo isto como tarefa mental para os srs.