DEV Community

renanpyd
renanpyd

Posted on

Como aprendi Apache Spark — Parte 3: Partições, paralelismo e performance

No artigo anterior, desmontamos a arquitetura de uma aplicação Apache Spark e entendemos a relação entre:

Application
    ↓
Driver
    ↓
Cluster Manager
    ↓
Executors
    ↓
Tasks
Enter fullscreen mode Exit fullscreen mode

Mas ficou uma pergunta importante.

Se eu tenho vários Executors disponíveis, como o Spark consegue dividir os dados para processá-los em paralelo?

A resposta começa em um dos conceitos mais importantes — e muitas vezes subestimados — do Spark:

Partições

Entender partições mudou bastante a maneira como passei a enxergar processamento distribuído.

Porque um cluster pode ter dezenas de máquinas, centenas de cores e muita memória.

Nada disso garante que esses recursos serão utilizados eficientemente.

No fim das contas, existe uma relação fundamental:

Dados
  ↓
Partitions
  ↓
Tasks
  ↓
Parallelism
  ↓
Performance
Enter fullscreen mode Exit fullscreen mode

E é isso que vamos explorar.


1. O que é uma partição?

Um dataset distribuído não é tratado necessariamente como um único bloco gigantesco.

Spark divide logicamente os dados em partições.

Imagine um dataset:

Dataset
│
├── Partition 0
├── Partition 1
├── Partition 2
└── Partition 3
Enter fullscreen mode Exit fullscreen mode

Essas partições podem ser processadas paralelamente pelos Executors.

Por exemplo:

                    DATASET

        ┌──────────────┼──────────────┐
        │              │              │
        ▼              ▼              ▼

   Partition 0    Partition 1    Partition 2
        │              │              │
        ▼              ▼              ▼

   Executor A     Executor B     Executor C
Enter fullscreen mode Exit fullscreen mode

Isso nos leva a uma relação extremamente importante:

Em uma determinada Stage, cada partição normalmente será processada por uma Task.

Podemos pensar:

Partition 0 ───► Task 0
Partition 1 ───► Task 1
Partition 2 ───► Task 2
Partition 3 ───► Task 3
Enter fullscreen mode Exit fullscreen mode

Portanto:

Partitions
     ↓
   Tasks
     ↓
Parallelism
Enter fullscreen mode Exit fullscreen mode

Essa relação ajuda a explicar muita coisa sobre performance no Spark.


2. Partição não é a mesma coisa que arquivo

Esse é um ponto importante.

Imagine que temos:

/data/
├── vendas_01.parquet
├── vendas_02.parquet
├── vendas_03.parquet
└── vendas_04.parquet
Enter fullscreen mode Exit fullscreen mode

É tentador pensar:

1 arquivo = 1 partição Spark
Enter fullscreen mode Exit fullscreen mode

Mas essa relação não é necessariamente verdadeira.

A forma como Spark cria partições durante a leitura depende de fatores como:

  • formato dos dados;
  • tamanho dos arquivos;
  • fonte de dados;
  • configuração da aplicação;
  • capacidade de divisão da fonte;
  • operações realizadas posteriormente.

Além disso, novas operações podem alterar completamente o particionamento.

Por isso precisamos separar mentalmente:

ARMAZENAMENTO

Arquivos
   ↓
Blocos / objetos / arquivos físicos
Enter fullscreen mode Exit fullscreen mode

de:

EXECUÇÃO SPARK

Dataset
   ↓
Partitions
   ↓
Tasks
Enter fullscreen mode Exit fullscreen mode

Partições pertencem ao modelo de execução distribuída do Spark.


3. Descobrindo quantas partições existem

Com RDDs podemos verificar diretamente:

rdd = sc.parallelize(range(100))

print(rdd.getNumPartitions())
Enter fullscreen mode Exit fullscreen mode

Também podemos escolher uma quantidade inicial:

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

Agora temos, conceitualmente:

