DevSecOpsARTIGO

Processamento de dados distribuídos com Hadoop – Parte 02: Indo mais longe

O primeiro artigo desta série
mostrou como usar o Hadoop em um cluster de nó único. Este artigo
continua com uma configuração mais avançada que usa múltiplos nós para o
processamento paralelo. Demonstra os vários tipos de nó necessários
para os clusters multinós e explora a funcionalidade MapReduce em um
ambiente paralelo. Este artigo também aborda os aspectos de
gerenciamento do Hadoop ? tanto na linha de comandos quanto baseados na
Web.

O verdadeiro poder da arquitetura de computação distribuída Hadoop está
na distribuição. Em outras palavras, a capacidade de distribuir o
trabalho em vários nós em paralelo permite que o Hadoop escale as
grandes infraestruturas e, da mesma forma, permite o processamento de
grandes quantidades de dados. Este artigo começa com uma decomposição de
uma arquitetura distribuída do Hadoop e, em seguida, explora a
configuração distribuída e o uso.

A arquitetura distribuída do Hadoop

Lembre-se de que, na Parte 1 desta série,
foi dito que todos os daemons do Hadoop eram executados no mesmo host.
Embora não exercesse o paralelismo do Hadoop, essa configuração
pseudodistribuída oferecia uma forma fácil de testar os recursos do
Hadoop com uma configuração mínima. Agora, vamos explorar o paralelismo
do Hadoop usando um cluster de máquinas.

Na Parte 1, a configuração do Hadoop definiu que todos os
daemons do Hadoop executam em um único nó. Vamos ver primeiro como o
Hadoop é distribuído naturalmente para a operação paralela. Em uma
configuração distribuída do Hadoop, você terá um nó principal e alguns
nós escravos (veja a Figura 1).

Figura 1. Decomposição em nó principal e nós escravos do Hadoop

Como mostrado na Figura 1, o nó principal é formado pelo namenode, namenode secundário e pelos daemons jobtracker (os chamados daemons principais)
Além disso, esse é o nó em que se gerencia o cluster para os fins desta
demonstração (usando o utilitário Hadoop e o navegador). Os nós
escravos são o tasktracker e o datanode (os daemons escravos).

A
diferença dessa configuração é que o nó principal contém os daemons que
proporcionam gerenciamento e coordenação do cluster do Hadoop, e o nó
escravo contém os daemons que implementam as funções de armazenamento do
Hadoop File System (HDFS) e da funcionalidade MapReduce (a função de
processamento de dados).

Para esta demonstração, você cria um nó principal e dois
nós escravos que ficam em uma única LAN. A Figura 2 mostra essa
configuração. Agora, vamos explorar a instalação do Hadoop para a
distribuição multinó e sua configuração.

Figura 2. Configuração de cluster do Hadoop

Para simplificar a implementação, você emprega a virtualização, que
oferece algumas vantagens. Embora o desempenho possa não ser vantajoso
nessa configuração, por meio da virtualização, é possível criar uma
instalação do Hadoop e em seguida cloná-la para os outros nós. Por esse
motivo, o cluster do Hadoop deve aparecer da forma apresentada a seguir,
executando os nós principal e escravo como máquinas virtuais (VMs) no
contexto de um hypervisor em um único host (veja a Figura 3).

Figura 3. Configuração de cluster do Hadoop em um ambiente virtual

Atualizando o Hadoop

Na Parte 1,
você instalou uma distribuição especial para o Hadoop que executava em
um único nó (pseudoconfiguração). Neste artigo, você atualiza para uma
configuração distribuída. Se você iniciou a série por este artigo, leia a
Parte 1 para instalar primeiro a pseudoconfiguração do Hadoop.

Na pseudoconfiguração, você não fez nenhuma configuração,
já que tudo foi configurado previamente para um único nó. Agora, é
necessário atualizar a configuração. Primeiro, verifique a configuração
atual com o comando update-alternatives, como mostrado na Listagem 1. Esse comando informa que a configuração está usando conf.pseudo (a prioridade mais alta).

Listagem 1. Verificando a configuração atual do Hadoop

$ <strong>update-alternatives --display hadoop-0.20-conf</strong>
hadoop-0.20-conf - status is auto.
 link currently points to /etc/hadoop-0.20/conf.pseudo
