Escolar Documentos
Profissional Documentos
Cultura Documentos
2022
Tecnologias de Big Data – Processamento de Dados Massivos
Pedro Lehman Toledo
© Copyright do Instituto de Gestão e Tecnologia da Informação.
Todos os direitos reservados.
2
Sumário
YARN ........................................................................................................................... 17
3
Agrupamento e Agregação ......................................................................................... 89
Joins........................................................................................................................... 102
4
Introdução ao Dataproc e GCS.................................................................................. 148
Referências………………....................................................................................................178
5
Capítulo 1. O cenário de Big Data
Assim, para que esse objetivo seja atingido, é necessário apresentar alguns
conceitos relacionados à manipulação de grandes quantidades de dados. Eles serão o
fundamento de uma compreensão adequada das tecnologias mais modernas utilizadas
nessa área – consequentemente, possibilitando um olhar crítico e analítico das
abordagens utilizadas em cada situação.
https://github.com/pltoledo/igti-dados-massivos
A coleta de dados aparenta ser uma técnica moderna, porém é um método tão
antigo quanto a civilização, e fez-se presente durante grande parte da história da
6
humanidade. Práticas como o censo e a coleta de dados demográficos são utilizadas
desde os tempos antigos, estão registradas em diversos achados arqueológicos datados
de milhares de anos atrás (HAKKERT, 1996). Posteriormente, a coleta e o
armazenamento de dados foram preponderantes para diversas descobertas científicas,
especialmente no campo da astronomia e biologia. Por fim, com a invenção do
computador e o advento da internet, esses processos foram aperfeiçoados a ponto de
possibilitar ao ser humano uma capacidade analítica até então nunca experimentada.
Nos dias de hoje, o termo Big Data tem sido muito utilizado, mas muitas vezes
de uma maneira um tanto liberal – como normalmente acontece quando um conceito é
popularizado, mas não necessariamente entendido. Sua primeira definição formal foi
feita pelo Grupo Gartner, em 2001, e a versão mais atual dessa definição é:
Após esta, outras definições buscaram enriquecer ainda mais esse conceito:
7
transformar processos, organizações, indústrias inteiras e até mesmo
a sociedade. (DEMIRKAN H; et al, 2015, tradução nossa)
8
• Veracidade: é necessário se certificar que os dados gerados não estejam
fora de controle. Essa característica se refere à confiabilidade e manutenção
dos dados coletados, o que se mostra um desafio cada vez maior com o
avanço das tecnologias de coleta e armazenamento de dados.
Fonte: Edureka.
Esse último V está diretamente relacionado com a parte final da definição dada
pelo Gartner: após apresentar as principais características da Big Data, é deixado bem
claro que a coleta e processamento desses dados possuem um propósito muito bem
9
definido de gerar valor para um negócio, por meio de ferramentas que possam auxiliar
na tomada de decisão, automatização de processos e insights de negócio.
Ainda que tenha potencial para ser um agente transformador do negócio, o uso
de Big Data demanda atenção e cuidados específicos. Críticos desse conceito apontam
conflitos éticos e sociais advindos de como a coleta de dados em massa pode se
configurar como uma invasão de privacidade das pessoas, especialmente se tratando de
dados que não são completamente anonimizados (BOYD e CRAWFORD, 2012). Isso leva
ao dilema do ponto de vista técnico, uma vez que quanto maior o volume de dados,
maior o desafio de manutenção e segurança, o que pode gerar um cenário de violação
de normas de privacidade estabelecidas ou até mesmo prejudicar a análise desses
dados. Acerca da análise, é válido ressaltar que, mesmo com grandes quantidades de
dados a disposição, ainda é importante ter o entendimento de como os dados foram
gerados e quais suas suposições e vieses, visto que Big Data não é sinônimo para
completude de dados, e considerar a origem da amostra analisada é mais importante
que o seu tamanho (BOYD e CRAWFORD, 2012).
Dessa forma, fica claro que a aplicação de tecnologias de Big Data tem
vantagens e desvantagens. Muitas vezes, “dados pequenos” podem ser suficientes para
responder todas as questões de negócio com uma complexidade menor e risco menor.
A escolha do que usar depende principalmente do objetivo a ser alcançado, e do valor
que isso irá gerar ao final do processo. Na próxima seção, será introduzida uma das
principais ferramentas utilizadas nos softwares modernos de processamento de grandes
volumes de dados.
Processamento distribuído
10
enfrentavam uma mudança de paradigma: devido a grandes limitações na dissipação de
calor, fabricantes de hardware deixaram de desenvolver processadores individuais que
fossem mais potentes, e passaram a adotar a estratégia de adicionar núcleos paralelos
às CPUs que fossem capazes de serem executadas na mesma velocidade. Esse cenário
foi propício para o surgimento de ideias que fundamentaram todas as tecnologias de
processamento de dados massivos que surgiram posteriormente.
Aliado a isso, em 2005 o Google anunciou a criação do Google File System (GFS),
MapReduce e Bigtable, tecnologias que tinham como objetivo solucionar o problema de
armazenamento e computação de dados que a empresa enfrentava, visto que os bancos
de dados relacionais clássicos não eram capazes de operar com a escala em que seu
popular mecanismo de pesquisa estava sendo desenvolvido.
11
Na Figura 2 é possível ver um esquema de comparação entre a computação serial e a
computação em paralelo.
Fonte: teldat.com.
12
• Tolerância a falhas: é possível desenhar a arquitetura distribuída de forma
que o cluster seja capaz de incorporar tarefas à outras máquinas, caso o
processamento em um nó seja interrompido, trazendo mais confiabilidade
ao sistema.
13
Ecossistema Hadoop
A partir do trabalho desenvolvido pelo Google por meio do GFS e MR, o Yahoo!,
que também enfrentava problemas relacionados a processamento de grandes
quantidades de dados em um mecanismo de pesquisa, criou o projeto Hadoop como a
sua própria abordagem para solucioná-los. As ferramentas desenvolvidas foram bem-
sucedidas, e em 2008, o Hadoop foi liberado como um projeto open source para a
Apache Software Foundation, a partir da qual foi desenvolvido um framework completo
de armazenamento e processamento distribuído, que está entre as mais tradicionais
ferramentas de Big Data. A capacidade de trabalhar com dados massivos – estruturados
e não estruturados, escalabilidade e o fato de ser gratuito, contribuíram para sua rápida
popularização no mercado.
14
• Apache HIVE: banco de dados que utiliza uma interface de SQL no ambiente
distribuído.
Fonte: Savvycom.
15
Os componentes apresentados foram concebidos para serem integráveis
dentro do Ecossistema Hadoop, todavia, também é possível utilizá-los em conjunto com
outras tecnologias externas, dependendo da necessidade do usuário.
16
pré-estabelecido, de forma que se uma máquina falhar o dado não é
perdido.
YARN
17
As principais vantagens de se utilizar o YARN são sua escalabilidade, alocação
dinâmica de clusters, compatibilidade com versões anteriores do Hadoop e integração
com diferentes ferramentas de processamento para diferentes cargas de trabalho,
sejam elas processamento em batch, streaming ou consultas interativas. Por causa
disso, o YARN é hoje um dos gerenciadores de recursos mais utilizados em ambientes
distribuídos.
MapReduce
18
Figura 5 – MapReduce Workflow.
Fonte: Yahoo!.
19
Capítulo 2. Introdução ao Apache Spark
20
do Spark gira em torno de quatro características principais: velocidade, usabilidade,
modularidade e extensibilidade.
Velocidade
21
Figura 6 – Daytona GraySort Benchmarks.
Fonte: Databricks.
Simplicidade
22
proporciona facilidade de aprendizado tanto para profissionais veteranos, quanto para
iniciantes na área.
Modularidade
Fonte: Databricks.
23
• Spark SQL: módulo direcionado para o processamento de dados
estruturados de forma tabular. Oferece um mecanismo de consultas SQL
interativas num ambiente distribuído. Pode ser integrado com o restante do
ecossistema para construir pipelines completas.
Extensibilidade
24
Figura 8 – Conectores do Spark.
25
para a aplicação, uma vez que armazena todas as informações relevantes e dá controle
ao usuário.
Apesar de sua arquitetura ser própria para deploy em clusters, o Spark pode
ser executado localmente em uma só máquina e, nesse caso, a paralelização e
distribuição das tarefas ocorre em termos das threads do processador do computador.
Abaixo podemos ver a arquitetura de uma aplicação do Spark:
26
do SQL, configurações da aplicação e acesso ao catálogo de metadados. Para mais,
diferentemente do que acontecia em versões anteriores, uma mesma SparkSession
pode ser utilizada para trabalhar como todos os módulos, não havendo a necessidade
de instanciar objetos diferentes para trabalhar com o Spark SQL e o ML, por exemplo.
27
É interessante observar como o modelo de programação é coerente entre as
duas linguagens, o que torna fácil a transição de uma linguagem de programação para a
outra e contribui para o tema de unificação e simplicidade do framework. O Spark é
escrito primariamente em Scala e, por isso, essa é considerada a linguagem “padrão” do
framework, mesmo assim quase todas as funcionalidades também estão disponíveis
para as demais linguagens. Além disso, o código do Spark é executado por Java Virtual
Machines (JVMs) e, no caso do Python e R, ainda que o código deva ser traduzido antes
da execução, a performance permanece relativamente a mesma.
28
Transformações, ações e Lazy Evaluation
As operações que podem ser aplicadas sobre os dados no Spark podem ser
classificadas em dois tipos: transformações e ações. Porém, para entender a diferença
entre elas é preciso antes compreender o conceito de lazy evaluation.
• select()
• filter()
29
• withColumn()
• count()
• show()
• toPandas()
• collect()
• approxQuantile()
30
Figura 12 – Transformações Narrow e Wide.
31
2. Planejamento Lógico: nessa fase, o Catalyst recebe a query do usuário e
identifica formas de otimizar o processo, principalmente movendo a ordem das
operações, ainda abstraindo as transformações a serem aplicadas. O plano
otimizado é então input do planejamento físico.
32
Durante o desenvolvimento, é possível verificar o plano de execução do Spark
para as transformações realizadas em um DataFrame, usando o método .explain().
Observe o exemplo abaixo utilizando o banco de dados do IMDB, em que são
selecionados os 15 títulos (filmes, séries, documentários etc.) com a melhor avaliação
no site:
33
Figura 15 – Plano Físico.
34
Uma análise cuidadosa do plano mostra quais operações serão realizadas
durante a execução, permitindo que o usuário identifique possíveis gargalos de
processamento e possíveis melhorias. Essa é uma parte importante da otimização do
Spark, e será abordado mais à frente.
35
Também é possível analisar o plano lógico de execução trocando a chamada ao
método para explain(True), mas via de regra o plano físico tem mais aplicações
práticas do que o plano lógico.
A instalação do Apache Spark é feita seguindo alguns passos simples, que são
comuns às instalações em máquinas Linux e Windows. No entanto, a instalação no
Windows requer alguns passos a mais, como será demonstrado abaixo.
Caso o Java não esteja instalado ou a versão instalada seja anterior a 1.8, será
necessário acessar o site oficial do Java e realizar o download e instalação. Por enquanto,
o Spark só é compatível com as versões 8 e 11.
36
Uma vez verificada essa dependência, é necessário garantir que as variáveis de
ambiente estejam configuradas apropriadamente. Considerando a versão 8, as variáveis
de ambiente do Java são:
37
Semelhantemente à instalação do Java, é necessário criar algumas variáveis de
ambiente após a extração dos arquivos do Spark. As variáveis são:
• SPARK_HOME = C:/spark/spark-3.1.2-bin-hadoop2.7
• HADOOP_HOME = C:/spark/spark-3.1.2-bin-hadoop2.7
Fonte: github.com/cdarlint/winutils.
38
Feito isso, salve os arquivos baixados no diretório SPARK_HOME/bin, que,
considerando o exemplo anterior, seria C:/spark/spark-3.1.2-bin-hadoop2.7/bin.
Por fim, existe um truque que não é obrigatório, mas que pode ser útil para
resolver problemas de alguns usuários. As etapas são as seguintes:
2. winutils.exe ls -F C:\tmp\hive
39
Observe acima que o shell padrão do Spark é em Scala. Para abrir o shell
interativo em Python, é necessário que o aplicativo suporte a execução do Python, mas,
uma vez que isso esteja garantido, basta executar:
É possível visualizar que ambos shells estão funcionando com a mesma versão
do Spark. Para finalizar os testes, o output do seguinte comando deve ser:
40
abertas dessa forma, as shells executam o Spark em modo local e uma SparkSession
é gerada automaticamente para o usuário.
41
Com isso, está finalizada a introdução ao Spark, seus principais conceitos, forma
de funcionamento, instalação e configuração inicial. Nos próximos capítulos serão
apresentadas as principais operações de manipulação de dados com a ferramenta.
42
Capítulo 3. Manipulando dados com Spark: Parte 1
DataFrames e Datasets
Os DataFrames e Datasets são abstrações feitas sobre os RDDs, o que faz com
que eles herdem todas as características vistas anteriormente, como a imutabilidade
dos dados e a lazy evaluation. Por isso, essas estruturas podem ser vistas como uma lista
de operações, especificadas na forma das transformações, a serem realizadas sobre os
dados representados por linhas e colunas.
43
Scala (uma vez que R e Pyhton são linguagens com tipagem dinâmica), mas os
DataFrames também estão disponíveis em Scala. Dessa forma, os Datasets devem ser
criados utilizando uma case class (Scala) ou JavaBeans (Java).
44
implementa tipos internos de dados que se relacionam com esses tipos nativos. O Spark
dispõe de duas categorias de tipos: básicos e complexos – e há uma gama de funções
disponíveis para operar sob cada um deles. Tipicamente, os tipos complexos se diferem
dos tipos básicos por aceitarem estruturas aninhadas e de datas.
E os tipos complexos:
45
Todas as APIs possuem os mesmos tipos internos, ainda que eles possam ser
mapeados para diferentes tipos nativos, dependendo da linguagem.
46
Figura 27 – Mudando o tipo da coluna.
Todos os tipos podem ser especificados com strings, mas, para os tipos
complexos, muitas vezes é melhor utilizar a API disponível.
• Permite que o usuário identifique erros nos dados logo na leitura, caso os
dados não sigam o schema especificado.
Existem duas formas de criar um schema: por meio da API de tipos apresentada
anteriormente ou por meio de uma string DDL (Data Definition Language), que fica
muito mais simples e fácil de ler (Damji et al, 2020).
47
Figura 28 – Definindo o Schema Programaticamente.
Usando o string DDL no mesmo exemplo anterior, é possível ver que o código
fica muito mais limpo e fácil de ler:
48
objetos iteráveis, normalmente tuplas ou listas, em que cada iterável representa uma
linha e cada elemento dentro dele representa uma coluna. Dessa forma, é possível
enxergar que essa lista é uma coletânea de indivíduos, e que cada indivíduo é uma
coletânea de variáveis. Os dados também podem ser passados como um RDD ou
pandas.DataFrame.
Existe ainda uma outra forma de gerar DataFrames no Spark, mas sem muitas
opções de customização. O método SparkSession.range() pode ser utilizado
para gerar um DataFrame com uma única coluna chamada id do tipo LongType, que
consiste basicamente de uma sequência de números. Os intervalos dessa sequência
podem ser definidos e são argumentos do método.
49
Leitura e escrita de dados
DataFrameReader
50
Além de especificar o formato dos dados sendo lidos, é possível passar o
schema, o caminho dos dados e as configurações extras de leitura, que são passadas
pelo método .option() – conforme discutido anteriormente. Os argumentos desse
método devem ser um único par de chave/valor, que denotam uma única opção de
leitura sendo configurada, e ambos os argumentos devem estar no formato de strings.
Esse método é bastante utilizado ao trabalhar com dados originados de arquivos JSON
ou CSV, uma vez que existem diversas formas de configurar a leitura de arquivos nesses
formatos. Também é possível usar .options(), uma alternativa que permite
especificar todas as configurações empregadas em uma mesma chamada:
51
Obs.: usar o dicionário como no exemplo acima só funciona em Python, pois é
uma característica nativa da linguagem.
DataFrameWriter
52
Uma característica importante do dispositivo de escrita do Spark é que o
número de arquivos ao final de uma operação de salvamento de dados está diretamente
relacionado ao número de partições do DataFrame. Isso significa que se um DataFrame
está dividido em 200 partições – que como já visto, são unidades de processamento –
os dados serão salvos em 200 arquivos, cada um com uma parte desses dados.
53
Figura 34 – Exemplo de diretório após escrita dos dados.
Cada um desses arquivos está salvo no formato parquet numa pasta chamada
“df_notas”, criada no diretório de trabalho em que o código foi executado. Cada uma
das partições representa um “pedaço” dos dados originais.
Parquet
54
• Preservação de metadados, incluindo os tipos das colunas, o que garante
eficiência e praticidade na escrita e leitura (não é necessário especificar
schemas para arquivos parquet).
Fonte: Databricks.
55
A principal opção de leitura e escrita de arquivos parquet é o modo de
compressão, denotado pelo argumento compression. Como dito anteriormente, o
fato desse formato de arquivo ser salvo de forma comprimida proporciona uma grande
economia de espaço em disco, e escolher a forma mais adequada de comprimir os dados
pode ajudar a melhorar a eficiência das operações de input/output. O default é a
compressão utilizando snappy, mas há algumas outras opções disponíveis.
JSON
56
Escrever arquivos JSON também é simples. De forma semelhante à escrita,
basta especificar corretamente os argumentos e métodos e o caminho do diretório de
destino:
57
CSV
58
Escrever arquivos CSV também é simples. De forma semelhante à escrita, basta
especificar corretamente os argumentos, métodos e o caminho do diretório de destino:
59
Vale destacar ainda a opção encoding, que permite mudar a representação
de strings internamente. Essa opção muitas vezes corrige erros em que caracteres de
colunas de texto vêm faltando ou são lidos com erro. Para saber mais sobre os diferentes
tipos de encoding, acesse esse link.
60
No caso acima, o Spark internamente reduz o DataFrame a somente uma
partição, de forma que os dados estejam concentrados em uma única unidade de
processamento. Assim, quando os dados forem salvos, eles serão escritos em somente
um arquivo. Essa alternativa está disponível independente da linguagem, mas requer
um ponto de atenção: como dito anteriormente, não é possível escolher o nome do
arquivo final, somente o nome da pasta em que o Spark salvará esse arquivo, e por isso
não é necessário colocar a extensão “.csv” ao final do caminho de destino.
ORC
61
Figura 42 – Lendo arquivos ORC.
62
definidas agem sobre as colunas para alterar o DataFrame de alguma forma, de modo
que todo o processo de manipulação tem de ser desenvolvido com a ideia de que as
operações são colunares. Programadores sem muita experiência com bancos de dados
relacionais ou a linguagem SQL muitas vezes encontram dificuldades, mas pensar dessa
forma é crucial para que seja desenvolvido um processo consistente e eficiente.
Colunas e expressões
63
Observe que ao adicionar o valor 5 à coluna “nota”, o valor foi adicionado sobre
cada um dos elementos da coluna. Esse é o comportamento padrão da maior parte das
funções e operações que agem sobre colunas no Spark. A função col, que foi utilizada
para fazer referência à coluna, faz parte do módulo pyspark.sql.functions
(Python) e org.apache.spark.sql.functions (Scala), que concentra todas as
funções utilizadas para transformar colunas. As colunas podem ser referenciadas das
seguintes formas:
• col(“column”) / column(“column”)
• df[“colum”]
• df.column
• df.col(“column”)
• $”column”
• ‘column
64
O último aspecto das colunas que é importante entender, antes de apresentar
as operações de manipulação de DataFrames, é o fato de que as colunas, nesse caso os
objetos de manipulação, nada mais são do que expressões. As expressões são
transformações sobre as colunas físicas do DataFrame, que tem como input os nomes
das colunas a serem transformadas e algum tipo de operação a ser realizada sobre elas,
seja uma operação matemática ou alguma função implementada. No caso mais simples,
uma expressão referencia uma coluna da mesma forma que a função col(). Para
acessar as expressões, deve-se utilizar a função expr(), presente no mesmo módulo
de funções. Alguns exemplos:
Seleção de Colunas
65
maneira semelhante, essa operação pode ser realizada no Apache Spark por meio do
método select():
66
A função alias() é um método das colunas e pode ser usado em qualquer
momento em que uma coluna for retornada. Também fica claro com o exemplo acima
que a ordem em que as colunas são escritas na função é a mesma ordem em que elas
serão exibidas no DataFrame resultante.
67
Obs.: o método distinct() retorna um DataFrame com os valores distintos
a nível de linha, ou seja, a função irá retornar linhas únicas que devem diferir em pelo
menos uma coluna. Dessa forma, esse método pode ser utilizado para remover valores
duplicados, o que também pode ser feito com dropDuplicates().
Renomeação de Colunas
df.withColumnRenamed(“nome_antigo”, “nome_novo”)
68
Filtragem de Linhas
Para filtrar linhas, é necessário denotar uma expressão que ao ser avaliada
retorna valores booleanos: true (verdadeiro) ou false (falso). Ela pode ser construída por
meio de um string - que pode usar sintaxe SQL e funciona da mesma maneira que a
função expr() - ou uma série de manipulações de colunas. Definida a expressão, ela
deve ser passada para o método filter() ou where(), que são basicamente
idênticos e não apresentam diferenças de performance. Exemplo:
69
Figura 51 – Filtro Múltiplo.
Observações
Quando nos referimos às colunas por meio da função col(), temos acesso a
diversos métodos das colunas que podem ser utilizados para auxiliar na filtragem do
DataFrame. Alguns deles são:
70
• like(): utilizado para verificar se uma coluna de texto contém algum
padrão especificado (não aceita regex). Funciona de forma similar ao "LIKE"
do SQL.
71
Utilizando expressões, a maioria desses métodos estarão acessíveis por meio
dos seus equivalentes em SQL.
Ordenação do DataFrame
72
Figura 52 – Ordenação do DataFrame.
Criação de Colunas
df.withColumn("nome_da_coluna", {expressão})
73
Figura 53 – Criação de Colunas.
Fica claro que, até o momento, só é possível criar, alterar e manipular colunas
já existentes em um DataFrame no momento de sua criação. No entanto, caso seja
necessário criar uma coluna a partir de uma constante ou algum elemento presente no
ambiente em que o Spark está sendo executado, é possível utilizar a função lit() para
criar uma coluna que replica este valor para todas as linhas:
74
Finalmente, é possível criar campos baseados em condições lógicas, de forma
similar à cláusula CASE WHEN do SQL. Usando a manipulação de colunas, é necessário
usar a função abaixo dentro de um método capaz de gerar novas colunas:
75
Trabalhando com diferentes tipos de dados
Uma vez que ficou claro a forma como criar e modificar colunas no Spark, é
possível entender algumas das principais funcionalidades disponíveis para a
manipulação de diferentes tipos de dados. A seguir serão apresentadas funções do
módulo pyspark.sql.functions (Python) e
org.apache.spark.sql.functions (Scala) que são comumente usadas em
uma pipeline de processamento.
Números
76
• sqrt(): retorna a raiz quadrada do valor.
77
Figura 56 – Exemplo com Números.
Strings
78
• initcap(): retorna a primeira letra de cada palavra no string em letras
maiúsculas.
79
Dica: Diante de uma coluna de texto, é de praxe realizar algumas operações
para já “normalizá-la” logo no início. Dessa forma, fica mais fácil encontrar erros e saber
o que esperar do campo sendo trabalhado. Segue o caminho padrão para realizar essa
limpeza:
3. Converter o string para um padrão de case, seja todas as letras maiúsculas, todas
minúsculas ou todas as palavras com a primeira letra maiúscula.
80
Observe que foi utilizada uma biblioteca Python que realiza o trabalho de
remoção de acentos (unidecode) em conjunto com a funcionalidade do Spark de criar
e aplicar no DataFrame, funções definidas pelo usuário. Isso será tratado em mais
detalhes no próximo capítulo.
Datas
81
• dayofweek() / dayofmonth() / dayofyear(): retorna o dia
relativo à semana, ao mês e ao ano, respectivamente.
‒ year: "{ano}-01-01".
‒ month: "{ano}-{mes}-01".
82
Figura 58 – Exemplo com Datas.
Arrays
83
• explode(): retorna uma nova linha para cada elemento no array.
84
• array_remove(): remove todos os elementos do array que são iguais ao
valor especificado.
85
Valores Nulos
As principais funcionalidades no Spark para lidar com nulos diz respeito a como
preencher ou remover esses valores, a nível de colunas ou do DataFrame inteiro. Os
métodos a nível de DataFrame são acessados com o atributo df.na, e são:
• drop(): retira do DataFrame as linhas com nulos, com base no que foi
passado para o argumento how.
‒ any (default): retira todas as linhas com pelo menos um valor nulo nas
colunas.
‒ all: somente retira as linhas com todos os valores nulos nas colunas.
86
Figura 60 – Tratando nulos no DataFrame.
87
Figura 61 – Tratando nulos a nível de coluna.
88
Capítulo 4. Manipulando dados com Spark: parte 2
Agrupamento e Agregação
89
• sumDistinct(): retorna a soma dos valores distintos da coluna;
Vale destacar que além de ser uma função, o count() também é chamado de
um método do Spark DataFrame. Isso significa que, quando há necessidade de contar as
90
linhas, esse método pode ser usado diretamente, com maior praticidade para o usuário.
Exemplos:
91
• approxQuantile(): calcula um ou mais quantis das colunas numéricas
de um DataFrame;
Obs.: O Spark ignora os valores nulos para calcular as agregações, com exceção
da função count().
• agg()
• count()
• sum()
• avg() / mean()
• max()
• min()
92
Figura 63 – Agrupamentos no Spark.
Pivotação
Além daqueles mostrados acima, há ainda o método pivot(), que pode ser
usado em conjunto com groupBy() para realizar uma operação de pivotação dos
dados. A pivotação nada mais é do que transformar cada um dos níveis de uma coluna
93
em colunas individuais, e realizando uma operação de agregação no processo para
preencher as novas células do DataFrame gerado. Assim, essa transformação só pode
ser utilizada após um agrupamento, e deve ser seguida obrigatoriamente por uma
chamada dos métodos agg() ou count(). Incrementando o exemplo anterior,
temos:
Figura 64 – Pivotação.
94
Observe no exemplo como foi calculado um valor da média de vendas para cada
combinação das colunas “grupo” e “anos”, sendo que a coluna “anos” foi transformada
em várias colunas iguais aos valores distintos que ela apresentava. Seria possível obter
resultados semelhantes em termos do resultado da agregação se fosse realizado um
agrupamento com as duas variáveis, do tipo
df.groupby(“grupo”,
“anos”).agg(round(mean(“vendas”)))
Window Functions
Tendo isso em vista, o Spark suporta três tipos de window functions: funções
de agregação, funções de ranqueamento e funções analíticas. Porém, antes de
introduzi-las, é necessário mostrar como especificar uma window function. Primeiro é
necessário importar o objeto Window dos módulos import
95
org.apache.spark.sql.expressions (Scala) ou pyspark.sql.window
(Python). A forma padrão de uma janela segue:
Window.partitionBy({columns})
.orderBy({columns})
.rowsBetween({lower}, {upper})
Em que cada um dos métodos é opcional para a definição da janela. Sobre esses
elementos:
96
‒ Window.unboundedFollowing: define que não há limites para as
linhas posteriores à linha atual, isto é, a função irá considerar todas as
linhas do grupo que ainda não passaram na janela. Deve ser usado no
segundo argumento (end).
Uma vez especificada a janela, para indicar que a função deve ser aplicada no
agrupamento deve ser utilizado o método over():
w = Window.partitionBy(“col1”, “col2”)
df.withColumn(“col_window”, function().over(w))
97
• dense_rank(): retorna o rank das linhas em uma sequência começando
em 1. No caso de empates, essa função não deixa “espaços” no
ranqueamento, isto é, se três indivíduos estivessem empatados em segundo
lugar, todas eles teriam rank 2 e o indivíduo em terceiro lugar teria rank 3;
98
Calculando Distintos
Podem existir situações em que é necessário ter uma coluna com uma
contagem de valores distintos. No entanto, a função countDistinct() não pode
ser utilizada como uma window function:
99
Figura 66 – Contando Distintos com Operações de Array.
Evitando Joins
100
Suponha uma necessidade durante o processamento de que seja preciso filtrar
as linhas de forma a só manter aqueles referentes ao ano mais recente, para cada um
dos grupos. O caminho mais natural seria utilizar um join:
Uma alternativa possível, sem usar joins, é utilizar window function da seguinte
forma:
101
Fica a critério do usuário definir qual a melhor maneira de ser utilizada em cada
pipeline, considerando aspectos como custo computacional, legibilidade e simplicidade
de compreensão do código.
Joins
• colunas_chave: colunas que vão ser utilizadas para fazer a junção das
tabelas. Pode ser especificada como
102
‒ outer/full/fullouter/full_outer : mantém as linhas dos
DataFrames da esquerda ou da direita, juntando as duas linhas quando
há igualdade de chaves e empilhando-as quando não há;
103
Figura 70 – Exemplos de Joins.
104
O último tipo de junção de tabelas que vale a pena mencionar é o produto
cartesiano, feito por meio do método crossJoin(). Um produto cartesiano junta as
tabelas sem utilizar chaves, mas em um esquema de “todos para todos”: é gerado um
novo DataFrame com todas as combinações possíveis de linhas entre os dois anteriores
DataFrames. Como é de se esperar, essa é uma operação bastante custosa e que tem
potencial grande de gerar erros de falta de memória. Para ilustrar: o produto cartesiano
de dois DataFrames de 10.000 linhas resultaria em um DataFrame com 10.000 x
10.000 = 100.000.000 de linhas, o que muito provavelmente iria requerer um poder
computacional bastante maior do que o alocado inicialmente. Exemplo:
105
Unions
106
Observe no exemplo acima como a ordem errada das colunas no momento da
união gera um comportamento não desejado sem lançar nenhum erro, já que havia
possibilidade de converter os valores numéricos para strings de forma a preservar um
schema aceitável.
107
Figura 74 – Padrão de Definição de UDFs.
108
Nos exemplos acima, é importante observar que Type e DataType apenas
denotam que o usuário deve passar o tipo correto de retorno das funções, e não são
tipos de dados reais.
109
Importante: As funções definidas pelo usuário em Python precisam ser
serializadas e requerem que um processo Python seja inicializado, o que pode
representar um grande gargalo na execução.
110
Figura 78 – Utilizando o método transform().
111
Figura 80 - Métodos Customizados.
112
Capítulo 5. Spark SQL
113
Dessa forma, os usuários podem desfrutar de todo o poder de computação do
Spark por meio dos RDDs, ao mesmo tempo em que utiliza de uma linguagem de
consultas conhecida, simples e consolidada.
Apesar dessa restrição, o Spark SQL é uma ferramenta bastante utilizada como
opção aos DataFrames e até mesmo em conjunto com eles, uma vez que eles são
plenamente intercambiáveis em termos de eficiência e modo de funcionamento
interno.
114
Em que warehouse_location é uma variável com o caminho onde os
databases e tabelas serão salvos.
Databases e catalog
SparkSession.catalog
115
Os databases do Spark são uma ferramenta para organizar tabelas. Eles podem
e devem ser vistos como algo muito próximo dos databases de servidores de bancos de
dados relacionais. O Spark utiliza por padrão um database chamado default, que serve
para criar tabelas, views e realizar consultas caso o usuário não tenha definido o seu
próprio. Um ponto importante é que essas estruturas persistem em diferentes sessões:
se o usuário mudar de database, todas as tabelas permanecerão no database anterior e
vão precisar ser consultadas de maneira diferente.
USE db2
SELECT * FROM db1.table
116
• DROP DATABASE IF EXISTS <nome_do_db>: deleta determinado
database dentre aqueles que foram definidos. Atenção: nunca delete o
database default do Spark.
Por fim, vale ressaltar que todas as operações definidas sobre os dados, seja no
contexto do SQL ou por meio dos DataFrames, são executadas no contexto de um
database.
117
Os comandos abaixo são usados para criar esses dois tipos de tabelas:
118
formato do arquivo na criação das unmanaged tables, pois isso otimiza bastante a
performance.
119
Figura 84 – Criando Views.
spark.catalog.dropGlobalTempView("nome_da_view")
spark.catalog.dropTempView("nome_da_view")
120
Figura 85 – Abrindo a Interface do Spark SQL.
121
propriedades aprendidas anteriormente. Inclusive, é possível utilizar a maior parte das
funções disponíveis dentro do módulo pyspark.sql.functions dentro do
contexto do Spark SQL e é possível checar quais estão disponíveis utilizando o
Catalog.
122
Capítulo 6. Otimizando Aplicações do Spark
• Arquivo spark-defaults.conf.
Caso uma mesma configuração seja especificada por mais de um dos métodos
acima, o Spark considera a seguinte ordem de precedência:
123
Abaixo uma classificação das possíveis configurações:
• Application properties.
• Runtime environment.
• Shuffle behavior.
• Spark UI.
• Memory management.
• Execution behavior.
• Networking.
• Scheduling.
• Dynamic allocation.
• Security.
• Encryption.
• Spark SQL.
• Spark streaming.
• SparkR.
spark-defaults.conf
124
que simplesmente indica o formato que o arquivo necessário e alguns exemplos de
configurações:
spark-submit
125
Figura 90 – Configurando com spark-submit.
SparkSession
126
Essas opções são definidas no momento da criação da sessão do Spark e como
dito anteriormente, essa sintaxe toma precedência em relação aos métodos anteriores.
É importante ressaltar que é possível alterar configurações de uma sessão já instanciada,
da seguinte forma:
No entanto, nem todas as configurações podem ser alteradas dessa forma. Para
checar se é possível que alguma delas seja alterada, o método
spark.conf.isModifiable("conf_name") retorna verdadeiro ou falso.
Enfim, pode-se destacar o método spark.conf.get("conf_name") para
verificar o valor atribuído a alguma configuração.
127
• spark.master: seleciona o modo de deploy da aplicação Spark. Será
tratado em mais detalhes no capítulo seguinte.
128
dynamic allocation, que permite que o programa requisite ou dispense recursos
conforme a necessidade das cargas de trabalho. Isso é bastante interessante em
situações em que as cargas são variáveis em termos de tamanho e imprevisíveis. As
principais configurações relacionadas a esse recurso são:
• spark.dynamicAllocation.executorIdleTimeout: configura
o tempo máximo de ociosidade de um executor até que o dynamic allocation
o derrube.
• spark.dynamicAllocation.initialExecutors: quantidade
inicial de executores na aplicação ao utilizar o dynamic allocation.
Por utilizar de Lazy Evaluation, o Spark muitas vezes fica com uma fila enorme
de operações para realizar no seu plano de execução. Assim, se for necessário consultar
um determinado DataFrame diversas vezes, pode ocorrer uma perda grande de
performance pelo fato de todas as operações terem de ser executadas novamente em
diferentes consultas.
129
que é chamado de persistência em memória. Essencialmente, elas fazem a mesma
coisa: salvam o DataFrame resultante das operações na memória, de forma que acessos
futuros a ele sejam muito mais rápidos. A única diferença é que a função persist()
oferece mais opções de armazenamento. As opções disponíveis são:
• cache():
‒ MEMORY_AND_DISK.
• persist():
‒ MEMORY_ONLY;
‒ MEMORY_ONLY_SER;
‒ MEMORY_AND_DISK;
‒ MEMORY_AND_DISK_SER;
‒ OFF_HEAP;
‒ DISK_ONLY.
Importante: as operações devem ser realizadas pelo menos uma vez depois de
o DataFrame ter sido "cacheado" para que ele seja de fato salvo na memória. Por causa
disso, o procedimento mais comum na hora de utilizar essas funções é chamar uma ação
logo em seguida:
130
Figura 93 – Ativando a persistência de dados na memória.
131
Na bucketização, os dados são salvos em n buckets, com base em uma coluna
passada pelo usuário. A vantagem de se utilizar essa estratégia de particionamento dos
dados é que operações que requerem um shuffle de dados são otimizadas, uma vez que
os dados com um mesmo identificador de bucket são organizados fisicamente em um
mesmo local, isto é, os dados são pré particionados, de forma que o Spark sabe
exatamente quais dados estão presentes em quais partições.
Obs.: a opção de bucketizar os dados só está disponível para dados salvos como
tabelas, utilizando o comando saveAsTable().
132
bucketização, essa funcionalidade pode ser acessada pela função partitionBy()
durante a utilização da classe DataFrameWriter, como pode ser visto abaixo:
Reparticionando DataFrames
133
los em paralelo. Dessa forma, podem acontecer problemas de falta de
memória.
134
Escolhendo o melhor tipo de Join
Como uma última maneira (dentre várias outras que ainda existem) de se
otimizar um processamento no Spark, é possível escolher diferentes tipos de algoritmo
para a realização de joins, a fim de escolher os mais adequados para cada situação.
Existem pelo menos três tipos de algoritmos para joins, sendo eles:
O BHJ é o algoritmo de join mais eficiente do Spark, mas só pode ser usado em
situações específicas. Ele requer que um dos DataFrames seja pequeno o suficiente para
caber na memória do driver e de cada um dos executores, pois a estratégia consiste em
enviar esses dados completos para cada um deles, de forma que só há necessidade de
realizar o shuffle de envio desses dados. O Spark costuma utilizar esse join
automaticamente com base em algumas configurações, como o
spark.sql.autoBroadcastJoinThreshold, que define o tamanho máximo
do menor DataFrame para que esse método seja escolhido, mas é sempre interessante
analisar cada situação e ter autonomia para indicar o seu uso.
135
Finalmente, o SHJ é um algoritmo que também usa shuffles, mas compensa
essa operação com o uso de um mapa de hash que exime a necessidade de ordenação
dos dados. A única condição é que um dos DataFrames seja significativamente menor
do que o outro, mas não tanto quanto o BHJ.
Utilizando a API, é possível indicar o uso dessas estratégias por meio do método
hint(), ou a função broadcast() para o caso do BHJ. Alguns exemplos:
136
Capítulo 7. Deploy de Aplicações do Spark
• Cluster mode.
• Client mode.
• Local mode.
Cluster Mode
137
Client Mode
Local Mode
138
A princípio, desenvolver uma aplicação do Spark não é muito diferente de
desenvolver outros programas do Python. A aplicação é composta por vários scripts .py,
sendo que um dele é executado como o principal, dependente ou não dos outros. Nesse
arquivo principal deve ser instanciada a SparkSession e é uma boa prática repassar
esse objeto inicial através da aplicação ao invés de instanciá-la mais de uma vez. Abaixo,
um exemplo desse arquivo, que utiliza os principais conceitos de classes e orientação ao
objeto:
139
Para enviar a aplicação para execução deve ser utilizado o comando spark-
submit. Com esse comando é possível indicar a forma de execução, configurar o Spark
(conforme visto no capítulo anterior) e passar o código Spark. O comando deve ser
chamado em um terminal de linha de comando, e tem a seguinte forma:
140
Obs.: ao especificar o deploy local no argumento --master, o número
colocado dentro do colchete indica o número de núcleos sendo usados na aplicação.
Colocar “*” indica que devem ser usados todos os núcleos disponíveis.
• Standalone.
• YARN.
• Apache Mesos.
141
$SPARK_HOME/sbin/start-master.sh
$SPARK_HOME/sbin/start-slave.sh <master-spark-URI>
A partir desse momento, o cluster está pronto para ser executado. A alternativa
para esse processo, que é bastante manual, é utilizar um arquivo de configuração que
pode inicializar o cluster por meio de scripts. O arquivo criado deve ser conf/slaves,
presente no diretório do Spark e ele deve conter os hostnames de todas as máquinas
em que é desejado inicializar workers do Spark, um por linha. Quando o cluster for
inicializado de fato, o master irá acessar cada uma dessas máquinas via SSH e requer
que o acesso seja feito por meio de uma chave (private key).
Depois de criar esse arquivo, é possível iniciar ou parar o cluster usando os shell
scripts presentes no diretório $SPARK_HOME/sbin:
142
• stop-master.sh: para a máquina master iniciada pelo script start-master.sh;
143
Figura 102 – Title Basics.
• Conversão de tipos.
144
ser encontradono repositório apresentado no primeiro capítulo. O script principal a ser
executado segue abaixo:
145
Figura 106 – Chamada ao spark-submit.
146
Figura 108 – DataFrame resultante da aplicação.
147
Capítulo 8. Spark na Nuvem
148
• Integrado: o Dataproc tem integração direta com outros serviços do Google
Cloud Platform, como BigQuery, Cloud Storage, Cloud Bigtable, Cloud Logging e
Cloud Monitoring.
Em relação ao seu uso com o Spark, podemos utilizar o Dataproc tanto como a
ferramenta responsável por processar os jobs enviados, bem como a infraestrutura por
trás do desenvolvimento exploratório feito por meio de Jupyter Notebooks.
149
• Criptografia de dados: por padrão, o Cloud Storage usa a criptografia no
servidor para criptografar seus dados. Também é possível usar opções
complementares de criptografia de dados, como chaves de criptografia
gerenciadas pelo cliente e chaves de criptografia fornecidas pelo cliente;
150
1. Criar uma conta do Google aqui.
151
Lembrando que é necessário cadastrar um perfil para pagamentos. Assim,
utilize um cartão de crédito com a possibilidade de pagamentos internacionais ou
procure outra forma de pagamento disponibilizada pelo Google. Apesar de habilitar
pagamentos, a ideia é utilizar o limite gratuito disponibilizado.
4. Depois de criar a conta, clique na aba projetos, crie um novo projeto e selecione-
o:
152
Depois de criar o projeto, é necessário configurar a conta de serviço. Para isso:
153
6. Crie uma conta de serviço:
154
Obs.: esse papel só está sendo utilizado para fins de demonstração, uma vez
que não é muito seguro utilizar para desenvolvimento um papel que dá acesso a todos
os componentes.
155
Nesse momento, será baixado um arquivo json com as credenciais de acesso
aos serviços do GCP. Guarde essas chaves com muito cuidado e atenção, pois são elas
que dão o acesso ao projeto criado na plataforma. Essas chaves também serão utilizadas
para conectar diretamente no GCS a partir do Spark.
Além disso, será mostrado como criar clusters com a possibilidade de uso dos
componentes web, como Jupyter Notebook, Jupyter Lab, Zeppelin, Anaconda etc., uma
vez que são ferramentas particularmente úteis para desenvolvimento e exploração
usando o Spark.
Após criar a conta e habilitar os serviços do GCP por meio da conta de serviços,
é necessário acessar o Dataproc a partir da aba lateral do console. Desse momento em
diante, basta seguir os passos abaixo:
156
Figura 116 – Ativando Dataproc.
157
configurações abaixo, especialmente o tipo single node, para não gerar gastos
excessivos nesse momento, e o componente Jupyter Notebook:
158
4) Na aba de configurar nós, é possível configurar as máquinas do cluster. Nesse
caso, será feita a configuração apenas do nó mestre, já que o cluster criado é
single node:
159
na criação dos cluster, como bibliotecas do Python. Caso não haja nenhuma
configuração, basta clicar em “criar cluster”:
6) O cluster levará cerca de 90seg para ser inicializado. Uma vez criado, o cluster
está disponível para uso. Para acessar os componentes habilitados, como o
Jupyter, basta clicar no nome do cluster e posteriormente em “Interfaces da
Web”. Uma vez nessa aba, basta clicar no nome do componente que se deseja
acessar:
160
Figura 121 – Acessando componentes.
7) Será aberta uma tela com duas pastas “Local Disk” e “GCS”. É recomendado
utilizar a pasta “GCS”, pois desta forma os notebooks serão salvos no Storage.
Uma vez dentro da pasta, é possível criar notebooks de diferentes kernels,
entre eles, pyspark:
161
Figura 122 – Acessando pasta.
Por fim, vale destacar que criar os notebooks na pasta “GCS” implica no
salvamento desses arquivos no bucket especificado pelo caminho abaixo:
162
Figura 124 – Caminho de salvamento.
163
Ao clicar nesse botão, será aberto o Google Cloud Shell, que disponibiliza a linha
de comando. Nesse terminal, será utilizado o gcloud, o SDK do Google para utilização
dos serviços cloud. O comando padrão para criar clusters no Dataproc é visto abaixo:
O que está na forma ${} são variáveis dependentes do projeto que devem ser
preenchidas pelo usuário. Também é possível passar diversas opções para replicar a
configuração feita por meio do console, como foi feito no caso a seguir:
164
Acessando dados no GCS
165
3) Escolha onde armazenar os dados. Nesse caso, deve ser utilizado a
mesma região que tem sido usada nas práticas com o Dataproc:
Depois disso, deve-se continuar com as configurações padrão nas outras etapas
e clicar em “Criar bucket”.
Com o bucket criado, é preciso subir dados para que sejam consultados. Para
fazer o upload de dados, siga os passos:
166
1) (Opcional) Crie uma pasta dentro do bucket para organizar os arquivos:
167
Assim, já é possível que os dados sejam acessados do GCS a partir de outros
dispositivos, como o Dataproc, ou então a partir do Spark executado localmente, como
será visto adiante.
Obs.: o objetivo desse guia não era ensinar a melhor forma ou as melhores
práticas de criação de buckests no GCP, mas simplesmente mostrar uma forma de
disponibilizar os dados para que sejam acessados da cloud com o Spark. É recomendado
procurar um material especializado para entender a melhor forma de usar essa
ferramenta.
gs://<nome_do_bucket>/<caminho_do_arquivo>
168
Dessa forma, para acessar os dados disponibilizados no GCS na seção anterior,
seria algo assim:
Para acessar de uma máquina local, além de usar o caminho no mesmo formato
do Dataproc, é preciso passar configurações extras para a SparkSession . Primeiro
é preciso entrar nesse site e baixar um conector do Google para a respectiva versão do
Hadoop do Spark instalado:
169
É preciso colocar esse conector em na pasta SPARK_HOME/jars:
.config("fs.gs.impl",
"com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem")
.config("fs.AbstractFileSystem.gs.impl",
"com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS")
.config("fs.gs.auth.service.account.enable", "true")
.config("fs.gs.auth.service.account.json.keyfile",
"path/to/keyfile")
170
Em que o “keyfile” é o arquivo .json que contém as chaves geradas durante o
processo de criação da conta de serviço do Google Cloud Platform. Agora, é possível
acessar os dados presentes em um bucket do GCS a partir de uma máquina local:
Por fim, para submeter uma aplicação do Spark usando o GCP, é necessário
instanciar um cluster do Dataproc e utilizar o Cloud Shell para enviar um job. Além disso,
é necessário fazer o upload dos scripts para um bucket do Storage. No exemplo a seguir
será utilizado os mesmos scripts do exemplo prático do capítulo anterior, alterando
somente os diretórios dos dados de forma a adequá-los a execução na nuvem. Os
diretórios devem estar especificados em termos dos buckets, seguindo a forma do
capítulo anterior para execução em notebooks do Dataproc. Por causa disso, o código
foi alterado, é necessário agora passar como argumento o modo de execução “local” ou
“cloud”. A versão mais recente está disponível no repositório apresentado no primeiro
capítulo.
171
Os passos para executar a aplicação Spark seguem abaixo:
2) Fazer o upload dos scripts para um bucket, o mesmo dos dados ou não:
172
Obs.: observe que os scripts são os mesmos utilizados no exemplo do capítulo
7, coma adição de um arquivo __init__.py ao diretório.
173
E que o DataFrame foi salvo com sucesso:
174
Encerrando o projeto no GCP
175
3) Por fim, clicar em “encerrar projeto”:
176
Capítulo 9. Conclusão
Espero que essa apostila tenha sido um bom guia prático para entender o
contexto de Big Data e suas ferramentas de processamento de dados massivos, em
especial o Apache Spark – talvez a mais importante no mercado atualmente. A área de
Big Data tem se expandido a cada ano e por isso, mais do que ensinar uma ferramenta,
espero que eu tenha passado a motivação do estudo desse campo e as principais ideias
por trás do desenvolvimento de soluções para esse desafio tão complexo e cada vez
mais comum de processar dados massivos e variados.
177
Referências
BOYD, D.; CRAWFORD, K. Critical Questions for Big Data. Information, Communication
& Society, p. 662-679, 2012.
DAMJI, J.S. et al. Learning Spark: Lightning-Fast Data Analytics. 2. ed. O'Reilly Media, Inc.
2020.
HADOOP HDFS Archicture Explanation and Assumptions. Data Flair, c2021. Disponível
em: <https://data-flair.training/blogs/hadoop-hdfs-architecture/>. Acesso em: 07 abr.
2022.
MANYKA, J.; et al. Big data: The next frontier for innovation, competition and
productivity, 2011. Disponível em:
<https://www.mckinsey.com/~/media/McKinsey/Business%20Functions/McKinsey%2
0Digital/Our%20Insights/Big%20data%20The%20next%20frontier%20for%20innovatio
n/MGI_big_data_exec_summary.pdf>. Acesso em: 07 abr. 2022.
178
O QUE é o Dataproc?. Google Cloud, 02 set. 2021. Disponível em:
<https://cloud.google.com/dataproc/docs/concepts/overview?hl=pt_br>. Acesso em:
07 abr. 2022.
SINHA, Shubham. Hadoop Ecosystem: Hadoop Tools for Crunching Big Data. Edureka!,
06 out. 2021. Disponível em: <https://www.edureka.co/blog/hadoop-ecosystem>.
Acesso em: 07 abr. 2022.
SINHA, Shubham. What is Hadoop? Introduction to Big Data & Hadoop. Edureka!, 06
out. 2021. Disponível em: <https://www.edureka.co/blog/what-is-hadoop/>. Acesso
em: 07 abr. 2022.
WHITE, T. Hadoop: The Definitive Guide. 3. ed. O'Reilly Media, Inc. 2012.
ZAHARIA, M.; CHAMBERS, B.. Spark: The Definitive Guide. 1. ed. O'Reilly Media, Inc.
2018.
179