100 elementos
      │
      ▼
4 partitions
      │
      ├── P0
      ├── P1
      ├── P2
      └── P3
Enter fullscreen mode Exit fullscreen mode

Com DataFrames também podemos acessar o RDD subjacente para inspeção:

df.rdd.getNumPartitions()
Enter fullscreen mode Exit fullscreen mode

Isso é útil durante análise e troubleshooting.

Mas saber quantas partições existem é apenas o começo.

A pergunta realmente importante é:

Essas partições estão adequadas para o workload?


4. Partições determinam oportunidades de paralelismo

Imagine um cluster com:

4 Executors
4 cores por Executor
Enter fullscreen mode Exit fullscreen mode

Temos potencialmente:

4 × 4 = 16 cores
Enter fullscreen mode Exit fullscreen mode

Simplificando, podemos pensar em até 16 Tasks executando simultaneamente, dependendo das configurações e dos recursos disponíveis.

Agora imagine que nosso Stage possui apenas:

4 partitions
Enter fullscreen mode Exit fullscreen mode

Teremos aproximadamente:

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

Mesmo tendo capacidade para mais execução concorrente.

Visualmente:

Capacidade disponível

Core 01 → Task
Core 02 → Task
Core 03 → Task
Core 04 → Task

Core 05 → ocioso
Core 06 → ocioso
Core 07 → ocioso
Core 08 → ocioso
...
Core 16 → ocioso
Enter fullscreen mode Exit fullscreen mode

Temos um cluster relativamente grande, mas pouco paralelismo.

Agora considere:

64 partitions
Enter fullscreen mode Exit fullscreen mode

Podemos ter ondas sucessivas de Tasks:

16 Tasks
   ↓
16 Tasks
   ↓
16 Tasks
   ↓
16 Tasks
Enter fullscreen mode Exit fullscreen mode

Os recursos podem ser utilizados de maneira muito mais eficiente.

Mas isso não significa que:

Quanto mais partições, melhor.

Porque existe o outro extremo.


5. Partições demais também são um problema

Imagine um dataset relativamente pequeno dividido em:

100.000 partitions
Enter fullscreen mode Exit fullscreen mode

Agora Spark precisa administrar:

100.000 Tasks
Enter fullscreen mode Exit fullscreen mode

Mesmo que cada Task processe pouquíssimos dados.

Cada Task possui algum custo operacional.

Existe overhead relacionado a:

  • scheduling;
  • serialização;
  • comunicação;
  • inicialização;
  • acompanhamento;
  • coordenação;
  • metadados.

Podemos chegar a uma situação como:

Task
├── 20 ms processando dados
└── overhead operacional significativo
Enter fullscreen mode Exit fullscreen mode

Multiplique isso por dezenas ou centenas de milhares de Tasks.

O problema deixa de ser processamento.

Passa a ser coordenação.

Esse é um padrão comum em sistemas distribuídos:

Paralelismo tem custo.


6. Partições grandes demais

Agora vamos para o outro extremo.

Imagine:

Dataset: 1 TB
Partitions: 4
Enter fullscreen mode Exit fullscreen mode

Simplificando:

Partition 1 ≈ 250 GB
Partition 2 ≈ 250 GB
Partition 3 ≈ 250 GB
Partition 4 ≈ 250 GB
Enter fullscreen mode Exit fullscreen mode

Isso pode gerar vários problemas.

Cada Task terá uma quantidade enorme de dados para processar.

Podemos ter:

  • baixa utilização do cluster;
  • pressão de memória;
  • spills para disco;
  • garbage collection;
  • Tasks muito longas;
  • maior custo de recuperação em caso de falha.

Visualmente:

Cluster

Executor 1 → ████████████████████████
Executor 2 → ████████████████████████
Executor 3 → ████████████████████████
Executor 4 → ████████████████████████