/etc/hadoop-0.20/conf.empty - priority 10
/etc/hadoop-0.20/conf.pseudo - priority 30
Current `best' version is /etc/hadoop-0.20/conf.pseudo.
$ 

Em seguida, crie uma nova configuração ao copiar uma configuração já
existente (nesse caso, conf.empty, como mostra a Listagem 1):

$ <strong>sudo cp -r /etc/hadoop-0.20/conf.empty /etc/hadoop-0.20/conf.dist</strong>
$ 

Por fim, ative e verifique a nova configuração:

Listagem 2. Ativando e verificando a configuração do Hadoop

$ <strong>sudo update-alternatives --install /etc/hadoop-0.20/conf hadoop-0.20-conf \<br />  /etc/hadoop-0.20/conf.dist 40</strong>
$ <strong>update-alternatives --display hadoop-0.20-conf</strong>
hadoop-0.20-conf - status is auto.
 link currently points to /etc/hadoop-0.20/conf.dist
/etc/hadoop-0.20/conf.empty - priority 10
/etc/hadoop-0.20/conf.pseudo - priority 30
/etc/hadoop-0.20/conf.dist - priority 40
Current `best' version is /etc/hadoop-0.20/conf.dist.
$ 

Agora, você tem uma nova configuração chamada conf.dist a qual
será usada para a nova configuração distribuída. Nesse estágio,
executando em um ambiente virtualizado, você clona esse nó em dois nós
adicionais que atuarão como os nós de dados.

Configurando o Hadoop para a operação distribuída

A próxima etapa é fazer com que todos os nós se conheçam. Isso é feito nos arquivos /etc/hadoop-0.20/conf.dist chamados principais e escravos. São designados endereços IP de forma estática aos três nós desse exemplo, como mostrado aqui (a partir de /etc/hosts):

Listagem 3. Nós de Hadoop para essa configuração (/etc/hosts)

master 192.168.108.133<br />slave1 192.168.108.134<br />slave2 192.168.108.135

No nó principal, você atualiza /etc/hadoop-0.20/conf.dist/masters para identificar o nó principal, o qual aparece como:

master

e em seguida identifica os nós escravos em /etc/hadoop-0.20/conf.dist/slaves, os quais contêm as duas linhas a seguir:

slave1<br />slave2

Depois, a partir de cada nó, conecte-se por meio de Secure Shell
(ssh) a cada um dos outros nós para certificar-se de que o ssh sem senha
está funcionando. Cada um desses arquivos (principais, escravos) é
usado pelos utilitários de início e parada do Hadoop que você usou na
Parte 1 dessa série.

Depois, continue com a configuração específica do Hadoop
no subdiretório /etc/hadoop-0.20/conf.dist. As seguintes alterações são
necessárias em todos os nós (no principal e nos dois escravos), conforme
o definido na documentação do Hadoop.

Primeiro, identifique o HDFS
principal no arquivo core-site.xml (Listagem 4), que define o host e a
porta do namenode (observe o uso do endereço IP do nó principal). O
arquivo core-site.xml define as propriedades principais do Hadoop.

Listagem 4. Definindo o HDFS principal em core-site.xml

<configuration><br /><br />  <property><br />    <name>fs.default.name<name><br />    <value>hdfs://master:54310<value><br />    <description>The name and URI of the default FS.</description><br />  <property><br /><br /><configuration>

Em seguida, identifique o jobtracker MapReduce. Esse jobtracker pode
existir no seu próprio nó mas, para essa configuração, coloque-o no nó
principal como mostrado na Listagem 5. O arquivo mapred-site.xml contém
as propriedades de MapReduce.

Listagem 5. Definindo o jobtracker MapReduce em mapred-site.xml

<configuration><br /><br />  <property><br />    <name>mapred.job.tracker<name><br />    <value>master:54311<value><br />    <description>Map Reduce jobtracker<description><br />  <property><br /><br /><configuration>

Por fim, defina o fator de replicação padrão (Listagem 6). Esse valor
define o número de réplicas que serão criadas e normalmente não é
superior a três. Nesse caso, você define como 2 (o número dos seus nós
de dados). Esse valor é definido em hdfs-site.xml, que contém as
propriedades do HDFS.

Listagem 6. Definindo a replicação padrão para os dados em hdfs-site.xml

