DEV Community

renanpyd
renanpyd

Posted on

Como aprendi Apache Spark — Parte 11: Dimensionamento de recursos, paralelismo e custos

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
Enter fullscreen mode Exit fullscreen mode

O processamento demora 40 minutos.

Alguém decide dobrar os recursos:

8 Executors
4 cores por Executor
8 GB de heap por Executor
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Podemos representar:

Executor 1
Executor 2
Executor 3
Executor 4
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Exemplo:

--executor-cores 4
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Se temos:

4 Executors
×
4 cores
Enter fullscreen mode Exit fullscreen mode

obtemos:

16 cores alocados aos Executors
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

mas apenas:

4 partições
Enter fullscreen mode Exit fullscreen mode

em determinada Stage.

Conceitualmente:

Available slots: 16

Task 1  ████████
Task 2  ████████
Task 3  ████████
Task 4  ████████

Idle slots: 12
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

e:

100.000 partições
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Média aproximada:

10 GB por partição
Enter fullscreen mode Exit fullscreen mode

Cenário B

1.000 partições
Enter fullscreen mode Exit fullscreen mode

Média aproximada:

100 MB por partição
Enter fullscreen mode Exit fullscreen mode

Cenário C

100.000 partições
Enter fullscreen mode Exit fullscreen mode

Média aproximada:

1 MB por partição
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Muitas partições
       |
       v
Mais paralelismo potencial
       +
Maior overhead de Tasks
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

mas uma Stage com:

8 Tasks
Enter fullscreen mode Exit fullscreen mode

A concorrência máxima daquela Stage será limitada pelas oito Tasks.

Agora imagine uma Stage com:

1.000 Tasks
Enter fullscreen mode Exit fullscreen mode

Os 32 slots podem executar ondas sucessivas de Tasks.

Conceitualmente:

Wave 1  → 32 Tasks
Wave 2  → 32 Tasks
Wave 3  → 32 Tasks
...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Em um cenário idealizado, com Tasks de duração semelhante:

128 / 32 = 4 ondas
Enter fullscreen mode Exit fullscreen mode

Agora imagine:

128 Tasks
16 slots
Enter fullscreen mode Exit fullscreen mode

Teríamos aproximadamente:

8 ondas
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

e partes:

Serial / Coordination Work
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Ela define a heap JVM do Executor.

Exemplo:

--executor-memory 8g
Enter fullscreen mode Exit fullscreen mode

Mas, como vimos na Parte 7:

Executor Heap
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A quantidade de memória apropriada depende das operações executadas.

Por exemplo:

Filter
Enter fullscreen mode Exit fullscreen mode

pode apresentar necessidades diferentes de:

Large Aggregation
Enter fullscreen mode Exit fullscreen mode

ou:

Sort-Merge Join
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Total:

32 cores
64 GB heap
Enter fullscreen mode Exit fullscreen mode

Configuração B

8 Executors
4 cores cada
8 GB heap cada
Enter fullscreen mode Exit fullscreen mode

Total:

32 cores
64 GB heap
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
)
Enter fullscreen mode Exit fullscreen mode

O particionamento depende do contexto e das configurações aplicáveis.

Podemos especificar:

rdd = sc.parallelize(
    range(1_000_000),
    100
)
Enter fullscreen mode Exit fullscreen mode

Agora solicitamos 100 partições para esse RDD.

Mas é importante não confundir:

spark.default.parallelism
Enter fullscreen mode Exit fullscreen mode

com:

spark.sql.shuffle.partitions
Enter fullscreen mode Exit fullscreen mode

São configurações utilizadas em contextos diferentes.


15. spark.sql.shuffle.partitions

No Spark SQL, uma configuração relevante é:

spark.sql.shuffle.partitions
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
)
Enter fullscreen mode Exit fullscreen mode

Podemos ter:

Input Partitions
       |
       v
Partial Aggregation
       |
       v
Shuffle
       |
       v
Reduce Partitions
       |
       v
Final Aggregation
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

durante toda a aplicação.

Mesmo quando existe pouco trabalho.

Com Dynamic Allocation:

Low Demand
    |
    v
Fewer Executors

High Demand
    |
    v
More Executors
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Fase 2 — Shuffle e agregação

High Parallelism
Enter fullscreen mode Exit fullscreen mode

Fase 3 — Escrita final

Lower Parallelism
Enter fullscreen mode Exit fullscreen mode

Podemos representar:

Executors
   ^
12 |          ███████████
10 |          ███████████
 8 |          ███████████
 6 |    █████████████████
 4 |    █████████████████████
 2 |█████████████████████████
   +----------------------------> Tempo
       Fase 1  Fase 2  Fase 3
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Distribuídas assim:

99 Tasks → 30 segundos
1 Task   → 20 minutos
Enter fullscreen mode Exit fullscreen mode

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 █████████████████████████████
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Memory-bound

A aplicação sofre pressão relacionada à memória.

Sinais possíveis:

High GC
Spill
OOM
Large Aggregations
Enter fullscreen mode Exit fullscreen mode

I/O-bound

A aplicação espera por leitura, escrita ou movimentação de dados.

Exemplos:

Slow Storage
Heavy Shuffle
Network Bottlenecks
Remote Reads
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Observamos:

Stage 1: 2 minutos
Stage 2: 3 minutos
Stage 3: 24 minutos
Stage 4: 1 minuto
Enter fullscreen mode Exit fullscreen mode

A Stage 3 domina o tempo total.

Agora investigamos:

Stage 3
    |
    v
Shuffle-heavy Join
    |
    v
Large Spill
    |
    v
Uneven Task Duration
Enter fullscreen mode Exit fullscreen mode

Talvez o problema seja:

Data Skew
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

mas a aplicação utiliza efetivamente apenas:

8 cores
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

com:

2 Executors
2 cores cada
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Pipeline B

Processamento a cada 15 minutos
SLA: 5 minutos
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

é 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Depois:

spark.sql.shuffle.partitions=400
Enter fullscreen mode Exit fullscreen mode

Mantendo os demais fatores comparáveis.

Observamos:

Task Duration
Spill
GC
Total Stage Duration
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

e mal em:

Cluster B
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

sem conhecer o workload.

Também evitaria:

Minimum Resources
Enter fullscreen mode Exit fullscreen mode

sem verificar se o SLA será atendido.

E evitaria alterar simultaneamente:

Executor Count
Executor Cores
Executor Memory
Shuffle Partitions
Dynamic Allocation
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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)