Executor 5 → ocioso
Executor 6 → ocioso
Executor 7 → ocioso
Executor 8 → ocioso
Enter fullscreen mode Exit fullscreen mode

O cluster possui recursos.

Mas não existem Tasks suficientes para utilizá-los.


7. Partições pequenas demais

Agora imagine o inverso:

Dataset: 1 TB
Partitions: 1.000.000
Enter fullscreen mode Exit fullscreen mode

Teríamos uma quantidade gigantesca de Tasks.

Conceitualmente:

1.000.000 partitions
          ↓
1.000.000 tasks
          ↓
scheduling
          ↓
coordenação
          ↓
overhead
Enter fullscreen mode Exit fullscreen mode

Mesmo que o cluster consiga executar muitas Tasks paralelamente, gerenciar essa quantidade pode ser extremamente caro.

Portanto, procuramos equilíbrio.

Poucas partitions
       ↓
baixo paralelismo
Tasks muito grandes


Partitions demais
       ↓
overhead excessivo
Tasks muito pequenas


Quantidade adequada
       ↓
bom paralelismo
Tasks administráveis
Enter fullscreen mode Exit fullscreen mode

Não existe um número universal perfeito.

O tamanho adequado depende do workload.


8. O problema mais perigoso: partições desbalanceadas

Até agora assumimos algo parecido com:

P1 = 1 GB
P2 = 1 GB
P3 = 1 GB
P4 = 1 GB
Enter fullscreen mode Exit fullscreen mode

Mas dados reais raramente são tão bonitos.

Podemos encontrar:

P1 = 500 MB
P2 = 600 MB
P3 = 550 MB
P4 = 18 GB
Enter fullscreen mode Exit fullscreen mode

Agora imagine quatro Tasks:

Task 1 → 20 segundos
Task 2 → 23 segundos
Task 3 → 21 segundos
Task 4 → 8 minutos
Enter fullscreen mode Exit fullscreen mode

O Stage não termina quando as três primeiras Tasks acabam.

Ele precisa esperar a última.

Visualmente:

Task 1 ████ ✓
Task 2 █████ ✓
Task 3 ████ ✓
Task 4 ███████████████████████████████████████...
Enter fullscreen mode Exit fullscreen mode

Isso é um sinal clássico de desequilíbrio.

E uma das causas pode ser:

Data Skew


9. O que é Data Skew?

Imagine dados de vendas agrupados por estado.

Suponha:

SP → 60 milhões
RJ → 15 milhões
MG → 10 milhões
BA → 5 milhões
AC → 200 mil
RR → 100 mil
Enter fullscreen mode Exit fullscreen mode

Se uma operação distribuir dados utilizando estado como chave, algumas partições podem ficar muito maiores que outras.

Podemos terminar com:

Partition SP █████████████████████████████████
Partition RJ █████████
Partition MG ██████
Partition BA ███
Partition AC ▏
Partition RR ▏
Enter fullscreen mode Exit fullscreen mode

Os Executors responsáveis pelas partições pequenas terminam rapidamente.

Enquanto isso, uma Task continua trabalhando.

O cluster pode ficar assim:

Executor 1 → idle
Executor 2 → idle
Executor 3 → idle
Executor 4 → idle
Executor 5 → █████████████ trabalhando
Enter fullscreen mode Exit fullscreen mode

Esse fenômeno é importante porque mostra que:

Ter muitas partições não garante paralelismo eficiente.

Precisamos também de boa distribuição dos dados entre elas.


10. repartition()

Spark oferece mecanismos para alterar o particionamento.

Um deles é:

df = df.repartition(100)
Enter fullscreen mode Exit fullscreen mode

Conceitualmente:

ANTES

P1
P2
P3
P4

       repartition(100)

              ↓

DEPOIS

P1
P2
P3
...
P100
Enter fullscreen mode Exit fullscreen mode

Mas existe um detalhe fundamental.

Redistribuir dados entre partições pode exigir movimentação pela rede.

