• Nenhum resultado encontrado

3.2 Modelando Programas de Processamento de Dados

3.2.1 Fluxo de Dados

Para definir o DAG que representa o fluxo de dados de um programa de pro- cessamento de dados, nos baseamos no modelo de grafos de fluxo de dados apresentado em (KAVI; BUCKLES; BHAT, 1986), que foi formalizado com Redes de Petri (MURATA, 1989). Nesse modelo, um grafo de fluxo de dados é representado como um grafo direcio- nado bipartido que possui dois tipos de vértices, as ligações (links) e os atores (actors). Atores são definidos como transições que representam operações sobre dados, enquanto ligações são definidas como lugares que representam locais reservados que recebem dados de um ator e transmitem dados para um ou mais atores. Atores e ligações são conectados através de arestas que representam canais de comunicação entre os vértices.

No nosso modelo, um programa 𝑃 é definido como um grafo direcionado bipartido que possui dois tipos de vértices, os conjuntos de dados distribuídos (𝐷), que representam as ligações (lugares), e as transformações (𝑇 ), que representam os atores (transições). Os conjuntos de dados e transformações são conectados por arestas direcionadas (𝐸):

𝑃 = ⟨𝐷 ∪ 𝑇, 𝐸⟩

Para exemplificar o modelo, vamos considerar o programa Spark apresentado na Figura 3.1. Esse programa faz a contagem de palavras em um conjunto de dados de textos. O programa recebe como entrada um RDD contendo linhas de texto (linha 1). Em seguida, as linhas de texto são separadas por palavras, formando um RDD de palavras (linha 2). Essas palavras são mapeadas para elementos chave/valor em que a chave é a própria palavra e o valor é o número inteiro 1 (linha 3). Por último, as palavras são contadas a partir de uma operação de agregação que agrupa os elementos de acordo com a chave (palavra) e soma os valores associados com essa chave (linha 4), resultando em um RDD que contém as palavras e suas frequências que é retornado como resultado do programa (linha 5).

No programa apresentado na Figura 3.1, podemos identificar quatro RDDs. Esses fazem parte do conjunto 𝐷 que contém todos os nós de conjuntos de dados distribuídos do programa 𝑃 . Esses conjuntos de dados, que são denotados com 𝑑𝑖, podem ser vistos a

1 def wordCount(input: RDD[String]) = {

2 val words = input.flatMap( (line: String) => line.split(" ") )

3 val pairs = words.map( (word: String) => (word, 1) )

4 val counts = pairs.reduceByKey( (a: Int, b: Int) => a + b) )

5 return counts

6 }

Figura 3.1 – Exemplo de programa de contagem de palavras em Spark. seguir: 𝐷 = {𝑑1, 𝑑2, 𝑑3, 𝑑4} 𝑑1 = input 𝑑2 = words 𝑑3 = pairs 𝑑4 = counts

No programa também podemos identificar a aplicação de três transformações que compõem o conjunto 𝑇 contendo todos os nós de transformações aplicadas em 𝑃 . Essas transformação, que são denotadas por 𝑡𝑖, podem ser vistas a seguir:

𝑇 = {𝑡1, 𝑡2, 𝑡3}

𝑡1 = 𝑓 𝑙𝑎𝑡𝑀 𝑎𝑝( (line: String) => line.split(“ ”) ) 𝑡2 = 𝑚𝑎𝑝( (word: String) => (word, 1) )

𝑡3 = 𝑟𝑒𝑑𝑢𝑐𝑒𝐵𝑦𝐾𝑒𝑦( (a: Int, b: Int) => a + b )

Uma transformação pertencente a 𝑇 recebe um ou dois conjuntos de dados per- tencentes a 𝐷 como entrada e produz um conjunto de dados também pertencente a 𝐷 como saída. Além disso, os conjuntos 𝐷 e 𝑇 são disjuntos, o que significa que não existem elementos que pertencem a ambos ao mesmo tempo, e finitos, cada conjunto possui uma quantidade finita de elementos:

𝐷 ∩ 𝑇 = ∅

Os conjuntos de dados em 𝐷 e as transformações em 𝑇 são conectados através de arestas. Essas são definidas como um subconjunto do produto cartesiano de conjunto de dados distribuídos por transformações, que caracterizam o conjunto de dados de entrada de uma transformação, ou do produto cartesiano de transformações por conjuntos de dados distribuídos, que caracterizam o conjunto de dados de saída de uma transformação:

𝐸 ⊆ (𝐷 × 𝑇 ) ∪ (𝑇 × 𝐷)

𝐸 = {(𝑑1, 𝑡1), (𝑡1, 𝑑2), (𝑑2, 𝑡2), (𝑡2, 𝑑3), (𝑑3, 𝑡3), (𝑡3, 𝑑4)}

Com a definição dos conjuntos 𝐷, 𝑇 e 𝐸 que representam o programa Spark apresentado na Figura 3.1, podemos representar o programa 𝑃 de forma gráfica. Essa representação pode ser vista na Figura 3.2. Os conjuntos de dados distribuídos em 𝐷 estão representados como círculos e as transformações em 𝑇 estão representadas como quadrados. As arestas estão representadas por setas que conectam os conjuntos de dados e transformações.

input flatMap words map pairs reduceByKey counts

Figura 3.2 – Representação gráfica do fluxo de dados do programa apresentado na Fi- gura 3.1 .

Consideramos dois subconjuntos de 𝐷, um que representa os conjuntos de dados de entrada (𝑅) de um programa, que são criados a partir de uma fonte de dados externa, como a partir da leitura de dados em um sistema de arquivos distribuídos tipo o HDFS, por exemplo, e um que representa os conjuntos de dados de saída (𝐶) de um programa, que representam conjuntos de dados que serão salvos ou coletados em algum coletor de dados externo, como ser salvo no HDFS, por exemplo. Por questão de simplicidade, não definimos no nosso modelo operações que geram os conjuntos de dados em 𝑅 ou que coletam os conjuntos de dados em 𝐶, assumimos que os vértices nesses conjuntos representam as entradas e saídas, respectivamente, do nosso programa 𝑃 . 𝑅 e 𝐶 são definidos como segue:

𝑅 = {𝑑 ∈ 𝐷|∀𝑡 ∈ 𝑇.(𝑡, 𝑑) /∈ 𝐸} 𝐶 = {𝑑 ∈ 𝐷|∀𝑡 ∈ 𝑇.(𝑑, 𝑡) /∈ 𝐸}

Considerando o nosso exemplo, os conjuntos 𝑅 e 𝐶 são definidos como: 𝑅 = {𝑑1}

𝐶 = {𝑑4}

Na representação gráfica apresentada na Figura 3.2, os conjuntos de dados de entrada em 𝑅 são representados por círculos na cor verde, e os conjuntos de dados de saída em 𝐶 são representados por círculos na cor vermelha.

O conjunto de conjuntos de dados de entrada de uma transformação 𝑡, e o con- junto de conjuntos de dados de saída de uma transformação 𝑡 são denotados 𝐼(𝑡) e 𝑂(𝑡):

𝐼(𝑡) = {𝑑 ∈ 𝐷|(𝑑, 𝑡) ∈ 𝐸} 𝑂(𝑡) = {𝑑 ∈ 𝐷|(𝑡, 𝑑) ∈ 𝐸}

De forma semelhante, o conjunto de transformações que deram origem a um conjunto de dados 𝑑 e o conjunto de transformações que recebem o conjunto de dados 𝑑 como entrada são denotados 𝐼(𝑑) e 𝑂(𝑑):

𝐼(𝑑) = {𝑡 ∈ 𝑇 |(𝑡, 𝑑) ∈ 𝐸} 𝑂(𝑑) = {𝑡 ∈ 𝑇 |(𝑑, 𝑡) ∈ 𝐸}

Existem transformações que operam sobre um único conjunto de dados (|𝐼(𝑡)| = 1), que vamos denominar transformações unárias, e transformações que operam sobre dois conjuntos de dados (|𝐼(𝑡)| = 2), que vamos denominar transformações binárias. Exemplos de transformações unárias são as transformações de mapeamento e agregação, como as transformações aplicadas no programa apresentado na Figura 3.1, e exemplos de transformações binárias são as transformações de junção. Em ambos os tipos de transformações, sempre é gerado um único conjunto de dados como saída (|𝑂(𝑡)| = 1). A partir disso, podemos derivar as seguintes propriedades:

1 ≤ |𝐼(𝑡)| ≤ 2 para todo 𝑡 ∈ 𝑇 |𝑂(𝑡)| = 1 para todo 𝑡 ∈ 𝑇

Os conjuntos de dados em 𝐷 podem ser gerados a partir da leitura de uma fonte externa, que são denominados como os conjuntos de entrada de 𝑃 que pertencem ao conjunto 𝑅 (|𝐼(𝑑)| = 0), ou podem ser resultados de uma transformação em 𝑇 (|𝐼(𝑑)| = 1). Além disso, os conjuntos de dados em 𝐷 podem ser entrada para uma ou mais transformações em 𝑇 (|𝑂(𝑑)| > 1), ou podem ser o conjunto de dados de saída do programa 𝑃 , pertencendo ao conjunto 𝐶 (|𝑂(𝑑)| = 0) . A partir disso, também podemos derivar as seguintes propriedades:

0 ≤ |𝐼(𝑑)| ≤ 1 para todo 𝑑 ∈ 𝐷 |𝑂(𝑑)| ≥ 0 para todo 𝑑 ∈ 𝐷

Tomando como referência a transformação 𝑡1 e o conjunto de dados 𝑑1 do nosso programa 𝑃 de exemplo, podemos ver que esses respeitam as propriedades apresentadas:

𝐼(𝑡1) = {𝑑1} 𝑂(𝑡1) = {𝑑2}

𝐼(𝑑1) = ∅ 𝑂(𝑑1) = {𝑡1}

O fluxo de dados representado por 𝑃 deve ser consistente em relação aos tipos dos conjuntos de dados em 𝐷 e as transformações em 𝑇 . Dessa forma, se um conjunto de dados

𝑑 está definido como entrada de uma transformação 𝑡, então o tipo de 𝑑 deve ser igual ao tipo do conjunto de dados que 𝑡 recebe como entrada. Da mesma forma, se o conjunto 𝑑 for definido como saída de 𝑡, então o tipo de 𝑑 deve ser igual ao tipo do conjunto de dados retornado por 𝑡. Para representar essas propriedades, vamos considerar que transformações binárias recebem os dois conjuntos de dados de entrada de forma ordenada. Também vamos considerar as funções 𝑖𝑛𝑝𝑢𝑡_𝑡𝑦𝑝𝑒1(𝑡), do tipo 𝑇 → 𝜏1, 𝑖𝑛𝑝𝑢𝑡_𝑡𝑦𝑝𝑒2(𝑡), do tipo 𝑇 → 𝜏2, e 𝑜𝑢𝑡𝑝𝑢𝑡_𝑡𝑦𝑝𝑒(𝑡), do tipo 𝑇 → 𝜏3, para denotar respectivamente o tipo 𝜏1 do primeiro conjunto de dados de entrada de uma transformação 𝑡 (para o caso de transformações unárias e binárias), o tipo 𝜏2 do segundo conjunto de dados de entrada de uma transformação 𝑡 (apenas no caso em que 𝑡 é uma transformação binária) e o tipo 𝜏3 do conjunto de dados de saída de uma transformação 𝑡. Além disso, vamos considerar a função 𝑡𝑦𝑝𝑒(𝑑), do tipo 𝐷 → 𝜏 , que denota o tipo (𝜏 ) de um conjunto de dados. A partir dessas funções, podemos definir que se um conjunto de dados 𝑑 é entrada para uma transformação unária 𝑡 ((𝑑, 𝑡) ∈ 𝐸 ∧ |𝐼(𝑡)| = 1), então o tipo de 𝑑 deve ser igual ao tipo do conjunto de dados de entrada de 𝑡 (𝑡𝑦𝑝𝑒(𝑑) = 𝑖𝑛𝑝𝑢𝑡_𝑡𝑦𝑝𝑒1(𝑡)):

∀𝑡 ∈ 𝑇, 𝑑 ∈ 𝐷.(𝑑, 𝑡) ∈ 𝐸 ∧ |𝐼(𝑡)| = 1 ⇒ 𝑡𝑦𝑝𝑒(𝑑) = 𝑖𝑛𝑝𝑢𝑡_𝑡𝑦𝑝𝑒1(𝑡)

