Fundamentos de IA
O que realmente acontece quando você executa um job do Spark?

Introdução
À medida que os conjuntos de dados crescem, processá-los com eficiência em uma única máquina pode se tornar um desafio devido às limitações de capacidade da CPU, memória e taxa de transferência do armazenamento.
Apache Spark enfrenta esses desafios oferecendo um mecanismo de processamento distribuído de dados que divide as cargas de trabalho em partições e executa tarefas em paralelo em vários núcleos de CPU e máquinas.
No entanto, o Spark não se resume simplesmente a adicionar mais máquinas.
Entender como seus componentes funcionam em conjunto — drivers, executores, RDDs, DataFrames, partições, jobs, estágios e tarefas — é essencial para projetar pipelines de dados eficientes e confiáveis.
Neste artigo, vou explicar a arquitetura do Spark, seus mecanismos de execução e as principais considerações de desempenho usando um exemplo prático de agregação de vendas de comércio eletrônico.
1. Por que usar o Apache Spark: processamento paralelo de dados
Apache Spark é um mecanismo de computação distribuída projetado para processamento de dados em grande escala e engenharia de dados.
O Spark processa big data aproveitando o processamento paralelo de dados:

Figura A. Arquitetura de alto nível de um cluster distribuído do Apache Spark (cluster, driver e nós de trabalho), comparada à execução em um único nó (criada por Kuriko IWAI)
Em comparação com o processamento em um único nó (à direita, Figura A), o Spark divide o trabalho em tarefas usando vários nós e as executa nos recursos disponíveis em várias máquinas.
Neste contexto, distribuído não significa que o uso de várias máquinas seja obrigatório. O Spark pode ser executado no modo local em uma única máquina. Mesmo nesse caso, os dados podem ser particionados e processados em paralelo.
2. Arquitetura central do Apache Spark
O Spark é composto por vários componentes principais:
- Driver do Spark (driver): processo central de coordenação da aplicação Spark.
- Gerenciador do cluster: serviço externo responsável por alocar recursos de hardware, como núcleos de CPU e memória.
- Nós de trabalho (nó de trabalho): máquina física ou virtual, ou nó de computação, no cluster.
- Executores: processos de trabalho distribuídos iniciados nos nós de trabalho.
- Java Virtual Machine (JVM): ambiente de execução necessário para executar o bytecode do Spark (já que o Spark é escrito em Scala/Java).
Primeiro, o driver coordena uma aplicação Spark e a transforma em um grafo acíclico direcionado (DAG) de tarefas de execução física, que são então agendadas nos executores.
Em seguida, o gerenciador do cluster aloca recursos de acordo com o sistema de gerenciamento de recursos da implantação.
Os nós de trabalho fornecem recursos computacionais aos executores, que executam tarefas e mantêm dados durante a execução delas.
Alguns ambientes gerenciados, como o Databricks, usam um executor por nó de trabalho, mas essa não é uma regra universal do Spark. Um nó de trabalho pode hospedar vários executores, dependendo da configuração do cluster.
2-1. Caso de uso de comércio eletrônico: estrutura dos dados de entrada
Vamos imaginar uma pequena empresa de comércio eletrônico.
Ela tem 10.000 registros de vendas, cada um com uma unidade de manutenção de estoque (SKU) e a quantidade vendida. Suponha que haja três SKUs possíveis: A, B, e C.
| SKU | Registros | Quantidade | Soma esperada |
|---|---|---|---|
| A | 4,000 | 2 | 8,000 |
| B | 3,500 | 3 | 10,500 |
| C | 2,300 | 4 | 9,200 |
| Inválidos / devoluções | 200 | -1 | -200 |
| Total geral | 10.000 |
Tabela 1. Exemplo da distribuição dos registros de vendas de comércio eletrônico e das agregações esperadas.
Nosso objetivo é calcular o total de unidades válidas vendidas por SKU:
- Filtrar os registros cujo
qty <= 0, em seguida - Agregar as amostras por SKU.
No conjunto de dados completo usado neste exemplo, suponha que 9.800 dos 10.000 registros permaneçam após a filtragem. Os totais finais esperados do Spark são:
[("A", 8000), ("B", 10500), ("C", 9200)]
2-2. Cluster Spark
Vou definir um cluster simplificado para nosso caso de uso.
Nós de trabalho
Vou configurar 3 nós de trabalho, cada um com uma instância de 8 vCPU e 32 GiB de RAM:

Figura B. Alocação de recursos entre os nós de trabalho (criada por Kuriko IWAI)
No total, há 24 vCPUs disponíveis no sistema.
Executores do Spark e alocação de CPU/memória
Digamos que eu reserve:
- 7 executores.
- 2 núcleos de vCPU para cada executor.
- 3 GiB de memória heap da JVM para cada executor.
A aplicação Spark solicita ao gerenciador do cluster que aloque recursos para esses 7 executores.
Em seguida, o gerenciador do cluster decide como alocar CPUs e memória com base no uso de recursos de cada worker.
Por exemplo, ele decide criar 14 slots de tarefa — 2 núcleos de vCPU por executor × 7 executores:

Figura C-1. Núcleos de vCPU, memória e alocação de executores entre workers, com desequilíbrio de recursos no Worker A
Observe que os 3 GiB de memória alocados ao executor não correspondem à memória total dele, pois é necessário reservar memória adicional para os workers e outras execuções.
Supondo que uma única tarefa exija 1 vCPU (spark.task.cpus = 1), cada worker aloca suas vCPUs:

Figura C-2. Alocação paralela de vCPUs entre workers
Isso evidencia dois pontos importantes:
- Cada worker tem uma taxa de uso da CPU própria (na Figura C-2, o uso do Worker A é de 75%, enquanto o dos Workers B e C permanece em 50%).
- Embora o sistema tenha 24 núcleos de vCPU (8 vCPUs por worker × 3 workers), só é possível usar 14 núcleos de vCPU; os 10 restantes ficam ociosos para essas tarefas.
2-3. A Java Virtual Machine (JVM) por trás do Spark
O Apache Spark é baseado na máquina virtual Java (JVM).
A JVM é um ambiente de execução necessário para executar o bytecode do Spark.
Como o Spark é escrito em Scala/Java:
- Ao usar Scala ou Java na aplicação: o código é executado de forma nativa dentro da JVM.
- Ao usar PySpark (Python): um processo Python executa o código, mas se comunica com um driver baseado na JVM por meio de uma ponte, como o Py4J. Os executores Spark nos nós workers continuam sendo executados dentro de uma JVM, que gerencia os workers Python responsáveis pela execução do código Python e das funções definidas pelo usuário em Python (UDFs do Python).
Em ambos os casos, a JVM gerencia as threads de execução, a alocação de memória e a coleta de lixo do driver e dos executores.
JVMs do driver e dos executores em uma aplicação PySpark
Em uma aplicação PySpark, o driver Python se comunica com o driver Spark em execução na JVM;
- Driver Python: local onde o código PySpark da aplicação é escrito e executado.
- JVM do driver: coordena a execução e o agendamento do Spark.
- JVM do executor: executa as tarefas do Spark e gerencia os recursos de execução. Processos workers Python são usados quando as tarefas exigem execução em Python.
- JVM do cliente (processo do cliente ou máquina de submissão): JVM em execução na máquina local ou em um nó de borda/gateway.
Como mostra a figura, a JVM do cliente se conecta primeiro à JVM do driver, que, em seguida, coordena as JVMs dos executores:

Figura D. Relação geral entre as JVMs do cliente, do driver e dos executores
A partir daí, a JVM do driver hospeda o driver do Spark para gerenciar a aplicação, enquanto as JVMs dos executores hospedam os executores individuais responsáveis por executar essas tarefas.
Cada driver da JVM tem uma heap da JVM para processar dados, armazenar dados em cache e gerenciar objetos do usuário com eficiência.
2-4. Análise aprofundada dos Resilient Distributed Datasets (RDDs)
Um conjunto de dados distribuído resiliente (RDD) é uma coleção imutável e particionada de elementos que o Spark pode processar em paralelo.
Os RDDs acompanham as dependências entre transformações, permitindo que o Spark recalcule partições perdidas em muitos cenários de falha.
O nome resume três propriedades:
- Resiliente: o Spark registra a linhagem — qual RDD depende de qual RDD anterior — e muitas vezes consegue reconstruir partições ausentes, em vez de manter réplicas de todos os resultados intermediários.
- Distribuído: o conjunto de dados é representado por partições que o Spark pode processar em diferentes executores. O uso de várias máquinas físicas não é obrigatório; o modo local também oferece suporte a RDDs.
- Conjunto de dados: um RDD pode conter pares de chave e valor, números inteiros, strings, tuplas e outros elementos compatíveis — não apenas linhas relacionais. As APIs de RDD incluem operações como
map,filter,reduceByKey,join, eaggregateByKey.
Um RDD é uma abstração lógica de coleção e computação, não uma declaração de que determinadas partições sejam atribuídas permanentemente a determinados workers.
Partições
Uma partição é um bloco lógico de dados e normalmente corresponde a uma tarefa dentro de um estágio.
Digamos que criamos 100 partições (essas 100 partições iniciais são uma escolha didática explícita).
Com 10.000 elementos de origem, isso resulta em aproximadamente 100 elementos por partição se a entrada for distribuída de maneira mais ou menos uniforme:

Figura E. Alocação paralela de partições entre 100 divisões iniciais dos dados
3. API RDD versus API DataFrame: otimização interna com Catalyst
O exemplo original com RDD ajuda a entender a linhagem, as partições e o embaralhamento.
Em processos ETL estruturados típicos, porém, a API DataFrame é o ponto de partida preferencial, pois oferece colunas, informações de esquema, expressões semelhantes a SQL e operações visíveis para o otimizador.
O diagrama abaixo ilustra essa relação:

Figura F. Relação entre os conceitos de grafos do Spark: linhagem de RDD, DataFrame e DAG de execução do Spark
Assim como o RDD tem uma linhagem que define as transformações, o plano de consulta do DataFrame representa sua consulta estruturada.
O Spark constrói o trabalho de execução com base nas dependências e nos operadores físicos.
A execução de um DataFrame pode usar mecanismos baseados em RDD internamente, mas isso não significa que os usuários precisem criar manualmente a linhagem de RDD ao escrever código para DataFrame.
3-1. Plano de consulta do DataFrame
Um plano de consulta do DataFrame é a sequência de etapas lógicas e físicas gerada por um mecanismo de execução (caixa roxa na Figura L).
Uma consulta do DataFrame passa por várias representações:
- Plano lógico: descrição das operações solicitadas (leitura, filtragem, agrupamento, agregação).
- Plano lógico analisado e otimizado: o Spark resolve as referências e aplica as otimizações lógicas pertinentes, como a poda de colunas ou o envio de predicados para as etapas anteriores.
- Plano físico: o Spark escolhe operadores executáveis, por exemplo, leituras, agregações parciais e finais, trocas de dados, junções e ordenações.
- Execução: o plano físico é executado usando os mecanismos de execução do Spark e, em última instância, envolve tarefas nos executores.
No nosso caso de uso, o plano físico pode incluir as seguintes etapas:
- Agregação parcial por hash,
- Troca de dados por SKU; e
- Agregação final por hash.
3-2. Comparação de ações: API RDD versus API DataFrame
Vamos comparar as APIs RDD e DataFrame usando como exemplo a agregação por SKU.
API RDD
Os elementos são (sku, qty) pares. Nossa lógica de agregação em Python é explícita, e o Spark segue as dependências do RDD:
result_rdd = (
raw_rdd
.filter(lambda row: row[1] > 0)
.reduceByKey(lambda a, b: a + b, numPartitions=4)
)
rdd_results = result_rdd.collect()
# Example:
# [('A', 8000), ('B', 10500), ('C', 9200)]
API DataFrame
Suponha que sales_df tenha as colunas sku (texto) e qty (numérica):
from pyspark.sql import functions as F
result_df = (
sales_df
.filter(F.col("qty") > 0)
.groupBy("sku")
.agg(F.sum("qty").alias("total_qty"))
)
df_results = result_df.collect()
# Example:
# [Row(sku='A', total_qty=8000),
# Row(sku='B', total_qty=10500),
# Row(sku='C', total_qty=9200)]
Aqui, os dois exemplos resolvem o mesmo problema de negócios:
Filtrar registros de vendas inválidos e agregar as quantidades válidas por SKU.
E, em ambos os casos, collect() aciona a execução do Spark e retorna todos os registros resultantes ao Driver.
A principal diferença está na representação do resultado:
- API RDD: retorna elementos Python, como
(sku, total_qty)tuplas. - API DataFrame: retorna objetos
Rowestruturados com campos nomeados, comoskuetotal_qty.
Veja uma comparação geral entre as APIs RDD e DataFrame:
| API RDD | API DataFrame / SQL | |
|---|---|---|
| Representação dos dados | Elementos genéricos, incluindo objetos arbitrários compatíveis | Linhas com colunas nomeadas e esquema |
| Estilo de programação | Transformações de baixo nível | Transformações relacionais declarativas |
| Agregações | Sim: reduceByKey, aggregateByKey, etc. |
Sim: groupBy, agg, SQL |
| Otimização de consultas | Menos informações semânticas relacionais expostas | Otimização do Catalyst e da execução física |
| Uso típico | Processamento especializado de baixo nível / ensino dos mecanismos de execução | ETL estruturado, análise de dados, engenharia de atributos |
| Execução | Tarefas, estágios e jobs do Spark | Tarefas, estágios e jobs do Spark |
Tabela 2. API RDD vs. API DataFrame — comparação de recursos e abstrações: (criado por Kuriko IWAI)
4. Conclusão
O objetivo do ajuste fino do Spark não é maximizar, de forma independente, o número de partições ou o uso da CPU.
O objetivo é reduzir o custo e a latência da execução de ponta a ponta sem causar instabilidade.
Neste artigo, aprendemos as relações fundamentais do sistema:
- Recursos: Os workers hospedam executores; os executores executam tarefas; a CPU e a memória configuradas limitam a concorrência.
- Programação com RDDs: As transformações produzem definições imutáveis de RDDs e a linhagem; as ações iniciam a computação.
- Programação com DataFrames: As transformações produzem operações de consulta lógica; a otimização escolhe operadores físicos executáveis; as ações iniciam a computação.
- Execução: O Spark organiza o trabalho em jobs, estágios e tarefas, e as tarefas processam partições nos executores. Os embaralhamentos e outras dependências criam limites de coordenação entre os estágios.
Entender como o Spark executa cargas de trabalho distribuídas é apenas o ponto de partida.
Você pode aprender um fluxo de computação em sete etapas usando a API RDD.