Ou seja:

repartition()
      ↓
   Shuffle
Enter fullscreen mode Exit fullscreen mode

Imagine:

Executor A                     Executor B

[A, A, B, C]                  [A, B, C, C]

             repartition

                   ↓

Executor A                     Executor B

[A, A, A]                     [B, B]
                               [C, C, C]
Enter fullscreen mode Exit fullscreen mode

Dados precisaram viajar entre executors.

E rede não é gratuita.

Por isso:

repartition() não deve ser usado como um botão mágico de performance.

Ele resolve alguns problemas criando outro custo: shuffle.


11. Repartition por coluna

Também podemos particionar logicamente usando uma ou mais colunas:

df = df.repartition(
    "customer_id"
)
Enter fullscreen mode Exit fullscreen mode

Ou especificar quantidade:

df = df.repartition(
    200,
    "customer_id"
)
Enter fullscreen mode Exit fullscreen mode

Isso pode ser útil quando operações posteriores dependem dessa chave.

Mas novamente precisamos pensar na distribuição.

Se customer_id estiver bem distribuído:

customer_id
     ↓
hash
     ↓
P1 P2 P3 P4 P5 ...
Enter fullscreen mode Exit fullscreen mode

podemos conseguir uma distribuição razoável.

Se a chave tiver forte skew, o resultado pode continuar ruim.

Particionar não corrige automaticamente uma distribuição ruim.


12. coalesce()

Outro método importante é:

df = df.coalesce(10)
Enter fullscreen mode Exit fullscreen mode

Normalmente utilizamos coalesce() quando queremos reduzir a quantidade de partições.

Por exemplo:

ANTES

P1 P2 P3 P4 P5 P6 P7 P8

        coalesce(4)

             ↓

DEPOIS

P1 P2 P3 P4
Enter fullscreen mode Exit fullscreen mode

A grande diferença conceitual é que coalesce() pode reduzir partições evitando uma redistribuição completa dos dados.

Compare:

repartition()
     ↓
redistribuição ampla
     ↓
shuffle
Enter fullscreen mode Exit fullscreen mode

com:

coalesce()
     ↓
combinação de partitions
     ↓
pode evitar full shuffle
Enter fullscreen mode Exit fullscreen mode

Isso pode tornar coalesce() mais barato quando o objetivo é simplesmente reduzir paralelismo.

Mas também existe uma consequência.


13. O perigo de usar coalesce(1)

Esse comando aparece bastante:

df.coalesce(1)
Enter fullscreen mode Exit fullscreen mode

Por quê?

Normalmente porque alguém quer:

"Salvar tudo em um único arquivo."

Funciona em determinados cenários.

Mas imagine:

100 partitions
       ↓
coalesce(1)
       ↓
1 partition
       ↓
1 Task
Enter fullscreen mode Exit fullscreen mode

Acabamos de destruir praticamente todo o paralelismo daquela etapa.

Imagine processar:

200 GB
Enter fullscreen mode Exit fullscreen mode

em uma única partição.

Podemos transformar um processamento distribuído em um enorme gargalo.

Por isso, quando encontro:

df.coalesce(1)
Enter fullscreen mode Exit fullscreen mode

em um pipeline de produção, minha primeira pergunta é:

Por que precisamos de exatamente um arquivo?

Às vezes existe uma justificativa.

Muitas vezes existe apenas uma dependência legada ou uma decisão que nunca foi questionada.


14. repartition() vs. coalesce()

Uma forma simples de lembrar:

                repartition()        coalesce()

Aumentar               ✓                 não é o uso típico

Reduzir                ✓                 ✓

Shuffle          normalmente sim     pode evitar full shuffle

Distribuição        melhor             pode ficar desigual

Custo              maior              geralmente menor
Enter fullscreen mode Exit fullscreen mode

Isso não significa:

coalesce = bom
repartition = ruim
Enter fullscreen mode Exit fullscreen mode