De forma semelhante, se 𝑡 for uma transformação binária (|𝐼(𝑡)| = 2), então o tipo de 𝑑 deve ser igual ao tipo do primeiro conjunto de dados de entrada de 𝑡 ou igual ao tipo do segundo conjunto de dados de entrada de 𝑡 (𝑡𝑦𝑝𝑒(𝑑) = 𝑖𝑛𝑝𝑢𝑡_𝑡𝑦𝑝𝑒1(𝑡) ∨ 𝑡𝑦𝑝𝑒(𝑑) = 𝑖𝑛𝑝𝑢𝑡_𝑡𝑦𝑝𝑒2(𝑡)):

∀𝑡 ∈ 𝑇, 𝑑 ∈ 𝐷.(𝑑, 𝑡) ∈ 𝐸 ∧ |𝐼(𝑡)| = 2 ⇒ 𝑡𝑦𝑝𝑒(𝑑) = 𝑖𝑛𝑝𝑢𝑡_𝑡𝑦𝑝𝑒1(𝑡) ∨ 𝑡𝑦𝑝𝑒(𝑑) = 𝑖𝑛𝑝𝑢𝑡_𝑡𝑦𝑝𝑒2(𝑡)

Para ambos os tipos de transformações, se um conjunto de dados 𝑑 é a saída de uma transformação 𝑡 ((𝑡, 𝑑) ∈ 𝐸), então o tipo de 𝑑 deve ser igual ao tipo do conjunto de dados de saída de 𝑡 (𝑡𝑦𝑝𝑒(𝑑) = 𝑜𝑢𝑡𝑝𝑢𝑡_𝑡𝑦𝑝𝑒(𝑡)):

∀𝑡 ∈ 𝑇, 𝑑 ∈ 𝐷.(𝑡, 𝑑) ∈ 𝐸 ⇒ 𝑡𝑦𝑝𝑒(𝑑) = 𝑜𝑢𝑡𝑝𝑢𝑡_𝑡𝑦𝑝𝑒(𝑡)

Por fim, com base nos tipos de operações existentes nos sistemas de processa- mento de grandes volumes de dados discutidos, observamos a existência de dois tipos de transformações, aquelas que requerem uma reorganização dos dados no cluster (data shuffling) e aquelas que não requerem essa reorganização. No nosso modelo, vamos repre- sentar esses dois tipos de transformações utilizando a nomenclatura utilizada pelo Apache Spark (ZAHARIA et al., 2012), que denomina como transformações estreitas (narrow transformations) aquelas que não requerem reorganização de dados no cluster, que vamos representar através do conjunto 𝑁 , e como transformações amplas (wide transformations) aquelas que requerem reorganização de dados no cluster, que vamos representar através do conjunto 𝑊 :

𝑁 ⊆ 𝑇 𝑊 ⊆ 𝑇

As semânticas das transformações estreitas e amplas serão discutidas na Se- ção 3.2.2. A Tabela 3.1 apresenta um resumo das definições apresentadas nesta seção. Tabela 3.1 – Descrição das representações do fluxo de dados de um programa de proces-

samento de grandes volumes de dados.

Representação Descrição

𝑃 Representação de um programa de processamento de grandes volumes de dados que é composto pelos conjuntos 𝐷, 𝑇 e 𝐸.

D Conjunto de conjuntos de dados de um programa 𝑃 .

𝑇 Conjunto de operações de transformações sobre conjuntos de dados de um programa 𝑃 .

𝐸 Conjunto de arestas que conectam os conjuntos de dados em 𝐷 e as trans- formações em 𝑇 de um programa 𝑃 .

𝑅 Subconjunto de 𝐷 que representa os conjuntos de dados de entrada de um programa 𝑃 .

𝐶 Subconjunto de 𝐷 que representa os conjuntos de dados de saída de um programa 𝑃 .

𝑊 Subconjunto de 𝑇 que representa transformações que requerem redistri- buição de dados (transformações amplas).

𝑁 Subconjunto de 𝑇 que representa transformações que não requerem redis- tribuição de dados (transformações estreitas).

𝐼(𝑡) Conjunto de conjunto de dados de entrada de uma transformação 𝑡.

𝑂(𝑡) Conjunto de conjunto de dados de saída de uma transformação 𝑡.

𝐼(𝑑) Conjunto de transformações que geram o conjunto de dados 𝑑.

𝑂(𝑑) Conjunto de transformações que recebem o conjunto de dados 𝑑 como entrada.