<configuration><br /><br />  <property><br />    <name>dfs.replication<name><br />    <value>2<value><br />    <description>Default block replication<description><br />  <property><br /><br /><configuration>

Os itens de configuração mostrados na Listagem 4, Listagem 5 e Listagem 6)
são os elementos necessários para a sua configuração distribuída.
Hadoop fornece um grande número de opções de configuração aqui, que
permitem padronizar todo o ambiente. A seção Recursos, ao final do artigo, fornece mais informações sobre o que está disponível.

Com a configuração completa, a próxima etapa é formatar o
namenode (o nó principal do HDFS). Para essa operação, use o utilitário
hadoop-0.20, especificando o namenode e a operação (-format):

Listagem 7. Formatando o namenode

user@master:~# <strong>sudo su -</strong>
root@master:~# <strong>hadoop-0.20 namenode -format</strong>
10/05/11 18:39:58 INFO namenode.NameNode: STARTUP_MSG: 
/************************************************************
STARTUP_MSG: Starting NameNode
STARTUP_MSG:   host = master/127.0.1.1
STARTUP_MSG:   args = [-format]
STARTUP_MSG:   version = 0.20.2+228
STARTUP_MSG:   build =  -r cfc3233ece0769b11af9add328261295aaf4d1ad; 
************************************************************/
10/05/11 18:39:59 INFO namenode.FSNamesystem: fsOwner=root,root
10/05/11 18:39:59 INFO namenode.FSNamesystem: supergroup=supergroup
10/05/11 18:39:59 INFO namenode.FSNamesystem: isPermissionEnabled=true
10/05/11 18:39:59 INFO common.Storage: Image file of size 94 saved in 0 seconds.
10/05/11 18:39:59 INFO common.Storage: 
  Storage directory /tmp/hadoop-root/dfs/name has been successfully formatted.
10/05/11 18:39:59 INFO namenode.NameNode: SHUTDOWN_MSG: 
/************************************************************
SHUTDOWN_MSG: Shutting down NameNode at master/127.0.1.1
************************************************************/
root@master:~# 

Com o namenode formatado, é hora de iniciar os daemons do Hadoop. Faça
isso da mesma forma que na pseudoconfiguração distribuída anterior na
Parte 1, mas o processo faz a mesma coisa para uma configuração
distribuída. Observe que esse código inicia o namenode e o namenode
secundário (como indica o comando jps):

Listagem 8. Iniciando o namenode

root@master:~# <strong>/usr/lib/hadoop-0.20/bin/start-dfs.sh</strong>
starting namenode, logging to 
  /usr/lib/hadoop-0.20/bin/../logs/hadoop-root-namenode-mtj-desktop.out
192.168.108.135: starting datanode, logging to 
  /usr/lib/hadoop-0.20/bin/../logs/hadoop-root-datanode-mtj-desktop.out
192.168.108.134: starting datanode, logging to 
  /usr/lib/hadoop-0.20/bin/../logs/hadoop-root-datanode-mtj-desktop.out
192.168.108.133: starting secondarynamenode, 
  logging to /usr/lib/hadoop-0.20/logs/hadoop-root-secondarynamenode-mtj-desktop.out
root@master:~# <strong>jps</strong>
7367 NameNode
7618 Jps
7522 SecondaryNameNode
root@master:~# 

Se inspecionar os nós escravos (nós de dados) agora, com jps, você verá que agora há um daemon datanode em cada nó:

Listagem 9. Inspecionando o datanode em um dos nós escravos

root@slave1:~# <strong>jps</strong>
10562 Jps
10451 DataNode
root@slave1:~# 

A próxima etapa é iniciar os daemons de MapReduce (jobtracker e tasktracker). Faça isso como mostrado na Listagem 10. Observe que o script inicia o jobtracker no nó principal (conforme o definido pela sua configuração; consulte a Listagem 5) e os tasktrackers em cada nó escravo. Um comando jps no nó principal mostra que agora o jobtracker está executando.

Listagem 10. Iniciando os daemons de MapReduce

root@master:~# <strong>/usr/lib/hadoop-0.20/bin/start-mapred.sh</strong>
starting jobtracker, logging to 
  /usr/lib/hadoop-0.20/logs/hadoop-root-jobtracker-mtj-desktop.out
192.168.108.134: starting tasktracker, logging to 
  /usr/lib/hadoop-0.20/bin/../logs/hadoop-root-tasktracker-mtj-desktop.out