Significa:

Cada operação resolve um problema diferente.

Se preciso redistribuir os dados de forma mais uniforme, repartition() pode ser exatamente o que quero.

Se tenho muitas partições pequenas e apenas quero consolidá-las, coalesce() pode ser mais apropriado.


15. Partições e Shuffle

Agora começamos a conectar os capítulos.

Considere:

resultado = (
    df
    .groupBy("customer_id")
    .count()
)
Enter fullscreen mode Exit fullscreen mode

Para contar todos os registros do mesmo cliente, Spark precisa garantir que dados da mesma chave possam ser processados conjuntamente.

Imagine:

Executor A

customer 1
customer 2
customer 3


Executor B

customer 1
customer 4
customer 2
Enter fullscreen mode Exit fullscreen mode

Precisamos reorganizar:

ANTES

Executor A       Executor B

1                1
2                4
3                2


        ↓ SHUFFLE ↓


DEPOIS

Partition A
1
1

Partition B
2
2

Partition C
3

Partition D
4
Enter fullscreen mode Exit fullscreen mode

É nesse momento que rede, disco, memória e particionamento começam a interagir.

Por isso shuffle é uma das operações mais caras e importantes de entender no Spark.


16. Partições e arquivos pequenos

Particionamento também aparece em outro problema clássico de Data Engineering:

Small Files Problem

Imagine um pipeline que grava:

/year=2026/month=10/day=23/

part-000001.parquet
part-000002.parquet
part-000003.parquet
...
part-200000.parquet
Enter fullscreen mode Exit fullscreen mode

Temos centenas de milhares de arquivos minúsculos.

Isso pode prejudicar:

  • leitura;
  • listagem de arquivos;
  • planejamento;
  • metadados;
  • armazenamento;
  • engines de consulta;
  • Data Lakes.

Muitas vezes a quantidade de arquivos gerados está relacionada à quantidade de partições no momento da escrita.

Por exemplo:

df.write.parquet(
    "/data/output"
)
Enter fullscreen mode Exit fullscreen mode

As partições do DataFrame influenciam a quantidade de tarefas de escrita e, consequentemente, podem influenciar a quantidade de arquivos produzidos.

Isso conecta performance computacional com arquitetura de armazenamento.


17. Particionamento de execução ≠ particionamento de armazenamento

Aqui existe uma distinção importante.

Podemos ter:

df.repartition(100)
Enter fullscreen mode Exit fullscreen mode

Isso está relacionado ao particionamento do DataFrame durante a execução.

Mas também podemos escrever:

df.write \
  .partitionBy("ano", "mes") \
  .parquet("/data/vendas")
Enter fullscreen mode Exit fullscreen mode

Agora estamos falando de organização física/lógica dos dados no armazenamento:

/data/vendas/

ano=2025/
    mes=12/

ano=2026/
    mes=01/

ano=2026/
    mes=02/
Enter fullscreen mode Exit fullscreen mode

São conceitos relacionados, mas diferentes.

Uma forma útil de separar:

repartition()
     ↓
EXECUÇÃO


partitionBy()
     ↓
ARMAZENAMENTO
Enter fullscreen mode Exit fullscreen mode

Confundir os dois pode levar a decisões ruins de arquitetura.


18. E quanto ao tamanho ideal de uma partição?

Essa é uma pergunta clássica:

Quantos MB uma partição Spark deveria ter?

A resposta correta é:

depende.

Você encontrará recomendações práticas em diferentes contextos, frequentemente trabalhando com partições na ordem de dezenas ou centenas de megabytes.

Mas transformar um número em regra universal é perigoso.

Precisamos considerar:

  • tipo de operação;
  • volume de dados;
  • formato;
  • compressão;
  • memória dos Executors;
  • quantidade de cores;
  • número de Executors;
  • custo de serialização;
  • skew;
  • shuffle;
  • origem dos dados;
  • destino dos dados.

