gerenciamento de texturas para aplicaÇÕes de...
Post on 28-Jul-2020
8 Views
Preview:
TRANSCRIPT
TECNOLOGIAS DE PROCESSAMENTO DE BIG DATA
APLICADAS À MANUTENÇÃO PREDITIVA DE
EQUIPAMENTOS
Raphael Barros de Oliveira Santos
Projeto de Graduação apresentado ao Curso de
Engenharia Eletrônica e de Computação da Escola
Politécnica, Universidade Federal do Rio de
Janeiro, como parte dos requisitos necessários à
obtenção do título de Engenheiro.
Orientadores:
Alexandre de Assis Bento Lima
Heraldo Luís Silveira de Almeida
Rio de Janeiro
Fevereiro de 2017
ii
iii
iv
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(s) autor(es).
v
À minha família, em especial aos meus pais,
que sempre me apoiaram em todos
os momentos de minha vida.
vi
AGRADECIMENTO
Primeiramente agradeço a Deus, que me acompanhou por toda esta jornada. Em
seguida, agradeço à minha família. Não há como expressar toda a gratidão que sinto por
ela, em especial aos meus pais. Tenho certeza que devo todas as minhas conquistas a eles.
Gostaria de agradecer também à minha namorada Louise, por toda a paciência e
por ter deixado meus dias mais felizes ao longo do desenvolvimento deste projeto. Um
agradecimento especial aos meus amigos, muitos deles conhecidos na faculdade. Caio,
Caio, Douglas, Marcelão, Rayssa e todos os outros que levarei para a vida inteira.
Agradeço à Radix Engenharia e Software por todo o apoio fornecido durante
minha estadia na empresa. Em especial, agradeço ao coordenador Leidson Germano e toda
a equipe de metais e mineração. Arthur, Gabriel, Rafael, Willian e muitos outros, que
tiveram paciência e me ensinaram muito mais do que eu julgava ser possível aprender em
um estágio.
Agradeço imensamente aos meus orientadores Alexandre de Assis e Heraldo Luís,
pois puderam dedicar seu tempo a este trabalho, permitindo que eu concluísse mais esta
etapa em minha vida. O apoio de vocês foi fundamental.
Finalmente, agradeço ao povo brasileiro, pois sua contribuição foi indispensável
para a minha formação. Espero retribuir este investimento depositado em mim.
vii
RESUMO
Este projeto de graduação consiste no desenvolvimento de um sistema que utiliza
tecnologias de processamento de Big Data para determinar a necessidade de manutenção
de equipamentos industriais através das informações de telemetria coletadas. O sistema
foi feito utilizando, além de outras tecnologias, o Apache Spark e o SGBD Cassandra.
Técnicas de processamento paralelo e distribuído foram empregadas em seu
desenvolvimento para atingir melhor desempenho.
Palavras-Chave: Big Data, manutenção preditiva, computação paralela, computação
distribuída, Apache Spark, Cassandra, banco de dados.
viii
ABSTRACT
This undergraduate project consists of the development of a system that uses
Big Data processing technologies to determine the need to maintain industrial equipment
through the collected telemetry information. The system was made using, in addition to
other technologies, Apache Spark and the Cassandra DBMS. Parallel and distributed
processing techniques were employed in its development to achieve better performance.
Key-words: Big Data, predictive maintenance, parallel computing, distributed
computing, Apache Spark, Cassandra, database.
ix
SIGLAS
API – Application Program Interface
ARPANET – Advanced Research Projects Agency Network
CID – Component Identifier
CSV – Comma-separated values
DAG – Direct Acyclic Graph
ESB – Enterprise Service Busses
FMI – Failure Mode Identifier
MID – Module Identifier
ORM – Object-Relational Mapping
RDD – Resilient Distributed Dataset
SEQUEL – Structured English Query Language
SGBD – Sistemas Gerenciadores de Bancos de Dados
SMTP – Simple Mail Transfer Protocol
SQL – Structured Query Language
XML – Extensible Markup Language
x
Sumário
Capítulo 1 ........................................................................................................................ 1
1.1 – Tema .................................................................................................................... 1
1.2 – Delimitação .......................................................................................................... 1
1.3 – Justificativa .......................................................................................................... 2
1.4 – Objetivos .............................................................................................................. 2
1.5 – Metodologia ......................................................................................................... 3
1.6 – Organização do documento.................................................................................. 4
Capítulo 2 ........................................................................................................................ 5
2.1 – Computação Paralela .......................................................................................... 5
2.2 – Computação Distribuída ..................................................................................... 9
2.3 – Programação Funcional ..................................................................................... 10
Capítulo 3 ...................................................................................................................... 21
3.1 – Bancos de Dados ................................................................................................ 21
3.1.1 – Bancos de Dados Relacionais ..................................................................... 21
3.1.2 – Bancos de Dados Não-Relacionais ............................................................. 23
3.1.3 – Comparações ............................................................................................... 25
3.2 – ESB .................................................................................................................... 26
3.2.1 – JBoss Fuse ................................................................................................... 27
3.3 – Linguagem Scala ................................................................................................ 27
3.4 – Apache Spark ..................................................................................................... 28
3.4.1 – Arquitetura .................................................................................................. 28
3.4.2 – Programação ................................................................................................ 30
3.5 – Telemetria dos equipamentos ........................................................................... 33
3.5.1 – Servidor VisionLink ................................................................................... 33
Capítulo 4 ...................................................................................................................... 35
4.1 – Empresas ............................................................................................................ 35
4.1.1 – Radix Engenharia e Software ...................................................................... 35
4.1.2 – Sotreq .......................................................................................................... 36
4.2 – Arquitetura ......................................................................................................... 36
4.2.1 – Configuração do cluster .............................................................................. 38
4.3 – Fontes de Informação ......................................................................................... 39
4.3.1 – Falhas .......................................................................................................... 39
4.3.2 – Padrões ........................................................................................................ 40
xi
4.4 – Estrutura dos Bancos de Dados ......................................................................... 43
4.4.1 – Oracle .......................................................................................................... 43
4.4.2 – Cassandra .................................................................................................... 46
4.5 – Aplicação Spark ................................................................................................. 48
Capítulo 5 ...................................................................................................................... 52
Capítulo 6 ...................................................................................................................... 56
Bibliografia .................................................................................................................... 57
Apêndice A .................................................................................................................... 59
xii
Lista de Figuras
Fig. 2.1 Lei de Moore [6] ................................................................................................. 6
Fig. 2.2 Modelo de Fork-Join [7] ..................................................................................... 7
Fig. 2.3 Paralelismo de Dados [8] .................................................................................... 8
Fig. 2.4 Lei de Amdahl [8] ............................................................................................... 9
Fig. 2.5 Comparação entre Sistemas Paralelos e Distribuídos [11]................................ 10
Fig. 2.6 Definição da interface IEnumerable .................................................................. 13
Fig. 2.7 Definição da interface IEnumerator .................................................................. 13
Fig. 2.8 Definição do método foreach ............................................................................ 14
Fig. 2.9 Aplicação do método ForEach .......................................................................... 14
Fig. 2.10 Função Map ..................................................................................................... 15
Fig. 2.11 Definição do método Map ............................................................................... 16
Fig. 2.12 Exemplo do uso de yield return ...................................................................... 16
Fig. 2.13 Exemplo do uso de Map .................................................................................. 17
Fig. 2.14 Função Filter ................................................................................................... 17
Fig. 2.15 Definição de Filter ........................................................................................... 17
Fig. 2.16 Exemplo de Filter ............................................................................................ 18
Fig. 2.17 Função Reduce ................................................................................................ 18
Fig. 2.18 Definição de Reduce ....................................................................................... 19
Fig. 2.19 Exemplo do uso de Reduce ............................................................................. 20
Fig. 3.1 Arquitetura do Spark em modo cluster [24] ...................................................... 29
Fig. 3.2 Exemplo de aplicação Spark: Contagem de IP ................................................. 32
Fig. 4.1 Arquitetura Geral do Sistema ............................................................................ 37
Fig. 4.2 Aplicação da Regra ........................................................................................... 42
Fig. 4.3 Mapeamento de PR_REGRAS ......................................................................... 45
Fig. 4.4 Mapeamento de PR_VIOLACOES ................................................................... 46
Fig. 4.5 Fluxo da Aplicação - Parte 1 ............................................................................. 49
Fig. 4.6 Fluxo da Aplicação - Parte 2 ............................................................................. 50
Fig. 4.7 Fluxo da Aplicação - Parte 3 ............................................................................. 51
Fig. 4.8 Fluxo da Aplicação - Parte 4 ............................................................................. 51
Fig. 5.1 Média de Violações por dia ............................................................................... 52
Fig. 5.2 DAG da Aplicação ............................................................................................ 53
Fig. 5.3 Linha do Tempo Estágio 1 ................................................................................ 54
Fig. 5.4 Linha do Tempo Estágio 2 ................................................................................ 55
xiii
Lista de Tabelas
Tabela 3.1 Exemplo de tabela em um banco Cassandra ................................................. 24
Tabela 3.2 Comparação entre os bancos de dados utilizados [18] ................................. 25
Tabela 3.3 Exemplos de Transformações. Adaptado de [25] ......................................... 31
Tabela 3.4 Exemplos de Ações. Adaptado de [25]......................................................... 31
Tabela 4.1 Regras ........................................................................................................... 44
Tabela 4.2 Violações ...................................................................................................... 44
Tabela 4.3 Falhas dos Equipamentos ............................................................................. 47
Tabela 4.4 Falhas Temporárias ....................................................................................... 48
1
Capítulo 1
Introdução
A apresentação do projeto é feita neste capítulo. Primeiramente, em 1.1, definimos
o assunto deste trabalho. Na seção 1.2, especificamos seus limites de atuação e escopo. A
motivação e justificativa são explicitadas posteriormente, em 1.3. Em seguida, os
objetivos gerais e específicos são descritos (1.4). Na seção 1.5, sintetizamos os métodos
que foram seguidos para o desenvolvimento deste projeto. Finalmente, toda a estrutura
deste documento é detalhada na seção 1.6.
1.1 – Tema
O tema deste projeto é a aplicação de tecnologias de Big Data sobre dados de
telemetria de equipamentos, mais especificamente os equipamentos fornecidos pela
Sotreq [1], empresa alvo deste trabalho. Escavadeiras, caminhões e carregadeiras estão
entre os equipamentos comercializados pela empresa.
Neste sentido, o problema a ser resolvido é criar uma aplicação e propor uma
arquitetura computacional que permita o processamento destes dados de forma escalável.
A aplicação deve ainda disponibilizar as informações úteis para o usuário de forma a
facilitar as indicações de peças e serviços para realizar a manutenção preditiva dos
equipamentos alvos.
1.2 – Delimitação
O projeto abrange apenas equipamentos com telemetria e indicativos de
necessidade de manutenção conhecidos. Técnicas de Aprendizado de Máquina e
Inteligência Artificial estão fora do escopo deste projeto.
Apesar do esforço feito em tornar a solução a mais genérica possível, o trabalho
foi realizado dentro da plataforma SotreqLink, desenvolvimento prestado pela empresa
Radix Engenharia e Software para a empresa Sotreq. Sendo assim, o projeto pode conter
2
algumas particularidades das empresas envolvidas que não necessariamente seriam
aplicadas ou representariam a melhor escolha em outras ocasiões. Adicionalmente, parte
da infraestrutura e fluxo de informação deve ser utilizado para não comprometer outros
sistemas já em funcionamento.
1.3 – Justificativa
Com o crescente número de dispositivos físicos, como veículos, sensores de todo
tipo e sistemas eletrônicos embarcados, ocorre também a integração dos mesmos a rede
de computadores, permitindo a troca e envio de mensagens. Tal aumento resulta também
em grandes e, potencialmente, complexas estruturas de dados que hoje podem ser
classificados como Big Data [2].
O tratamento desses dados é de grande interesse para empresas, pois podem
fornecer informações valiosas sobre seus clientes ou equipamentos, permitindo assim um
aumento de sua produtividade. Este tratamento, no entanto, não pode ser realizado através
de abordagens tradicionais, visto que computadores comuns, trabalhando de forma
isolada, podem não possuir a capacidade de processamento necessária e computadores de
grande porte, por sua vez, podem ser financeiramente inviáveis.
Desta forma, se faz necessário o estudo de soluções alternativas à computação
tradicional para que este problema possa ser solucionado de maneira mais rápida e de
baixo custo. No nosso caso em particular, o tratamento das informações fornecidas pela
telemetria dos equipamentos pode proporcionar indicativos a respeito da necessidade de
manutenção dos mesmos, resultando em uma manutenção direcionada e,
consequentemente, mais eficaz e barata.
1.4 – Objetivos
Considerando o grande volume de dados obtidos pela telemetria dos
equipamentos, o objetivo geral é desenvolver uma solução que seja capaz de atender aos
requisitos de velocidade mínima de processamento, escalável, distribuída e ainda possa
garantir a disponibilidade destas informações para os operadores do sistema. Desta forma,
o projeto pretende cumprir os seguintes objetivos específicos:
3
Utilizar os recursos computacionais disponíveis de forma eficiente
Tempo de processamento inferior à necessidade da aplicação
Arquitetura escalável permitindo o redimensionamento dos recursos
computacionais
1.5 – Metodologia
Este trabalho utiliza as informações de telemetria de equipamentos para gerar
indicativos a respeito de uma necessidade de manutenção. A partir do uso de padrões pré-
estabelecidos de telemetria, pretende-se identificá-los ao longo da operação do
equipamento alvo e assim disponibilizar para o usuário a ocorrência desses padrões.
Desta forma, foi decidido recolher junto aos técnicos competentes, e usuários
desta aplicação, os padrões de telemetria a serem encontrados. Cada técnico deverá,
portanto, adicionar e editar neste sistema o que julgar conveniente para a manutenção dos
equipamentos.
Posteriormente, foram estudadas e escolhidas as arquiteturas e ferramentas
computacionais para serem utilizadas no desenvolvimento do projeto, tendo sempre em
consideração as delimitações e objetivos expostos anteriormente.
Para atender o objetivo de garantir a disponibilidade das informações, estas
deverão ser armazenados de forma distribuída. É importante ressaltar que a
disponibilidade dos dados é um problema crescente em grandes redes de computadores.
Assim, a arquitetura escolhida deverá minimizar a existência de um Ponto Único de Falha
(SPOF – Single Point of Failure) [3].
Após a escolha das ferramentas, foi realizado o desenvolvimento da lógica de
programação da aplicação. Esta utilizou, principalmente, técnicas de computação
distribuída. Desta forma, técnicas de transformações de dados (mapeamentos, filtros,
reduções) utilizadas em processamento de Big Data foram preferencialmente
empregadas.
4
O trabalho terá obtido êxito ao conseguir criar uma aplicação que identifique os
padrões de erro a serem encontrados nos dados de telemetria e os disponibilize para o
usuário realizar as intervenções que julgar necessárias. É preciso ainda que a aplicação
atenda aos requisitos de paralelização e velocidade do processamento, alta
disponibilidade e rápido acesso aos dados obtidos.
1.6 – Organização do documento
No capítulo 2 discutimos os conceitos de computação paralela e distribuída,
evidenciando as vantagens de seu uso. Fazemos uma introdução ao paradigma de
programação funcional.
No capítulo 3 são descritas as tecnologias utilizadas neste projeto, incluindo
bancos de dados, linguagens de programação e motores de processamento.
No capítulo 4 está contido o desenvolvimento da aplicação, assim como a
arquitetura geral dos servidores e a modelagem dos bancos de dados.
No capítulo 5 é apresentada uma síntese dos resultados, obtidos principalmente
através de consultas ao banco de dados e análise das métricas fornecidas pelo motor de
processamento.
No capítulo 6 são sintetizadas as conclusões deste projeto, comparando os
resultados obtidos com os objetivos planejados.
5
Capítulo 2
Computação Paralela e Distribuída e
Programação Funcional
Com o aumento da quantidade e complexidade dos dados e tarefas
computacionais, alternativas à computação sequencial tradicional estão cada vez mais em
evidência. Apesar de essas discussões datarem da década de 1960, elas se tornaram mais
populares a partir da década de 2000. O barateamento de computadores de uso geral, o
aumento do número de núcleos em um processador e a oferta de serviços de computação
em nuvem contribuíram significativamente para o crescimento do uso destas tecnologias.
Nesta sessão são discutidas, principalmente, duas alternativas que podem ser
utilizadas de forma combinada ou isolada: Computação Paralela e Computação
Distribuída.
2.1 – Computação Paralela
Tradicionalmente, softwares de computadores foram escritos para computação
sequencial. Nesta arquitetura, uma única instrução é executada de cada vez e, após o
término de uma instrução, a próxima é executada. Sendo assim, a frequência de operação
de um processador era o principal fator que determinava a velocidade de processamento
do mesmo. Com o objetivo de melhorar a performance de seus processadores, os
fabricantes investiram principalmente no aumento de sua frequência.
O aumento da frequência dos processadores foi dominante desde meados da
década de 1980 até o início dos anos 2000, quando limitações físicas impediram um
aumento ainda maior. Uma das principais limitações a este aumento é a potência dissipada
pelo processador, que é proporcional a sua frequência de operação, além de outros fatores
[4]. Portanto, o escalamento da frequência se tornava uma opção não mais viável para o
melhoramento dos processadores, o que incentivou o estudo de alternativas.
6
De acordo com a Lei de Moore (Fig. 2.1), que ainda se encontra em vigência, o
número de transistores em um microprocessador dobra a cada período de 18 a 24 meses
[5]. Desta forma, os fabricantes de microprocessadores decidiram não utilizar estes
transistores adicionais para escalar a frequência, e sim para adicionar processamento
paralelo, na forma de novos núcleos de processamento. A partir de então, processadores
de múltiplos núcleos têm a capacidade de executar computação paralela.
Fig. 2.1 Lei de Moore [6]
Um padrão de projeto típico de programas paralelos é o modelo de Fork-Join,
como pode ser visto na Fig. 2.2. Este modelo consiste em, a partir de um programa
sequencial, identificar tarefas que têm potencial para serem executadas simultaneamente
e as separar, realizando a operação de Fork. Após o término de todas as tarefas, o
programa reúne os resultados com uma operação de Join, para seguir assim seu fluxo
sequencial.
7
Fig. 2.2 Modelo de Fork-Join [7]
Existem diversas formas de computação que podem ser classificadas como
paralela. Estas formas incluem paralelismo em nível de bits, instruções, tarefas. Também
é possível realizar paralelismo distribuindo a informação em diferentes computadores, o
que é discutido na sessão seguinte. Para fim de simplicidade e para manter o escopo deste
documento, discutimos aqui a forma de paralelização utilizada neste projeto:
Paralelização de Dados.
Nesta forma de paralelização, estruturas de dados regulares, contendo o mesmo
tipo de dado e tendo tamanhos conhecidos, podem ser divididas igualmente entre os
processadores e/ou seus respectivos núcleos1. Cada núcleo, portanto, realiza a mesma
operação sobre os dados, o que pode diminuir drasticamente o tempo de processamento
total da tarefa. A Fig. 2.3 ilustra este procedimento.
Para exemplificar este funcionamento, será considerada uma coleção contendo n
elementos. Supõe-se ainda que seja necessário realizar a mesma operação em todos os
elementos da coleção com o tempo de processamento por elemento igual a 𝑇𝑒. Em um
programa sequencial, o tempo total de processamento seria, portanto, igual a 𝑛 ∗ 𝑇𝑒.
Porém, em um processador com m núcleos, a mesma tarefa poderia ser dividida entre os
núcleos para ser executada simultaneamente. Assim o tempo total em um programa
paralelo, excluindo os tempos de particionamento e reunião dos dados, seria, idealmente,
igual a 𝑡𝑒𝑡𝑜 (𝑛
𝑚) ∗ 𝑇𝑒
1 Existem outras formas de particionamento de dados quando o tamanho não é conhecido. Uma referência
pode ser encontrada em [24].
8
Fig. 2.3 Paralelismo de Dados [8]
Evidenciando o aumento da velocidade de processamento em programas
paralelos, a Lei de Amdahl [9] relaciona o ganho de velocidade na execução de toda a
tarefa com o aumento de velocidade promovido por recursos adicionais e a proporção do
tempo de execução que se aproveita por recursos adicionais. De forma simplificada,
tempos a equação:
𝑆𝑙𝑎𝑡ê𝑛𝑐𝑖𝑎(𝑠) =
1
(1 − 𝑝) +𝑝𝑠
(2.1)
Em nosso caso simplificado, 𝑆𝑙𝑎𝑡ê𝑛𝑐𝑖𝑎 é o aumento total de velocidade da tarefa, 𝑠 é o
número de processadores (ou núcleos) e 𝑝, a proporção da tarefa que pode ser
paralelizada. É interessante notar que esta Lei nos mostra que o limite superior para o
aumento de velocidade de uma tarefa é limitado pela parte serial, ou que não pode ser
paralelizada (1-p). Assim, se a proporção paralela de uma tarefa corresponde a 80% do
tempo de processamento, o limite máximo teórico do ganho de velocidade se dá quando
tomamos o limite de 𝑠 tendendo ao infinito, tendo como nesse caso um ganho de 5 vezes
a velocidade. A Lei de Amdahl mostra o potencial que o paralelismo pode ter em uma
aplicação, como mostra a Fig. 2.4.
9
Fig. 2.4 Lei de Amdahl [8]
2.2 – Computação Distribuída
Sistemas distribuídos estão presentes desde o final da década de 1960 e início da
década de 1970. A sua utilização teve início principalmente em redes locais e foi
expandida pela ARPANET com a distribuição de servidores pelos Estados Unidos. A sua
aplicação de maior sucesso e o primeiro exemplo de aplicação distribuída em larga-escala
foi o e-mail, principal incentivo da expansão da ARPANET para a formação da Internet.
Hoje em dia, diversas aplicações são construídas sobre sistemas distribuídos, que vão
desde a transferências de arquivos via FTP até jogos multijogadores online.
Um sistema distribuído pode ser definido um conjunto de computadores
conectados em rede que se comunicam e coordenam suas tarefas através de troca de
mensagens [10]. Com o fim de manter o escopo deste documento, são discutidos apenas
os aspectos de sistemas distribuídos relacionados com este projeto, especialmente no que
tange à Computação Paralela.
A principal diferença entre um sistema paralelo e um distribuído é a memória.
No já visto Sistema Paralelo, todos os processadores (ou núcleos) têm acesso à mesma
memória, ou seja, apresenta memória compartilhada. Entretanto, um sistema distribuído
10
opera com um conjunto de computadores onde cada nó da rede acessa somente a sua
memória interna, caracterizando assim a memória distribuída. Esta característica é
importante de deve ser considerada no desenvolvimento da aplicação, como será visto
adiante. A Fig. 2.5 ilustra a comparação entre sistemas distribuídos e paralelos.
Fig. 2.5 Comparação entre Sistemas Paralelos e Distribuídos [11]
O uso de Sistemas distribuídos possui ainda algumas vantagens sobre o uso de um
computador de grande porte, em especial no contexto deste projeto de processamento de
Big Data:
Escalabilidade de Recursos Computacionais
Economicamente mais acessível
Redução do risco de Single Point of Failure
Facilidade de Manutenção
É importante lembrar que essas caraterísticas vão ao encontro dos requisitos do
projeto discutidos no Capítulo I, especialmente a escalabilidade dos recursos
computacionais.
2.3 – Programação Funcional
Programação funcional pode ser definida como um tipo de Paradigma de
Programação, ou seja, um estilo ou forma de se estruturar os elementos de um programa
11
de computador. Este estilo enfatiza o uso de funções matemáticas em detrimento de
afirmações, como atribuições e chamadas de retorno.
Este paradigma foi fundado sobre o sistema de Lambda Cálculo de 1932, sistema
lógico-matemático que estuda principalmente as funções recursivas. Para manter o
escopo deste documento, são detalhados apenas os aspectos mais relevantes que foram
utilizados neste projeto. O artigo original pode ser visto em [12].
Como dito anteriormente, o estilo funcional de programação se utiliza
primariamente da avaliação de funções. E, para tal, é comumente utilizada também uma
outra notação. Tradicionalmente, podemos declarar a função identidade na forma:
𝐼𝑑𝑒𝑛𝑡𝑖𝑑𝑎𝑑𝑒(𝑥) = 𝑥 (2.2)
Por outro lado, a programação funcional nos permite declarar funções que não são
nomeadas. Estas funções são muito úteis e foram largamente utilizadas neste projeto. Elas
aceleram o desenvolvimento e contribuem para um melhor entendimento do código
escrito. Estas são chamadas de Funções Anônimas e podem ser escritas utilizando a
notação lambda na forma:
𝑥 → 𝑥 (2.3)
O termo à esquerda do operador lambda (→) é o argumento da função, enquanto
à sua direita temos o retorno da função. No caso da função identidade, ela apenas retorna
o próprio argumento, não realizando nenhuma operação significativa. Podemos ainda
declarar funções com múltiplos argumentos, como segue:
(𝑥, 𝑦) →𝑥
2+𝑦
2
(2.4)
Temos, portanto, que a função acima retorna a média aritmética de dois números.
A princípio, o uso de funções anônimas simplesmente nos permite omitir seu
identificador, mas não obtemos vantagem com isso. Sem o seu nome, não poderíamos
12
chamar a função em outros locais do programa, gerando uma grande perda de reuso de
código e dificultando o desenvolvimento como um todo.
Entretanto, o uso de funções anônimas se torna uma ferramenta muito poderosa
se utilizada em conjunto com um outro conceito da programação funcional: Funções de
Alta Ordem. De maneira geral, funções de alta ordem são funções que podem receber
outras funções como argumentos e/ou retornar outras funções como resultado [13]. Um
exemplo disso no cálculo matemático é o operador diferencial, que tem como argumento
uma função e retorna outra.
Utilizando desta propriedade, podemos escrever uma função como na forma que
segue:
(𝑥, 𝑦) → 𝑥(𝑦) (2.5)
Temos agora uma mudança fundamental em relação às funções mostradas
anteriormente. Observa-se que agora o argumento 𝑥 não é mais um valor e sim uma outra
função. Neste caso, a função anônima acima apenas aplica uma outra função passada
como argumento em 𝑥 ao valor passado em 𝑦 e retorna seu resultado.
O uso de funções de alta ordem nos permite escrever funções ou, dentro do
paradigma de Programação Orientada a Objetos, métodos que possam ser aplicadas em
estruturas de dados que cumpram certos pré-requisitos. Particularmente, o uso de funções
de alta ordem é muito útil em padrões de projeto do tipo Iterador, que implementam o
processo de Iteração a ser executado em uma coleção.
Na linguagem de programação C#, que foi utilizada neste projeto para
desenvolver a aplicação de servidor que interage com o usuário, a interface IEnumerable
é responsável por retornar um Iterador, como pode ser vista na sua definição na Fig. 2.6:
public interface IEnumerable {
IEnumerator GetEnumerator(); }
13
Fig. 2.6 Definição da interface IEnumerable
Observa-se que o único método que deve ser implementado nesta interface é o
GetEnumerator(), que irá retornar uma outra interface IEnumerator. Esta interface, em
C#, é quem define os métodos que deverão ser implementados por um Iterador. Desta
forma, todo objeto que implementa a interface IEnumerable pode ser iterado e utilizado
em laços de repetição do tipo foreach. A definição da interface IEnumerator nos mostra
os métodos e propriedades que precisam ser implementadas por um Iterador, como é visto
na Fig. 2.7:
public interface IEnumerator {
object Current { get; } bool MoveNext(); void Reset();
}
Fig. 2.7 Definição da interface IEnumerator
Temos, portanto, que o Iterador precisa implementar os métodos MoveNext e
Reset, além de disponibilizar a propriedade Current. O método Reset tem a função de
posicionar o Iterador imediatamente antes do início da coleção em questão. O método
MoveNext avança o Iterador para o próximo elemento da coleção, retornando o valor
lógico “verdadeiro” caso exista um objeto na nova posição. Se o Iterador chegar ao fim
da coleção e nenhum objeto for encontrado, o método retorna “falso”. A propriedade
Current apenas disponibiliza o elemento atual do Iterador.
Sabendo agora a definição das interfaces IEnumerable e IEnumerator e
utilizando-se do suporte de C# para funções de alta ordem, podemos escrever um método
de extensão para a interface IEnumerable que realiza a operação de foreach, ou seja,
realiza uma operação para cada elemento da coleção. O método pode ser visto na Fig. 2.8.
14
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action) {
IEnumerator<T> enumerator = source.GetEnumerator(); enumerator.Reset(); while (enumerator.MoveNext())
action(enumerator.Current); }
Fig. 2.8 Definição do método foreach
Como pode ser visto, o método de extensão ForEach recebe como argumentos a
própria coleção em source e uma ação em action, que nada mais é que a sintaxe da
linguagem C# para uma função sem retorno. O método acima apenas executa a função
Reset para garantir que o Iterador se encontra no início da coleção e executa o método
MoveNext para atravessar toda a coleção enquanto forem encontrados elementos. Por fim,
a função passada como argumento é aplicada em cada elemento da coleção.
O método ForEach definido acima é um típico caso de uso de funções de alta
ordem. É muito conveniente utilizá-lo em conjunto com funções anônimas durante o
desenvolvimento de um programa. Um exemplo de sua utilização pode ser vista na Fig.
2.9:
int[] collection = new int[] { 1, 2, 3, 4, 5}; collection.ForEach(x => Console.WriteLine(x));
//Resultado: //1 2 3 4 5
Fig. 2.9 Aplicação do método ForEach
É importante notar que, como o método de extensão foi realizado em uma
interface genérica, podemos utilizá-lo em todos os objetos que a implementam, neste caso
um Array. O mesmo método poderia ser utilizado em filas, listas, pilhas ou quaisquer
estruturas de dados que implementem a interface IEnumerable. Isto permite reuso de
código, abstração e outros benefícios que são discutidos adiante. A notação lambda se
torna agora vantajosa, pois não precisamos declarar uma função “Imprimir” para poder
ser usada como argumento e imprimir todos os valores da coleção, o que contribui para
um código mais limpo e inteligível.
15
Existem ainda muitas outras operações que podem ser aplicadas sobre coleções.
Além da já discutida operação foreach, que realiza uma ação sobre cada objeto da
coleção, diversas funções genéricas podem ser implementadas da mesma forma. A seguir
serão discutidas três dessas operações mais comuns: Map, Filter e Reduce.
A função Map, como o próprio nome diz, consiste em mapear todos os elementos
de uma coleção (o domínio) para o conjunto imagem correspondente da função que é
passada como argumento. Portanto, seja uma função 𝑓: 𝑋 → 𝑌 o argumento de Map, a
coleção original composta por um subconjunto de 𝑋 é transformada em um subconjunto
de 𝑌 composto pela imagem de cada elemento do domínio 𝑋. A Fig. 2.10 exemplifica a
aplicação de Map.
[ 𝑥1𝑥2𝑥3𝑥4𝑥5] 𝑓:𝑋→𝑌→
[ 𝑓(𝑥1)𝑓(𝑥2)𝑓(𝑥3)𝑓(𝑥4)𝑓(𝑥5)]
Fig. 2.10 Função Map
Assim como foi feito no exemplo anterior, podemos utilizar um Iterador para
percorrer uma coleção e aplicar a função do argumento de Map em cada elemento. Como
resultado, o método Map retornará uma nova coleção contendo os elementos da imagem
da função do argumento, de acordo com a definição do parágrafo anterior. O método Map
pode ser implementado da mesma forma que o exemplo de ForEach, utilizando a interface
IEnumerable, como pode ser visto na Fig. 2.11:
public static IEnumerable<TResult> Map<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource,TResult> function) {
IEnumerator<TSource> enumerator = source.GetEnumerator(); enumerator.Reset(); while (enumerator.MoveNext())
yield return function(enumerator.Current); }
16
Fig. 2.11 Definição do método Map
O método Map, assim como o ForEach, recebe uma interface IEnumerable source
de tipo genérico TSource. Porém, desta vez, Map retorna uma outra interface de tipo
genérico TResult. Como esperado, a função do argumento de Map também deve ter os
mesmos tipos de entrada e saída respectivamente, como pode ser visto pelo tipo da função
Func<TSource,TResult>. Da mesma forma que ForEach, Map usa um Iterador para
percorrer a coleção, e retornar o resultado da função function aplicada em cada elemento.
A diferença aqui se encontra na sintaxe yield return de C#. Diferente de um return
comum, yield return não representa um retorno único do método Map e sim o retorno de
cada elemento formando uma coleção. Desta forma, Map apresenta vários pontos de
retorno. A Fig. 2.12 ilustrar o uso de yield return.
public static IEnumerable<int> Inteiros() {
yield return 1; yield return 2; yield return 3; yield return 4; yield return 5;
} public static void Executar() {
IEnumerable<int> inteiros = Inteiros(); inteiros.ForEach(x => Console.WriteLine(x)); //Resultado: //1 2 3 4 5
}
Fig. 2.12 Exemplo do uso de yield return
Podemos observar que o método Inteiros() apresenta várias linhas com a sintaxe
yield return. Cada linha retorna um elemento da coleção, neste caso apenas um inteiro. O
método Map funciona da mesma forma: enquanto o enumerador retornar verdadeiro para
o método MoveNext(), Map irá invocar yield return, retornando assim elemento por
elemento da nova coleção. Adiante, temos um exemplo do uso de Map (Fig. 2.13), onde
cada valor da coleção original é mapeado para o seu dobro.
17
int[] collection = new int[] { 1, 2, 3, 4, 5 }; collection.Map(x => x*2).ForEach(x => Console.WriteLine(x)); //Resultado: //2 4 6 8 10
Fig. 2.13 Exemplo do uso de Map
A próxima operação sobre coleções que será discutida é Filter. Como o próprio
nome indica, Filter consiste em aplicar um filtro sobre todos os elementos da coleção,
retornando apenas os elementos que passarem pelo filtro especificado. Portanto, seja a
função 𝑓: 𝑋 → 𝐵 o argumento de Filter, B um booleano e a coleção original composta por
elementos do tipo 𝑋, a coleção retornada por Filter será formada pelos mesmos elementos
da coleção original que retornarem verdadeiro quando 𝑓 for aplicado em seus elementos.
A Fig. 2.14 ilustra o uso de Filter.
[ 𝑥1𝑥2𝑥3𝑥4𝑥5] 𝑓:𝑋→𝐵→ [
𝑥1𝑥4𝑥5]
Fig. 2.14 Função Filter
Uma implementação pode ser feita a partir das mesmas estruturas de dados
utilizadas em Map, assim como o uso da interface IEnumerable e da sintaxe especial yield
return. A implementação pode ser vista na Fig. 2.15.
public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, Func<T,bool> test) {
IEnumerator<T> enumerator = source.GetEnumerator(); enumerator.Reset(); while (enumerator.MoveNext()) if (test(enumerator.Current)) yield return enumerator.Current; }
Fig. 2.15 Definição de Filter
18
Como pode ser visto, Filter recebe como argumento, além da própria coleção do
tipo genérico T, uma função teste que avalia cada elemento de source e retorna um valor
booleano. Aqueles elementos em que a função teste retornar verdadeiro como resultado,
serão retornados para a nova coleção utilizando-se yield return. A seguir, temos um
exemplo da utilização de Filter (Fig. 2.16), onde a condição de que o elemento deve ser
maior que 2 é passada como argumento.
int[] collection = new int[] { 1, 2, 3, 4, 5 }; collection.Filter(x => x > 2).ForEach(x => Console.WriteLine(x)); //Resultado: //3 4 5
Fig. 2.16 Exemplo de Filter
Finalmente, temos a última operação que será discutida nesta sessão: Reduce. O
método de redução é significativamente diferente dos métodos Map e Filter apresentados
anteriormente. Apesar de Reduce também utilizar um Iterador para percorrer uma
coleção, o objetivo de Reduce não é retornar uma nova coleção e sim único objeto que
contenha alguma informação significativa da coleção, como visto na Fig. 2.17. Portanto,
Reduce realiza um mapeamento de todo um conjunto para um único elemento. Um
exemplo de operação de mesma natureza é o determinante de matrizes, que a partir do
conjunto de números da matriz, calcula-se um único valor que a representa.
[ 𝑥1𝑥2𝑥3𝑥4𝑥5] 𝑓:𝑋2→𝑋→ 𝑥𝑎
Fig. 2.17 Função Reduce
Para realizar uma operação de redução, além de um Iterador, utiliza-se também
um Acumulador. Acumuladores são estruturas responsáveis por armazenar resultados
intermediários de operações de uma Iteração. Fazendo uma associação com o exemplo de
19
determinante exibido anteriormente, este pode ser determinado por um somatório de
produtos. Assim, o acumulador poderia armazenar o resultado do primeiro produto e, a
cada novo produto, somar o seu próprio valor ao novo produto, realizando a acumulação.
Desta forma, o método Reduce precisa definir exatamente como essa acumulação
deve ser realizada. Isto é feito definindo-se uma outra função, que deve ser passada como
argumento a Reduce. Em contraste com os métodos Map e Filter, onde esta função tinha
apenas um argumento (o elemento da coleção), a função argumento de Reduce deve ter
dois argumentos: um para o acumulador e outro para o elemento corrente. Como o
resultado da função será usado para incrementar o acumulador, o próprio acumulador, o
elemento corrente e o retorno da função devem ser do mesmo tipo. Assim, esta função
deve ser da forma 𝑓: 𝑋2 → 𝑋. Uma implementação do método Reduce pode ser vista na
Fig. 2.18.
public static T Reduce<T>(this IEnumerable<T> source, Func<T, T, T> funcao) {
T accumulator; IEnumerator<T> enumerator = source.GetEnumerator(); enumerator.Reset(); if (!enumerator.MoveNext()) //Se o iterador for vazio, retorna o padrão do tipo
return default(T); accumulator = enumerator.Current; while (enumerator.MoveNext())
accumulator = funcao(accumulator, enumerator.Current); return accumulator;
}
Fig. 2.18 Definição de Reduce
De acordo com o que foi dito anteriormente, o método Reduce retorna agora om
objeto do tipo T em vez de um IEnumerable<T>, como era nos casos de Map e Filter.
Outra diferença significativa é a função argumento, que agora possui dois argumentos,
sendo o primeiro o acumulador e o segundo o elemento corrente. A palavra chave default
retorna o padrão do tipo T e sua referência pode ser encontrada em [14]. É importante
notar que o acumulador é o destino da função passada como argumento a Reduce. Por
fim, Reduce retorna o próprio acumulador. A Fig. 2.19 ilustra um exemplo de sua
aplicação.
20
int[] collection = new int[] { 1, 2, 3, 4, 5 }; int sum = collection.Reduce((acc, x) => acc + x); Console.WriteLine(sum); //Resultado: 15 int prod = collection.Reduce((acc, x) => acc * x); Console.WriteLine(prod); //Resultado: 120
Fig. 2.19 Exemplo do uso de Reduce
Em seu primeiro uso, o acumulador está sendo somado com o elemento corrente,
assim o método Reduce retorna o somatório de todos os elementos da coleção. Já em seu
segundo uso, o acumulador é multiplicado com o elemento corrente, logo Reduce retorna
o produtório da coleção.
Concluindo esta sessão sobre programação funcional, pode-se observar um pouco
da utilidade deste paradigma de programação, assim como o uso de funções anônimas e
de Alta Ordem. Além de nos fornecer maior facilidade de desenvolvimento, leitura e
limpeza do código, as ferramentas fornecidas por este paradigma nos fornecem um alto
grau de abstração e permite que os desenvolvedores tenham foco nas tarefas mais
importantes. Existem muitas bibliotecas que implementam as operações sobre coleções
descritas acima (e muitas outras), possibilitando que a atenção do desenvolvedor altere
de como fazer para o que fazer. Assim, é permitido que o desenvolvedor tenha uma
abstração maior sobre quais transformações em coleções devem ser feitas em vez de como
realizá-las, através de um laço for por exemplo. É importante ressaltar ainda que a própria
natureza das operações sobre coleções tem um grande potencial para paralelismo. Pode-
se observar, por exemplo, que a execução de Map não está atrelada a um algoritmo
sequencial. De fato, podemos aplicar a função passada como argumento de Map
simultaneamente em todos os elementos a ainda obteríamos o mesmo resultado, pois o
resultado do mapeamento de um elemento não depende de outro. O mesmo ocorre para
as funções Filter e Reduce. Não é surpresa que muitas bibliotecas que implementam
paralelismo utilizam-se de programação funcional para expor suas ferramentas ao
desenvolvedor. Todas estas vantagens contribuem para uma menor complexidade de
programação, maior facilidade de manutenção e, principalmente, menor tempo de
desenvolvimento, o que é de nosso interesse enquanto desenvolvedores.
21
Capítulo 3
Tecnologias Utilizadas
Neste capítulo são descritas as diversas tecnologias empregadas neste trabalho.
Na sessão 4.1 discorremos sobre os bancos de dados e os diferentes modelos existentes.
Em 4.2, definimos o que é um ESB e exemplificamos as vantagens de sua utilização. A
linguagem Scala é apresentada em 4.3, seguida pela máquina de processamento Spark em
4.4. Finalmente, o capítulo é encerrado descrevendo o processo de aquisição de dados dos
equipamentos.
3.1 – Bancos de Dados
Grandes quantidades de dados, para serem analisados, exigem uma maneira
confiável e segura de armazenamento e manipulação. Para isso, foram desenvolvidos
diversos esquemas de bancos de dados. Um banco de dados é definido como uma coleção
de dados organizada e modelada de forma conveniente objetivando a aplicação em que
será utilizada.
Existem pelo menos duas grandes categorias de banco que foram usados neste
projeto. Da categoria de bancos de dados relacionais, foi utilizado o Sistema
Gerenciador de Bancos de Dados (SGBD) Oracle, enquanto na categoria de bancos de
dados não relacionais foi utilizado o SGBD NoSQL (não somente SQL) Cassandra.
3.1.1 – Bancos de Dados Relacionais
Bancos de dados relacionais, cuja origem data da década de 1970, modelam
objetos na forma de tuplas de valores armazenadas em Relações (representadas como
tabelas). Cada relação possui um Esquema, que determina a estrutura de suas tuplas. Esses
bancos modelam relacionamentos entre dados de forma explícita, através da utilização de
chaves estrangeiras, que relacionam uma ou mais colunas de uma tabela com colunas de
outra, ligando assim tuplas que se relacionam.
22
A origem desse modelo de banco pode ser lida em [15]. Seus paradigmas
principais são a separação da estrutura interna de armazenamento de dados da forma com
que estes dados são exibidos nas aplicações que os utilizam. Uma das primeiras
linguagens desenvolvidas para este modelo relacional, com o objetivo de obter e
manipular dados, foi a SEQUEL (Structured English Query Language) [16]. Neste artigo,
o autor introduz o conceito de uma linguagem universal para operações em dados, o que
veio a se tornar o que hoje conhecemos como SQL (Structured Query Language, ou
Linguagem de Consulta Estruturada).
3.1.1.1 – SQL
SQL é uma linguagem utilizada para inclusão, obtenção, modificação e exclusão
de dados. Ela se baseia no conceito de operações sobre conjuntos: ao invés de loops
explícitos, utiliza-se uma linguagem declarativa para descrever a operação que se deseja
executar, deixando para o SGBD a reponsabilidade de transformá-las em operações
explícitas (Álgebra Relacional). SQL começou a ser desenvolvida também na década de
1970 e passo por inúmeras adições e padronizações até chegar a forma que se tem hoje.
3.1.1.2 – ORM
Uma ferramenta utilizada para se trabalhar com bancos relacionais são as
chamadas ORMs (Object-Relational Mapping, ou mapeamentos objeto-relacionais) que
convertem relações de um banco de dados para objetos (transformando relações em listas
de objetos) de linguagem orientada a objetos. Nesse projeto utilizamos o Entity
Framework, que é uma biblioteca para ADO.NET de conversão de dados em bancos
Relacionais para objetos em C#.
Bancos relacionais são padrões em praticamente todo tipo de indústria e são os
mais utilizados como bancos de dados para propósitos gerais. Entretanto esse tipo de
banco de dados tem diversas deficiências, das quais podemos citar:
Bancos de dados relacionais tradicionais trabalham bem com um volume
moderado de dados (centenas de milhares e até alguns milhões de objetos), mas
perdem eficiência para volumes maiores. Por exemplo, os dados vindos dos
23
sensores de uma única máquina moderna podem chegar na casa dos milhões em
apenas um dia, o que é uma característica de aplicações de Big Data;
De forma semelhante, esses bancos não são eficientes para efetuar operações
(filtragem, soma, etc.) em volumes de dados desta magnitude;
Bancos de dados relacionais têm problemas para manter alta disponibilidade e
consistência de dados quando utilizados na forma de sistemas distribuídos por um
número muito grande de nós, típicos em aplicações de Big Data.
3.1.2 – Bancos de Dados Não-Relacionais
Bancos de dados não relacionais sempre estiveram presentes, mas sua
popularidade tem crescido no cenário atual de desenvolvimento de software pelo aumento
da utilização de Big Data devido ao grande volume de dados complexos sendo obtidos e
gerados por todas as áreas da indústria. Outro fator relevante para sua popularidade é seu
uso cada vez mais frequente em sistemas distribuídos. É mais economicamente viável o
uso de vários computadores conectados em rede (local ou internet) em comparação com
a manutenção de um único servidor centralizado.
3.1.2.1 – Cassandra
Apache Cassandra é um SGBD de código-aberto NoSQL criado para a
manipulação e consulta de um alto volume de dados de forma distribuída, onde cada nó
do sistema funciona de forma simétrica (isto é, sem necessidade de um mestre). As
informações presentes nessa seção foram retiradas principalmente da documentação do
Cassandra do site da DATASTAX [17], que é um distribuidor de uma versão proprietária
do Cassandra. Neste projeto, foi utilizada a distribuição open-source do software, assim
como a documentação da Datastax e seus softwares livres como o DevCenter para a
visualização de dados.
Se por um lado o Cassandra não fornece diretamente informações estatísticas ou
de agregações sobre os dados inseridos, a sua capacidade de armazenamento e obtenção
de dados pode alcançar a marca de bilhões de registros, sendo esta uma de suas principais
vantagens.
24
O funcionamento do armazenamento de dados nesse banco de dados se assemelha
à utilização da estrutura de dados Dicionário, presente na maioria das linguagens de
programação. Os dados são armazenados com chaves (que podem ter múltiplas colunas)
e os valores são obtidos através dessas chaves (tendo necessidade de obter informações
sobre os dados presentes, é preciso iterar por todo o conjunto de dados, o que é ineficiente
e custoso).
No exemplo a seguir, descrevemos simplificadamente o funcionamento do uso
das chaves do banco Cassandra. Deseja-se armazenar informações sobre sensores de um
equipamento, o que foi feito de acordo com a Tabela 3.1.
Chave Valor
Número de Série Sensor Data Medição Unidade
AAA001 Temperatura 18/11/2016 300 K
AAA001 Temperatura 19/11/2016 305 K
AAA001 Velocidade 18/11/2016 36 km/h
AAA002 Temperatura 21/11/2016 298 K
AAA002 Pressão 21/11/2016 11200 Pa
Tabela 3.1 Exemplo de tabela em um banco Cassandra
O acesso aos valores só pode ser feito utilizando as chaves de forma sequencial.
Primeiramente devemos determinar a chave para o número de série. Se precisarmos de
uma consulta mais específica, podemos determinar a chave para o sensor. Consultas
possíveis neste banco seriam:
Obter todos os dados de sensores de todos os equipamentos;
Obter todos os dados dos sensores do equipamento AAA001;
Obter todos os dados dos sensores de temperatura do equipamento AAA001.
Por outro lado, as seguintes consultas não poderiam ser feitas diretamente pelo
Cassandra:
Obter todos os sensores de temperatura de todos os equipamentos;
25
Obter todos os sensores de temperatura do equipamento AAA001 com
medição abaixo de 300 K;
Contagem do total de dados da tabela (para grande quantidade de dados).
A solução padrão para essas dificuldades é a replicação de dados: os dados de
cada tabela são também inseridos em outras tabelas, cada uma com uma chave apropriada
para a execução de uma consulta específica. Além da definição das tabelas, é importante
tomar cuidado com a sua fragmentação, ou seja, com a forma através da qual elas serão
subdivididas entre os nós.
3.1.3 – Comparações
Na Tabela 3.2temos algumas diferenças significativas entre os bancos discutidos
anteriormente.
Oracle Cassandra
Tipo SGBD Relacional SGBD Não Relacional
Orientado a Colunas
Licença Comercial Open-source
Implementação C e C++ Java
Tipada Sim Sim
Linguagem SQL CQL
Método de Fragmentação2 Horizontal Sharding
Consistência3 Imediata Eventual
Quantidade de dados4 Moderada Alta
Velocidade de inserção4 Alta Baixa
Velocidade de Obtenção4 Moderada Alta
Tabela 3.2 Comparação entre os bancos de dados utilizados [18]
2 Como o conjunto de dados é divido em disco e/ou memória 3 Momento em modificações serão alteradas em todo o sistema após a transação 4 Dados comparativos
26
3.2 – ESB
Como já mencionado neste documento, este projeto faz parte de uma plataforma
maior que integra diversos módulos, cada qual com o seu funcionamento interno distinto.
Desta forma, se faz necessária a utilização de alguma plataforma integradora para mediar
a comunicação entre os diversos sistemas, evitando realizar uma integração para cada
combinação de módulos se comunicarem diretamente entre si.
ESBs (Enterprise Service Busses ou Barramentos de Serviços) são padrões de
arquitetura de software para a implementação de múltiplos serviços que se comunicam
mutuamente. As discussões apresentadas aqui são baseadas em [19].
ESBs são uma arquitetura orientada a serviços. Múltiplos produtores e
consumidores oferecem e consomem serviços. Cada um deles compõe um “bloco” no
sistema. Esses blocos se comunicam através de mensagens, compostas de um header –
meta-dados sobre a mensagem enviada, sobre o fluxo que ela deve seguir e sobre o
formato dos dados contidos, e um body – os dados da mensagem em si. Essas mensagens
trafegam em um barramento, onde estão as interconexões desses sistemas.
Para exemplificar a vantagem do uso de ESBs, é descrita uma integração típica de
sistemas. Seja uma aplicação, denominada Consumidor 1, que deseja se conectar a um
serviço SMTP de e-mails. Ao invés de fazê-lo diretamente (através de bibliotecas), a
aplicação se conecta a um método do ESB que envia o e-mail diretamente, chamado
Produtor 1. A princípio, a complexidade aumentou e nada foi ganho. Se houver uma outra
aplicação, nomeada Consumidor 2, ela poderá utilizar o serviço presente no barramento.
Agora supomos que o primeiro serviço deseja também acessar uma aplicação Web
(Produtor 2) para obter dados que vão ser acoplados ao e-mail. Podemos acessar essa
aplicação Web também mediada pelo ESB. Nesse caso, ganhamos também a capacidade
do Consumidor 2 de acessar a informação do Produtor 1, uma vez que ele já está
conectado ao ESB.
Matematicamente, temos que o número de conexões com o ESB cresce em
progressão linear (cada aplicação precisa se comunicar com o ESB), enquanto as
27
interconexões entre os múltiplos sistemas crescem em progressão exponencial. Desta
forma, o uso de ESBs se torna vantajoso principalmente para diminuir a complexidade
do sistema como um todo.
3.2.1 – JBoss Fuse
JBoss Fuse é uma plataforma open-source desenvolvida para o JBoss Developer
Studio baseada na plataforma Apache ServiceMix, que agrega tecnologias como o
Apache ActiveMQ e Apache Karaf. O Fuse foi a plataforma ESB utilizada na plataforma
SotreqLink [20].
Além de sua capacidade integradora, a plataforma Fuse nos permite realizar
agendamento de tarefas. Podemos decidir em que momento determinada tarefa deverá ser
executada e com que frequência. Este agendamento foi utilizado na plataforma
SotreqLink como um todo, além deste projeto específico.
3.3 – Linguagem Scala
O nome Scala é um acrônimo para “Scalable Language”, que significa
“Linguagem Escalável” em tradução livre. O desenvolvimento da linguagem começou
em 2001, se tornando pública somente em 2004. A discussão aqui se baseia em sua
própria documentação presente em [21] e foca no que tange o interesse deste projeto.
O código fonte de Scala é compilado para o Java Bytecode sendo, portanto,
executado na máquina virtual Java. Além disso, bibliotecas Java e Scala podem ser
utilizadas em ambas as linguagens indiscriminadamente, caracterizando a
interoperabilidade das linguagens. Como Java é uma das linguagens mais populares do
mundo [22], existem diversas bibliotecas desenvolvidas e maduras. A interoperabilidade
faz com que Scala, apesar de ser uma linguagem bem mais recente que Java, possa
usufruir dessas ferramentas, o que caracteriza uma grande vantagem.
Sobre os paradigmas de programação, Scala é considerada como multiparadigma.
Como Java, Scala suporta os paradigmas de Orientação a Objetos, Imperativa e
Concorrente. Entretanto, Scala também tem suporte à programação funcional, absorvendo
28
as qualidades discutidas na sessão anterior, o que Java não possui5. Por se tratar de uma
linguagem flexível, pois mescla vários estilos de programação, Scala é uma opção popular
para implementar o modelo de Fork-Join, facilitando para o desenvolvedor a alternância
entre os estilos imperativos, para a fração sequencial do programa, e funcional, para a
fração paralela do programa. Finalmente, Scala apresenta uma sintaxe mais concisa e
inteligível que Java, por exemplo, agilizando o desenvolvimento como um todo.
Por todas as vantagens descritas acima, Scala se torna uma opção atraente para
este projeto. Adicionalmente, a linguagem também conta com o suporte dos drivers do
Cassandra e da API do Apache Spark, framework de processamento para Big Data que é
descrito adiante.
3.4 – Apache Spark
O Apache Spark pode ser definido como um mecanismo rápido e genérico para
processamento de dados em larga escala. A plataforma ocupa uma posição de destaque
neste projeto, visto que é o motor de processamento que implementa o paralelismo e a
distribuição comentados nos capítulos anteriores. A maior parte da discussão a seguir foi
baseada no próprio site da plataforma [23].
3.4.1 – Arquitetura
Spark pode ser utilizado tanto localmente (em apenas uma máquina) quanto em
um cluster de computadores. A operação local implementa o paralelismo entre os
processadores (ou núcleos) de um computador apenas. Enquanto é valido para aplicações
não distribuídas, este modo de operação foge ao objetivo deste trabalho de implementar
uma solução distribuída. Portanto, será descrito aqui o modo de operação do Spark em
um cluster, chamado de cluster mode (Fig. 3.1).
5 Java 8 apresenta suporte a funções anônimas e notação lambda, sendo um avanço na direção de
programação funcional da linguagem.
29
Fig. 3.1 Arquitetura do Spark em modo cluster [24]
A arquitetura de um cluster Spark apresenta três elementos principais: Cluster
Manager, Driver Program e Worker Node. O Cluster Manager é um serviço externo
responsável por gerenciar, coordenar e distribuir as tarefas da aplicação em questão.
Adicionalmente, é ele que realoca as tarefas e redistribui os recursos em caso de falha de
um dos nós do cluster. Em caso de falha do driver, por exemplo, um nó worker é
substituído para executar o driver. O nó em que se encontra o cluster manager é
comumente chamado de Mestre. É importante notar, no entanto, que o nó mestre pode
ser fisicamente o mesmo em que se encontra o driver, um worker ou até mesmo estar em
uma máquina dedicada.
O Driver Program é o processo principal executado em uma aplicação Spark,
normalmente executando a função Main() da aplicação. É neste nó que se executa o fluxo
principal do programa, responsável pelas operações não distribuídas do cluster. Ele ainda
é responsável por instanciar o SparkContext, que coordena os diversos conjuntos de
processos de um cluster. É através do SparkContext que a API do Spark é exposta e
permite ao desenvolvedor realizar a distribuição de trabalho quando e como desejar.
Exemplos do uso da API e do SparkContext serão vistos adiante.
Finalmente, o Worker Node são os nós que podem executar código da aplicação.
Para tal, eles utilizam executores, que processam as tarefas requisitadas e mantêm dados
30
em memória ou os armazenam em disco. Um nó worker pode ter uma ou mais instâncias
de executores. Os Workers devem executar as tarefas sempre que possível,
principalmente as mais custosas, evitando assim a ociosidade de recursos, sobrecarga do
nó driver e ainda diminuir o tempo de execução.
3.4.2 – Programação
Como foi dito anteriormente, uma aplicação Spark consiste principalmente em um
Driver Program, que executa a função principal do programa e realiza as operações
paralelas em um cluster. Para realizar o paralelismo, o Spark dos disponibiliza uma
abstração chamada de RDD (Resilient Distributed Dataset). RDD é uma coleção de dados
particionada através dos nós do cluster e pode ser operada em paralelo. Adicionalmente,
RDDs são tolerantes a falhas, significando que, se um nó do cluster falhar sobre uma
operação em RDD, a aplicação consegue se recuperar e executar a tarefa requisitada
posteriormente. RDD é a principal forma de implementação de paralelismo que Spark
oferece. Um RDD pode ser criado invocando o método de paralelização diretamente do
SparkContext sobre uma coleção, ou ainda referenciado um conjunto de dados
distribuído.
Uma outra abstração que Spark oferece é o de variáveis compartilhadas. Por
padrão, Spark envia uma cópia de cada variável que será utilizada na função executada
em paralelo. No entanto, em alguns casos é necessário que que uma variável seja
compartilhada entre tarefas ou entre os nós de um cluster. Existem dois tipos de variáveis
compartilhadas no Spark: Variáveis Broadcast e Acumuladores. Enquanto
acumuladores foram discutidos na sessão sobre programação funcional, uma variável
broadcast é usada para armazenar um valor na memória de todos os nós do cluster. É
muito útil para minimizar o reenvio e a redistribuição de informação entre os nós do
cluster (o que é conhecido como Shuffle), garantindo que a informação já esteja
disponível na memória local de cada nó antecipadamente, aumentando a performance.
O desenvolvimento de uma aplicação Spark é feito em torno das abstrações
descritas acima, principalmente sobre RDDs. Existem dois tipos de operações suportadas
sobre RDDs: Transformações e Ações. Transformações são operações que retornam
uma nova coleção a partir de uma outra coleção. Como exemplo, as operações Map e
31
Filter discutidas anteriormente são transformações no contexto do Spark.
Consequentemente, transformações sobre RDDs retornam novos RDDs. A Tabela 3.3
apresenta alguns exemplos de Transformações.
Transformação Descrição
map(func) Retorna um novo RDD formado pelo retorno de func em cada
elemento.
filter(func) Retorna um novo RDD formado pelos elementos em que func
retornou verdadeiro.
flatMap(func) Similar a map, porém func pode retornar 0 ou mais elementos.
O RDD retornado será a concatenação de todos os elementos.
groupByKey() Quando chamado sobre um RDD do tipo chave-valor, retorna
um RDD do tipo chave-coleção.
reduceByKey(func)
Quando chamado sobre um RDD do tipo chave-valor, retorna
outro RDD do tipo chave-valor onde o valor é o resultado da
agregação dos valores de mesma chave por func
Tabela 3.3 Exemplos de Transformações. Adaptado de [25]
Por outro lado, Ações são operações que retornam um valor para o programa
driver após realizado o processamento em um conjunto de dados. A operação Reduce
descrita anteriormente é um claro exemplo. Ela agrega todos os elementos de um RDD
utilizando a função passada como argumento e retorna o resultado para o driver. Segue a
Tabela 3.4 com alguns exemplos de ações.
Ação Descrição
reduce(func) Agrega os elementos do RDD utilizando func e retorna o valor para
o driver.
collect() Retorna todos os valores de um RDD para o driver na forma de um
Array.
count() Retorna o número de elementos de um RDD para o driver.
take(n) Retorna os primeiros n elementos do RDD para o driver.
Tabela 3.4 Exemplos de Ações. Adaptado de [25]
32
As operações de transformação no Spark são do tipo Lazy. Operações lazy não
são executadas imediatamente. Em vez disto, transformações em RDDs retornam
ponteiros para outros RDDs. As transformações são efetivamente avaliadas apenas
quando se precisa do resultado. Isto ocorre quando uma ação precisa do resultado para
ser enviado para o driver. O uso de operações lazy permite que o Spark possa otimizar as
transformações dependendo do resultado exigido, ganhando performance no processo.
Adicionalmente, o Spark oferece suporte à persistência. O desenvolvedor pode
escolher persistir um RDD em memória ou disco, conforme achar conveniente. Por
exemplo, se mais de uma ação precisa de um estado intermediário de um mesmo RDD, é
conveniente que o mesmo seja persistido em memória para evitar que a mesma
transformação seja executada mais de uma vez. O método cache, por exemplo, realiza a
persistência em memória de um RDD.
Para ilustrar o uso de uma aplicação Spark, a Fig. 3.2 ilustra um código escrito em
Scala.
1
2
3
4
5
6
7
8
9
10
11
12
13
def GetMostUsedIp() : (String, Int) = {
val lines : RDD[String] = sparkContext.textFile("Log_Ip.csv") val ips : RDD[String] = lines.map(x => x.split(",").head) val ipsInt : RDD[(String, Int)] = ips.map(x => (x,1)) val ipsQuantity : RDD[(String, Int)] = ipsInt.reduceByKey((acc, x) => acc + x) val mostUsedIp : (String, Int) = ipsQuantity.reduce((acc, x) => { if(acc._2 >= x._2) return acc return x }) mostUsedIp
}
Fig. 3.2 Exemplo de aplicação Spark: Contagem de IP
O objetivo da função GetMostUsedIp() é extrair o endereço de IP que mais acessa
uma aplicação web. O servidor da aplicação conta com um arquivo de log do tipo CSV
cujo nome é “Log_Ip.csv”, onde cada linha do arquivo representa um registro e cada
33
elemento do registro é separado por virgulas. Cada registro no nosso arquivo de log é
composto pelo IP no usuário e pelo momento de acesso, nesta ordem.
O primeiro comando executado pela função é o método textFile invocado através
da instância de SparkContext. Passando o nome do arquivo como argumento, textFile
distribui o arquivo entre os workers do cluster, sendo cada linha do arquivo um elemento
do RDD, formando assim um RDD de String. Como estamos interessados apenas no
número de acessos, a linha 4 do código acima separa a linha do arquivo CSV pela vírgula
e extrai apenas o primeiro elemento (head), descartando a hora de acesso. Posteriormente
é realizado um novo mapeamento na linha 5, desta vez colocando o IP como chave e o
número 1 como valor de nossa tupla. Isto será útil para a operação seguinte, o
reduceByKey. Quando invocado, reduceByKey irá retorna um RDD do tipo chave-valor,
onde a chave será o IP em questão e o valor a quantidade de vezes que o IP foi registrado
em nosso log. Finalmente, a função do argumento de Reduce na linha 7 retorna para o
acumulador apenas a tupla em que o IP apresentar o maior número de acessos. Assim,
temos que, ao final de sua execução, a função GetMostUsedIp() irá retornar uma tupla
contendo o IP com o maior número de acessos e sua quantidade de acessos.
3.5 – Telemetria dos equipamentos
Para realizarmos a manutenção preditiva dos equipamentos, objetivo principal
deste projeto, são necessárias informações sobre os mesmos. Os equipamentos Sotreq,
majoritariamente de fabricação Caterpillar, contam com um avançado sistema de
hardware que realiza o monitoramento das máquinas. Estas informações são
fundamentais para a realização da manutenção preditiva.
3.5.1 – Servidor VisionLink
O servidor VisionLink é um webservice para o qual as máquinas Sotreq enviam
mensagens sobre o seu estado interno. Entre essas mensagens, temos informações sobre
a utilização de combustível, horários de operações e falhas e/ou diagnósticos ocorridos.
Este sistema está conectado com módulos ProductLink: módulos de hardware
integrados aos equipamentos da Sotreq que coletam informações dos componentes dessa
34
máquina e enviam informações para um servidor central da plataforma VisionLink
através de conexão via satélite [26]. Entretanto, esta conexão pode ser interrompida,
ocasionando em um atraso na entrega da falha ao servidor. Esta característica deve ser
considerada no desenvolvimento da aplicação.
Este servidor é subdividido em URLs, uma para cada tipo de mensagem
(combustível, falhas, estado atual, etc.). Essas mensagens são representadas em XML em
uma fila, onde são enfileiradas em ordem decrescente de recebimento.
Para manter o escopo deste documento, será descrito o formato e o teor das
mensagens relativas à saúde do equipamento. Estas mensagens enviam 5 tipos de
informações diferentes que em conjunto informam ao técnico competente o exato
problema que que o equipamento pode possuir. Segue a lista com os tipos de informações:
Evento/Diagnóstico: Diz se a informação é referente a um evento ou diagnóstico
MID: Código numérico que identifica o módulo de controle eletrônico que
detectou a falha
CID: Código numérico que identifica o componente envolvido na falha
FMI: Código numérico que informa o tipo de falha ocorrido
Timestamp: Momento em que a falha ocorreu
35
Capítulo 4
Desenvolvimento da Aplicação
Neste capítulo, é descrita a implementação da proposta deste trabalho utilizando
as tecnologias discutidas no capítulo anterior. Primeiramente, detalharemos as empresas
envolvidas neste projeto, mencionando suas áreas de atuação e contribuição para este
trabalho.
Na segunda seção, é descrita a arquitetura proposta para o nosso sistema, exibindo
o fluxo de informação desde a interface com o operador até o processamento das
informações de falhas dos equipamentos, e retornando para o usuário apenas a informação
útil, que é a detecção dos padrões de falhas.
Em seguida, detalhamos a primeira parte do fluxo de dados, começando com os
padrões de falhas que se deseja extrair, até chegarem ao cluster.
Posteriormente, serão descritas as estratégias de processamento distribuído
realizadas pelo Spark e sua integração com o Cassandra. É visto também como as
informações a respeito do equipamento foram salvas no banco e seu impacto.
4.1 – Empresas
Duas empresas participaram da realização deste trabalho. A primeira é a Radix
Engenharia e Software, prestadora do desenvolvimento da plataforma SotreqLink. A
segunda é a Sotreq, o cliente deste projeto.
4.1.1 – Radix Engenharia e Software
A Radix Engenharia e Software foi fundada em abril de 2010 pelos antigos sócios
da Chemtech. Ainda no seu primeiro ano de funcionamento, a empresa superou a marca
de 100 funcionários. Há cinco anos ocupa um lugar de destaque na Lista das Melhores
Empresas para se Trabalhar, prêmio este concedido pela Great Place to Work Institute em
36
parceria com grandes veículos de comunicação do país. Sediada no Rio de Janeiro, a
empresa também possui escritórios em Volta Redonda (RJ), Belo Horizonte (MG), São
José dos Campos (SP), São Paulo (SP) e Houston (EUA) [27].
A Radix conta como clientes empresas como: AkerSolutions, Braskem, CSN,
Deten, FosBrasil, GDK, iMusica, Keppel Fels, Laborvida, Monsanto, Nitriflex,
Petrobras, Supervia, TV Globo, Vale.
Atualmente, a Radix conta com diversos projetos de engenharia e software nas
áreas de petróleo e gás, metais e mineração, agronegócio entre outras.
4.1.2 – Sotreq
O grupo Sotreq é o revendedor oficial de peças, máquinas, serviços e sistemas da
Caterpillar no Brasil. Fundada em 1941 com um contrato firmado com a Caterpillar,
passou as décadas seguintes expandindo seu negócio por todo o Brasil. Atualmente possui
mais de 40 filiais e é uma das maiores revendedoras Caterpillar do mundo [28].
A Sotreq é uma empresa de capital 100% nacional, cujo diferencial é o suporte
aos produtos vendidos, contando com diversas unidades de apoio a reposição de peças,
sistemas de monitoramento e serviços mecânicos. A Sotreq fornece equipamentos nas
áreas de construção civil, mineração, petróleo e gás.
Visando a modernizar sua área de software e agregar valor aos serviços prestados,
em 2015 o Grupo Sotreq adquiriu 50% da Radix. Com essa aquisição, foi criada a
Unidade de Negócio de Metais, Mineração e Agronegócio na Radix, onde este projeto foi
desenvolvido.
4.2 – Arquitetura
A plataforma SotreqLink, na qual este projeto está inserido, é composta por
diversos módulos e serviços, que se comunicam e complementam. É dentro desta
estrutura já estabelecida que este projeto deve ser inserido. São expostos aqui apenas os
componentes relevantes da arquitetura para este projeto, sendo abstraídos outros serviços
e servidores da rede. A arquitetura proposta é detalhada na Fig. 4.1.
37
Fig. 4.1 Arquitetura Geral do Sistema
As setas da figura representam o sentido do fluxo de dados. De acordo com a
arquitetura atual, o operador do sistema acessa através de um único servidor, que foi
batizado com o nome do projeto. O servidor SotreqLink é o responsável por abrigar o
website que realiza a interface com o usuário. É nele também que são implementadas as
diversas regras de negócio do sistema, incluindo a geração de padrões de falhas dos
equipamentos, além de salvar diversas outras informações no banco de dados. O website
ainda exibe todos os dados e/ou resultados importantes para o usuário, o que explica o
fluxo bidirecional de informação entre o servidor e o usuário.
O servidor SotreqLink, por sua vez, estabelece comunicação apenas com o banco
de dados Oracle (além do operador). Como foi discutido no Capítulo 2, o Oracle é
classificado como um SGBD Relacional, apresentando, portanto, as vantagens que este
tipo de ferramenta possui. Como o servidor SotreqLink apresenta para o usuário apenas
as informações relevantes, ele não apresenta um volume de dados muito grande, se
comparado ao Cassandra, por exemplo. É neste banco também que se encontra o cadastro
dos equipamentos cobertos pela manutenção preditiva. Isto faz com que a escolha por um
banco relacional consiga atender perfeitamente à demanda da aplicação, além de evitar
replicação de dados e oferecer diversas ferramentas úteis à busca que não existem em
bancos não relacionais.
38
No centro da imagem encontra-se o JBoss Fuse, que é a solução ESB integrada
ao projeto. Apresentado no capítulo anterior, ESBs tem o objetivo de facilitar a integração
entre os diversos sistemas, evitando a complexidade de realizar essas integrações de
forma independente. Por estar conectado aos diversos servidores, o JBoss é responsável
por executar rotinas agendadas, principalmente no que diz respeito ao remanejamento de
dados entre os servidores. Por este motivo, o JBoss Fuse ocupa um lugar central em nossa
arquitetura, atuando com um grande hub.
O webservice Visionlink, responsável por enviar os dados de telemetria dos
equipamentos, é o único da arquitetura que apenas envia dados ao JBoss. Apesar do
servidor JBoss realizar as requisições ao VisionLink, nenhum dado útil é enviado, o que
justifica uma única seta da direção do JBoss.
Finalmente, temos o cluster Spark/Cassandra à direita da figura, se comunicando
com o JBoss. A escolha de posicionar o cluster se comunicando com o JBoss vai ao
encontro da arquitetura da plataforma como um todo. Além de manter o padrão do
projeto, permite que outros serviços também usufruam do poder de
processamento/armazenamento que o cluster pode proporcionar. Poderíamos ter
posicionado o cluster se comunicando com o servidor SotreqLink por exemplo, mas,
apesar de facilitar a aplicação deste projeto, resultaria em uma dificuldade muito maior
para que outros módulos pudessem acessar o cluster.
4.2.1 – Configuração do cluster
Para este projeto foram disponibilizados pela Radix 3 computadores para compor
o cluster. Cada um deles conta com 2GB de memória RAM e 6 núcleos de processamento.
Todas as máquinas executam instâncias Spark, sendo uma delas o nó driver e os outros 2
os workers, conforme visto na sessão sobre arquitetura do Spark.
Sobre o banco de dados Cassandra, foi decidido que ele ficaria distribuído por
duas dessas máquinas, preferencialmente os nós executores. Desta forma, os nós
executores terão acesso direto aos dados em forma já distribuída, não tendo a necessidade
de realizar uma operação de paralelização e minimizando o Shuffle. Por não realizar o
39
processamento sobre o conjunto RDD, o nó driver não precisa de acesso direto ao
Cassandra.
Outro aspecto importante na configuração do cluster é o fator de replicação dos
dados do Cassandra. A replicação ajuda a garantir a disponibilidade do sistema, assim
como facilita a própria distribuição da carga de trabalho. Foi escolhido um fator de
replicação 2, ou seja, todas as máquinas Cassandra terão acesso a todos os dados
simultaneamente. A estratégia de replicação poderá ser alterada no futuro, caso se mostre
inviável manter todos os nós Cassandra com todas as informações. Em um cenário de
crescimento do cluster e/ou aumento do volume de informação, será conveniente alterar
o fator de replicação.
4.3 – Fontes de Informação
Nesta seção é descrito como são obtidas as informações necessárias para o
reconhecimento de um padrão de falha. Naturalmente, precisamos das próprias falhas dos
equipamentos e do fornecimento de um padrão para ser encontrado.
4.3.1 – Falhas
Como foi discutido no capítulo anterior, as falhas dos equipamentos são
detectadas automaticamente por meio de sensoriamento eletrônico. A comunicação com
o webservice VisionLink é realizada via satélite, permitindo que a comunicação seja
estabelecida inclusive em regiões com difícil acesso à internet. Mesmo assim, alguns
equipamentos apresentam atraso ao reportar a sua saúde, o que deve ser levado em
consideração no desenvolvimento da aplicação.
Considerando que as falhas podem chegar a qualquer momento no VisionLink,
uma rotina no JBoss monitora constantemente o webservice para, tão logo uma falha
esteja disponível, ela ser recolhida e enviada para o nó driver do Spark, que por sua vez
irá paralelizar a informação entre os executores do cluster para serem salvas no Cassandra
em formato adequado.
40
4.3.2 – Padrões
De acordo com a proposta deste projeto, a manutenção preditiva é realizada
através da análise das falhas reportadas pelos equipamentos em campo. Para tal, o técnico
competente deve informar ao sistema o padrão de falhas que deseja ser encontrado no
histórico de falhas do equipamento.
A inclusão destes padrões é feita pela interface web do servidor SotreqLink. A
partir das condições que o usuário inserir no sistema, automaticamente é gerada uma
Regra, que é uma expressão que sintetiza o padrão de erro a ser reconhecido. A seguir,
detalha-se o conceito de regra.
4.3.2.1 – Regras
De maneira geral, regra é a tradução das condições impostas pelo operador do
sistema em uma expressão lógica que deverá ser utilizada como fonte na interpretação
das falhas ocorridas. Quando uma regra identifica um padrão de falha, dizemos que foi
gerada uma violação.
Cada regra possui como alvo um modelo de equipamentos. Desta forma, a mesma
regra se aplica a todos os equipamentos que possuem em seu cadastro o modelo alvo da
regra. É importante ressaltar que mais de uma regra pode ter como alvo o mesmo modelo.
A expressão de uma regra é composta por três tipos de informação: Tags,
Operadores e Argumentos. Uma tag é a representação dos códigos de uma falha. A tag
é especificada pelo operador ao fornecer um evento ou diagnóstico, e os códigos MID,
CID e FMI. Foi convencionado que a tag seria precedida pelo símbolo “#” para ser
facilmente identificada pelo interpretador. A seguir temos um exemplo de tag evento,
com códigos MID = 123, CID = 456 e FMI = 789:
#1.123.456.789 (4.1)
41
Os operadores por sua vez representam as restrições lógicas do padrão
informados pelo técnico competente. Eles podem vir acompanhados, ou não, de um
argumento. Vejamos a seguinte regra exemplo com o operador “OU”:
#1.123.456.789 OU #2.234.345.456 (4.2)
Neste caso, o operador está expressando que deseja ser comunicado quando
quaisquer das duas tags acima forem reportados pelo equipamento. Vejamos agora um
exemplo com o operador “E”:
#1.123.456.789 E:60 #2.234.345.456 (4.3)
Na regra acima, o operador expressa que deseja ser notificado se ambas as tags
forem registradas em um intervalo de 60 segundos. Portanto, o operador “E” é
acompanhado de um argumento, que representa o tempo, em segundos, da janela de filtro
que será aplicado.
Por último, temos o operador “tti”, que significa tempo para ignorar. Este
operador é sempre aplicado por último, já sobre as violações da regra. Ele informa para o
interpretador o tempo mínimo que deve se passar para uma nova violação ser considerada.
Vejamos um exemplo com o uso de “tti”:
#1.123.456.789 tti:120 (4.4)
Agora, suponhamos que o equipamento alvo apresentou a tag em questão nos
momentos 𝑡0 = 0𝑠, 𝑡1 = 100𝑠 , 𝑡2 = 150𝑠 e 𝑡3 = 200𝑠. Teríamos como resultado
apenas as violações ocorridas em 𝑡0 e 𝑡2, pois o intervalo entre 𝑡0 e 𝑡1 é inferior ao
argumento de “tti”, assim como o intervalo entre 𝑡2 e 𝑡3.
Regras também têm suporte a múltiplos operadores, assim com o uso de
parênteses para se definir a prioridade das operações. Vejamos a seguir uma regra mais
complexa, utilizando-se de todos os operadores apresentados (as tags foram substituídas
por letras para facilitar o entendimento).
42
( #A E:20 #B ) OU ( #C E:30 #D ) tti:40 (4.5)
Para facilitar o entendimento da aplicação das condições, iremos demonstrar
como a regra acima deve ser interpretada de acordo com as falhas reportadas pelo
equipamento. Na Fig. 4.2, temos um caso hipotético, onde é demonstrada a execução de
cada passo da regra. Acima de cada seta, está representado o tempo, enquanto abaixo
estão representadas as falhas ou violações.
Fig. 4.2 Aplicação da Regra
A primeira condição aplicada gera duas violações (em vermelho), representadas
por #V1 e #V2. Foi convencionado que a violação ocorreria no momento da última tag
envolvida na condição. No caso de #V1, como a tag #A antecede a tag #B, a violação
#V1 ocorre no mesmo momento que a tag #B. O mesmo ocorre na aplicação da segunda
condição (em azul), onde a violação #V3 ocorre no mesmo momento de #D. O operador
OU, por sua vez, concatena os resultados das duas condições acima, retornando as
violações #V1, #V2 e #V3. O último filtro, “tti:40”, irá desconsiderar qualquer violação
cuja diferença de tempo entre ela e a anterior seja menor que o tempo indicado. Como
43
#V3 ocorre cerca de 20 segundos após #V1, #V3 deve ser descartada. Assim, a
interpretação da regra com as tags fornecidas acima deve retornar apenas as violações
#V1 e #V2.
4.4 – Estrutura dos Bancos de Dados
Nesta sessão são descritas as organizações das informações nos bancos de dados
utilizados neste projeto. Como foi visto na arquitetura geral, temos dois diferentes bancos:
Oracle e Cassandra. Ainda como foi dito anteriormente, a informação significativa para
o usuário deverá ser armazenada no banco Oracle, enquanto os dados não processados
serão convenientemente armazenados no banco Cassandra.
4.4.1 – Oracle
Existem dois tipos de informação que serão armazenados no banco Oracle com o
objetivo de serem facilmente obtidas e manipuladas pelo operador do sistema. As Regras,
que serão adicionadas pelo operador conforme a necessidade, e as Violações, que serão
adicionadas após o processamento das falhas realizados no cluster.
Com foi visto na sessão anterior, uma regra é formada pela sua expressão lógica
e pelo seu alvo, que define em que equipamentos as regras serão aplicadas. O alvo é um
código que define um modelo de equipamento e, portanto, todos os equipamentos que
pertencem ao modelo são alvos da regra.
Para armazenar a regra, foi criada uma tabela com o nome PR_REGRA, onde o
prefixo PR indica apenas que a tabela está relacionada ao módulo de manutenção
preditiva. Esta tabela conta com três colunas, ID_REGRA, DE_MODELO e
DE_EXPRESSAO. A coluna ID_REGRA é a chave primária da tabela e um identificador
único do registro. DE_MODELO armazena o nome do modelo dos equipamentos que
serão os alvos das regras. Finalmente, DE_EXPRESSAO armazena as condições lógicas
da regra que serão aplicadas sobre as falhas. Segue um exemplo na Tabela 4.1 (as tags
foram substituídas por letras para facilitar o entendimento):
44
PR_REGRAS
ID_REGRA DE_MODELO DE_EXPRESSAO
1 JAP #A OU #B
2 JAP (#C E:30 #D) OU (#E E:10 #C)
3 HWM #A E:40 #B
4 FAO #B OU #C
5 MNS #A OU #B OU #C
Tabela 4.1 Regras
O banco Oracle também será responsável por armazenar as violações. A violação
deve armazenar o momento da violação e o número de série do equipamento envolvido.
Também é importante indicarmos a regra que deu origem a violação, através do uso de
chave estrangeira.
Desta forma, foi criada a tabela PR_VIOLACOES, com as seguintes colunas:
ID_VIOLACAO, ID_REGRA, DE_SERIAL_NUMBER e DT_TIMESTAMP. A coluna
ID_VIOLACAO é a chave primária da tabela e identificador único do registro.
ID_REGRA é a chave estrangeira para a tabela PR_REGRAS e indica a regra que deu
origem a violação. DE_SERIAL_NUMBER representa o número de série do
equipamento e DT_TIMESTAMP o momento que a violação ocorreu. Um exemplo de
PR_VIOLACOES pode ser visto na Tabela 4.2.
PR_VIOLACOES
ID_VIOLACAO ID_REGRA DE_SERIAL_NUMBER DT_TIMESTAMP
1 2 HFW019345 17/11/2016 14:17
2 2 HFW019345 17/11/2016 14:19
3 6 JAP001245 18/11/2016 11:39
4 7 FAO099344 19/11/2016 18:20
5 10 FLW010345 21/11/2016 10:11
Tabela 4.2 Violações
Foi visto na sessão sobre tecnologias utilizadas que este projeto utilizou o ORM
Entity Framework da Microsoft em linguagem C#. Veremos em seguida como ficou o
45
mapeamento destas classes na aplicação do servidor SotreqLink. Primeiramente, veremos
a tabela PR_REGRAS, ilustrada pela Fig. 4.3.
[Table("PR_REGRAS")] public class Regras : BaseRegister { [Column("ID_REGRA")] public override int Id { get; set; } [Column("DE_MODELO")] public string Modelo { get; set; } [Column("DE_EXPRESSAO")] public string Expressao { get; set; } public virtual ICollection<Violacoes> Violacoes { get; set; } }
Fig. 4.3 Mapeamento de PR_REGRAS
Podemos observar que, além das colunas terem sido mapeadas conforme a tabela,
existe a inclusão de uma coleção Violacoes. Esta coleção é formada pelos registros da
tabela PR_VIOLACOES que apontam, através da chave estrangeira, para a tabela
PR_REGRAS. Desta forma, um objeto de Regras possui uma coleção de violações que
apontam para o seu Id. Vejamos a seguir o mapeamento de PR_VIOLACOES (Fig. 4.4).
46
[Table("PR_VIOLACOES")] public class Violacoes : BaseRegister { [Column("ID_VIOLACAO")] public override int Id { get; set; } [Column("ID_REGRA")] public int IdRegra { get; set; } [ForeignKey("IdRegra")] public virtual Regra Regra { get; set; } [Column("DE_SERIAL_NUMBER")] public string SerialNumber { get; set; } [Column("DT_TIMESTAMP")] public DateTime Timestamp { get; set; } }
Fig. 4.4 Mapeamento de PR_VIOLACOES
Assim como ocorreu no caso anterior, temos as propriedades mapeadas de acordo
com as colunas que representam. Além disso, temos a indicação de que a propriedade
IdRegra é uma chave estrangeira que aponta para o objeto Regra. Desta forma, um objeto
da classe Violacoes ganha acesso ao objeto Regra apontado por ele.
4.4.2 – Cassandra
O banco de dados Cassandra é responsável por armazenar os dados de falhas dos
equipamentos, que serão processados para serem obtidas as violações das regras
correspondentes. Para atingir tal objetivo, iremos utilizar duas tabelas: PR_FALHAS e
PR_FALHAS_TEMP.
A tabela PR_FALHAS será constituída por seis colunas: BUCKET, TAG,
MODEL, TIMESTAMP, SERIAL_NUMBER e INSERT_TIME. A coluna BUCKET é
a primeira chave primária e irá conter um inteiro que representa o dia em que a violação
foi recebida. Esta estratégia nos permite realizar uma busca por todas as falhas que
aconteceram no mesmo dia, indo ao encontro da proposta de realizar o processamento
das falhas uma vez ao dia. A coluna TAG irá conter o código de falha, enquanto as colunas
MODEL, SERIAL_NUMBER e TIMESTAMP armazenarão, respectivamente, o modelo
47
do equipamento, o número de série do equipamento e o momento da falha. Finalmente, a
coluna INSERT_TIME é a única coluna que não faz parte da chave e irá conter o
momento em que o dado foi inserido no banco de dados. A Tabela 4.3 é um exemplo.
PR_FALHAS
Chave Valor
BUCKET TAG MODEL SERIAL_NUMBER TIMESTAMP INSERT_TIME
20161118 #A JAP JAP00100 16/11/2016 13:34 18/11/2016
20:30
20161118 #A JAP JAP00100 16/11/2016 17:49 18/11/2016
20:30
20161118 #B HWV HWV00241 16/11/2016 11:10 18/11/2016
20:30
20161119 #C HWV HWV00257 18/11/2016 18:44 19/11/2016
20:30
20161119 #C FLW FLW00123 18/11/2016 18:47 19/11/2016
20:30
Tabela 4.3 Falhas dos Equipamentos
A tabela PR_FALHAS_TEMP, por sua vez, terá a função de armazenar as falhas
que não se converteram em violações e ocorreram ainda após a última violação registrada.
Isto é necessário pois, como foi visto na descrição do webservice VisionLink, as falhas
podem chegar com atraso no sistema. Por isso, uma falha que não gerou uma violação em
um determinado dia, poderá gerar caso uma outra falha necessária chegue ao sistema
posteriormente. Foi acordado com o cliente que sete dias é o período máximo que uma
regra deve permanecer na tabela temporária. A tabela temporária deverá conter uma
coluna a mais, a ID_REGRA, que contém o identificador da regra em que a falha não se
converteu em uma violação. O identificador da regra é importante para distinguir as falhas
que foram convertidas em violações em uma regra, mas não foram em outra. Neste caso,
é interessante que apenas as regras correspondentes possam reavaliar estas falhas
posteriormente. A coluna BUCKET foi alterada para receber apenas um inteiro. O
objetivo do bucket agora é auxiliar na escrita e limpeza da tabela. Como a tabela será
reescrita a cada processamento, é conveniente que primeiro sejam salvos os novos dados
temporários para depois remover os antigos. Assim, a cada operação de reescrita, basta
alterar o bucket no momento da escrita e posteriormente apagar os dados com o bucket
anterior. Um exemplo pode ser visto na Tabela 4.4.
48
PR_FALHAS_TEMP
Chave Valor
BUCKET TAG MODEL SERIAL_NUMBER ID_REGRA TIMESTAMP INSERT_TIME
1 #A JAP JAP00100 10 16/11/2011
13:34
16/11/2011
20:30
1 #A JAP JAP00100 10 16/11/2011
17:49
16/11/2011
20:30
1 #B HWV HWV00241 21 16/11/2011
11:10
16/11/2011
20:30
1 #C HWV HWV00257 33 18/11/2011
18:44
18/11/2011
20:30
1 #C FLW FLW00123 48 18/11/2011
18:47
18/11/2011
20:30
Tabela 4.4 Falhas Temporárias
4.5 – Aplicação Spark
Uma vez que as estruturas dos bancos de dados foram descritas, podemos agora
discutir a aplicação Spark, que será responsável por organizar os dados das diferentes
fontes de informação descritas, e processá-las de maneira distribuída e paralela, conforme
o objetivo deste trabalho. A aplicação foi desenvolvida utilizando-se majoritariamente
dos conceitos de programação funcional, apresentados anteriormente.
O nó driver do cluster é o responsável por receber as regras armazenadas no banco
Oracle. As informações provenientes do Cassandra já se encontram distribuídas no cluster
devido à natureza do próprio banco de dados. Uma vez iniciado o processamento, o driver
invoca o método Processar, que recebe como argumentos a coleção de regras do Oracle,
enviadas pelo JBoss, e o bucket, que representa a data dos dados a serem processados.
Como o processamento ocorre uma vez ao dia, a data enviada é a do dia anterior. O código
do método principal, assim como métodos auxiliares e as classes que modelam os
elementos do banco, estão disponíveis no Apêndice A.
O principal objetivo do método Processar é organizar as diferentes informações
de maneira eficiente para que possam ser processadas de forma paralela e distribuída pela
aplicação. Dentro do contexto Spark, transformações sobre conjuntos deverão ser
49
utilizadas para que o interpretador das regras possa operar de maneira eficiente, ou seja,
paralela e distribuída.
O método interpretador necessita de três argumentos, de acordo com o que foi
visto na sessão sobre regras: Falhas de um equipamento para uma dada regra, a expressão
lógica e o identificador da regra. O identificador serve apenas para associar a violação à
sua regra de origem. Estas três informações devem estar disponíveis de maneira
organizada para a interpretação ser realizada.
Temos, a princípio, três tipos de dados diferentes: falhas, falhas temporárias e
regras. Apenas as duas primeiras encontram-se distribuídas no cluster, enquanto as regras
encontram-se no driver da aplicação. O primeiro passo é determinar o bucket atual e o
próximo das falhas temporárias, que serão utilizados no fim deste processo. Em seguida,
é importante filtrarmos as falhas que não são alvos de nenhuma regra, assim como as
regras que não possuem nenhuma falha como alvo. A solução encontrada para isso foi
utilizar uma técnica chamada de broadcast join. Para facilitar o entendimento de todo o
fluxo, são demonstradas as transformações realizadas em um caso exemplo, começando
pela Fig. 4.5. Neste exemplo não estão sendo consideradas as falhas temporárias, por
simplicidade.
Fig. 4.5 Fluxo da Aplicação - Parte 1
A Fig. 4.5 nos mostra o início da aplicação. O objeto Regra é uma tripla contendo
o identificador da regra, o modelo do equipamento alvo e a expressão lógica da regra. O
objeto Falha é uma sêxtupla que contêm o bucket, a tag, o modelo, o número de série, o
momento da falha e o momento de inserção no banco Cassandra. Ambos os objetos foram
50
definidos conforme as respectivas modelagens nos bancos de dados. A primeira operação
realizada é um FlatMap que retorna para cada Regra o conjunto das tags envolvidas em
sua expressão. O resultado é um conjunto chave-valor, onde a chave é a regra e o valor
uma da tags da expressão.
Fig. 4.6 Fluxo da Aplicação - Parte 2
A Fig. 4.6 exibe a segunda parte do fluxo. Primeiramente é realizada uma
operação de agrupamento por modelo do equipamento e tag, resultando em uma estrutura
do tipo dicionário. A chave é composta pelo modelo e tag, enquanto o valor do dicionário
é um conjunto de regras associadas a esta chave. Em seguida é realizada um broadcast
neste dicionário para que uma cópia desta estrutura fique disponível em cada nó do
cluster. Como o dicionário está disponível na memória local de cada worker, as falhas
podem acessá-lo de maneira eficiente e sem Shuffle de dados através de uma operação
Map que realiza essa consulta ao dicionário, como mostra a segunda linha da figura. Neste
caso, a consulta irá retorna um conjunto de regras que se relacionam com a falha em
questão. Caso a falha não encontre nenhuma regra correspondente no dicionário, o valor
“null” é retornado. Por fim, realizamos uma operação de filtro, retornando apenas as
falhas que tenham alguma regra associada e eliminado as falhas que retornaram “null” na
consulta ao dicionário. Este processo é chamado de broadcast join pois realizamos uma
operação equivalente ao comando join da linguagem SQL de bancos relacionais
utilizando uma variável broadcast do Spark.
51
Fig. 4.7 Fluxo da Aplicação - Parte 3
A terceira parte do fluxo da aplicação é exemplificada na Fig. 4.7. Uma
transformação de FlatMap é necessária para planificar o conjunto de regras associadas a
cada falha. Assim, uma falha é replicada para cada regra associada em seu conjunto de
regras original. Desta forma a estrutura resultando é uma dupla onde os elementos são a
falha e a regra respectivamente. A operação seguinte é um Map simples, onde o objetivo
é criar uma estrutura chave-valor onde a chave é uma tripla contendo o número de série
do equipamento, o identificador da regra e a sua expressão lógica, enquanto o valor é a
falha do elemento original. Esta transformação é necessária para a operação de
agrupamento que será feita na próxima parte do fluxo, visto na Fig. 4.8.
Fig. 4.8 Fluxo da Aplicação - Parte 4
A Fig. 4.8 nos mostra a quarta e última parte do fluxo da aplicação. A primeira
transformação realizada é um agrupamento por chave. Como a chave da nossa estrutura
chave-valor é o número de série do equipamento, o identificador da regra e a expressão
da regra, temos que o resultado do agrupamento são todas as falhas de um mesmo
equipamento para uma dada regra. Assim, a estrutura resultante é exatamente os
argumentos necessários para serem passados para o método interpretar, que é feito através
de uma transformação FlatMap. Por fim, o resultado desta última operação são as
violações de cada equipamento para cada regra, cumprindo assim o objetivo da aplicação.
52
Capítulo 5
Resultados
Desde a implementação deste projeto, o processamento de falhas e regras ocorre
uma vez ao dia, como especificado. Para verificar a quantidade média de violações que
foram geradas pela aplicação, foi executada uma query em nosso banco de dados
agrupando as violações por dia de ocorrência. A pesquisa nos retornou uma média de
aproximadamente 4210 violações por dia, como pode ser visto na Fig. 5.1.
Fig. 5.1 Média de Violações por dia
Além de atestar o funcionamento da solução, é interessante verificarmos o
comportamento da aplicação no que diz respeito ao paralelismo e distribuição. O Spark
possui uma interface de usuário web que nos fornece diversas métricas da aplicação. Para
cada ação executada, a interface nos fornece um DAG (Direct Acyclic Graph), que é uma
representação gráfica das transformações executadas em uma ação. O DAG da ação
collect(), que reúne as violações para o driver do cluster, pode ser visto na Fig. 5.2.
53
Fig. 5.2 DAG da Aplicação
Podemos ver que o Spark dividiu a ação em dois estágios. No primeiro é realizado
o broadcast join e a união dos dois conjuntos de falhas. No segundo ocorre o agrupamento
por chave para então interpretar as regras. A cor verde na operação map do segundo
estágio indica que foi realizado um cache. Para cada estágio, a interface web nos fornece
ainda métricas específicas, como a linha do tempo das tarefas por executor. A Fig. 5.3
ilustra a linha do tempo do primeiro estágio.
54
Fig. 5.3 Linha do Tempo Estágio 1
De acordo com a figura, o tempo total gasto do primeiro estágio é próximo de 6
segundos. Podemos observar que praticamente a totalidade do tempo foi usada com
processamento (em verde) em vez de outras atividades, como a serialização da tarefa. Ao
final de cada tarefa, uma pequena porção do tempo foi utilizado para Shuffle (em
amarelo). Isto é justificado pela primeira transformação do estágio seguinte, que é um
agrupamento por chave. Operações de agrupamento causam redistribuição dos dados no
cluster.
É importante notar ainda que cada executor consegue realizar, simultaneamente,
até 6 tarefas. Isto coincide com o número de núcleos especificados na configuração do
cluster. Finalmente, percebemos que o segundo executor realiza um trabalho
consideravelmente maior que o primeiro, cerca de 3 vezes mais. Isto pode ocorrer devido
as operações de filtro realizadas neste estágio. Filtros podem impactar cada nó de maneira
diferente, pois não é garantido que a mesma quantidade de dados seja filtrada em todos
os nós, deixando-os desbalanceados.
Podemos realizar análise semelhante no segundo estágio, conforme está ilustrado
na Fig. 5.4.
55
Fig. 5.4 Linha do Tempo Estágio 2
A duração total deste estágio é de cerca de 100 milissegundos. Como no estágio
anterior, a maior parte do tempo foi usada com processamento. Além disso, cada executor
realiza 6 tarefas simultaneamente.
Podemos verificar, no entanto, duas diferenças em relação ao estágio anterior. A
primeira delas é a existência de tempo de leitura de shuffle (em laranja) em duas tarefas.
Isto é necessário para reunir as informações que foram distribuídas por escrita de shuffle
no estágio anterior. A segunda diferença é a existência de tempo de serialização de
resultado ao final de cada tarefa (em roxo). Por se tratar do último estágio, o resultado
final precisa ser enviado para o driver, o que ocorre por meio de serialização. Assim como
as tarefas passam por desserialização ao chegarem no executor, o resultado deve ser
serializado para ser enviado ao driver.
56
Capítulo 6
Conclusões e Trabalhos Futuros
Para concluir este projeto, vamos analisar os resultados obtidos no capítulo
anterior. Podemos dizer que obtivemos sucesso, em primeiro lugar, por conseguirmos
identificar os padrões de erros gerados pelo técnico. Estes padrões, que chamamos de
violações, podem ser tratados de acordo com o interesse da empresa contratante.
Futuramente, estas violações terão tratamento automático do sistema, de acordo com a
regra de origem.
Também era objetivo deste trabalho realizar a identificação dos erros utilizando
tecnologias de Big Data, paralelismo e sistemas distribuídos. Como pode ser visto nas
Fig. 5.3 e Fig. 5.4, o processamento ocorreu de forma distribuída, envolvendo todos os
nós workers, e paralela, onde cada worker realizou até 6 tarefas simultaneamente. Um
ponto de atenção é o não balanceamento de dados evidenciado na Fig. 5.3. Uma sugestão
seria realizar uma redistribuição dos dados após a etapa de filtros, caso necessário.
Um outro objetivo deste projeto era conseguir uma arquitetura escalável conforme
a necessidade. Ao utilizarmos as ferramentas Spark e Cassandra, podemos adicionar mais
computadores ao cluster com o mínimo de esforço, bastando apenas registrar os novos
computadores ao cluster, não precisando alterar a aplicação. Finalmente, temos que o
tempo total do processamento ficou em torno de apenas 6 segundos, atendendo as
necessidades do cliente no momento.
O próximo passo é automatizar a geração de padrões de erros do sistema, não
precisando que o operador os adicione manualmente. Técnicas de inteligência artificial e
aprendizado de máquina podem ser utilizadas para relacionar a necessidade de reparo dos
equipamentos com seu histórico de falhas, extraindo assim o padrão de falhas que indica
uma real necessidade de manutenção.
57
Bibliografia
[1] SOTREQ, “Sotreq,” [Online]. Available: http://sotreq.com.br/.
[2] A. McAfee, “Big Data: The Management Revolution,” Harvard Business Review,
pp. 59 - 68, 2012.
[3] K. Dooley, Designing Large Scale Lans, O'Reilly Media, 2001.
[4] J. M. Rabaey, Digital integrated circuits: a design perspective, Upper Saddle
River, NJ, USA: Prentice-Hall, Inc., 1996.
[5] G. E. Moore, “Cramming more components,” Electronics, vol. 38, nº 8, 1965.
[6] McGill School of Computer Science, “Moore`s law,” [Online]. Available:
https://www.cs.mcgill.ca/~rwest/link-suggestion/wpcd_2008-
09_augmented/wp/m/Moore%2527s_law.htm. [Acesso em 19 dezembro 2016].
[7] B. Barney, “OpenMP,” [Online]. Available:
https://computing.llnl.gov/tutorials/openMP/. [Acesso em 20 dezembro 2016].
[8] B. Barney, “Introduction to Parallel Computing,” [Online]. Available:
https://computing.llnl.gov/tutorials/parallel_comp/. [Acesso em 22 dezembro
2016].
[9] G. M. Amdahl, “Validity of the single processor approach to achieving large scale
computing capabilities,” ACM, pp. 483-485, 1967.
[10] A. S. Tanenbaum e M. V. Steen, DISTRIBUTED SYSTEMS, Upper Saddle
River, N, USA: Prentice-Hall, 2007.
[11] N. Taing, “Parallel and Distributed Computing,” [Online]. Available:
http://lycog.com/distributed-systems/parallel-and-distributed-computing/. [Acesso
em 17 dezembro 2016].
[12] A. Church, “A set of postulates for the foundation of logic,” Annals of
mathematics, pp. 346-366, 1 abril 1932.
[13] École Polytechnique Federale de Lausanne, “Higher-order Functions,” [Online].
Available: http://docs.scala-lang.org/tutorials/tour/higher-order-functions. [Acesso
em 07 novembro 2016].
[14] Microsoft, “Palavra-Chave default em código genérico,” [Online]. Available:
https://msdn.microsoft.com/pt-br/library/xwth0h0d.aspx. [Acesso em 10
novembro 2016].
[15] E. F. CODD, “A relational model of data for large shared data banks,”
Communications of the ACM, vol. 13, nº 6, 1970.
[16] D. D. Chamberlin e R. F. Boyce, “SEQUEL: A structured English query
language,” Proceedings of the 1974 ACM SIGFIDET (now SIGMOD) workshop
on Data description, access and control, pp. 249-264, 01 Maio 1974.
[17] DATASTAX, “Apache Cassandra 2.0 Documentation,” [Online]. Available:
http://docs.datastax.com/en/archived/cassandra/2.0/. [Acesso em 07 novembro
2016].
58
[18] SOLID IT GMBH, “DB-Engines - Knowledge Base of Relational and NoSQL
Database Management Systems,” [Online]. Available: http://db-engines.com/en.
[Acesso em 16 outubro 2016].
[19] G. Hohpe e B. Woolf, Enterprise Integration Patterns: Designing, Building, and
Deploying Messaging Solutions, Boston, MA, USA: Addison-Wesley Longman
Publishing Co., Inc., 2003.
[20] RED HAT SOFTWARE, “Product Documentation for Red Hat JBoss Fuse,”
[Online]. Available: https://access.redhat.com/documentation/en/red-hat-jboss-
fuse/6.0/. [Acesso em 21 11 2016].
[21] École Polytechnique Federale de Lausanne, “Documentation,” [Online].
Available: https://www.scala-lang.org/documentation/ . [Acesso em 20 novembro
2016].
[22] S. CASS, “The 2016 Top Programming Languages,” [Online]. Available:
http://spectrum.ieee.org/computing/software/the-2016-top-programming-
languages . [Acesso em 20 novembro 2016].
[23] SPARK, “Documentation,” [Online]. Available:
http://spark.apache.org/docs/latest/ . [Acesso em 24 novembro 2016].
[24] SPARK, “Cluster Mode Overview,” [Online]. Available:
http://spark.apache.org/docs/latest/cluster-overview.html. [Acesso em 24
novembro 2016].
[25] SPARK, “Spark Programming Guide,” [Online]. Available:
http://spark.apache.org/docs/latest/programming-guide.html. [Acesso em 02
dezembro 2016].
[26] VISIONLINK, VisionLink VL-Ready API User Guide.
[27] RADIX ENGENHARIA E SOWTWARE, “A Empresa,” [Online]. Available:
http://www.radixeng.com.br/sobre/. [Acesso em 02 Outubro 2016].
[28] SOTREQ, “História,” [Online]. Available: http://sotreq.com.br/empresa2/historia/
. [Acesso em 10 Outubro 2016].
[29] ESSEY, “Parallel Programming with .NET,” Micrososft, [Online]. Available:
https://blogs.msdn.microsoft.com/pfxteam/2009/05/28/partitioning-in-plinq/.
[Acesso em 30 novembro 2016].
59
Apêndice A
Código da Aplicação
Processador e métodos auxiliares
/*
* PROCESSADOR
*/
import java.util.Date
import org.apache.spark.broadcast.Broadcast
import schemas.OracleTables.{Regra, Violacao}
import Spark.SparkManager._
import com.datastax.spark.connector._
import com.datastax.spark.connector.cql.CassandraConnector
import org.apache.spark.rdd.RDD
import schemas.CassandraTables.{Falha, FalhaTemp}
object Processor {
/*
* Conector com o banco Cassandra
*/
val cassandraConnector = CassandraConnector.apply(sparkContext.getConf)
/*
* Metodo principal da aplicacao
*/
def Processar(regras : Iterable[Regra], bucket: BigInt) : Iterable[Violacao] = {
//Determinar bucktet atual e proximo das falhas temporarias
val bucketAtual = BucketTempAtual()
val proximoBucket = ProximoBucket(bucketAtual)
//Organizar Regras para mapped-join
val regrasExpandidasPorTag = ExpandirRegrasPorTag(regras)
val regrasAgrupadasPorModeloETag =
AgruparRegrasPorModeloETag(regrasExpandidasPorTag)
val regrasAgrupadasPorIdEModelo = AgruparRegrasPorIdEModelo(regrasExpandidasPorTag)
val broadcastRegrasModeloTag = sparkContext.broadcast(regrasAgrupadasPorModeloETag)
val broadcastRegraIdModelo = sparkContext.broadcast(regrasAgrupadasPorIdEModelo)
//Organizar Falhas Para Join
val falhasModeloETag = ObterFalhasCassandraPorModeloETag(bucket)
val falhasTempIdRegraEModelo = ObterFalhasTemporariasCassandraPorIdRegraEModelo()
//Realizar mappped-join
val FalhasERegras = JoinRegrasComFalhas(broadcastRegrasModeloTag, falhasModeloETag)
val FalhasTempERegras =JoinRegrasComFalhasTemp(broadcastRegraIdModelo,
falhasTempIdRegraEModelo)
//Realizar Union das falhas com as falhas temorarias
60
val unionFalhasERegras = FalhasERegras.union(FalhasTempERegras)
//Realizar Agrupamento por SerialNumber e Regra
val falhasAgrupadas = AgruparFalhasPorSerialNumberERegra(unionFalhasERegras)
//Processar as falhas com as regras
val violacoesEFalhasTemp = ProcessarFalhas(falhasAgrupadas, proximoBucket).cache()
val violacoes = violacoesEFalhasTemp.flatMap(x => x._1)
val falhasTemp = violacoesEFalhasTemp.flatMap(x => x._2)
//Coletar Resultado e Salvar no Cassandra
val violacoesColetadas = violacoes.collect()
falhasTemp.saveToCassandra("sotreq","pr_falhas_temp")
ExcluirFalhasTemporariasAntigas(bucketAtual)
violacoesColetadas
}
/*
* Exclui todas as falhas temporarias com
* o bucket fornecido
*/
def ExcluirFalhasTemporariasAntigas(bucket : BigInt): Unit ={
if(bucket == null)
return
val session = cassandraConnector.openSession()
session.execute("DELETE FROM sotreq.pr_falhas_temp WHERE bucket = " + bucket + " ")
session.close()
}
/*
* Define qual e o proximo bucket
* bucket alterna entre 1 e 2
*/
def ProximoBucket(bucket : BigInt) : BigInt = {
if(bucket == null)
return 1
if( bucket == 1)
return 2
1
}
/*
* Define qual e o bucket das falhas temporarias atual
*/
def BucketTempAtual(): BigInt ={
val falhaTemp =
sparkContext.cassandraTable[FalhaTemp]("sotreq","pr_falhas_temp").first()
if(falhaTemp == null)
return null
falhaTemp.Bucket
}
/*
* Invoca o interpretador e realiza o filtro para definir
* as falhas temporarias
*/
61
def ProcessarFalhas(falhasERegras : RDD[((String, Int, String), Iterable[Falha])],
proximoBucket : BigInt): RDD[(Iterable[Violacao], Iterable[FalhaTemp])] ={
val violacoesEFalhasTemp = falhasERegras.map(x => {
val violacoesETempoLimite = Interpretador.Interpretar(x._2,x._1._2,x._1._3)
val falhasTemp =
FiltroFalhas.FiltrarFalhasTemporarias(violacoesETempoLimite._2,x._1._2, proximoBucket,
x._2)
(violacoesETempoLimite._1, falhasTemp)
})
violacoesEFalhasTemp
}
/*
* Realiza um agrupamento do conjunto falhas-regras
* reunindo todas as falhas que se relacionam com o
* mesmo numero de serie, Id da regra e sua expressao
*/
def AgruparFalhasPorSerialNumberERegra(falhasERegras :
RDD[((String,String,Int,String,String),Falha)]): RDD[((String, Int, String),
Iterable[Falha])] = {
val agrupamento = falhasERegras.map(x => ((x._1._1, x._1._3, x._1._4), x._2))
.groupByKey()
agrupamento
}
/*
* Realiza uma operacao de Join entre as regras e as falhas temporarias
* O objetivo e fitrar somente as falhas que tenham regras para serem processadas
* e as regras que possuam as falhas correspondentes
* Regras e falhas possuem as mesmas chaves (IdRegra e Tag)
* Chave (SerialNumber, Modelo, IdRegra, Expressao, Tag)
* e valor Falha
*/
def JoinRegrasComFalhasTemp (broadcastRegras: Broadcast[Map[(Int,String),
Iterable[Regra]]], falhas: RDD[((Int,String), FalhaTemp)] )
: RDD[((String,String,Int,String,String),Falha)] = {
//Filtrar os resultados onde nao foi encontrada uma regra correspondente
val joinnedFalhasERegras = falhas.map( falha => (falha._1, falha._2,
broadcastRegras.value.getOrElse(falha._1, null)))
//Sobram agora um conjunto de regras e uma falha que se aplicam ao mesmo RegraId e
Tag
.filter(x => x._3 != null)
//Expande o resultado para futuro agrupamento
.flatMap(x => x._3.map(regra => ((x._2.Serial_number, x._2.Model, regra.Id_regra,
regra.Expressao, x._2.Tag), x._2)))
//TrasformaParaFalha
.map(x => (x._1, new
Falha(x._2.Bucket,x._2.Tag,x._2.Model,x._2.Serial_number,x._2.Timestamp,
x._2.Insert_time)))
joinnedFalhasERegras
}
/*
* Realiza uma operacao de Join entre as regras e as falhas
* O objetivo e fitrar somente as falhas que tenham regras para serem processadas
* e as regras que possuam as falhas correspondentes
* Tanto as regras quanto as falhas possuem as mesmas chaves (Modelo e Tag)
62
* Chave: (SerialNumber, Modelo, IdRegra, Expressao, Tag)
* Valor: Falha
*/
def JoinRegrasComFalhas (broadcastRegras: Broadcast[Map[(String,String),
Iterable[Regra]]], falhas: RDD[((String,String), Falha)] )
: RDD[((String,String,Int,String,String),Falha)] = {
//Filtrar os resultados onde nao foi encontrada uma regra correspondente
val joinnedFalhasERegras = falhas.map( falha => (falha._1, falha._2,
broadcastRegras.value.getOrElse(falha._1, null)))
//Sobram um conjunto de regras e uma falha que se aplicam ao mesmo Modelo e Tag
.filter(x => x._3 != null)
//Expande o resultado para futuro agrupamento
.flatMap(x => x._3.map(regra => ((x._2.Serial_number, x._2.Model, regra.Id_regra,
regra.Expressao, x._2.Tag), x._2)))
joinnedFalhasERegras
}
/*
* Realiza um agrupamento onde a chave e (IdRegra, Modelo)
* e o valor e uma colecao de Regras que tenham o IdRegra
* e alvo o Modelo
*/
def AgruparRegrasPorIdEModelo (conjuntoRegraTag: Iterable[(Regra, String)]) :
Map[(Int,String), Iterable[Regra]] = {
val agrupamentoPorIdEModelo = conjuntoRegraTag.groupBy(x => (x._1.Id_regra, x._2))
.map(agrupamento => (agrupamento._1, agrupamento._2.map(conjunto => conjunto._1)))
agrupamentoPorIdEModelo
}
/*
* Realiza um agrupamento onde a chave e (Modelo, Tag)
* e o valor e uma colecao de Regras que tenham como alvo o modelo
* e contenham na expressao a tag
*/
def AgruparRegrasPorModeloETag(conjuntoRegraTag : Iterable[(Regra,String)]):
Map[(String,String), Iterable[Regra]] = {
val agrupamentoPorModeloETag : Map[(String,String), Iterable[Regra]] =
conjuntoRegraTag.groupBy(x => (x._1.Modelo, x._2))
.map( agrupamento => (agrupamento._1, agrupamento._2.map(conjunto =>
conjunto._1)))
agrupamentoPorModeloETag
}
/*
* FlatMap sobre as regras, retornando para cada regra um conjunto com a chave
* a propria regra e o valor cada Tag presente na regra
*/
def ExpandirRegrasPorTag(oracleRules: Iterable[Regra]) : Iterable[(Regra,String)] ={
val regrasExpandidas = oracleRules.flatMap(regra =>
ExtrairTagsDaRegra(regra).map(tag => (regra,tag)))
regrasExpandidas
}
/*
* Retorna um RDD de falhas
63
* Chaves: Modelo, Tag
*/
def ObterFalhasCassandraPorModeloETag(bucket: BigInt): RDD[((String,String), Falha)] =
{
val falhasPorChave : RDD[((String,String), Falha)] =
sparkContext.cassandraTable[Falha]("sotreq", "pr_falhas")
.where("bucket = ?", bucket)
.map(x => ((x.Model, x.Tag), x))
falhasPorChave
}
/*
* Retorna um RDD de falhas temporarias
* Chaves: Id_regra, Modelo
*/
def ObterFalhasTemporariasCassandraPorIdRegraEModelo(): RDD[((Int,String), FalhaTemp)]
= {
val tempoAtual = new Date()
val falhasTempPorChave: RDD[((Int, String), FalhaTemp)] =
sparkContext.cassandraTable[FalhaTemp]("sotreq", "pr_falhas_temp")
.filter(falhaTemp => FiltroFalhas.FalhaTempRecente(falhaTemp,tempoAtual))
.map(x => ((x.Id_regra, x.Model), x))
falhasTempPorChave
}
/*
* Retorna uma colecao das tags envolvidas em cada expressao da regra
*/
def ExtrairTagsDaRegra(r:Regra):Iterator[String] = {
"(\\#[\\w.]+)(?!.*\\1)".r.findAllMatchIn(r.Expressao).map(x => x.group(0))
}
}
Filtro de Falhas
/*
* FILTRO DE FALHAS
*/
import java.util.Date
import schemas.CassandraTables.{Falha, FalhaTemp}
object FiltroFalhas {
/*
* Periodo considerado recente: Uma semana em ms
*/
val periodoRecente : Long = 604800000
/*
* Determina se a falha temporaria e recente
*/
def FalhaTempRecente(falha : FalhaTemp, tempoAtual : Date): Boolean ={
if (tempoAtual.getTime - falha.Insert_time.getTime < periodoRecente)
64
return true
false
}
/*
* Determina se a falha deve ser salva na tabela de fahas tmporarias
*/
def FiltrarFalhasTemporarias(momentoLmite: Date, IdRegra : Int, bucket: BigInt, falhas
: Iterable[Falha]): Iterable[FalhaTemp] = {
val falhasTemporarias = falhas.filter(falha => falha.Timestamp.getTime >
momentoLmite.getTime )
.map(falha => new FalhaTemp(bucket, falha.Tag, falha.Model,falha.Serial_number,
IdRegra, falha.Timestamp, falha.Insert_time))
falhasTemporarias
}
}
Mapeamento do Banco Oracle
/*
* MAPEAMENTO DO BANCO ORACLE
*/
import java.util.Date
object OracleTables {
case class Regra (Id_regra: Int, Modelo: String, Expressao: String)
case class Violacao (Id_regra: Int, Serial_number: String, Timestamp: Date)
}
Mapeamento do Banco Cassandra
/*
* MAPEAMENTO DO BANCO CASSANDRA
*/
import java.util.Date
object CassandraTables {
case class Falha(Bucket : BigInt, Tag : String, Model:String, Serial_number : String,
Timestamp: Date, Insert_time: Date)
case class FalhaTemp(Bucket : BigInt, Tag : String, Model:String, Serial_number :
String, Id_regra : Int, Timestamp: Date, Insert_time: Date)
}
top related