192.168.108.135: starting tasktracker, logging to 
  /usr/lib/hadoop-0.20/bin/../logs/hadoop-root-tasktracker-mtj-desktop.out
root@master:~# <strong>jps</strong>
7367 NameNode
7842 JobTracker
7938 Jps
7522 SecondaryNameNode
root@master:~# 

Por fim, verifique um nó escravo com jps. Aqui, você vê que um daemon de tasktracker se uniu ao daemon de datanode para cada nó de dados escravo:

Listagem 11. Inspecionando o datanode em um dos nós escravos

root@slave1:~# <strong>jps</strong>
7785 DataNode
8114 Jps
7991 TaskTracker
root@slave1:~# 

As relações entre os scripts de início, os nós e os daemons que são
iniciados são mostradas na Figura 4. Como você pode ver, o script start-dfs inicia os namenodes e datanodes, e o script start-mapred inicia o jobtracker e os tasktrackers.

Figura 4. Relação dos scripts de início e dos daemons de cada nó

Testando o HDFS

Agora que o Hadoop está executando no seu cluster, você pode fazer
alguns testes para certificar-se de que esteja funcionando (consulte a
Listagem 12). Primeiro, emita um comando do sistema de arquivos (fs) por meio do utilitário

hadoop-0.20 e solicite uma operação de df (sem disco). No Linux, esse comando simplesmente identifica o espaço
consumido e disponível para o dispositivo específico. Em um sistema de
arquivos recém-formatados, você não usou espaço nenhum.

Em seguida,
realize uma operação de ls na raiz do HDFS, crie um subdiretório, liste o seu conteúdo e remova-o. Por fim, você pode realizar um fsck (verificação do sistema de arquivos) no HDFS por meio do comando fsck com o utilitário hadoop-0.20.
Toda essa operação informa ? juntamente com várias outras informações
(como a detecção de 2 nós de dados) ? que o sistema de arquivos
encontra-se em bom estado.

Listagem 12. Verificando o HDFS

root@master:~# <strong>hadoop-0.20 fs -df</strong>
File system		Size	Used	Avail		Use%
/		16078839808	73728	3490967552	0%
root@master:~# <strong>hadoop-0.20 fs -ls /</strong>
Found 1 items
drwxr-xr-x   - root supergroup          0 2010-05-12 12:16 /tmp
root@master:~# <strong>hadoop-0.20 fs -mkdir test</strong>
root@master:~# <strong>hadoop-0.20 fs -ls test</strong>
root@master:~# <strong>hadoop-0.20 fs -rmr test</strong>
Deleted hdfs://192.168.108.133:54310/user/root/test
root@master:~# <strong>hadoop-0.20 fsck /</strong>
.Status: HEALTHY
 Total size:	4 B
 Total dirs:	6
 Total files:	1
 Total blocks (validated):	1 (avg. block size 4 B)
 Minimally replicated blocks:	1 (100.0 %)
 Over-replicated blocks:	0 (0.0 %)
 Under-replicated blocks:	0 (0.0 %)
 Mis-replicated blocks:		0 (0.0 %)
 Default replication factor:	2
 Average block replication:	2.0
 Corrupt blocks:		0
 Missing replicas:		0 (0.0 %)
 Number of data-nodes:		2
 Number of racks:		1

The filesystem under path '/' is HEALTHY
root@master:~# 

Realizando uma tarefa de MapReduce

A próxima etapa é realizar uma tarefa de MapReduce para confirmar que
toda a configuração está funcionando corretamente (consulte a Listagem
13). A primeira etapa desse processo é introduzir alguns dados. Comece
criando um diretório para conter os seus dados de entrada (chamados de entrada); faça isso usando o utilitário hadoop-0.20 com o comando mkdir. Em seguida, use o comando put de hadoop-0.20 para colocar dois arquivos no HDFS. É possível verificar o conteúdo do diretório de entrada por meio do comando ls do utilitário Hadoop.

Listagem 13. Gerando dados de entrada