Em vez de decorar:

Partition = X MB
Enter fullscreen mode Exit fullscreen mode

prefiro pensar:

A Task termina rápido demais?
        ↓
Talvez partitions pequenas demais


A Task demora excessivamente?
        ↓
Talvez partition grande


Algumas Tasks demoram muito mais?
        ↓
Talvez skew


Muitos cores ociosos?
        ↓
Talvez poucas partitions


Scheduler sobrecarregado?
        ↓
Talvez partitions demais
Enter fullscreen mode Exit fullscreen mode

O Spark UI ajuda muito nessa análise.


19. Um exemplo prático

Imagine:

Dataset = 800 GB

Cluster:
10 Executors
4 cores por Executor
Enter fullscreen mode Exit fullscreen mode

Temos aproximadamente:

40 cores
Enter fullscreen mode Exit fullscreen mode

Agora suponha:

8 partitions
Enter fullscreen mode Exit fullscreen mode

Temos no máximo oito Tasks para aquela etapa.

Então:

40 cores disponíveis
 8 Tasks
Enter fullscreen mode Exit fullscreen mode

Grande parte do cluster ficará ociosa.

Agora imagine:

800 partitions
Enter fullscreen mode Exit fullscreen mode

Teríamos aproximadamente:

1 GB por partition
Enter fullscreen mode Exit fullscreen mode

Ainda pode ser grande dependendo da operação.

Se utilizarmos:

3200 partitions
Enter fullscreen mode Exit fullscreen mode

teríamos aproximadamente:

250 MB por partition
Enter fullscreen mode Exit fullscreen mode

Isso poderia ser um ponto inicial interessante para investigação.

Mas atenção:

não estou dizendo que 3200 é a configuração correta.

Estou mostrando como raciocinar.

A validação real vem das métricas:

Spark UI
   │
   ├── Task Duration
   ├── Input Size
   ├── Shuffle Read
   ├── Shuffle Write
   ├── Spill
   ├── GC Time
   └── Executor Utilization
Enter fullscreen mode Exit fullscreen mode

Engenharia de performance não deveria ser:

"Vi alguém usando 3200 partitions."

Deveria ser:

"Escolhi um ponto inicial, medi o comportamento e ajustei baseado em evidências."


20. O número de cores não conta toda a história

Suponha:

Executors: 10
Cores por Executor: 4

Total: 40 cores
Enter fullscreen mode Exit fullscreen mode

Podemos pensar inicialmente:

40 cores
   ↓
até aproximadamente 40 Tasks simultâneas
Enter fullscreen mode Exit fullscreen mode

Mas isso não significa que:

40 partitions
Enter fullscreen mode Exit fullscreen mode

é necessariamente ideal.

Por quê?

Porque se temos exatamente 40 Tasks e uma delas é muito lenta:

39 Tasks → 30 segundos
 1 Task  → 8 minutos
Enter fullscreen mode Exit fullscreen mode

depois dos primeiros 30 segundos grande parte do cluster pode ficar ociosa esperando aquela última Task.

Ter múltiplas ondas de Tasks pode ajudar na utilização dos recursos.

Novamente:

não existe uma fórmula universal.

Existe análise de workload.


21. Quando particionamento vira arquitetura

Esse assunto parece inicialmente uma otimização de baixo nível.

Mas não é.

Particionamento influencia:

                    PARTITIONING
                         │
          ┌──────────────┼──────────────┐
          │              │              │
          ▼              ▼              ▼
      Parallelism      Memory         Network
          │              │              │
          ▼              ▼              ▼
        Tasks          Spill          Shuffle
          │              │              │
          └──────────────┼──────────────┘
                         ▼
                    PERFORMANCE
                         │
                         ▼
                       COST
Enter fullscreen mode Exit fullscreen mode

Em cloud isso fica ainda mais importante.

Um pipeline ineficiente não significa apenas:

"demorou mais."

Pode significar:

"custou mais."

Se um cluster fica ativo durante duas horas em vez de vinte minutos, existe impacto financeiro.

Por isso performance também é uma preocupação de arquitetura e FinOps.


22. Um exemplo de raciocínio de troubleshooting

Imagine que um Job demora 45 minutos.

No Spark UI encontramos:

Stage 8

Tasks: 500
Median Duration: 20 s
Max Duration: 31 min
Enter fullscreen mode Exit fullscreen mode

Isso é um enorme sinal.

Não começaria aumentando memória aleatoriamente.

Perguntaria:

Por que uma Task levou 31 minutos
quando a mediana foi 20 segundos?
Enter fullscreen mode Exit fullscreen mode

Possíveis hipóteses:

Task lenta
   │
   ├── partition muito grande?
   ├── data skew?
   ├── shuffle excessivo?
   ├── spill para disco?
   ├── garbage collection?
   ├── problema no executor?
   └── problema de I/O?
Enter fullscreen mode Exit fullscreen mode

Esse é o tipo de raciocínio que quero desenvolver ao longo desta série.

Não decorar configurações.

Mas saber onde procurar.


23. repartition() também pode piorar tudo

Imagine um pipeline:

df = spark.read.parquet("/data/events")

df = df.repartition(5000)

resultado = (
    df
    .filter("event_type = 'purchase'")
    .groupBy("customer_id")
    .count()
)
Enter fullscreen mode Exit fullscreen mode

Talvez alguém tenha adicionado:

repartition(5000)
Enter fullscreen mode Exit fullscreen mode

porque:

"Mais partições deixam Spark mais rápido."

Mas acabamos de introduzir potencialmente uma redistribuição cara antes mesmo da operação principal.

Podemos ter:

Read
 ↓
Repartition
 ↓
SHUFFLE
 ↓
Filter
 ↓
GroupBy
 ↓
SHUFFLE
 ↓
Aggregation
Enter fullscreen mode Exit fullscreen mode

Talvez tenhamos criado dois movimentos grandes de dados onde não precisávamos.

Esse é um exemplo perfeito de uma regra importante:

Otimização sem compreender o plano de execução pode virar desotimização.


24. O melhor repartition() às vezes é nenhum

Antes de escrever:

df.repartition(...)
Enter fullscreen mode Exit fullscreen mode

eu perguntaria:

Qual problema estou tentando resolver?

Se a resposta for:

"Não sei, mas acho que melhora performance."

Não é uma boa justificativa.

Perguntas melhores:

Tenho poucas partitions?

Tenho muitas?

Existe skew?

Preciso distribuir por determinada chave?

Estou preparando os dados para um join?

Estou gerando small files?

Existe um shuffle inevitável logo depois?

Qual é o plano físico?
Enter fullscreen mode Exit fullscreen mode

Só depois escolheria a estratégia.


25. O que mudou no Spark moderno

Existe uma diferença importante entre estudar Spark historicamente e trabalhar com versões modernas.

Hoje o Spark possui mecanismos muito mais sofisticados de otimização.

Um deles é o:

Adaptive Query Execution — AQE

Com AQE, Spark pode utilizar informações coletadas durante a própria execução para ajustar partes do plano.

Entre outras capacidades, isso pode ajudar com:

  • coalescing de shuffle partitions;
  • otimização de joins;
  • tratamento de determinados cenários de skew.

Isso representa uma evolução enorme.

Historicamente, muitas decisões precisavam ser tomadas antecipadamente com informações limitadas.

Hoje Spark consegue adaptar determinadas decisões durante a execução.

Mas isso não elimina a necessidade de entender partições.

Na verdade, torna o conhecimento ainda mais útil.

Porque agora podemos entender o que o engine está tentando otimizar por nós.


26. Como penso em partições hoje

