universidade federal do rio de janeiromonografias.poli.ufrj.br/monografias/monopoli10012527.pdf ·...
TRANSCRIPT
1
UNIVERSIDADE FEDERAL DO RIO DE JANEIRO
Escola Politécnica – Departamento de Eletrônica e de Computação
Centro de Tecnologia, bloco H, sala H-217, Cidade Universitária.
Rio de Janeiro – RJ – CEP 21949-900
Este exemplar é de propriedade da Universidade Federal do Rio de Janeiro, que
poderá incluí-lo em base de dados, armazenar em computador, microfilmar ou adotar
qualquer forma de arquivamento.
É permitida a menção, reprodução parcial ou integral e a transmissão entre
bibliotecas deste trabalho, sem modificação de seu texto, em qualquer meio que esteja
ou venha a ser fixado, para pesquisa acadêmica, comentários e citações, desde que sem
finalidade comercial e que seja feita a referência bibliográfica completa.
Os conceitos expressos neste trabalho são de responsabilidade do autor e do
orientador.
2
Magalhães, Pedro da Silveira
Framework de ETL (Extração-Transformação-Carga)
utilizando processamento em paralelo por técnica de pipeline /
Pedro da Silveira Magalhães – Rio de Janeiro: UFRJ/POLI-
COPPE, 2013.
XI, 42 p.: il.; 29,7 cm.
Orientador: Antônio Cláudio Gómez de Sousa
Projeto (graduação) – UFRJ/POLI/Departamento de
Eletrônica e de Computação – COPPE, 2013.
Referências Bibliográficas: p. 40-42.
1. [Extração]. 2. [Transformação]. 3. [Carga]. 4. [ETL]. I. de Souza,
Antônio Cláudio Gómez. II. Universidade Federal do Rio de
Janeiro, Poli/COPPE. III. Título.
4
AGRADECIMENTO
“Agradeço ao professor Antônio Cláudio Gomez por ter me orientado durante o
Projeto de Graduação. Agradeço também a empresa Stone Age por ter me dado a
liberdade de muitas vezes abdicar do trabalho colocando sempre a formação do
profissional a frente.”
5
RESUMO
Projeto de Graduação apresentado à Escola Politécnica/COPPE/UFRJ como
parte dos requisitos necessários para a obtenção do grau de Engenheiro de Computação
e Informação.
FRAMEWORK DE ETL (EXTRAÇÃO-TRANSFORMAÇÃO-CARGA)
UTILIZANDO PROCESSAMENTO PARALELO POR TÉCNICA DE PIPELINE.
Pedro da Silveira Magalhães
Março/2013
Orientador: Antônio Cláudio Gómez de Sousa
Curso: Engenharia de Computação e Informação
Um ciclo de ETL (extract, transform e load), refere-se a um processo no
domínio de banco de dados muito utilizado em sistemas de Data Warehouse. Esse ciclo
envolve três etapas distintas: a extração de dados de fontes, transformação dos dados e a
carga no sistema alvo.
Um ciclo ETL pode ser consideravelmente complexo e problemas podem
ocorrer quando esses processos são modelados de maneira incorreta. Na maioria das
vezes os dados advindos do sistema transacional são de baixa qualidade, apresentando
problemas como: domínio de dados mal definidos, erros de digitação, heterogeneidade
das fontes de dados envolvidas, etc. Para lidar com esses desafios e complexidade, as
empresas estão cada vez mais investindo em ferramentas de ETL, ao invés de criar seus
processos a partir do zero. Ao utilizar um ferramental de apoio, as chances de sucesso
aumentam, pois permitem auto documentação, visualização do fluxo de dados,
reutilização de componentes, design estruturado e padronizado, desempenho e
consequentemente maior produtividade e qualidade.
O objetivo do projeto é criar um framework capaz de apoiar o processo de
construção/execução de um ciclo ETL entregando os benefícios anteriormente citados.
7
ABSTRACT
Undergraduate Project presented to POLI/COPPE/UFRJ as a partial fulfillment
of requirements for the degree of Computer and Information Engineer.
ETL Framework using parallel processing by pipeline technique.
Pedro da Silveira Magalhães
March/2013
Advisor: Antônio Cláudio Gómez de Sousa
Course: Computer and Information Engineering
ETL cycle (extraction, transform and load), refers to a process in the context of
database systems widely used in the Data Warehouse. This cycle involves three distinct
steps: data sources extraction, data transformation and load on the target system.
ETL cycle can be considerably complex and problems may occur when these
processes are modeled incorrectly. Most often the data from the transactional system are
of low quality, presenting problems as: the data poorly defined, typing errors,
heterogeneity of data sources involved, etc.. To deal with these challenges and
complexity, companies are increasingly investing in ETL tools instead of creating their
processes from scratch. When using supporting tools, the chances of success increase
because they allow self documentation, visualization of data flow, component reuse,
structured and standardized design, better performance, higher productivity and quality.
The project goal is to create a framework able to support the process of
building / running ETL cycle delivering the benefits mentioned above.
Keywords: extract, transform, Data Warehouse, ETL.
8
SIGLAS
ETL: Extract, Transform, Load
XML: eXtensible Markup Language
IDE: Integrated Development Enviroment
CSV: Comma Separate Value
HTML: HyperText Markup Language
ODS: Operational Data Store
MDM: Master Data Management
XSD: XML Schema Definition
9
Sumário
Sumário .................................................................................................................................................... 9
Capítulo 1: Introdução ................................................................................................................10
1.1. Contextualização ............................................................................................ 10
1.2. Motivação ....................................................................................................... 10
1.3. Objetivo .......................................................................................................... 11
1.4. Metodologia .................................................................................................... 12
1.5. Estrutura do documento .................................................................................. 12
Capítulo 2: Ciclo ETL ...................................................................................................................14
2.1. Introdução ....................................................................................................... 14
1.6. Extract (Extração)........................................................................................... 14
1.7. Transform (Transformação) ........................................................................... 14
1.8. Load (Carga) ................................................................................................... 15
1.9. Exemplo prático – Duas Abordagens ............................................................. 15
Capítulo 3: Extreme DataStream ............................................................................................18
1.10. Introdução ....................................................................................................... 18
1.11. Fluxo de Dados ............................................................................................... 19
1.12. Metadados – Layout ....................................................................................... 22
1.13. Nós de Processamento .................................................................................... 24
1.14. Arestas ............................................................................................................ 27
Capítulo 4: Implementação .......................................................................................................28
1.15. Visão geral ...................................................................................................... 28
1.16. Padrões de Projeto .......................................................................................... 28
1.17. Diagrama de Classes ....................................................................................... 29
1.18. O problema Produtor-Consumidor ................................................................. 34
1.19. Criando Novos Nós de Processamento ........................................................... 38
Capítulo 5: Exemplo de Uso – Paralelismo ........................................................................40
1.20. Introdução ....................................................................................................... 40
1.21. O Problema ..................................................................................................... 40
1.22. Hardware Utilizado......................................................................................... 40
1.23. Grafos Utilizados ............................................................................................ 41
1.24. Resultados ....................................................................................................... 43
1.25. Conclusões ...................................................................................................... 45
Capítulo 6: Conclusões e Lições Aprendidas ....................................................................47
Referências ...........................................................................................................................................49
10
Capítulo 1:
Introdução
1.1. Contextualização
Falar que a análise de dados é de suma importância para as empresas é um
eufemismo. De fato, nenhum negócio pode sobreviver sem analisar os dados
disponíveis. Podemos mostrar situações como: analisar a aceitação de um suco de frutas
baseando-se em dados de vendas históricos, verificar sazonalidade de vendas de
determinado produto, prever fraudes em cartões de crédito baseando-se no
comportamento passado do cliente, encontrar correlações entre eventos (Ex.: temporada
de furacões e venda de produtos não perecíveis). Todas essas situações são indicativos
suficientes para concluir que a análise de dados está à frente de qualquer negócio.
Assim, esta pode ser definida como o processo de inspecionar, higienizar, transformar e
modelar os dados com o objetivo de retirar informação, conclusões e ajudar no processo
decisório de uma empresa.
1.2. Motivação
Atualmente os dados estão presentes em vários meios digitais como bases de
dados de transações, posts do Facebook, Twitter, redes de sensores, logs de transações,
mensagens de texto, e-mail e etc. Tendo em vista que os dados já estão presentes e na
maioria das vezes digitalizados, o grande desafio se dá na
disponibilização/processamento/armazenamento desse dado para o usuário final, seja
ele outro sistema ou um indivíduo. Essa informação apresenta três tipos de
caraterísticas: possuem grandes volumes, da ordem de bilhões de registros, possuem
velocidades diferentes (eventos ocorrendo em diferentes janelas de tempo, como taxa de
amostragem de um sensor, compras em sites da internet e posts de redes sociais.) e
possuem com grande variedade de fontes de dados. Esses “3Vs” (volume, variedade e
velocidade) descrevem, segundo Gartner, que é a consultoria líder em pesquisa de
tecnologia da informação no mundo, o conceito de Big Data.
11
1.3. Objetivo
Dado o cenário anteriormente explicitado, temos a convicção que a análise de
dados pode ser uma área bem complexa. Ela possui diversas facetas, técnicas, apoiados
por diversos nomes, em diferentes negócios. A mineração de dados pode ser entendida
como a técnica que foca na modelagem e descoberta de conhecimento para realizar
predições. A parte de Inteligência de Negócios cobre a análise de dados utilizando
informações do negócio, utilizando consultas agregadas e respondendo a perguntas
descritivas do tipo On-line Analytical Processing (OLAP). Por exemplo, quantas
escovas de dente foram vendidas entre abril e maio no estado do RJ. A análise preditiva
foca na aplicação de modelos para realizar previsões . Por exemplo, baseado em um
histórico de transações bancárias, qual a probabilidade de uma determinada transação
ser fraudulenta ou não.
Todas as análises acima citadas partem do pressuposto de que já foi realizado
um prévio tratamento nos dados. Que estes já foram higienizados, estruturados,
padronizados e transformados. A esse ciclo damos o nome de Extract, Transform and
Load (ETL).
O ciclo de ETL envolve a extração dos dados de diversas fontes, a
transformação do dado (padronização, higienização, junção de diversas fontes, etc) e a
carga no sistema alvo, normalmente um banco de dados. O ciclo de ETL pode envolver
uma considerável complexidade, e grandes problemas operacionais podem ocorrer caso
seja mal modelado. Por ser um processo de natureza complexa, o ciclo ETL é
normalmente apoiado por ferramentas de mercado. Essas ferramentas trazem algumas
vantagens em relação ao processo normal, onde o desenvolvedor cria aplicações para
realizar a extração/transformação/carga dos dados. São elas: auto documentação
(permitem a visualização do fluxo de dados graficamente), reutilização de componentes,
design estruturado e padronizado, melhor desempenho (paralelismo) e consequente
aumento de produtividade e qualidade do processo.
Assim, o objetivo deste projeto é criar um framework capaz de apoiar o processo
de construção/execução de um ciclo ETL oferecendo os benefícios anteriormente
citados. Esse framework possibilitará a paralelização de processos e utilizará a técnica
de pipeline para prover ganhos de desempenho sobre a metodologia tradicional.
12
1.4. Metodologia
Para o desenvolvimento do nosso projeto adotamos o modelo de
desenvolvimento de software sequencial, conhecido como modelo em cascata.
Escolhemos esse modelo, pois partimos de requisitos e fases bem definidas, pois já
existem ferramentas parecidas como a que vamos construir. Assim nosso projeto ficou
constituído em cinco fases: Levantamento de requisitos, Projeto do Sistema,
Construção, Integração. Durante a construção realizamos testes unitários e de integração
em componentes críticos do sistema. Ao final, foram realizamos testes de validação para
verificar aderência com os requisitos.
Como se trata de um framework que deve ser extensível, tentamos criar nossas
classes bem desacopladas e com alto grau de coesão. Para isso, fizemos usos de alguns
padrões de projeto como: Fábrica Abstrata (Abstract Factory) e Singleton.
Por ser extensível, considerá-lo-emos como um framework caixa-cinza, ou seja,
já possui componentes e funcionalidades prontas, porém é permitido a partir da
extensão e inserção de novas classes, a criação de novos componentes e
funcionalidades. Mostraremos como estendê-lo na seção “Criando Novos Nós de
Processamento”
Utilizamos a linguagem Object Pascal, que é uma extensão da linguagem Pascal
com suporte a Orientação a Objeto, para o desenvolvimento do nosso framework. A
IDE utilizada foi a Borland Delphi Enterprise 7.
1.5. Estrutura do documento
Esse projeto de final de graduação está dividido em seis capítulos:
- Introdução: começamos o capítulo contextualizando e mostrando ao leitor em que área
da Engenharia de Computação estamos abordando. Em seguida, descreveremos qual a
motivação do projeto e os objetivos que queremos alcançar. Descreveremos as
ferramentas e o modelo de ciclo de vida que iremos utilizar.
- Ciclo ETL: nesse capítulo situaremos o leito no que se refere a um ciclo ETL.
13
Descreveremos cada uma das etapas do ciclo (Extração/Transformação/Carga) e
daremos alguns exemplos. Ao final mostraremos duas abordagens de criação de um
ciclo ETL e listaremos suas vantagens e desvantagens.
- Extreme DataStream: descreveremos para o leitor o principal componente do
framework. Definiremos alguns conceitos pertinentes as projeto como fluxo de dados,
metadados, nós de processamento e arestas. Mostraremos como definir esses conceitos
em nossa abstração de grafo utilizando uma estrutura XML.
- Implementação: mostraremos nesse capítulo artefatos como diagramas de classe,
diagrama de sequência. No nível de código fonte mostraremos quais padrões de projeto
utilizamos. Definiremos o problema de produtor-consumidor e como ele se adequa em
nossa necessidade. Por fim, mostraremos como é possível estender o framework para a
criação de novos nós de processamento.
- Exemplo de Uso – Paralelismo: partiremos para um caso de uso onde mostraremos a
capacidade de paralelizar um determinado ciclo ETL, descreveremos o problema e o
hardware utilizado. Mostraremos quatro versões de grafos e analisaremos a execução
dos mesmo, mostrando os resultado e as conclusões.
- Conclusões e Lições Aprendidas: nesse capítulo faremos um apanhado geral sobre o
projeto. O que aprendemos, quais pontos altos e baixos, erros e acertos durante o projeto
e das ferramentas escolhidas.
14
Capítulo 2:
Ciclo ETL
2.1. Introdução
Um ciclo de ETL (Extract, Transform e Load), refere-se a um processo no
domínio de banco de dados muito utilizado para realizar carga em sistemas de Data
Warehouse, Operational Data Store (ODS) e Datamarts. Também é vastamente
utilizado na área de integração de dados, migração de dados e Master Data
Management (MDM). Este ciclo envolve três etapas distintas: extração, transformação e
carga.
1.6. Extract (Extração)
A primeira parte de um ciclo ETL envolve extrair dados de sistemas fontes.
Esses sistemas podem ser bem heterogêneas, com dados em diversos formatos, e devem
ser consolidados para realizar a carga no Data Warehouse. Alguns formatos comuns de
dados incluem: banco de dados relacionais e arquivos planos, como arquivo CSV,
XML, HTML e etc.
Outra maneira de realizar o processo de ETL é a utilizando dados online, ou
seja, conforme os dados vão chegando através de um fluxo de dados, são tratados,
transformados e carregados no sistema alvo.
Uma etapa intrínseca à extração se refere a tratar somente dados em formatos
esperados e descartar dados de fontes desconhecidos. Ou seja, os dados advindos dos
sistemas fonte devem estar em um padrão ou estrutura previamente conhecidos.
1.7. Transform (Transformação)
Nessa fase é aplicadas uma série de regras e funções sobre os dados vindos da
etapa de extração para transformá-los em entradas para o processo de carga. Alguns
processos desta fase são:
1. Ordenação de um arquivo por uma determinada chave.
2. Selecionar somente algumas informações. Por exemplo, se o arquivo fonte tiver três
tipos de informações (nome, idade e nacionalidade) e queremos utilizar somente nome e
15
idade.
3. Padronização: como os arquivos podem vir de diversas fontes, podemos ter atributos
que representam a mesma informação, mas com dados diferentes. Exemplo: Em um
arquivo podemos ter masculino igual à “1” e feminino igual “2” e em outro Masculino
igual a “M” e Feminino igual a “F”.
4. Derivação de um novo cálculo. Por exemplo, valor_venda =
quantidade*preco_unitario.
5. Junção de múltiplas fontes de dados por chaves de junção. Podemos também efetuar
duplicação de registros.
Podemos agregar registros para formar um dado estatístico. Por exemplo, sumarizar
múltiplas linhas de um arquivo de transações para ter o total de vendas por loja, total de
vendas por região, etc.
6. Transpor o arquivo original, isto é, linhas viram colunas e colunas viram linhas.
7. Dividir um arquivo CSV em arquivos separados, um para cada coluna
8. Validar e buscar dados em tabelas ou arquivos de referência.
9. Aplicar qualquer tipo de validação simples ou complexa no dado. Por exemplo,
validar se um determinado campo apresenta uma data válida. Caso haja alguma exceção
poderá haver rejeição parcial ou completa.
1.8. Load (Carga)
A fase de carga é o momento em que os dados advindos da etapa anterior são
carregados no sistema alvo, que normalmente é um Data Warehouse. Essas cargas, em
sua maioria, são realizadas periodicamente (diariamente, semanalmente ou
mensalmente). Isso ocorre, porque o DW é usualmente utilizado para guardar
informações históricas. Conforme os dados são carregados no sistema, o banco de dados
é responsável por tratar as condições de integridade dos dados que estão sendo
inseridos, como por exemplo, realizar uma validação de data, disparar uma trigger para
atualizar outra tabela, tratar a unicidade de um dado.
1.9. Exemplo prático – Duas Abordagens
O ciclo de ETL pode envolver uma grande complexidade e problemas podem
ocorrer caso este seja mal modelado. Descreveremos sucintamente um exemplo de ciclo
ETL e mostraremos duas abordagens para sua construção. Em seguida, mostraremos
16
suas vantagens e desvantagens.
Figura 1 - Representação de um ciclo ETL, utilizando um grafo.
Acima apresentamos um ciclo ETL, descrito como um grafo.
Clientes.txt e Itens.txt: arquivos de entrada do ciclo ETL.
Leitor de arquivos: realizar leitura dos arquivos de entrada.
Consulta Receita: processo responsável por realizar uma consulta à Receita
Federal, utilizando CPF como chave e trazendo o nome da pessoa.
Cálculo da Nota Fiscal: A partir dos itens, o processo calcula a nota fiscal.
Ordenação: realiza a ordenação dos arquivos, um pela chave fkCliente e outra
pela chave pkCliente.
Junção: realiza a junção dos arquivos vindos das arestas nove e onze, utilizando
as chaves fkCliente e pkCliente.
Escritor de Arquivos: realizar a escrita do arquivo final em disco.
Arquivo de Saída: arquivo final do processo.
Abordagem Monolítica:
Nessa abordagem todo o ciclo ETL é criado a partir do zero, e todas as
transformações dos dados são realizadas através um único programa, seja ele um
executável ou um script. Imagine o ciclo completo ETL descrito acima sendo executado
apenas por uma aplicação. Teríamos um total acoplamento, uma baixa coesão e sem
possibilidade de reutilização dos componentes (dependendo da modelagem essa
reutilização pode ser dá em nível de código fonte apenas). Toda manutenção/evolução
seria feita em apenas um componente, o que acarretaria possíveis novos erros de difícil
rastreabilidade.
17
Abordagem um módulo por nó de processamento:
Nessa abordagem temos um executável/script por nó de processamento, onde um
nó X possui como entrada o arquivo A1 e uma saída A2. A saída do nó X será a entrada
para o nó Y. Assim o ciclo segue, onde a saída de um nó de processamento é entrada
para outro. Nessa abordagem, todos os nós de processamento devem possuir uma
maneira de ler/escrever no disco já que esta é à maneira de comunicação entre eles.
Para a descrição das vantagens e desvantagens entre as duas abordagens
chamaremos a monolítica, de abordagem A, e a outra de abordagem B. Na abordagem
A temos a vantagem em relação a B de realizarmos menos E/S no disco. Isso acarreta
um ganho de desempenho por não utilizar um dos recursos mais lentos do computador,
que é o acesso ao disco físico. Em contrapartida, na abordagem B podemos paralelizar o
ciclo ETL, ou seja, utilizar melhor os recursos de CPU em detrimento do E/S. Em um
ciclo ETL do tipo CPU bound, onde o maior tempo é gasto utilizando a CPU, o ciclo B
tende a possuir um melhor desempenho em relação ao A, pois realiza um melhor uso
dos recursos conseguindo paralelizar as tarefas. No exemplo acima, utilizando a
abordagem B, poderíamos executar os nós de processamento “Leitor de Arquivos”,
“Cálculo da Nota Fiscal” e “Ordenação por fkCliente” concorrentemente com “Leitor
de Arquivos”, “Consulta a Receita”, “Valida Node” e “Ordenação por pkCliente”.
Na abordagem A temos baixíssima reutilização apenas em nível de código fonte.
Na abordagem B, os nós de processamento são módulos separados e podemos
intercambiá-los para criar novos ciclos ETL.
Apresentaremos nas próximas seções nossa abordagem para a criação do ciclo
ETL, e tentaremos unir o melhor das duas abordagens anteriormente citadas, ou seja, um
ganho de produtividade por reutilização de nós de processamento sem perda de
desempenho.
18
Capítulo 3:
Extreme DataStream
1.10. Introdução
Ferramentas de ETL são construídas para poupar esforço (tempo e dinheiro)
eliminando, quase em sua totalidade, a escrita de código. Dentre algumas vantagens
podemos citar: auto documentação (permitem a visualização do fluxo de dados
graficamente), reutilização de componentes, design estruturado e padronizado, melhor
desempenho (paralelismo).
Nessas ferramentas podemos modelar o ciclo ETL utilizando a abstração de um
grafo, onde as arestas representam os dados fluindo de um processo para o outro, e os
vértices representam os nós de processamento. Podemos utilizar como exemplo a figura
já citada na seção anterior.
O objetivo do framework Extreme DataStream é permitir a execução de um
fluxo ETL, além de prover uma biblioteca onde novos nós podem ser incorporados ao
sistema de maneira simples. Não será desenvolvido nenhum tipo de interface gráfica
para a criação do fluxo ETL. Utilizaremos um arquivo XML para a descrição do fluxo
em si. Quando falarmos de nós, vértices ou blocos, estaremos nos referindo a nós de
processamento, ou seja, estruturas que realizam efetivamente o processo dos dados.
Quanto falarmos em arestas, estaremos no referindo ao o fluxo de dados flui através dos
nós de processamento, como os nós de processamento são interligados.
Conforme visto, nas duas abordagens anteriores, temos vantagens e
desvantagens. Abaixo citamos as resumidamente:
Abordagem Monolítica (Abordagem A)
- Vantagens: Menor quantidade de E/S no disco rígido em relação à abordagem
B, acarretando melhor desempenho.
- Desvantagens: Baixa reutilização, apenas em nível de código fonte.
Abordagem um módulo por nó de processamento (Abordagem B)
- Vantagens: Alta reutilização em nível de componentes. Possibilidade de
paralelizar o processo.
- Desvantagens: Baixo desempenho, pois os nós se comunicam através de
19
arquivos, ou seja, o arquivo gerado por um nó é entrada para o próximo nó.
Em nosso framework, tentaremos unir o melhor das duas abordagens, ou seja,
um alto desempenho com possibilidade de paralelismo, sem a necessidade de criação de
arquivos intermediários entre os processos e reutilização em nível de componentes.
Para não haver necessidade da criação de arquivos intermediários, utilizaremos a
memória RAM da máquina, ou seja, cada nó de processamento será uma unidade
autônoma provendo/consumindo dados de um buffer. Em alguns nós poderá haver a
necessidade de criação de arquivos temporários, como por exemplo, uma ordenação de
um arquivo que não cabe fisicamente na memória. Fica claramente evidenciado que
temos um problema clássico de produtor-consumidor, que será abordado nas próximas
seções. Resolvendo esse problema, estaremos retirando a única desvantagem da
abordagem B, e permanecendo com todas as suas vantagens. Claro que, neste momento,
não estamos levando em consideração o tempo gasto na sincronização entre os
processos/threads.
O desenvolvedor do ciclo ETL deverá descrever seu grafo utilizando um arquivo
XML, em um formato específico, que será descrito na próxima seção. Não é intuito
desse Projeto de Graduação, criar uma ferramenta de apoio para a criação desse grafo.
1.11. Fluxo de Dados
Para descrever nosso grafo do ciclo ETL utilizaremos um documento em
formato XML. Para garantirmos que esse documento esteja corretamente formatado, a
ferramenta utilizará um XML Schema.
Um arquivo que contenha as definições da linguagem XML Schema é chamado
de XML Schema Definition (XSD), o qual descreve a estrutura de um documento XML.
Essa linguagem foi amplamente utilizada no desenvolvimento da nota fiscal eletrônica
brasileira. Abaixo demonstramos o conteúdo do XSD. Explicaremos em detalhes cada
uma das seções desse documento nas próximas seções.
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified" attributeFormDefault="unqualified">
<xs:element name="etlManager"
type="ETL_MANAGER_TYPE"></xs:element>
20
<xs:complexType name="ETL_MANAGER_TYPE">
<xs:sequence>
<xs:element name="metadatas" type="METADATA_LIST"/>
<xs:element name="phases" type="PHASE_LIST"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="METADATA_LIST">
<xs:sequence>
<xs:element name="metadata" type="METADATA_TYPE"
minOccurs="1" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="METADATA_TYPE">
<xs:sequence>
<xs:element name="field" type="FIELD_TYPE" minOccurs="1"
maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="memory" type="xs:integer" use="required"/>
</xs:complexType>
<xs:complexType name="FIELD_TYPE">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="type" type="FIELD_ENUM" use="required"/>
<xs:attribute name="size" type="xs:integer"
use="required"/>
<xs:attribute name="initialPos" type="xs:integer"
use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="FIELD_ENUM">
<xs:restriction base="xs:string">
<xs:enumeration value="string"/>
<xs:enumeration value="integer"/>
<xs:enumeration value="byte"/>
<xs:enumeration value="longInt"/>
21
</xs:restriction>
</xs:simpleType>
<xs:complexType name="PHASE_LIST">
<xs:sequence>
<xs:element name="phase" type="PHASE_TYPE" minOccurs="1"
maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="PHASE_TYPE">
<xs:sequence>
<xs:element name="edges" type="EDGE_LIST"/>
<xs:element name="processes" type="PROCESS_LIST"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" use="required"/>
</xs:complexType>
<xs:complexType name="EDGE_LIST">
<xs:sequence>
<xs:element name="edge" type="EDGE_TYPE" minOccurs="1"
maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="PROCESS_LIST">
<xs:sequence>
<xs:element name="process" type="PROCESS_TYPE" minOccurs="1"
maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="PROCESS_TYPE">
<xs:sequence>
<xs:any maxOccurs="unbounded" processContents="skip"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="type" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="EDGE_TYPE">
22
<xs:attribute name="fromProcess" type="xs:string"
use="required"/>
<xs:attribute name="fromPort" type="xs:integer"
use="required"/>
<xs:attribute name="toProcess" type="xs:string"
use="required"/>
<xs:attribute name="toPort" type="xs:integer" use="required"/>
<xs:attribute name="metadata" type="xs:string"
use="required"/>
</xs:complexType>
</xs:schema>
1.12. Metadados – Layout
Metadados são dados sobre outros dados. No nosso contexto, os metadados farão
a descrição dos dados que trafegam entre as arestas do nosso ciclo ETL. Esses
metadados serão sempre formatados como registros de tamanho fixo.
Em um registro de tamanho fixo, um campo sempre começa e termina no
mesmo lugar em todos os registros. Por exemplo:
Pedro da Silveira Magalhaes 01000022421030RJ
Jose Carlos da Silva 00900033920103SP
Renata Souza 00080010103242MG
Luis Felipe 00150000123242BA
O conjunto de dados acima representa um arquivo onde seus registros possuem
tamanho fixo.
Nome – Posição Inicial = 1 – Posição Final = 30
Renda – Posição Inicial = 30 – Posição Final= 36
CEP – Posição Inicial = 36 – Posição Final = 44
UF – Posição Inicial = 44 – Posição Final = 46
Para o caso citado acima, o registro possui um tamanho de 46 bytes. Não são
necessários delimitadores, pois todos os campos possuem tamanho fixo.
Abaixo, descrevemos o XSD para a sessão de declaração de metadados:
<xs:complexType name="METADATA_TYPE">
<xs:sequence>
23
<xs:element name="field" type="FIELD_TYPE" minOccurs="1"
maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="memory" type="xs:integer" use="required"/>
</xs:complexType>
<xs:complexType name="FIELD_TYPE">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="type" type="FIELD_ENUM" use="required"/>
<xs:attribute name="size" type="xs:integer" use="required"/>
<xs:attribute name="initialPos" type="xs:integer"
use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="FIELD_ENUM">
<xs:restriction base="xs:string">
<xs:enumeration value="string"/>
<xs:enumeration value="integer"/>
<xs:enumeration value="byte"/>
<xs:enumeration value="longInt"/>
</xs:restriction>
</xs:simpleType>
Na seção “metadata” devemos definir um ID único para nosso metadado.
Quando um dado flui de um nó para outro, esta informação deverá ser armazenada na
aresta que interliga esses dois nós. O dado permanece na aresta, alocado na memória
RAM do computador, e o tamanho do buffer de alocação é configurado através da tag
“memory”. Em outros termos, a tag “memory” define o tamanho máximo da fila no
problema produtor-consumidor. Destacamos uma seção apenas para descrever o
processo de produtor-consumidor.
Na seção “field” descrevemos informações sobre o campo do registro de
24
tamanho fixo. Para isso temos os seguintes atributos:
- type: representa um tipo enumerado que define o tipo de dado no campo. Pode ser
string, integer, byte ou um longInt. Apesar de termos listados todos esses tipos de
campos, somente está implementado na solução o campo do tipo string.
- initialPos: define onde o campo começa dentro do registro de tamanho fixo.
- size: define o tamanho do campo.
Destacamos abaixo um exemplo de um metadado definido:
<metadata id="Metadata0" memory="40000">
<field type="string" size="2" initialPos="0">VALOR</field>
<field type="string" size="2" initialPos="2">CRLF</field>
</metadata>
1.13. Nós de Processamento
São as estruturas que efetivamente realizam o trabalho sobre os dados. O intuito
do projeto não é prover os mais variados tipos de transformações sobre os dados, e sim
uma maneira simples para o desenvolvedor criar os seus próprios nós de processamento.
A criação de nós bem coesos e flexíveis garante ao desenvolvedor do fluxo de ETL uma
maior reutilização dos blocos já criados. Alguns exemplos de nós de processamento são:
- nó capaz de realizar a ordenação de um arquivo a partir de uma chave.
- nó capaz de retirar a duplicidade de registros, utilizando a escolha de uma
chave única.
- nó capaz de realizar a junção de dois fluxos de dados utilizando uma chave de
junção.
- nó capaz de filtrar os registros de um fluxo de dados utilizando uma expressão
lógica / matemática.
Os nós de processamento possuem como entrada fluxos de dados. Esses fluxos
de dados pode se originar de qualquer recurso do sistema, seja um disco físico, a própria
memória RAM ou uma interface de rede, bastando para isso criar nós específicos.
Abaixo descrevemos o XSD somente da seção de criação de nós de
processamento.
<xs:complexType name="PROCESS_LIST">
<xs:sequence>
<xs:element name="process" type="PROCESS_TYPE" minOccurs="1"
25
maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="PROCESS_TYPE">
<xs:sequence>
<xs:any maxOccurs="unbounded" processContents="skip"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="type" type="xs:string" use="required"/>
</xs:complexType>
Na seção “process” definimos um nó de processamento que fará parte do nosso ciclo
ETL. Esses nós devem conter sempre o atributo “id” e o atributo “type”. O “id” define
unicamente um processo dentro do ciclo, enquanto o “type” define o tipo do processo.
Para executar nossos exemplos criamos apenas três tipos de processos, que são eles
FileReader, FileWriter e Factorial.
- FileReader
Nó responsável pela leitura de um arquivo texto de tamanho fixo.
Exemplo:
<process id="FileReader0" type="FileReader">
<recordSize>4</recordSize>
<filePath>D:\Projetos\Factorial.txt</filePath>
</process>
O tamanho do registro do arquivo é indicado pela tag “recordSize”. O caminho do
arquivo é indicado pela tag “filePath”. Esse leitor possui a capacidade de ler partes
segmentadas do arquivo de entrada, dependendo da quantidade de arestas de saída
ligadas a ele. Considere o trecho de ciclo ETL abaixo, onde o “FileReader” aparece
lendo como entrada o arquivo “Numeros.txt” e ligado a três processos que são
responsáveis por realizar um cálculo de score.
26
Figura 2 - Exemplo de Utilização do FileReader para realizar paralelismo
Assim o nó “FileReader” se comportará da seguinte maneira: ele entregará
blocos de registros aos nós conectados a ele, utilizando uma política de round-robin.
Dessa maneira, conseguiremos paralelizar o cálculo do score, onde cada um dos nós
“CalculaScore” executará sua tarefa em um processador. Mais a frente mostraremos um
exemplo prático e os resultados obtidos com esse paralelismo.
- FileWritter
Nó responsável pela escrita dos dados em disco. Análogo ao “FileReader”, esse escritor
de arquivos possui a capacidade de receber N fluxos de dados, ou seja, podemos
conectar quantas arestas quisermos em um nó do tipo “FileWriter”. Cada uma dessas
arestas será conectada a uma porta diferente. Assim o “FileWriter” lerá cada fluxo de
dados utilizando um política de round-roubin e escrevendo o resultado em arquivo
físico, diretamente no disco rígido.
Exemplo:
<process id="FileWriter0" type="FileWriter">
<filePath>D:\Factorial_out.txt</filePath>
</process>
O caminho do arquivo de saída, o qual será efetivamente gravado em disco, é indicado
pela tag “filePath”.
- Factorial
Nó responsável pelo cálculo do fatorial de um número. Esse nó, por ser muito
específico, provavelmente não apresenta nenhuma utilização prática. Ele foi apenas
criado para demostrar a capacidade da ferramenta de realizar tarefas em paralelo.
Exemplo:
27
<process id="Factorial1" type="Factorial">
<factorialField>VALOR</factorialField>
</process>
A tag “factorialField” indica qual campo deverá ser calculado o fatorial.
1.14. Arestas
Quando ligamos um nó de processamento a outro, criamos uma aresta. Os dados
que trafegam de um nó pra outro ficam temporariamente armazenados na aresta, em um
buffer na memória RAM da máquina.
Abaixo descrevemos o XSD somente da seção de criação de nós de
processamento.
<xs:complexType name="EDGE_TYPE">
<xs:atribute name="fromProcess" type="xs:string"
use="required"/>
<xs:attribute name="fromPort" type="xs:integer"
use="required"/>
<xs:attribute name="toProcess" type="xs:string"
use="required"/>
<xs:attribute name="toPort" type="xs:integer" use="required"/>
<xs:attribute name="metadata" type="xs:string"
use="required"/>
</xs:complexType>
A tag “fromProcess” e “fromPort” indica de qual nó/porta parte a aresta. A tag
“toProcess” e “toPort” indica em qual nó/porta a aresta chega. Assim para definir uma
aresta, ou seja, de onde ela parte até onde ela chega, devemos defini-la com duas tuplas:
(fromProcess, fromPort) e (toProcess e toPort). A tag “metadata” indica o formato dos
dados que trafegam na aresta.
Abaixo segue um exemplo de definição de duas arestas.
<edges>
<edge fromProcess="FileReader0" fromPort="0"
toProcess="Factorial1" toPort="0" metadata="Metadata0"/>
<edge fromProcess="Factorial1" fromPort="0"
toProcess="FileWriter0" toPort="0" metadata="Metadata1"/>
</edges>
28
Capítulo 4:
Implementação
1.15. Visão geral
Para modelar nosso framework partimos dos conceitos chaves como: porta,
processo, aresta e fases. Modelamos seus relacionamos através de um modelo
conceitual que em seguida deu entrada no nosso diagrama de classes. Utilizamos o
diagrama de sequência apenas para descrever o problema de Produtor-Consumidor.
Utilizando o diagrama de classes, fica claro como deve ser feita a extensão do
código para a criação de novos componentes. Na seção “Criando Novos Nós de
Processamento” será mostrado como estender o framework para a criação de novos
componentes.
1.16. Padrões de Projeto
Descreve uma solução geral que pode ser utilizada para um problema recorrente
no desenvolvimento de software orientado a objetos. Assim os padrões de projeto
definem relações e interações entre as classes/objetos em um nível de abstração mais
alto.
Os padrões de projeto têm como objetivo facilitar as reutilizações de soluções e
estabelecer um vocabulário comum facilitando documentação e o entendimento do
código gerado.
Em nossa implementação utilizamos dois padrões de projeto:
* Factory (Fábrica)
Objetivo
- Criar um objeto sem expor a lógica de instanciação ao cliente
- Criação de um novo objeto através de uma interface comum
Utilização
- Cliente necessita de um produto, mas ao invés de criá-lo diretamente, ele pede
à fábrica por um novo produto, provendo a informação sobre o tipo de objeto
que ele precisa.
29
- A fábrica instancia um novo produto concreto e retorna ao cliente o objeto
recentemente criado.
- O cliente utiliza o objeto como um produto abstrato sem se preocupar com sua
implementação concreta.
Vantagem
- Conseguir adicionar um novo produto sem alterar a interface de criação,
alterando apenas a fábrica. Existem certas implementações de fábrica que
permite a inclusão de novos tipos concretos sem alterar a fábrica.
* Singleton
Às vezes é importante termos apenas uma instância da classe. Usualmente
singletons são utilizadas para centralizar o gerenciamento de recursos internos ou
externos provendo um ponto global de acesso.
Objetivo
- Garantir que apenas uma instância da classe será criada
- Prover um ponto de acesso global ao objeto
Figura 3 - Modelo de classes do padrão Singleton
Utilização
O padrão Singleton define a operação getInstance, a qual expõe uma única
instância a ser acessada pelos clientes. O método getInstance é responsável por
criar uma única instância da classe caso ela ainda não tenha sido criado e
retorná-la.
1.17. Diagrama de Classes
30
Figura 4 - Diagrama de Classes (Visão de Processos)
A classe TProcessFactory, juntamente com a classe abstrata TProcess,
representa a implementação do padrão de projeto Fábrica. Esta Fábrica
(TProcessFactory) é responsável pela criação dos nós de processamento do sistema.
Cada nó de processamento é executado em threads separadas, por isso a classe TProcess
é filha da classe Thread. Para criação de um novo nó devemos passar a sua descrição
representada pela classe TProcessDescription.
Cada processo possui uma lista de portas de saídas e entradas, representadas
respectivamente, pelas classes TInputCollection e TOutputCollection. Estas duas classes
utilizam outra Fábrica (TPortFactory) que é responsável pela criação da porta de
comunicação.
31
Figura 5 - Diagrama de Classes (Visão Comunicação entre Processos)
A classe TPortFactory juntamente com a classe TPortDescription implementa
novamente o padrão Fábrica. Essa fábrica é responsável pela criação tanto de portas de
saída (classe abstrata TOutput) como portas de entrada (classe abstrata TInput).
32
As classes TFixedRecordOutput e TFixedRecordInput representam as portas de
saída e entrada respectivamente, que tratam os registros de tamanho fixo. Essas duas
classes utilizam a classe TDataPacket (sua dependência se dá pela classe TPort que é
pai das classes TInput e TOutput e possuí um atributo do tipo TDataPacket) que abstrai
um ResultSet (um conjunto de registros). Esta classe é onde os dados são armazenados
para posterior escrita deles no buffer de transferência.
Figura 6 - Diagrama de Classes (Visão Comunicação entre Processos - Classes Abstratas)
A classe TEdge abstrai a aresta que liga duas portas de nós de processamento.
Nessa classe se encontra o buffer responsável pelo armazenamento do TDataPacket.
O método Write da classe TOutput escreve dados para a classe TDataPacket. Assim que
o buffer do TDatapacket está cheio, ele é escrito no TBuffer, utilizando o método Write.
A parte de leitura é análoga. Na seção Diagrama de Sequência descreveremos essa
interação.
33
Figura 7 - Diagrama de Classes (Visão Metadados)
A estrutura de classes acima é responsável pela leitura do XML que define o
grafo. Temos como classe raiz, a classe TETLGraphXML que é implementada
utilizando o padrão de projeto Singleton. Tomamos essa decisão por precisar de um
ponto único de acesso à abstração do grafo.
34
Figura 8 - Diagrama de Classes (Visão Controlador do Grafo)
A classe TGraphManager é interface para gerenciamento da execução do grafo. Ao
criarmos a TPhase, criaremos todos os processes dessa fase. Para isso, utilizei
novamente o padrão de projeto fábrica, representado pelas classes TProcessFactory e
TProcess. A classe TPhase possui o método Launch(), que quando chamado executa a
fase do grafo. Cada processo e fase é executado em threads separada. Além destas
threads, temos a classe TWacthDogThread que verifica de tempos em tempos o estado
de execução dos processos.
1.18. O problema Produtor-Consumidor
O problema de produtor-consumidor é um exemplo clássico de sincronização.
Ele estabelece os processos concorrentes que compartilham uma área de escrita/leitura.
35
O produtor é responsável por gerar os dados que serão disponibilizados na área de
leitura/escrita. Concorrentemente, o consumidor estará lendo e removendo os dados
dessa área. O problema é ter certeza que o produtor não tentará adicionar dados na área
de leitura/escrita quando estiver cheia ,e que o consumidor não tentará remover dados
quando a área estiver vazia.
Assim, quando temos a área de escrita/leitura cheia o produtor deve esperar até
que haja espaço. O mesmo ocorre para o consumidor que deve esperar até que exista
alguma informação na área para ser consumida. Assim que o produtor coloca dados na
área de leitura/escrita ele avisa o consumidor, para ele iniciar o processamento. Quando
o consumidor retira dados da área ele avisa ao produtor. Essa solução é atingida através
de uma comunicação entre processos, que pode ser implementada utilizando recursos do
sistema operacional como semáforos e mutexes.
Abaixo mostramos uma solução retirada do Wikipédia, que é a mesma que
utilizaremos ao abordar o problema em nosso projeto. Ela utiliza o recurso de semáforos
para realizar a sincronização entre processos.
semaphore fillCount = 0; // itens produzidos
semaphore emptyCount = BUFFER_SIZE; // espaço remanescente
procedure producer() {
while (true) {
item = produceItem();
down(emptyCount);
putItemIntoBuffer(item);
up(fillCount);
}
}
procedure consumer() {
while (true) {
down(fillCount);
item = removeItemFromBuffer();
up(emptyCount);
consumeItem(item);
}
}
No contexto de nossa aplicação nos deparamos com o problema de produtor
36
consumidor no momento de trafegar os dados entres os nós de processamento. Para
ilustramos o problema do produtor-consumidor no contexto de nossa aplicação
utilizamos o diagrama de sequências abaixo:
Figura 9 - Diagrama de sequência mostrando o problema do Produtor-Consumidor
A classe TOutput é responsável pela escrita dos dados, utilizando a chamada ao
método Write da classe TEdge. A classe TEdge escreve no buffer, realizando a tratativa
do problema produtor-consumidor. Ela verifica se possui espaço no buffer pelo o
semáforo EmptyCount. Caso haja espaço, ela realiza a escrita e notifica o outro
semáforo FillCount que os dados podem ser consumidos. Caso não haja ela “dorme” e
espera o outro processo acordá-la.
37
A classe TInput é responsável pela leitura dos dados, utilizando a chamada ao
método Read da classe TEdge. Ela verifica se possui algum dados no buffer, verificando
o semáforo FillCount. Caso haja dados para serem consumidos, ela realiza a leitura do
dado. Caso não haja, ela “dorme” e espera o outro processo acordá-la.
Abaixo colocamos dois trechos de código, um do método Write e outro do
método Read da classe TEdge, em linguagem pascal.
procedure TEdge.Write(Content: PAnsiChar; Size: integer);
begin
//espera ter uma unidade disponivel no semaforo
WaitForSingleObject(EmptyCount, INFINITE);
//move informações para o buffer ponteiro por pt
Move(Size, pt^, SizeOf(Integer));
pt := pt + SizeOf(Integer);
Move(Content^, pt^, Size);
pt := FBuffer;
//incrementa o semaforo com uma unidade
ReleaseSemaphore(FillCount, 1, nil);
end;
function TEdge.Read(var Content: PAnsiChar; var Size: integer):
boolean;
begin
EOF := true;
WaitForSingleObject(FillCount, INFINITE);
//lê o tamanho dos dados do buffer
Move(pt^, Size, SizeOf(Integer));
pt := pt + SizeOf(Integer);
if (Size <> -1) then
begin
Move(pt^, FContent^, Size);
Content := FContent;
EOF := false;
end;
//reseta ponteiro
pt := FBuffer;
ReleaseSemaphore(EmptyCount, 1, nil);
end;
38
1.19. Criando Novos Nós de Processamento
Um dos objetivos do framework é a possibilidade de estendê-lo e criar novos
componentes (novos nós de processamento). Para isso montamos nossa estrutura de
classes de modo que esta tarefa fosse bem simples, bastando para isso realizar a
extensão de classes do framework.
Para a criação de novos nós de processamento devemos estender a classe
TProcess e inserir um novo tipo de nó na fábrica TProcessFactory. As informações
sobre o nó, ou seja, os metadados do nó estão disponíveis através da propriedade
FXMLNode herdada da classe TProcess. Para acessar as portas de entrada e saída
utilizamos as propriedades FInputCollection do tipo TInputCollection e
FOutputCollection do tipo TOutputCollection, que são herdadas da classe TProcess.
Abaixo colocamos trechos de código mostrando a inclusão do nó Fatorial
type
TFactorial = class(TProcess)
....
public
procedure Run(); override;
procedure TFactorial.Run;
...
begin
while (InputPort.Read(Value)) do //le dados da porta de entrada
begin
move(Value^, FNumberStr[1], 2); //valor a ser calculado
FactorialStr := IntToStr(GetFatorial(StrToInt(FNumberStr)));
....
OutStr := FOutNumber + #13#10;
OutPutPort.Write(Pansichar(OutStr)); //valor fatorial
end
InputPort.Close(); //fecha porta de entrada
OutPutPort.Close(); //fecha a porta de saída
end;
type
TProcessFactory = class
....
39
class function TProcessFactory.GetProcess(ProcessDesc:
TProcessDescription): TProcess;
begin
...
if (ProcessDesc.ProcessType = 'Factorial') then
begin
Result := TFactorial.Create(ProcessDesc.PhaseID,
ProcessDesc.ProcessID);
Exit;
end;
end;
40
Capítulo 5:
Exemplo de Uso – Paralelismo
1.20. Introdução
O objeto desta seção é demonstrar a utilização do framework para a execução de
um processo ETL. Para demonstrar a capacidade de realizar paralelismo utilizaremos
quatro tipos de grafo. Todos os grafos possuem a mesma saída, porém utilizam níveis
de paralelismo diferentes. Mostraremos o ganho em paralelizar o ciclo ETL através
desses exemplos.
1.21. O Problema
O problema que trataremos é bastante simples e capaz de ser paralelizado. Ele
consiste em calcular o fatorial de uma lista de números. Para aumentar a utilização de
CPU, ao invés de calcular apenas uma vez o fatorial do número, realizaremos esse
procedimento 10 vezes.
- Entrada do problema (lista de números):
Um arquivo texto, com 35.126.784 números inteiros, variando de 1 a 10, separador por
CR+LF. Esse arquivo possui um total de 140.507.136 bytes, ou seja, 35.126.784 * 4
bytes (2 bytes representando os números e mais 2 bytes de CR+LF).
- Saída
Um arquivo texto, com 35.126.784 números inteiros, variando de 1 a 3.628.800,
separados por CF+LF. Esse arquivo possui um total de 316.141.056 bytes, ou seja.
35.126.784 * 9 bytes (7 bytes representando o resultado do fatorial e mais 2 bytes de
CR+LF).
1.22. Hardware Utilizado
O objetivo do exemplo de uso é demonstrar o paralelismo e o ganho de
desempenho no ciclo ETL. Nosso ciclo possui a característica de ser CPU Bound. Dado
isso necessitamos descrever o processador e sua quantidade de núcleos.
O processador utilizado será o Core i3 da família de processadores Intel,
destinado a Desktops x86-64. Ele possui o recurso de Hyper-Threading que lhe
41
possibilita simular a existência de um maior número de núcleos, fazendo com que o seu
desempenho aumente. Mais especificamente este processador possuí dois núcleos de
processamento físico e dois virtuais, ou seja, ele possuí dois núcleos de processamento
físico e simula mais dois.
A tecnologia Hyper-Threading é uma tecnologia usada em processadores que
realizam a simulação de mais dois núcleos. Essa técnica é utilizada para oferecer uma
maior eficiência na utilização de recursos do processador. Segundo a Intel, o uso dessa
tecnologia oferece um aumento de desempenho de até 30%.
Abaixo mostramos uma imagem da janela do Gerenciador de Tarefas do
Windows, demonstrando a presença de quatro núcleos de processamento:
Figura 10 - Gerenciador de Tarefas do Windows. Como podemos ver o computador utilizado
possui quatro processadores.
1.23. Grafos Utilizados
Executaremos quatro grafos em nosso exemplo. O primeiro deles possuirá
42
apenas um nó de cálculo de fatorial, o segundo dois, e assim sucessivamente. Para
exemplificar o grafo segue abaixo uma figura do grafo com um nó de processamento e
outro com quatro nós de processamento.
Figura 11 - Representação do grafo com apenas um nó de cálculo de fatorial
<edges>
<edge fromProcess="FileReader0" fromPort="0"
toProcess="Factorial1" toPort="0" metadata="Metadata0"/>
<edge fromProcess="Factorial1" fromPort="0"
toProcess="FileWriter0" toPort="0" metadata="Metadata1"/>
</edges>
Acima apresentamos a listagem de arestas do grafo com um nó de
processamento de fatorial.
Figura 12 - Representação do grafo com quatro nós de cálculo de fatorial
43
<edges>
<edge fromProcess="FileReader0" fromPort="0"
toProcess="Factorial1" toPort="0" metadata="Metadata0"/>
<edge fromProcess="FileReader0" fromPort="1"
toProcess="Factorial2" toPort="0" metadata="Metadata0"/>
<edge fromProcess="FileReader0" fromPort="2"
toProcess="Factorial3" toPort="0" metadata="Metadata0"/>
<edge fromProcess="FileReader0" fromPort="3"
toProcess="Factorial4" toPort="0" metadata="Metadata0"/>
<edge fromProcess="Factorial1" fromPort="0"
toprocess="FileWriter0" toPort="0" metadata="Metadata1"/>
<edge fromProcess="Factorial2" fromPort="0"
toProcess="FileWriter0" toPort="1" metadata="Metadata1"/>
<edge fromProcess="Factorial3" fromPort="0"
toProcess="FileWriter0" toPort="2" metadata="Metadata1"/>
<edge fromProcess="Factorial4" fromPort="0"
toProcess="FileWriter0" toPort="3" metadata="Metadata1"/>
</edges>
Acima apresentamos um XML de exemplo com um grafo com quatro nós de
processamento do cálculo do fatorial.
1.24. Resultados
Realizamos a medição da execução dos grafos utilizando o aplicativo do
Windows “Monitor de Desempenho” (perfmon.exe).
- Execução do Grafo com um nó de Fatorial
Consumo de CPU médio: 25,13%
Tempo total: 135.532 milissegundos
Figura 13 - Uso de CPU x Tempo – um processador
44
- Execução do Grafo com dois nós de Fatorial
Consumo de CPU médio: 51,76%
Tempo total: 80.995 milissegundos
Figura 14- Uso de CPU x Tempo – dois processadores
- Execução do Grafo com três nós de Fatorial
Consumo de CPU médio: 72,02%
Tempo total: 74.110 milissegundos
Figura 15- Uso de CPU x Tempo – três processadores
- Execução do Grafo com quatro nós de Fatorial
Consumo de CPU médio: 99,22%
45
Tempo total: 68.484 milissegundos
Figura 16- Uso de CPU x Tempo – quatro processadores
1.25. Conclusões
Processos Tempo(ms) Consumo de CPU Fator (CPU) Fator(Tempo)%
Um 135.532 25,13% - -
Dois 80.995 51,76% 2,06 0,40239
Três 74.110 72,02% 1,39 0,085
Quatro 68.484 99,02% 1,37 0,07591
Figura 17 - Tabela com indicadores e demonstrativo dos resultados.
A coluna “Fator (CPU)” representa o quanto se aumenta de consumo da CPU de
uma execução para outra. A coluna “Fator (Tempo)” representa quanto se diminuiu de
tempo de uma execução para outra.
Conforme esperado, conseguimos diminuir o tempo de a execução da tarefa
utilizando paralelismo. Quando comparamos os casos de “um processo” e outro com
“dois processos” de fatorial, o consumo de CPU aumentou em 2,06 vezes e
conseguimos uma diminuição de tempo da ordem de 0,40 ou 40%. Ou seja, dobramos a
quantidade de processos utilizados, e conseguimos um ganho de tempo bem próximo de
dois 50%. Porém, nos outros casos nosso ganho já foi bem inferior, mesmo dobrando o
46
tempo de consumo do CPU. Na minha visão esse ganho não foi tão expressivo, pois
conforme citado na seção “Hardware utilizado”, o computador utilizado possui dois
cores físicos e dois virtuais. Segunda a Intel, o ganho de desempenho do processador
virtual é de até 30% mas conforme verificado em nossos exemplos o ganho foi em torno
de 8-9%.
Figura 18- Gráfico Consumo de CPU x Número de processos
O gráfico acima mostrar o Consumo de CPU x Tempo gasto para execução dos
grafos. Podemos ver que o a linha em vermelho (tempo gasto) que liga o “processo um”
ao “processo dois”, possui o menor coeficiente angular. Ou seja, a maior eficiência
(aumento do número de CPUs proporcional à diminuição do tempo gasto), foi atingida
nessa fase. Nos outros, casos essa eficiência não foi expressiva, ou seja, o coeficiente
angular ficou mais próximo de zero.
47
Capítulo 6:
Conclusões e Lições Aprendidas
Nesse momento torna-se necessário verificar se os objetivos propostos foram
atingidos. Para isso, revisitaremos a seção “1.3 Objetivo” onde citamos as seguintes
vantagens:
- Auto-documentação: quando utilizamos a abstração de um grafo para demonstrar o
ciclo ETL, conseguimos visualizar facilmente o fluxo de dados e as dependências entre
os nós de processamento. Além disso, como os nós de processamento são coesos fica
fácil compreender qual é a responsabilidade/objetivo de cada nó. Nossa abstração ainda
é feita em baixo nível, utilizando arquivos XML. Idealmente deveria haver uma
ferramenta capaz de plotar/editar o grafo de ciclo de ETL visualmente.
- Reutilização de componentes: utilizando padrões de projeto, boas práticas de
arquitetura de código e uma linguagem orientada a objetos, foi possível criar um
framework simples de ser estendido. Com isto ficou facilitada a tarefa e criar novos
componentes, assim como reutilizar componentes previamente criados.
- Melhor desempenho: por utilizarmos técnica de pipeline e paralelismo, obtivemos um
desempenho melhor do que técnicas tradicionais de tratamento de dados. Realizamos o
mesmo tratamento citado no capítulo 5 criando um executável para o processo, e o
tempo total ficou em torno de 153.121 milissegundos. Logo, até no primeiro grafo de
exemplo, conseguimos ter um ganho de desempenho.
- Aumento de produtividade e qualidade do processo: O aumento de produtividade e
qualidade do processo só será percebido com o passar do tempo, onde novos
componentes serão incluídos no framework.
Partindo da proposta inicial de criação de um framework que permitisse
paralelismo e pipeline, conseguimos concluir o objeto. No que tange ao ferramental de
apoio, utilizamos a linguagem object pascal do ambiente de desenvolvimento da
Borland, que carece de uma maior comunidade de desenvolvedores, bibliotecas de
apoio e documentação. Para um sistema mais robusto acredito que deveria ter escolhido
a linguagem C++. Porém, por ser uma linguagem onde possuíamos maior domínio,
consegui atingir o objetivo dentro de um prazo aceitável.
48
Utilizamos o ciclo de vida em cascata por já possuir uma definição bem clara
dos requisitos, e conforme esperado, o mesmo se adequou ao processo de engenharia de
software.
O trabalho final foi importante para meu aprendizado em áreas como arquitetura
de código (utilizando padrões de projeto), concorrência de processos/threads, XML, e
XML Schema.
49
Referências
1. Producer-Consumer Problem, Wikipédia. Disponível em:
<https://en.wikipedia.org/wiki/Producer%E2%80%93consumer_problem>.
Acessado em 15 jun. 2013
2. Factory Pattern, OODesign.com – Object Oriented Design. Disponível em:
<http://www.oodesign.com/factory-pattern.html>. Acessado em 15 jun. 2013.
3. Singleton Pattern, OODesign.com – Object Oriented Design. Disponível em:
< http://www.oodesign.com/singleton-pattern.html >. Acessado em 15 jun.
2013.
4. XML Schema Tutorial, w3schools.com. Disponível em:
<http://www.w3schools.com/schema/>. Acessado em 15 jun. 2013.