Nota de atualização (2026): este artigo conecta os fundamentos da minha jornada de aprendizado do Apache Spark aos mecanismos modernos de dimensionamento e gerenciamento de recursos. Configurações, valores padrão e recursos como Dynamic Allocation e Adaptive Query Execution evoluíram ao longo das versões. Os exemplos são ilustrativos e devem ser validados na versão e plataforma utilizadas.
Nos artigos anteriores, construímos uma visão da arquitetura do Apache Spark.
Entendemos o papel do Driver, dos Executors, das partições, das Tasks, do shuffle e do gerenciamento de memória.
Na Parte 10, vimos como aplicações Spark são submetidas e executadas em diferentes ambientes, incluindo Standalone, YARN e Kubernetes.
Agora surge uma pergunta prática:
Quantos Executors uma aplicação Spark realmente precisa?
E outras igualmente importantes:
- Quantos cores devemos atribuir a cada Executor?
- Quanta memória é necessária?
- Quantas partições devemos utilizar?
- Quando aumentar recursos melhora a performance?
- Quando estamos apenas aumentando custos?
- Como funciona Dynamic Allocation?
- Como identificar um cluster subutilizado?
Essas perguntas fazem parte de uma disciplina maior: engenharia de performance e capacidade.
O objetivo não é encontrar uma configuração mágica.
É construir um processo de dimensionamento baseado em comportamento observado, métricas e custo.
1. O problema de dimensionar recursos sem medir
Imagine um pipeline Spark que processa 500 GB diariamente.
A primeira configuração utiliza:
4 Executors
4 cores por Executor
8 GB de heap por Executor
O processamento demora 40 minutos.
Alguém decide dobrar os recursos:
8 Executors
4 cores por Executor
8 GB de heap por Executor
Agora o processamento demora 32 minutos.
Houve uma melhora.
Mas também aumentamos a quantidade de recursos alocados.
A pergunta correta não é apenas:
O Job ficou mais rápido?
Precisamos perguntar:
Quanto custou cada execução antes e depois?
E:
O ganho de desempenho justifica o consumo adicional?
É aqui que performance e FinOps começam a se encontrar.
2. O modelo básico de recursos do Spark
Uma aplicação Spark utiliza diferentes tipos de recursos.
Podemos representar:
SPARK APPLICATION
|
v
DRIVER
|
v
EXECUTOR POOL
|
+-----------+-----------+
| | |
v v v
Executor 1 Executor 2 Executor 3
| | |
v v v
CPU/RAM CPU/RAM CPU/RAM
| | |
v v v
Tasks Tasks Tasks
O Driver coordena a aplicação.
Os Executors disponibilizam recursos para executar Tasks.
O paralelismo efetivo depende tanto dos recursos quanto do trabalho disponível.
Isso nos leva a uma distinção essencial:
Recursos disponíveis não são necessariamente recursos utilizados.
3. Executor Instances
Uma configuração importante é:
spark.executor.instances
Ela define a quantidade inicial ou desejada de Executors em cenários de alocação estática, conforme o Cluster Manager e as demais configurações.
Exemplo:
--conf spark.executor.instances=4
Podemos representar:
Executor 1
Executor 2
Executor 3
Executor 4
Mas quatro Executors não significam automaticamente quatro Tasks simultâneas.
Precisamos considerar a quantidade de cores atribuída a cada Executor e os recursos solicitados por Task.
4. Executor Cores
Outra configuração:
spark.executor.cores
Exemplo:
--executor-cores 4
Em uma configuração comum, na qual cada Task utiliza um core, um Executor com quatro cores pode executar até quatro Tasks simultaneamente.
Conceitualmente:
Executor
|
+-- Core 1 → Task A
+-- Core 2 → Task B
+-- Core 3 → Task C
+-- Core 4 → Task D
Se temos:
4 Executors
×
4 cores
obtemos:
16 cores alocados aos Executors
Em condições apropriadas, isso permite até aproximadamente 16 Tasks simultâneas.
Entretanto, a concorrência real pode ser limitada por outros fatores, como recursos por Task, disponibilidade de partições, scheduling e características do ambiente.
5. O que acontece quando temos poucas partições?
Imagine:
16 cores disponíveis
mas apenas:
4 partições
em determinada Stage.
Conceitualmente:
Available slots: 16
Task 1 ████████
Task 2 ████████
Task 3 ████████
Task 4 ████████
Idle slots: 12
Não importa que tenhamos 16 cores disponíveis.
Se aquela Stage possui apenas quatro Tasks executáveis, não conseguiremos ocupar todos os slots com esse trabalho.
Esse é um ponto central:
O paralelismo de uma Stage é limitado, entre outros fatores, pela quantidade de Tasks disponíveis.
Portanto, aumentar Executors sem considerar partições pode produzir desperdício.
6. E quando temos partições demais?
Agora imagine:
16 cores disponíveis
e:
100.000 partições
Ter muitas partições não significa necessariamente melhor performance.
Cada Task possui custos associados a:
- Scheduling.
- Inicialização.
- Serialização.
- Comunicação.
- Gerenciamento.
- Leitura de dados.
- Processamento.
Se as Tasks forem muito pequenas, o overhead pode se tornar significativo.
Podemos encontrar:
Task execution: 20 ms
Scheduling overhead: relevante
Nesse cenário, estamos criando um volume enorme de unidades de trabalho para processar quantidades pequenas de dados.
O objetivo é encontrar uma granularidade apropriada.
7. Partições pequenas vs. partições grandes
Considere um dataset de 100 GB.
Cenário A
10 partições
Média aproximada:
10 GB por partição
Cenário B
1.000 partições
Média aproximada:
100 MB por partição
Cenário C
100.000 partições
Média aproximada:
1 MB por partição
Esses valores representam apenas divisões matemáticas simplificadas.
Na prática, o tamanho de uma partição depende da distribuição dos dados, do formato, da operação e da estratégia de particionamento.
Mesmo assim, o exemplo demonstra o trade-off:
Poucas partições
|
v
Menos paralelismo potencial
+
Tasks possivelmente grandes
Muitas partições
|
v
Mais paralelismo potencial
+
Maior overhead de Tasks
A configuração ideal depende do workload.
8. Paralelismo não é a mesma coisa que concorrência
Esses conceitos são frequentemente confundidos.
Paralelismo disponível representa a capacidade de executar trabalho simultaneamente.
Concorrência efetiva representa quantas Tasks estão realmente executando naquele momento.
Imagine:
32 slots disponíveis
mas uma Stage com:
8 Tasks
A concorrência máxima daquela Stage será limitada pelas oito Tasks.
Agora imagine uma Stage com:
1.000 Tasks
Os 32 slots podem executar ondas sucessivas de Tasks.
Conceitualmente:
Wave 1 → 32 Tasks
Wave 2 → 32 Tasks
Wave 3 → 32 Tasks
...
A duração total depende da quantidade de ondas, da duração das Tasks e de possíveis desequilíbrios.
9. O conceito de Task Waves
Uma forma útil de raciocinar é por ondas de execução.
Considere:
128 Tasks
32 slots
Em um cenário idealizado, com Tasks de duração semelhante:
128 / 32 = 4 ondas
Agora imagine:
128 Tasks
16 slots
Teríamos aproximadamente:
8 ondas
Isso sugere que aumentar slots pode reduzir o tempo de execução.
Mas somente enquanto existir trabalho paralelizável suficiente e os demais recursos não se tornarem gargalos.
Se a aplicação estiver limitada por rede, armazenamento ou skew, aumentar cores pode produzir ganhos muito menores.
10. Amdahl e os limites do paralelismo
Existe um princípio clássico da computação paralela: nem todo trabalho pode ser acelerado indefinidamente.
Uma aplicação pode possuir partes:
Parallelizable Work
e partes:
Serial / Coordination Work
A Lei de Amdahl descreve o limite teórico de aceleração quando apenas uma fração do trabalho pode ser paralelizada:
[
S(N)=\frac{1}{(1-P)+\frac{P}{N}}
]
Onde:
- (S(N)) é o speedup.
- (P) é a fração paralelizável.
- (N) é a quantidade de unidades de processamento.
Por exemplo, se 90% do trabalho for paralelizável, o limite teórico de speedup, mesmo com recursos infinitos, será 10 vezes.
Em Spark, o comportamento real também envolve rede, shuffle, armazenamento, overhead e distribuição de dados.
Portanto, aumentar Executors não garante aceleração linear.
11. Executor Memory
Outra configuração fundamental é:
spark.executor.memory
Ela define a heap JVM do Executor.
Exemplo:
--executor-memory 8g
Mas, como vimos na Parte 7:
Executor Heap
não representa necessariamente toda a memória utilizada pelo processo ou container.
Também precisamos considerar:
Memory Overhead
Off-Heap
Python Workers
Native Memory
A quantidade de memória apropriada depende das operações executadas.
Por exemplo:
Filter
pode apresentar necessidades diferentes de:
Large Aggregation
ou:
Sort-Merge Join
Não existe uma proporção universal entre memória e cores que funcione para todos os Jobs.
12. A relação entre cores e memória
Imagine um Executor com:
8 cores
8 GB de heap
Em um cenário comum de uma Task por core, várias Tasks podem disputar os recursos disponíveis.
Agora compare:
4 cores
8 GB de heap
A quantidade de Tasks concorrentes potencialmente diminui.
Isso pode reduzir a pressão de memória por Executor em determinados workloads.
Mas também pode reduzir a capacidade de processamento paralelo.
Portanto:
Executor Cores
+
Executor Memory
+
Task Memory Demand
precisam ser analisados conjuntamente.
Não devemos ajustar esses parâmetros isoladamente.
13. Executors maiores ou mais Executors?
Considere duas configurações ilustrativas.
Configuração A
4 Executors
8 cores cada
16 GB heap cada
Total:
32 cores
64 GB heap
Configuração B
8 Executors
4 cores cada
8 GB heap cada
Total:
32 cores
64 GB heap
Os recursos agregados são semelhantes.
Mas o comportamento pode ser diferente.
Entre os fatores envolvidos estão:
- Concorrência dentro de cada Executor.
- Pressão de memória.
- Garbage Collection.
- Distribuição de Tasks.
- Custos de processos.
- Overhead por Executor.
- Falhas e recuperação.
- Restrições dos nós.
Nenhuma das configurações é automaticamente superior.
O resultado precisa ser medido.
14. spark.default.parallelism
Uma configuração conhecida é:
spark.default.parallelism
Ela influencia o paralelismo padrão de determinadas operações RDD quando o número de partições não é informado explicitamente.
Por exemplo:
rdd = sc.parallelize(
range(1_000_000)
)
O particionamento depende do contexto e das configurações aplicáveis.
Podemos especificar:
rdd = sc.parallelize(
range(1_000_000),
100
)
Agora solicitamos 100 partições para esse RDD.
Mas é importante não confundir:
spark.default.parallelism
com:
spark.sql.shuffle.partitions
São configurações utilizadas em contextos diferentes.
15. spark.sql.shuffle.partitions
No Spark SQL, uma configuração relevante é:
spark.sql.shuffle.partitions
Ela define o número inicial de partições utilizado em determinadas operações de shuffle, como joins e agregações.
Exemplo:
--conf spark.sql.shuffle.partitions=200
Esse valor não significa que todos os DataFrames terão 200 partições.
Ele se aplica a situações específicas do planejamento de shuffle.
Além disso, mecanismos modernos como Adaptive Query Execution podem modificar o particionamento efetivo durante a execução.
Portanto, o valor configurado não deve ser interpretado como uma garantia de quantidade final de Tasks em todas as Stages.
16. O impacto do Shuffle no dimensionamento
Imagine:
resultado = (
vendas
.groupBy("customer_id")
.sum("valor")
)
Podemos ter:
Input Partitions
|
v
Partial Aggregation
|
v
Shuffle
|
v
Reduce Partitions
|
v
Final Aggregation
Se as partições de shuffle forem grandes demais, podemos observar:
- Spill.
- Pressão de memória.
- Tasks demoradas.
- GC.
- OOM.
Se forem pequenas demais, podemos observar:
- Muitas Tasks.
- Overhead.
- Fragmentação do trabalho.
- Baixa eficiência.
Dimensionar recursos exige compreender o plano de execução.
17. Adaptive Query Execution
O Adaptive Query Execution, conhecido como AQE, permite que Spark SQL adapte determinados aspectos do plano físico utilizando estatísticas obtidas durante a execução.
Entre os recursos possíveis estão:
- Coalescing de partições após shuffle.
- Tratamento de determinados casos de skew.
- Alteração de estratégias de join quando aplicável.
- Ajustes baseados em estatísticas de execução.
Conceitualmente:
Initial Physical Plan
|
v
Execution Statistics
|
v
Adaptive Optimization
|
v
Updated Physical Plan
Isso reduz a necessidade de acertar manualmente todos os detalhes antecipadamente.
Mas AQE não elimina a necessidade de engenharia de performance.
Ele não corrige automaticamente qualquer problema de distribuição ou dimensionamento.
18. Dynamic Allocation
Agora chegamos a um recurso importante para eficiência operacional.
Dynamic Allocation permite que Spark ajuste a quantidade de Executors durante a execução, conforme a demanda e as condições do ambiente.
Sem Dynamic Allocation, podemos imaginar:
Executors = 10
durante toda a aplicação.
Mesmo quando existe pouco trabalho.
Com Dynamic Allocation:
Low Demand
|
v
Fewer Executors
High Demand
|
v
More Executors
O objetivo é aproximar a quantidade de recursos alocados da necessidade real.
19. Como Dynamic Allocation funciona conceitualmente?
Imagine um pipeline com três fases.
Fase 1 — Leitura e preparação
Moderate Parallelism
Fase 2 — Shuffle e agregação
High Parallelism
Fase 3 — Escrita final
Lower Parallelism
Podemos representar:
Executors
^
12 | ███████████
10 | ███████████
8 | ███████████
6 | █████████████████
4 | █████████████████████
2 |█████████████████████████
+----------------------------> Tempo
Fase 1 Fase 2 Fase 3
Esse é apenas um desenho conceitual.
Na prática, o comportamento depende de políticas de alocação, backlog de Tasks, timeouts, limites e suporte da infraestrutura.
20. Configurando Dynamic Allocation
Um exemplo ilustrativo:
spark-submit \
--master yarn \
--deploy-mode cluster \
--conf spark.dynamicAllocation.enabled=true \
--conf spark.dynamicAllocation.minExecutors=2 \
--conf spark.dynamicAllocation.initialExecutors=4 \
--conf spark.dynamicAllocation.maxExecutors=12 \
app.py
Nesse exemplo, estabelecemos limites de alocação.
Mas habilitar Dynamic Allocation não significa que qualquer cluster esteja automaticamente preparado.
O ambiente precisa suportar os mecanismos necessários para preservar ou recuperar dados de shuffle quando Executors são removidos, conforme a configuração e a versão utilizadas.
As estratégias disponíveis podem envolver shuffle tracking, external shuffle service ou outros mecanismos suportados.
É importante validar a documentação da versão e da plataforma.
21. Dynamic Allocation não significa custo mínimo automático
Imagine que a aplicação solicita mais Executors.
O Cluster Manager pode ou não conseguir disponibilizá-los imediatamente.
Além disso, a infraestrutura pode cobrar por recursos provisionados, não apenas por Executors efetivamente ocupados.
Por exemplo:
Spark Executors
|
v
Kubernetes Pods
|
v
Cluster Nodes
|
v
Cloud Billing
Reduzir Executors não significa necessariamente reduzir instantaneamente a quantidade de máquinas cobradas.
Isso depende da arquitetura de autoscaling e do modelo de cobrança.
Portanto:
Dynamic Allocation e autoscaling da infraestrutura são mecanismos relacionados, mas diferentes.
22. Dynamic Allocation vs. Cluster Autoscaling
Essa distinção merece atenção.
Dynamic Allocation
Ajusta a quantidade de Executors solicitados pela aplicação Spark.
Cluster Autoscaling
Ajusta a capacidade computacional disponível no cluster, quando suportado e configurado.
Conceitualmente:
Spark Workload
|
v
Dynamic Allocation
|
v
Executor Demand
|
v
Cluster Autoscaler
|
v
Compute Nodes
Os dois mecanismos podem trabalhar juntos.
Mas também podem operar de maneira independente.
Uma aplicação pode solicitar Executors adicionais e permanecer aguardando porque não existem recursos disponíveis.
23. O papel do Data Skew
Agora imagine que temos:
100 Tasks
Distribuídas assim:
99 Tasks → 30 segundos
1 Task → 20 minutos
A Stage continuará aguardando a Task mais lenta.
Adicionar dezenas de Executors pode não resolver.
Porque o problema não é falta de slots.
É uma distribuição desigual do trabalho.
Conceitualmente:
Task 1 ███
Task 2 ███
Task 3 ███
...
Task 99 ███
Task 100 █████████████████████████████
Isso é um exemplo clássico de straggler.
Data skew pode transformar um cluster grande em uma aplicação lenta.
24. Quando aumentar Executors ajuda?
Aumentar Executors pode ajudar quando:
- Existem muitas Tasks aguardando recursos.
- O processamento é suficientemente paralelizável.
- O cluster possui capacidade disponível.
- A aplicação não está dominada por gargalos externos.
- As Tasks estão razoavelmente equilibradas.
Mas pode trazer pouco benefício quando:
- Existem poucas partições.
- O workload é dominado por uma única Task.
- O armazenamento é o gargalo.
- O shuffle está saturando a rede.
- Existe uma etapa serial relevante.
- O Driver está sobrecarregado.
Por isso, a decisão precisa ser baseada em métricas.
25. CPU-bound, Memory-bound e I/O-bound
Uma classificação útil para diagnóstico é identificar o principal recurso limitante.
CPU-bound
A aplicação passa grande parte do tempo realizando computação.
Exemplos possíveis:
Complex Transformations
Heavy Computation
Expensive Expressions
Memory-bound
A aplicação sofre pressão relacionada à memória.
Sinais possíveis:
High GC
Spill
OOM
Large Aggregations
I/O-bound
A aplicação espera por leitura, escrita ou movimentação de dados.
Exemplos:
Slow Storage
Heavy Shuffle
Network Bottlenecks
Remote Reads
Na prática, um Job pode apresentar diferentes gargalos em Stages diferentes.
Por isso, classificar a aplicação inteira com um único rótulo pode ser insuficiente.
26. O que medir na Spark UI?
Quando estou dimensionando recursos, quero observar:
Jobs e Stages
- Duração.
- Quantidade de Tasks.
- Stages críticas.
- Tempo de execução.
Tasks
- Distribuição de duração.
- Shuffle Read.
- Shuffle Write.
- Spill.
- GC Time.
- Input Size.
- Output Size.
Executors
- Tasks executadas.
- Falhas.
- Memória.
- GC.
- Dados persistidos.
- Comportamento agregado.
SQL
- Plano físico.
- Exchanges.
- Joins.
- Agregações.
- Métricas dos operadores.
A pergunta central é:
Onde o tempo está sendo gasto?
27. Um exemplo de diagnóstico
Imagine:
Job Duration: 30 minutos
Observamos:
Stage 1: 2 minutos
Stage 2: 3 minutos
Stage 3: 24 minutos
Stage 4: 1 minuto
A Stage 3 domina o tempo total.
Agora investigamos:
Stage 3
|
v
Shuffle-heavy Join
|
v
Large Spill
|
v
Uneven Task Duration
Talvez o problema seja:
Data Skew
Nesse caso, simplesmente dobrar o cluster pode não ser a melhor intervenção.
Precisamos avaliar distribuição de chaves, estratégia de join e particionamento.
28. Benchmarking de configurações
Em vez de escolher recursos por intuição, podemos realizar experimentos controlados.
Imagine três configurações.
| Configuração | Executors | Cores/Executor | Heap/Executor |
|---|---|---|---|
| A | 4 | 4 | 8 GB |
| B | 8 | 4 | 8 GB |
| C | 8 | 2 | 8 GB |
Executamos o mesmo workload com dados comparáveis.
Registramos:
- Tempo total.
- Duração por Stage.
- CPU.
- GC.
- Spill.
- Shuffle.
- Recursos alocados.
- Custo estimado.
O objetivo é descobrir qual configuração entrega o melhor equilíbrio para o requisito operacional.
29. Exemplo de análise de custo
Considere um exemplo hipotético.
| Configuração | Tempo | Custo/hora do conjunto de recursos | Custo por execução |
|---|---|---|---|
| A | 40 min | R$ 12 | R$ 8,00 |
| B | 30 min | R$ 24 | R$ 12,00 |
| C | 25 min | R$ 36 | R$ 15,00 |
Os valores são ilustrativos.
A configuração C é mais rápida.
Mas a configuração A possui menor custo direto por execução.
A decisão depende do SLA.
Se o pipeline precisa terminar em até 60 minutos, A pode ser suficiente.
Se precisa terminar em até 30 minutos, B ou C podem ser consideradas.
A escolha correta depende do objetivo de negócio.
30. Custo por execução vs. tempo de execução
Uma métrica útil é:
[
\text{Custo por execução}
\text{Custo por hora}
\times
\text{Tempo em horas}
]
Exemplo:
Custo/hora = R$ 24
Tempo = 30 minutos
Então:
[
24 \times 0{,}5 = R\$ 12
]
Esse cálculo simplificado não inclui necessariamente armazenamento, transferência, serviços auxiliares ou regras específicas de cobrança.
Mas ajuda a comparar configurações.
Em produção, devemos considerar o custo total relevante.
31. Custo por volume processado
Outra métrica interessante:
[
\text{Custo por TB}
\frac{\text{Custo da execução}}{\text{TB processados}}
]
Imagine:
Dados processados = 2 TB
Custo = R$ 20
Então:
[
\text{Custo por TB} = R\$ 10
]
Essa métrica ajuda a comparar eficiência ao longo do tempo.
Mas precisamos manter comparáveis:
- Tipo de processamento.
- Complexidade.
- Formato dos dados.
- Volume.
- SLA.
- Infraestrutura.
Processar 1 TB com um filtro simples não equivale a processar 1 TB com múltiplos joins e agregações complexas.
32. O problema da superprovisão
Superprovisão acontece quando alocamos recursos significativamente acima da necessidade real.
Imagine:
64 cores disponíveis
mas a aplicação utiliza efetivamente apenas:
8 cores
durante grande parte do tempo.
Podemos estar pagando por capacidade ociosa.
Entretanto, baixa utilização não deve ser analisada isoladamente.
Pode existir uma exigência de baixa latência ou necessidade de absorver picos.
Por isso, precisamos relacionar utilização com SLA e características do workload.
33. O problema do subdimensionamento
O extremo oposto também é ruim.
Imagine:
Dataset: 2 TB
com:
2 Executors
2 cores cada
Talvez o Job funcione.
Mas pode apresentar:
- Muitas ondas de Tasks.
- Tempo excessivo.
- Pressão de memória.
- Spill.
- Baixa capacidade de absorver variações.
Se o SLA exigir processamento rápido, os recursos podem ser insuficientes.
Portanto, o objetivo não é utilizar o menor cluster possível em qualquer circunstância.
É utilizar recursos suficientes para atender aos requisitos com eficiência.
34. Dimensionamento também depende do SLA
Imagine dois pipelines.
Pipeline A
Processamento diário
SLA: 4 horas
Pipeline B
Processamento a cada 15 minutos
SLA: 5 minutos
Mesmo que processem volumes semelhantes, os requisitos operacionais são diferentes.
O Pipeline B pode precisar de:
- Mais paralelismo.
- Inicialização mais rápida.
- Menor latência.
- Capacidade disponível.
- Estratégias diferentes de provisionamento.
Portanto:
Resource Sizing
|
v
Business SLA
é uma relação fundamental.
35. Um processo de tuning orientado por evidências
Minha abordagem preferida é iterativa.
1. Establish Baseline
|
v
2. Measure
|
v
3. Identify Bottleneck
|
v
4. Form Hypothesis
|
v
5. Change One Variable
|
v
6. Run Benchmark
|
v
7. Compare Results
|
v
8. Keep or Revert
Esse processo evita alterar vários parâmetros ao mesmo tempo e depois não saber qual mudança produziu o resultado.
Também facilita documentar decisões técnicas.
36. Exemplo de tuning controlado
Imagine que identificamos:
Shuffle partitions muito grandes
Hipótese:
Aumentar o número inicial de partições de shuffle pode reduzir o tamanho do trabalho por Task.
Testamos:
spark.sql.shuffle.partitions=200
Depois:
spark.sql.shuffle.partitions=400
Mantendo os demais fatores comparáveis.
Observamos:
Task Duration
Spill
GC
Total Stage Duration
Se a Stage melhorar sem criar overhead excessivo, a mudança pode ser válida.
Se piorar, revertimos.
A ideia não é que 400 seja melhor que 200.
A ideia é testar uma hipótese com métricas.
37. Evite copiar configurações de outros clusters
Uma configuração pode funcionar muito bem em:
Cluster A
e mal em:
Cluster B
Mesmo com o mesmo código.
As diferenças podem envolver:
- CPU.
- Memória.
- Rede.
- Disco.
- Armazenamento remoto.
- Versão do Spark.
- JVM.
- Cluster Manager.
- Dados.
- Concorrência com outras aplicações.
Por isso, parâmetros de tuning não devem ser tratados como receitas universais.
O que funciona em uma plataforma gerenciada pode não funcionar da mesma maneira em um cluster local ou em Kubernetes.
38. Um exemplo de configuração inicial
Considere um ambiente YARN compatível com alocação estática.
Um exemplo de submissão seria:
spark-submit \
--master yarn \
--deploy-mode cluster \
--driver-memory 4g \
--executor-memory 8g \
--executor-cores 4 \
--num-executors 4 \
--conf spark.dynamicAllocation.enabled=false \
--conf spark.sql.shuffle.partitions=200 \
app.py
Essa configuração representa apenas um ponto de partida ilustrativo.
Não significa que seja adequada para um dataset específico.
Em particular, precisamos validar:
- Memória total incluindo overhead.
- Capacidade do cluster.
- Quantidade de partições.
- Carga concorrente.
- Comportamento de shuffle.
- SLA.
39. O que eu não faria em produção
Eu evitaria começar com:
Maximum Executors
Maximum Memory
Maximum Cores
sem conhecer o workload.
Também evitaria:
Minimum Resources
sem verificar se o SLA será atendido.
E evitaria alterar simultaneamente:
Executor Count
Executor Cores
Executor Memory
Shuffle Partitions
Dynamic Allocation
porque isso dificulta identificar a causa dos ganhos ou regressões.
Ajustes devem ser acompanhados de observabilidade e experimentação.
40. O papel do FinOps em Engenharia de Dados
FinOps não é apenas reduzir a conta da cloud.
É relacionar consumo tecnológico com valor entregue.
Em Spark, isso significa entender:
Data Volume
|
v
Compute Resources
|
v
Execution Time
|
v
Infrastructure Cost
|
v
Business Outcome
Um pipeline que termina cinco minutos mais rápido pode justificar maior custo se isso evitar uma penalidade de SLA.
Outro pipeline pode economizar muito dinheiro simplesmente reduzindo recursos ociosos.
A decisão precisa considerar contexto.
41. Métricas que eu acompanharia
Para uma aplicação Spark em produção, considero útil acompanhar:
| Métrica | O que ajuda a avaliar |
|---|---|
| Duração total | Desempenho geral |
| Duração por Stage | Gargalos específicos |
| Quantidade de Tasks | Granularidade e paralelismo |
| Task Duration | Distribuição de trabalho |
| Shuffle Read/Write | Movimentação de dados |
| Spill | Pressão sobre memória e disco |
| GC Time | Pressão de memória JVM |
| Executor CPU | Utilização de processamento |
| Executor Memory | Consumo de recursos |
| Failed Tasks | Estabilidade |
| Custo por execução | Eficiência financeira |
| SLA Compliance | Resultado operacional |
O valor real aparece quando relacionamos essas métricas.
Por exemplo:
High Cost
+
Low CPU Utilization
+
Long Runtime
pode indicar um gargalo que não será resolvido simplesmente aumentando CPU.
42. O modelo mental completo
Depois de estudar arquitetura, memória, shuffle e implantação, podemos construir uma visão integrada:
WORKLOAD
|
v
DATA VOLUME
|
v
PARTITIONS
|
v
TASKS
|
v
PARALLELISM
|
v
EXECUTOR RESOURCES
|
+------------+------------+
| | |
v v v
CPU MEMORY NETWORK
| | |
+------------+------------+
|
v
EXECUTION TIME
|
v
RESOURCE COST
|
v
BUSINESS SLA
Esse modelo ajuda a compreender por que dimensionamento não pode ser reduzido a uma única configuração.
43. A principal lição deste capítulo
Se eu pudesse resumir dimensionamento de recursos no Spark em uma frase:
O melhor cluster não é necessariamente o maior ou o menor, mas aquele que atende ao SLA com o menor custo total aceitável e estabilidade adequada.
Para chegar lá, precisamos compreender:
- O workload.
- A distribuição dos dados.
- O paralelismo.
- O comportamento das Tasks.
- O consumo de memória.
- O shuffle.
- Os gargalos.
- A infraestrutura.
- O custo.
Dimensionamento é um processo contínuo de engenharia.
44. Como penso hoje
Quando alguém pergunta:
Quantos Executors devo utilizar?
Minha resposta começa com outras perguntas:
Qual volume de dados?
|
v
Quais transformações?
|
v
Quantas partições?
|
v
Qual é o plano físico?
|
v
Quanto shuffle?
|
v
Qual é o consumo de memória?
|
v
Qual é o SLA?
|
v
Qual é o custo atual?
|
v
Qual gargalo foi observado?
Só depois considero valores específicos de configuração.
Porque o objetivo não é decorar parâmetros.
É entender como o Spark transforma recursos em processamento.
Próximo artigo
Até aqui, estudamos como o Spark executa aplicações e utiliza recursos.
Agora vamos avançar para um tema que influencia diretamente a performance e a arquitetura de Data Lakes:
formatos de armazenamento e leitura de dados.
No próximo capítulo, vamos explorar:
- CSV.
- JSON.
- Parquet.
- ORC.
- Formatos row-based e columnar.
- Compressão.
- Schema.
- Predicate Pushdown.
- Column Pruning.
- Partition Pruning.
- Small Files Problem.
- Estratégias de leitura e escrita.
- Impacto no Spark SQL.
- Relação entre formatos, performance e custo.
A pergunta central será:
Por que o mesmo dataset pode ter comportamentos tão diferentes no Spark dependendo de como está armazenado?
Série: Como aprendi Apache Spark
← Parte 10: Client Mode vs. Cluster Mode — onde o Spark realmente executa
Próximo: Parte 12 — CSV, JSON, Parquet e ORC: como o formato dos dados afeta a performance no Spark
Top comments (0)