Meu modelo mental é aproximadamente este:

                  DATASET
                     │
                     ▼
                PARTITIONS
                     │
          ┌──────────┼──────────┐
          │          │          │
          ▼          ▼          ▼
       Size       Balance      Count
          │          │          │
          └──────────┼──────────┘
                     ▼
                   TASKS
                     │
                     ▼
                PARALLELISM
                     │
          ┌──────────┼──────────┐
          ▼          ▼          ▼
         CPU       Memory     Network
          │          │          │
          └──────────┼──────────┘
                     ▼
                 PERFORMANCE
                     │
                     ▼
                    COST
Enter fullscreen mode Exit fullscreen mode

Não penso:

"Qual número de partitions devo usar?"

Penso:

"Como os dados estão sendo distribuídos e como isso afeta a execução?"

Essa é uma pergunta muito melhor.


27. Checklist rápido de particionamento

Quando encontro problemas de performance, algumas perguntas ajudam bastante:

  • Quantas partições existem?
  • Qual o tamanho médio?
  • Qual o tamanho máximo?
  • As Tasks possuem duração semelhante?
  • Existe uma ou poucas Tasks muito mais lentas?
  • Todos os cores estão sendo utilizados?
  • Existe shuffle?
  • Quanto está sendo lido e escrito no shuffle?
  • Existe spill para disco?
  • Há indícios de data skew?
  • Algum repartition() foi adicionado sem necessidade?
  • Existe coalesce(1)?
  • Estamos gerando arquivos pequenos demais?
  • O problema está na execução ou no layout do armazenamento?

Essas perguntas transformam:

"Meu Spark está lento."

em uma investigação muito mais objetiva.


28. A principal lição deste capítulo

Se eu pudesse resumir tudo em uma frase:

O cluster fornece capacidade. As partições determinam quanto dessa capacidade conseguimos explorar.

Um cluster enorme com particionamento ruim continua sendo um sistema mal utilizado.

Podemos ter:

100 CPUs disponíveis
Enter fullscreen mode Exit fullscreen mode

e apenas:

4 Tasks
Enter fullscreen mode Exit fullscreen mode

Ou:

100 CPUs disponíveis
Enter fullscreen mode Exit fullscreen mode

e:

1.000.000 Tasks minúsculas
Enter fullscreen mode Exit fullscreen mode

Nos dois casos existe desperdício.

O objetivo é encontrar uma distribuição que permita ao Spark executar trabalho suficiente em paralelo sem transformar coordenação, memória ou rede no novo gargalo.


Próximo artigo

Neste capítulo apareceu repetidamente uma palavra:

Shuffle

Ela surgiu em:

repartition()
Enter fullscreen mode Exit fullscreen mode

em:

groupBy()
Enter fullscreen mode Exit fullscreen mode

em:

reduceByKey()
Enter fullscreen mode Exit fullscreen mode

e também aparecerá quando falarmos sobre:

join()
Enter fullscreen mode Exit fullscreen mode

Shuffle é uma das operações mais importantes para entender performance em Apache Spark.

No próximo artigo vamos descobrir:

  • o que realmente acontece durante um shuffle;
  • por que dados precisam atravessar a rede;
  • narrow vs. wide dependencies;
  • shuffle read;
  • shuffle write;
  • agregações;
  • joins;
  • impacto sobre memória e disco;
  • spill;
  • e por que reduzir movimentação de dados costuma ser uma das melhores otimizações possíveis.

Vamos seguir o fluxo:

Partitions
     ↓
Wide Transformation
     ↓
Shuffle Write
     ↓
Network
     ↓
Shuffle Read
     ↓
New Partitions
     ↓
Next Stage
Enter fullscreen mode Exit fullscreen mode

É aqui que começamos a entrar de verdade em performance tuning de Spark.


[← Parte 2: Por dentro da arquitetura — Driver, Executors, Jobs, Stages e Tasks]

Próximo: Parte 4 — Shuffle no Apache Spark: onde a performance começa a ficar cara

Top comments (0)