root@master:~# <strong>hadoop-0.20 fs -mkdir input</strong>
root@master:~# <strong>hadoop-0.20 fs -put \<br />  /usr/src/linux-source-2.6.27/Doc*/memory-barriers.txt input</strong>
root@master:~# <strong>hadoop-0.20 fs -put \<br />  /usr/src/linux-source-2.6.27/Doc*/rt-mutex-design.txt input</strong>
root@master:~# <strong>hadoop-0.20 fs -ls input</strong>
Found 2 items
-rw-r--r--  2 root supergroup  78031 2010-05-12 14:16 /user/root/input/memory-barriers.txt
-rw-r--r--  2 root supergroup  33567 2010-05-12 14:16 /user/root/input/rt-mutex-design.txt
root@master:~# 

Em seguida, inicie a tarefa de contagem de palavras de MapReduce. Como
acontece no modelo pseudodistribuído, você especifica o subdiretório de
entrada (que contém os arquivos de entrada) e o diretório de saída (que
não existe mas será criado pelo namenode e preenchido com os dados de
resultado):

Listagem 14. Executando a tarefa de contagem de palavras de MapReduce no cluster

root@master:~# <strong>hadoop-0.20 jar \<br />  /usr/lib/hadoop-0.20/hadoop-0.20.2+228-examples.jar wordcount input output</strong>
10/05/12 19:04:37 INFO input.FileInputFormat: Total input paths to process : 2
10/05/12 19:04:38 INFO mapred.JobClient: Running job: job_201005121900_0001
10/05/12 19:04:39 INFO mapred.JobClient:  map 0% reduce 0%
10/05/12 19:04:59 INFO mapred.JobClient:  map 50% reduce 0%
10/05/12 19:05:08 INFO mapred.JobClient:  map 100% reduce 16%
10/05/12 19:05:17 INFO mapred.JobClient:  map 100% reduce 100%
10/05/12 19:05:19 INFO mapred.JobClient: Job complete: job_201005121900_0001
10/05/12 19:05:19 INFO mapred.JobClient: Counters: 17
10/05/12 19:05:19 INFO mapred.JobClient:   Job Counters 
10/05/12 19:05:19 INFO mapred.JobClient:     Launched reduce tasks=1
10/05/12 19:05:19 INFO mapred.JobClient:     Launched map tasks=2
10/05/12 19:05:19 INFO mapred.JobClient:     Data-local map tasks=2
10/05/12 19:05:19 INFO mapred.JobClient:   FileSystemCounters
10/05/12 19:05:19 INFO mapred.JobClient:     FILE_BYTES_READ=47556
10/05/12 19:05:19 INFO mapred.JobClient:     HDFS_BYTES_READ=111598
10/05/12 19:05:19 INFO mapred.JobClient:     FILE_BYTES_WRITTEN=95182
10/05/12 19:05:19 INFO mapred.JobClient:     HDFS_BYTES_WRITTEN=30949
10/05/12 19:05:19 INFO mapred.JobClient:   Map-Reduce Framework
10/05/12 19:05:19 INFO mapred.JobClient:     Reduce input groups=2974
10/05/12 19:05:19 INFO mapred.JobClient:     Combine output records=3381
10/05/12 19:05:19 INFO mapred.JobClient:     Map input records=2937
10/05/12 19:05:19 INFO mapred.JobClient:     Reduce shuffle bytes=47562
10/05/12 19:05:19 INFO mapred.JobClient:     Reduce output records=2974
10/05/12 19:05:19 INFO mapred.JobClient:     Spilled Records=6762
10/05/12 19:05:19 INFO mapred.JobClient:     Map output bytes=168718
10/05/12 19:05:19 INFO mapred.JobClient:     Combine input records=17457
10/05/12 19:05:19 INFO mapred.JobClient:     Map output records=17457
10/05/12 19:05:19 INFO mapred.JobClient:     Reduce input records=3381
root@master:~#

A etapa final é explorar os dados de saída. Já que a tarefa de contagem
de palavras de MapReduce foi executada, o resultado é um único arquivo
(reduzido a partir dos arquivos de mapeamento processados). Esse arquivo
contém uma lista de tuplas que representam as palavras encontradas nos
arquivos de entrada e o número de vezes que apareceram em todos os
arquivos de entrada:

Listagem 15. Inspecionando a saída da tarefa de MapReduce

root@master:~# <strong>hadoop-0.20 fs -ls output</strong>
Found 2 items
drwxr-xr-x   - root supergroup          0 2010-05-12 19:04 /user/root/output/_logs
-rw-r--r--   2 root supergroup      30949 2010-05-12 19:05 /user/root/output/part-r-00000
root@master:~# <strong>hadoop-0.20 fs -cat output/part-r-00000 | head -13</strong>
!=	1
"Atomic	2
"Cache	2
"Control	1
"Examples	1
"Has	7
"Inter-CPU	1
"LOAD	1
"LOCK"	1
"Locking	1
"Locks	1
"MMIO	1
"Pending	5
root@master:~# 

Interfaces de gerenciamento Web

Embora o utilitário hadoop-0.20 seja extremamente versátil e
rico, às vezes é mais conveniente usar uma GUI. É possível anexar ao
namenode para a inspeção do sistema de arquivos por meio de
http://master:50070 e ao jobtracker por meio de http://master:50030. Por
meio do namenode, é possível inspecionar o HDFS, como mostrado na
Figura 5, no qual você inspeciona o diretório de entrada (que contém os
dados de entrada ? lembre-se da Listagem 13).

Figura 5. Inspecionando o HDFS por meio do namenode

Por meio do jobtracker, é possível inspecionar as tarefas concluídas
ou em execução. Na Figura 6, é possível ver uma inspeção da sua última
tarefa (da Listagem 14).
Essa figura mostra os diversos dados emitidos como saída da solicitação
do arquivo de Java (JAR) e também o status e o número de tarefas.
Observe que foram realizadas duas tarefas de mapeamento (uma para cada
arquivo de entrada) e uma tarefa de redução (para reduzir as duas
entradas de mapa).

Figura 6. Verificando o status de uma tarefa concluída

Por fim, é possível verificar o status de seus datanodes por meio do
namenode. A página principal do namenode identifica o número de nodos
ativos e inativos (como links) e permite que você os inspecione melhor. A
página mostrada na Figura 7 mostra os datanodes ativos e também as
estatísticas de cada um.

Figura 7. Verificando o status dos datanodes ativos

Várias outras visualizações são possíveis por meio das interfaces da Web
de namenode e jobtracker, mas por uma questão de concisão, mostra-se
esse conjunto de amostra. Nas páginas da Web de namenode e jobtracker,
encontram-se vários links referentes a informações adicionais sobre a
configuração e operação do Hadoop (inclusive registros de tempo de
execução).

Indo além

Nessa parte, foi visto como é possível transformar uma configuração
pseudodistribuída de Cloudera em uma configuração totalmente
distribuída. Surpreendentemente, as poucas etapas e uma interface
idêntica para os aplicativos de MapReduce fazem do Hadoop uma ferramenta
particularmente útil para o processamento distribuído.

Também é
interessante explorar a escalabilidade do Hadoop. Adicionando novos
datanodes (e atualizando os arquivos XML e os arquivos escravos no
principal), pode-se escalar facilmente o Hadoop para obter níveis ainda
mais altos de processamento paralelo. Na próxima parte, terceira e última da nossa série de artigos sobre o Hadoop, explorará como desenvolver um aplicativo de
MapReduce para o Hadoop.

Recursos

Aprender

Obter produtos e tecnologias

  • O Hadoop é desenvolvido por meio da Apache Software Foundation.
  • Avalie os produtos da IBM
    da forma que melhor lhe convém: Faça o download de uma versão de teste
    de produto, experimente um produto on-line, use um produto em um
    ambiente de nuvem ou passe algumas horas no SOA Sandbox aprendendo como implementar Arquitetura Orientada a Serviço de forma eficiente.

artigo publicado originalmente no developerWorks Brasil, por M. Tim Jones


M. Tim Jones é arquiteto de firmware integrado e autor das obras Artificial Intelligence: A Systems Approach, GNU/Linux Application Programming (agora, na segunda edição), AI Application Programming (na segunda edição) e BSD Sockets Programming from a Multilanguage Perspective.
Sua experiência em engenharia vai desde o desenvolvimento de kernels
para espaçonaves geossíncronas até a arquitetura de sistemas integrados e
o desenvolvimento de protocolos de rede. Tim é engenheiro consultor da
Emulex Corp. em Longmont, Colorado.

é o portal de tecnologia da IBM para profissionais de TI de todo o mundo. Colabore, aprenda a criar aplicativos inovadores e compreenda tecnologias avançadas. Acesse mais artigos e tutoriais em www.ibm.com/developerworks/br

Ver perfil