DEV Community

renanpyd
renanpyd

Posted on

Como aprendi Apache Spark — Parte 8: RDD Lineage e tolerância a falhas — como o Spark se recupera

Nos primeiros artigos desta série, começamos com uma das abstrações que ajudaram a definir o Apache Spark:

RDD — Resilient Distributed Dataset.

Falamos bastante sobre:

Distributed Dataset
Enter fullscreen mode Exit fullscreen mode

Vimos:

Partitions
Tasks
Executors
Shuffle
Memory
Enter fullscreen mode Exit fullscreen mode

Mas existe uma palavra no nome que merece um capítulo próprio:

Resilient.

Por que um RDD é resiliente?

A resposta está em uma das ideias mais elegantes da arquitetura original do Spark:

Lineage.

Em vez de depender apenas da replicação de cada resultado intermediário, Spark mantém informações sobre como os dados foram produzidos.

Se parte deles for perdida, em muitos casos ela pode ser reconstruída.

Essa ideia conecta:

Immutability
    ↓
Transformations
    ↓
Lineage
    ↓
DAG
    ↓
Recomputation
    ↓
Fault Tolerance
Enter fullscreen mode Exit fullscreen mode

E muda a forma como enxergamos falhas em processamento distribuído.


1. Falhas fazem parte de sistemas distribuídos

Imagine um cluster:

             DRIVER
                │
      ┌─────────┼─────────┐
      ▼         ▼         ▼
  Executor 1 Executor 2 Executor 3
Enter fullscreen mode Exit fullscreen mode

Agora imagine que estamos processando:

10 TB
Enter fullscreen mode Exit fullscreen mode

durante horas.

Nesse período, alguma coisa pode acontecer:

Executor failure
Machine failure
Process crash
Network problem
Disk problem
Container termination
Enter fullscreen mode Exit fullscreen mode

Em um sistema distribuído, a pergunta não deveria ser apenas:

E se alguma coisa falhar?

Uma pergunta melhor é:

Como o sistema continua quando alguma coisa falhar?

Essa diferença é fundamental.


2. Um problema simples

Imagine um dataset dividido em quatro partições:

RDD A

P1
P2
P3
P4
Enter fullscreen mode Exit fullscreen mode

Distribuídas assim:

Executor 1
├── P1
└── P2

Executor 2
├── P3
└── P4
Enter fullscreen mode Exit fullscreen mode

Agora o Executor 2 desaparece.

Executor 1
├── P1
└── P2

Executor 2
└── X
Enter fullscreen mode Exit fullscreen mode

Perdemos:

P3
P4
Enter fullscreen mode Exit fullscreen mode

O que fazer?

Uma possibilidade seria manter cópias de tudo.

Outra possibilidade é saber como reconstruir o que foi perdido.

Essa segunda ideia é central no modelo de RDDs.


3. RDDs são imutáveis

Uma característica importante dos RDDs é:

RDDs são imutáveis.

Quando fazemos:

rdd2 = rdd1.map(lambda x: x * 2)
Enter fullscreen mode Exit fullscreen mode

não estamos modificando rdd1.

Estamos descrevendo outro RDD:

RDD1
 │
 │ map(x * 2)
 ▼
RDD2
Enter fullscreen mode Exit fullscreen mode

Depois:

rdd3 = rdd2.filter(lambda x: x > 100)
Enter fullscreen mode Exit fullscreen mode

temos:

RDD1
 │
 │ map
 ▼
RDD2
 │
 │ filter
 ▼
RDD3
Enter fullscreen mode Exit fullscreen mode

Cada transformação cria uma nova abstração derivada da anterior.

Essa propriedade ajuda a tornar a linhagem previsível.


4. O que é Lineage?

Lineage representa a sequência de dependências e transformações utilizada para produzir um dataset.

Considere:

rdd1 = sc.textFile("/data/events")

rdd2 = rdd1.map(
    lambda line: line.split(",")
)

rdd3 = rdd2.filter(
    lambda row: row[2] == "purchase"
)

rdd4 = rdd3.map(
    lambda row: (row[1], 1)
)

rdd5 = rdd4.reduceByKey(
    lambda a, b: a + b
)
Enter fullscreen mode Exit fullscreen mode

Conceitualmente:

textFile
   ↓
 RDD1
   ↓
  map
   ↓
 RDD2
   ↓
 filter
   ↓
 RDD3
   ↓
  map
   ↓
 RDD4
   ↓
reduceByKey
   ↓
 RDD5
Enter fullscreen mode Exit fullscreen mode

Essa cadeia descreve:

Como RDD5 foi derivado dos dados originais.

Isso é lineage.


5. Lineage não significa guardar todas as cópias

Esse ponto é importante.

Imagine:

RDD1
 ↓
RDD2
 ↓
RDD3
 ↓
RDD4
 ↓
RDD5
Enter fullscreen mode Exit fullscreen mode

Não precisamos interpretar isso como:

Memory

RDD1 complete
RDD2 complete
RDD3 complete
RDD4 complete
RDD5 complete
Enter fullscreen mode Exit fullscreen mode

Lineage não significa manter automaticamente todas as versões materializadas.

O que Spark preserva é a informação necessária sobre as dependências entre os datasets.

Podemos pensar:

Data
+
Transformation Graph
=
Ability to recompute
Enter fullscreen mode Exit fullscreen mode

Essa diferença é essencial.


6. Lazy Evaluation e Lineage

Agora podemos conectar outro conceito da série.

Quando escrevemos:

rdd2 = rdd1.map(...)
rdd3 = rdd2.filter(...)
rdd4 = rdd3.map(...)
Enter fullscreen mode Exit fullscreen mode

Spark não precisa executar imediatamente cada transformação.

Ele constrói relações entre os RDDs.

Conceitualmente:

rdd1
  │
 map
  │
rdd2
  │
filter
  │
rdd3
  │
 map
  │
rdd4
Enter fullscreen mode Exit fullscreen mode

Quando uma Action aparece:

rdd4.count()
Enter fullscreen mode Exit fullscreen mode

temos:

Lineage
   ↓
Action
   ↓
Job
   ↓
Stages
   ↓
Tasks
Enter fullscreen mode Exit fullscreen mode

Lazy evaluation e lineage estão profundamente conectados.


7. O DAG

Essa cadeia de dependências não precisa ser apenas linear.

Podemos ter:

          RDD A
         /     \
        /       \
       ▼         ▼
    RDD B      RDD C
       \         /
        \       /
         ▼     ▼
          RDD D
Enter fullscreen mode Exit fullscreen mode

Isso forma um:

Directed Acyclic Graph — DAG

ou:

Grafo Direcionado Acíclico
Enter fullscreen mode Exit fullscreen mode

Direcionado porque existem relações de dependência.

Acíclico porque a computação não forma ciclos arbitrários de dependência entre esses datasets.

Conceitualmente:

Source
  ↓
Transform
  ↓
Transform ─────┐
  │            │
  ▼            ▼
Branch A    Branch B
  │            │
  └──────┬─────┘
         ▼
       Result
Enter fullscreen mode Exit fullscreen mode

O DAG representa a estrutura da computação.


8. DAG não é exatamente a mesma coisa que Stage

Essa distinção é importante.

Podemos ter uma sequência de transformações:

map
 ↓
filter
 ↓
map
 ↓
reduceByKey
 ↓
map
Enter fullscreen mode Exit fullscreen mode

Mas isso não significa necessariamente:

5 transformations
=
5 stages
Enter fullscreen mode Exit fullscreen mode

Como vimos anteriormente, dependências narrow podem ser agrupadas em um mesmo Stage.

Shuffle frequentemente cria uma fronteira.

Então podemos ter:

STAGE 1

map
 ↓
filter
 ↓
map
 ↓

====== SHUFFLE ======

STAGE 2

reduce
 ↓
map
Enter fullscreen mode Exit fullscreen mode

Portanto:

Lineage / DAG
      ↓
Stage boundaries
      ↓
Tasks
Enter fullscreen mode Exit fullscreen mode

são conceitos relacionados, mas não idênticos.


9. Narrow Dependencies

Vamos revisitar um conceito importante.

Considere:

rdd2 = rdd1.map(
    lambda x: x * 2
)
Enter fullscreen mode Exit fullscreen mode

Podemos ter:

RDD1 P1 ───► RDD2 P1
RDD1 P2 ───► RDD2 P2
RDD1 P3 ───► RDD2 P3
Enter fullscreen mode Exit fullscreen mode

Cada partição filha depende de uma quantidade limitada de partições anteriores.

Isso caracteriza uma dependência estreita:

Narrow Dependency.

Agora imagine que perdemos:

RDD2 P2
Enter fullscreen mode Exit fullscreen mode

Spark pode precisar reconstruir apenas o caminho relevante:

RDD1 P2
   ↓
 map
   ↓
RDD2 P2
Enter fullscreen mode Exit fullscreen mode

Essa granularidade é extremamente importante para tolerância a falhas.


10. Recomputation

Aqui chegamos ao coração da resiliência.

Imagine:

Source

P1 P2 P3 P4
 │  │  │  │
 ▼  ▼  ▼  ▼

Map

P1' P2' P3' P4'
Enter fullscreen mode Exit fullscreen mode

Agora:

P3'
Enter fullscreen mode Exit fullscreen mode

é perdida.

Spark conhece sua dependência:

Source P3
    ↓
   map
    ↓
  P3'
Enter fullscreen mode Exit fullscreen mode

Então pode reconstruí-la.

Não precisa necessariamente recalcular:

P1'
P2'
P4'
Enter fullscreen mode Exit fullscreen mode

A ideia é:

Recompute what was lost from the lineage.

Essa é uma das razões para a palavra:

Resilient.


11. Resiliência sem replicar tudo

Uma abordagem possível para tolerância a falhas seria:

Resultado intermediário
       ↓
Copy 1
Copy 2
Copy 3
Enter fullscreen mode Exit fullscreen mode

Mas isso possui custos:

Storage
Network
Replication
Coordination
Enter fullscreen mode Exit fullscreen mode

RDDs introduziram uma ideia poderosa para datasets derivados deterministicamente:

Don't replicate every intermediate result
              ↓
Keep lineage
              ↓
Recompute lost partitions
Enter fullscreen mode Exit fullscreen mode

Isso não elimina a necessidade de persistência ou replicação em todos os contextos.

Mas cria um mecanismo elegante para recuperar dados derivados.


12. Um exemplo mais completo

Imagine:

raw = sc.textFile(
    "/data/logs"
)

parsed = raw.map(parse_line)

errors = parsed.filter(
    lambda x: x.level == "ERROR"
)

by_service = errors.map(
    lambda x: (x.service, 1)
)

counts = by_service.reduceByKey(
    lambda a, b: a + b
)
Enter fullscreen mode Exit fullscreen mode

Temos:

TEXT FILE
    ↓
   raw
    ↓
  parse
    ↓
 parsed
    ↓
 filter
    ↓
 errors
    ↓
   map
    ↓
by_service
    ↓
reduceByKey
    ↓
 counts
Enter fullscreen mode Exit fullscreen mode

Agora imagine uma partição de errors sendo perdida.

Spark conhece:

textFile
   ↓
 parse
   ↓
 filter
   ↓
errors partition
Enter fullscreen mode Exit fullscreen mode

Então pode reconstruir aquela partição a partir da origem e das transformações necessárias.


13. E depois de um Shuffle?

Agora a situação fica mais interessante.

Considere:

counts = by_service.reduceByKey(
    lambda a, b: a + b
)
Enter fullscreen mode Exit fullscreen mode

Como vimos na Parte 4:

Map-side work
      ↓
Shuffle Write
      ↓
Network
      ↓
Shuffle Read
      ↓
Reduce-side work
Enter fullscreen mode Exit fullscreen mode

Temos uma wide dependency.

Agora uma partição posterior pode depender de dados produzidos por várias partições anteriores.

Conceitualmente:

P1 ───┐
P2 ───┼──► Shuffle ───► R1
P3 ───┤
P4 ───┘
Enter fullscreen mode Exit fullscreen mode

Se determinado resultado for perdido, a recomputação pode ser mais cara porque envolve dependências mais amplas.

Isso mostra que:

Nem todo lineage possui o mesmo custo de recuperação.


14. Narrow vs. Wide também importa para Fault Tolerance

Compare:

Narrow

P1 ───► P1'
P2 ───► P2'
P3 ───► P3'
Enter fullscreen mode Exit fullscreen mode

Perdeu P2'?

Recompute P2
     ↓
     P2'
Enter fullscreen mode Exit fullscreen mode

Agora:

Wide

P1 ───┐
P2 ───┼──► P'
P3 ───┤
P4 ───┘
Enter fullscreen mode Exit fullscreen mode

A dependência é mais ampla.

Isso afeta:

  • custo de recomputação;
  • organização dos Stages;
  • shuffle;
  • recuperação.

Mais uma vez, shuffle aparece como uma fronteira importante da arquitetura.


15. Então por que usar Cache?

Se Spark consegue recomputar tudo, poderíamos perguntar:

Por que utilizar cache?

Porque recomputação também custa.

Imagine:

Source
 ↓
Expensive Transformation A
 ↓
Expensive Transformation B
 ↓
Dataset X
Enter fullscreen mode Exit fullscreen mode

Agora utilizamos Dataset X cinco vezes:

             Dataset X
            /    |    \
           ▼     ▼     ▼
         Job A Job B Job C
Enter fullscreen mode Exit fullscreen mode

Sem persistência, determinadas partes da linhagem podem precisar ser recomputadas.

Se Dataset X for caro de produzir, podemos decidir:

dataset_x.cache()
Enter fullscreen mode Exit fullscreen mode

Agora trocamos:

Repeated Computation
Enter fullscreen mode Exit fullscreen mode

por:

Storage
Enter fullscreen mode Exit fullscreen mode

Esse é um trade-off.


16. Cache vs. Recomputation

Podemos pensar assim:

                 Dataset
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
      Recompute             Cache
          │                   │
          ▼                   ▼
     More CPU             More Memory
     More Time            Storage Cost
          │                   │
          └─────────┬─────────┘
                    ▼
                 Trade-off
Enter fullscreen mode Exit fullscreen mode

A pergunta não é:

Cache é bom?

A pergunta é:

O custo de manter esse dataset é menor do que o custo esperado de recomputá-lo?

Essa é uma pergunta muito mais útil.


17. Persistência e tolerância a falhas

Agora existe uma nuance importante.

Se persistimos um RDD e uma partição persistida é perdida devido à falha de um Executor, o lineage ainda pode permitir sua recomputação.

Imagine:

RDD X cached

Executor 1
├── P1
└── P2

Executor 2
├── P3
└── P4
Enter fullscreen mode Exit fullscreen mode

Executor 2 falha:

P3 X
P4 X
Enter fullscreen mode Exit fullscreen mode

Spark ainda conhece:

Lineage
Enter fullscreen mode Exit fullscreen mode

e pode reconstruir essas partições quando necessário.

Então:

Cache
Enter fullscreen mode Exit fullscreen mode

e:

Lineage
Enter fullscreen mode Exit fullscreen mode

não são mecanismos concorrentes.

Eles se complementam.


18. persist()

Como vimos na Parte 7, podemos usar:

rdd.persist()
Enter fullscreen mode Exit fullscreen mode

para controlar estratégias de persistência.

Conceitualmente:

Persistence
    │
    ├── Memory
    ├── Disk
    ├── Serialized forms
    └── Replication options
Enter fullscreen mode Exit fullscreen mode

A escolha depende do workload.

Por exemplo:

Dataset pequeno
+
reutilizado frequentemente
+
caro de recomputar
Enter fullscreen mode Exit fullscreen mode

pode justificar uma estratégia diferente de:

Dataset enorme
+
utilizado uma vez
Enter fullscreen mode Exit fullscreen mode

Persistência deve responder a uma necessidade real.


19. Mas Lineage pode crescer

Agora imagine um pipeline muito longo:

RDD1
 ↓
RDD2
 ↓
RDD3
 ↓
RDD4
 ↓
RDD5
 ↓
RDD6
 ↓
...
 ↓
RDD100
 ↓
...
 ↓
RDD500
Enter fullscreen mode Exit fullscreen mode

Temos uma longa cadeia de dependências.

Em determinados workloads iterativos ou pipelines extensos, essa linhagem pode se tornar indesejavelmente longa ou cara para recuperação.

Queremos então, em alguns cenários, interromper essa dependência histórica.

É aqui que aparece outro conceito:

Checkpoint.


20. O que é Checkpoint?

A ideia do checkpoint é materializar o estado de um dataset em armazenamento confiável e cortar sua dependência da linhagem anterior para fins de recuperação.

Antes:

Source
 ↓
A
 ↓
B
 ↓
C
 ↓
D
 ↓
E
Enter fullscreen mode Exit fullscreen mode

Se E precisar ser reconstruído:

Source → A → B → C → D → E
Enter fullscreen mode Exit fullscreen mode

Agora imagine um checkpoint em C:

Source
 ↓
A
 ↓
B
 ↓
C
 ↓
CHECKPOINT
 ↓
D
 ↓
E
Enter fullscreen mode Exit fullscreen mode

A recuperação posterior pode partir do checkpoint:

CHECKPOINT
    ↓
    D
    ↓
    E
Enter fullscreen mode Exit fullscreen mode

Isso trunca a linhagem anterior.


21. Configurando Checkpoint em RDD

Em um contexto de RDD, podemos definir um diretório de checkpoint:

sc.setCheckpointDir(
    "/path/to/reliable/checkpoints"
)
Enter fullscreen mode Exit fullscreen mode

Depois:

rdd.checkpoint()
Enter fullscreen mode Exit fullscreen mode

Quando o RDD for materializado por uma Action, o checkpoint poderá ser efetivamente produzido.

Por exemplo:

rdd.checkpoint()

rdd.count()
Enter fullscreen mode Exit fullscreen mode

Conceitualmente:

checkpoint()
    ↓
mark RDD
    ↓
Action
    ↓
compute
    ↓
write checkpoint
Enter fullscreen mode Exit fullscreen mode

Assim como cache, existe interação com lazy evaluation.


22. Checkpoint não é Cache

Essa distinção é fundamental.

Cache

Objetivo principal:

Reuse
 ↓
Performance
Enter fullscreen mode Exit fullscreen mode

Checkpoint

Objetivo principal:

Materialize reliable state
        ↓
Truncate lineage
        ↓
Recovery / lineage management
Enter fullscreen mode Exit fullscreen mode

Simplificando:

CACHE
"Evite recomputar algo que vou reutilizar."
Enter fullscreen mode Exit fullscreen mode

versus:

CHECKPOINT
"Crie um novo ponto materializado e corte a dependência da linhagem anterior."
Enter fullscreen mode Exit fullscreen mode

Os dois podem aparecer juntos.

Mas resolvem problemas diferentes.


23. Cache não corta Lineage

Imagine:

A
 ↓
B
 ↓
C
 ↓
D
Enter fullscreen mode Exit fullscreen mode

Fazemos:

d.cache()
Enter fullscreen mode Exit fullscreen mode

Ainda existe conceitualmente:

A
 ↓
B
 ↓
C
 ↓
D [cached]
Enter fullscreen mode Exit fullscreen mode

O lineage continua conhecido.

Se a partição persistida for perdida, Spark pode usar essa linhagem para reconstruí-la.

Agora com checkpoint:

A
 ↓
B
 ↓
C
 ↓
D
 ↓
CHECKPOINT
Enter fullscreen mode Exit fullscreen mode

a linhagem anterior pode ser truncada depois que o checkpoint é materializado corretamente.

Essa é uma diferença arquitetural importante.


24. Checkpoint também tem custo

Checkpoint não é gratuito.

Precisamos:

Compute
   ↓
Write
   ↓
Reliable Storage
Enter fullscreen mode Exit fullscreen mode

Isso envolve:

  • I/O;
  • armazenamento;
  • tempo;
  • gerenciamento.

Portanto, não queremos:

checkpoint everything
Enter fullscreen mode Exit fullscreen mode

Queremos checkpoint quando existe uma razão arquitetural.

Por exemplo:

  • lineage muito longo;
  • processamento iterativo;
  • necessidade de ponto confiável;
  • custo elevado de reconstrução.

25. Um pipeline iterativo

Imagine um algoritmo que executa repetidamente:

Iteration 1
    ↓
Iteration 2
    ↓
Iteration 3
    ↓
Iteration 4
    ↓
...
    ↓
Iteration 100
Enter fullscreen mode Exit fullscreen mode

Se cada iteração depende da anterior, podemos formar uma linhagem extensa.

Conceitualmente:

Initial
 ↓
I1
 ↓
I2
 ↓
I3
 ↓
...
 ↓
I100
Enter fullscreen mode Exit fullscreen mode

Em determinadas situações, podemos estabelecer checkpoints intermediários:

Initial
 ↓
I1
 ↓
...
 ↓
I20
 ↓
CHECKPOINT
 ↓
I21
 ↓
...
 ↓
I40
 ↓
CHECKPOINT
Enter fullscreen mode Exit fullscreen mode

Assim reduzimos a profundidade necessária para recuperação futura.


26. Falha de Executor não significa falha da aplicação

Agora podemos enxergar resiliência de forma melhor.

Temos:

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

Um desaparece:

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

Isso não implica necessariamente:

APPLICATION FAILED
Enter fullscreen mode Exit fullscreen mode

Spark pode reagendar trabalho e reconstruir dados necessários quando a situação permite.

Conceitualmente:

Executor failure
      ↓
Lost Tasks / Data
      ↓
Scheduler
      ↓
Re-execution
      ↓
Lineage
      ↓
Recompute
      ↓
Continue
Enter fullscreen mode Exit fullscreen mode

Essa capacidade é central em computação distribuída.


27. Task Failure

Também podemos ter falha em uma Task específica.

Por exemplo:

Stage 10

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

Spark pode tentar executar novamente a Task.

Task 3
  ↓
Retry
  ↓
Task 3 ✓
Enter fullscreen mode Exit fullscreen mode

Talvez em outro Executor.

Isso ajuda com falhas transitórias.

Mas existe uma distinção importante.


28. Falha transitória vs. falha determinística

Imagine:

Task failed
Enter fullscreen mode Exit fullscreen mode

porque uma máquina teve um problema temporário.

Retry pode funcionar:

Attempt 1 → FAIL
Attempt 2 → SUCCESS
Enter fullscreen mode Exit fullscreen mode

Agora imagine código:

def transform(x):
    return 100 / x
Enter fullscreen mode Exit fullscreen mode

e temos:

x = 0
Enter fullscreen mode Exit fullscreen mode

A Task falha.

Spark tenta novamente.

Attempt 1 → divide by zero
Attempt 2 → divide by zero
Attempt 3 → divide by zero
Enter fullscreen mode Exit fullscreen mode

Não importa quantas máquinas utilizarmos.

O problema está nos dados ou no código.

Isso nos ensina:

Fault tolerance não corrige lógica determinística incorreta.


29. Recomputation também não corrige dados ruins

Imagine:

corrupted input
Enter fullscreen mode Exit fullscreen mode

A partição falha.

Spark tenta novamente:

read
 ↓
fail
 ↓
retry
 ↓
read same corrupted data
 ↓
fail
Enter fullscreen mode Exit fullscreen mode

Lineage ajuda a recuperar computação perdida.

Não transforma entrada inválida em entrada válida.

Resiliência tem limites.


30. O Driver continua sendo especial

Até agora falamos muito sobre Executors.

Mas existe um componente central:

DRIVER
Enter fullscreen mode Exit fullscreen mode

O Driver coordena a aplicação.

Se um Executor falha, Spark pode frequentemente reagendar trabalho.

A falha do Driver possui implicações diferentes porque ali está o processo que coordena a aplicação.

A capacidade de recuperar o Driver depende também:

  • do modo de implantação;
  • do cluster manager;
  • da configuração;
  • da plataforma utilizada;
  • dos mecanismos externos de supervisão/restart.

Portanto:

Executor fault tolerance
Enter fullscreen mode Exit fullscreen mode

não deve ser confundida automaticamente com:

Driver high availability
Enter fullscreen mode Exit fullscreen mode

São problemas relacionados, mas diferentes.


31. Lineage não substitui durabilidade da fonte

Imagine que nosso RDD começa em:

/data/events
Enter fullscreen mode Exit fullscreen mode

e depois:

Source
 ↓
Map
 ↓
Filter
 ↓
RDD
Enter fullscreen mode Exit fullscreen mode

Se perdemos uma partição intermediária, podemos tentar reconstruí-la a partir da fonte.

Mas e se a fonte original desaparecer?

Source X
Enter fullscreen mode Exit fullscreen mode

Agora lineage sabe:

como
Enter fullscreen mode Exit fullscreen mode

reconstruir.

Mas não possui necessariamente:

de onde
Enter fullscreen mode Exit fullscreen mode

obter os dados originais.

Portanto:

Lineage não substitui armazenamento durável dos dados de origem.

Essa distinção é muito importante em arquitetura de dados.


32. Fault tolerance não é Backup

Outro erro conceitual seria pensar:

Spark é fault tolerant
        ↓
não preciso de backup
Enter fullscreen mode Exit fullscreen mode

Não.

São problemas diferentes.

Fault tolerance

Ajuda o sistema a continuar ou recuperar processamento diante de determinadas falhas.

Backup

Protege dados contra cenários como:

  • exclusão;
  • corrupção;
  • desastre;
  • erro operacional;
  • perda da fonte.

Podemos ter um engine extremamente resiliente processando uma fonte que foi permanentemente destruída.

O engine não recria magicamente dados que deixaram de existir.


33. Lineage e DataFrames

Até agora usei RDDs porque lineage é um conceito especialmente natural para explicar a arquitetura original do Spark.

Mas DataFrames também participam de uma computação baseada em dependências e planos.

Quando escrevemos:

resultado = (
    vendas
    .filter("ano = 2026")
    .groupBy("estado")
    .sum("valor")
)
Enter fullscreen mode Exit fullscreen mode

continuamos tendo:

Source
 ↓
Transformations
 ↓
Dependencies
 ↓
Stages
 ↓
Tasks
Enter fullscreen mode Exit fullscreen mode

A diferença é que agora também existe a camada estruturada que vimos na Parte 6:

DataFrame
 ↓
Logical Plan
 ↓
Catalyst
 ↓
Physical Plan
 ↓
Distributed Execution
Enter fullscreen mode Exit fullscreen mode

Os fundamentos não desapareceram.

Eles ganharam novas abstrações.


34. Lineage vs. Query Plan

Também não devemos tratar:

Lineage
Enter fullscreen mode Exit fullscreen mode

e:

Query Plan
Enter fullscreen mode Exit fullscreen mode

como exatamente a mesma coisa.

Uma forma útil de pensar é:

Lineage

Relaciona datasets e dependências necessárias para derivação/recomputação.

Query Plan

Representa operações lógicas e físicas usadas pelo engine estruturado para planejar e executar uma consulta.

Eles se encontram na execução distribuída, mas são conceitos distintos.


35. DAG Scheduler

Agora podemos encaixar outra peça da arquitetura.

Spark possui componentes responsáveis por transformar a computação em trabalho executável.

Uma visão conceitual:

Application
    ↓
Action
    ↓
Job
    ↓
DAG Scheduler
    ↓
Stages
    ↓
Task Sets
    ↓
Executors
Enter fullscreen mode Exit fullscreen mode

O DAG Scheduler consegue analisar dependências e identificar fronteiras de Stage, especialmente ao redor de shuffles.

Podemos pensar:

Narrow
 ↓
same Stage where possible

Wide
 ↓
Shuffle
 ↓
new Stage boundary
Enter fullscreen mode Exit fullscreen mode

Isso conecta lineage diretamente ao modelo de execução que estudamos na Parte 2.


36. Task Scheduling

Depois que o trabalho é dividido em Stages e Tasks, precisamos executar essas Tasks nos Executors.

Conceitualmente:

DAG Scheduler
     ↓
Stages
     ↓
Task Scheduler
     ↓
Executors
Enter fullscreen mode Exit fullscreen mode

Agora entram preocupações como:

  • recursos disponíveis;
  • localização;
  • execução;
  • retries;
  • status das Tasks.

O Spark não é apenas uma biblioteca de transformação.

Existe um sistema de scheduling distribuído por trás da API.


37. Data Locality

Existe ainda uma ideia importante.

Se os dados estão próximos de determinado Executor ou nó, pode ser vantajoso executar o processamento próximo deles.

Em vez de:

Move 100 GB of data
      ↓
to computation
Enter fullscreen mode Exit fullscreen mode

podemos preferir, quando a arquitetura permite:

Move computation
      ↓
to data
Enter fullscreen mode Exit fullscreen mode

Essa filosofia foi extremamente importante no ecossistema de Big Data.

No Spark, scheduling pode considerar locality dependendo da fonte e do ambiente.

Novamente encontramos o princípio:

Mover computação costuma ser mais barato do que mover grandes volumes de dados.


38. Falhas e Shuffle

Shuffle torna a recuperação mais interessante.

Imagine:

Stage 1
  ↓
Shuffle Write
  ↓
Stage 2
Enter fullscreen mode Exit fullscreen mode

Os outputs intermediários de shuffle são importantes para o Stage seguinte.

Se dados intermediários necessários forem perdidos devido a falhas, Spark pode precisar recomputar partes anteriores.

Conceitualmente:

Lost Shuffle Data
       ↓
Need previous output
       ↓
Recompute upstream tasks
       ↓
Regenerate shuffle data
       ↓
Continue downstream work
Enter fullscreen mode Exit fullscreen mode

Agora entendemos por que lineage continua relevante mesmo depois de falarmos sobre shuffle.


39. Recomputation tem custo

Fault tolerance não significa:

failure = free
Enter fullscreen mode Exit fullscreen mode

Imagine:

2-hour computation
      ↓
lost intermediate data
      ↓
recompute
Enter fullscreen mode Exit fullscreen mode

O sistema pode conseguir se recuperar.

Mas isso custa:

  • CPU;
  • tempo;
  • rede;
  • I/O;
  • dinheiro.

Então existe outra pergunta arquitetural:

Qual é o custo aceitável de recomputação?

É aqui que persistência e checkpoint podem entrar.


40. Cache, Checkpoint ou Recompute?

Podemos montar um modelo mental:

Dataset intermediário
        │
        ▼
Será reutilizado?
   │           │
  sim         não
   │           │
   ▼           ▼
Cache?     Recompute?
   │
   ▼
É caro de reconstruir
e lineage é problemático?
   │
   ▼
Checkpoint?
Enter fullscreen mode Exit fullscreen mode

Não é uma árvore de decisão absoluta.

É apenas uma forma de pensar.

A escolha depende de:

Cost of computation
Cost of storage
Reuse
Failure probability
Lineage length
Recovery requirements
Enter fullscreen mode Exit fullscreen mode

41. Um exemplo prático

Imagine:

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

base = (
    raw
    .filter("event_date >= '2026-01-01'")
    .select(
        "customer_id",
        "event_type",
        "value"
    )
)
Enter fullscreen mode Exit fullscreen mode

Essa transformação é usada por vários outputs:

por_cliente = (
    base
    .groupBy("customer_id")
    .count()
)

por_evento = (
    base
    .groupBy("event_type")
    .count()
)

receita = (
    base
    .groupBy("customer_id")
    .sum("value")
)
Enter fullscreen mode Exit fullscreen mode

Temos:

                  RAW
                   │
                   ▼
             Filter + Select
                   │
                   ▼
                  BASE
             ┌─────┼─────┐
             ▼     ▼     ▼
         Customer Event Revenue
Enter fullscreen mode Exit fullscreen mode

Se base for caro de produzir e reutilizado várias vezes:

base.cache()
Enter fullscreen mode Exit fullscreen mode

pode fazer sentido.

A decisão nasce do DAG.

Não de uma regra genérica de que:

cache = performance
Enter fullscreen mode Exit fullscreen mode

42. Um exemplo de Checkpoint

Agora imagine um processamento iterativo ou uma cadeia extremamente extensa.

Em RDD:

rdd = sc.textFile("/data/source")

for i in range(100):
    rdd = expensive_transformation(rdd)
Enter fullscreen mode Exit fullscreen mode

A linhagem pode crescer:

Source
 ↓
T1
 ↓
T2
 ↓
T3
 ↓
...
 ↓
T100
Enter fullscreen mode Exit fullscreen mode

Podemos decidir estabelecer checkpoints em pontos apropriados.

Conceitualmente:

Source
 ↓
T1
 ↓
...
 ↓
T25
 ↓
CHECKPOINT
 ↓
T26
 ↓
...
 ↓
T50
 ↓
CHECKPOINT
Enter fullscreen mode Exit fullscreen mode

Assim controlamos o tamanho da cadeia de recuperação.


43. localCheckpoint() merece cuidado

Spark também possui mecanismos de checkpoint local em determinados contextos.

Mas existe uma diferença conceitual importante:

Reliable checkpoint
Enter fullscreen mode Exit fullscreen mode

busca armazenamento confiável para quebrar a linhagem mantendo capacidade adequada de recuperação.

Já um checkpoint baseado apenas em armazenamento local dos Executors não oferece necessariamente a mesma garantia de tolerância a falhas.

Portanto:

Não devemos tratar todo tipo de checkpoint como equivalente em resiliência.

A escolha depende do objetivo.


44. Resiliência também depende do storage

Se o checkpoint precisa servir como ponto confiável de recuperação, precisamos pensar:

Onde ele está armazenado?
Enter fullscreen mode Exit fullscreen mode

Um diretório local efêmero possui características muito diferentes de armazenamento distribuído e durável.

Então a arquitetura completa é:

Spark
   +
Lineage
   +
Checkpoint strategy
   +
Reliable storage
Enter fullscreen mode Exit fullscreen mode

Fault tolerance não acontece isoladamente.


45. O que acontece quando uma máquina desaparece?

Agora podemos responder de forma muito melhor.

Imagine:

Executor 3
    ↓
FAIL
Enter fullscreen mode Exit fullscreen mode

Spark pode identificar trabalho perdido.

Depois:

Lost partition
      ↓
Find lineage
      ↓
Find required ancestors
      ↓
Schedule recomputation
      ↓
Execute elsewhere
      ↓
Rebuild data
      ↓
Continue
Enter fullscreen mode Exit fullscreen mode

Essa sequência explica o:

Resilient

de:

Resilient Distributed Dataset.


46. O que eu olho quando existem muitas recomputações?

Se uma aplicação está refazendo trabalho demais, quero investigar:

Executors estão instáveis?

Existe perda de containers?

Existe pressão de memória?

Existe GC excessivo?

Existe problema de infraestrutura?

Existe shuffle data sendo perdida?

A linhagem é cara?

Falta persistência em um ponto estratégico?
Enter fullscreen mode Exit fullscreen mode

Ou seja, recomputation é um mecanismo de recuperação.

Mas recomputation excessiva também pode ser sintoma.


47. Resiliência não significa ignorar infraestrutura

Podemos ter:

Executor failure
 ↓
Retry
 ↓
Executor failure
 ↓
Retry
 ↓
Executor failure
Enter fullscreen mode Exit fullscreen mode

Em algum momento a pergunta deixa de ser:

Spark consegue tentar novamente?

e passa a ser:

Por que meus Executors continuam morrendo?

Talvez exista:

OOM
Disk failure
Container eviction
Network instability
Bad node
Data skew
Enter fullscreen mode Exit fullscreen mode

Fault tolerance deve absorver falhas ocasionais.

Não deve ser usada para esconder um cluster permanentemente instável.


48. Observabilidade novamente

No troubleshooting, quero relacionar:

Application
    ↓
Jobs
    ↓
Stages
    ↓
Tasks
    ↓
Executors
Enter fullscreen mode Exit fullscreen mode

com:

Failures
Retries
Recomputation
Shuffle
Memory
GC
Spill
Enter fullscreen mode Exit fullscreen mode

Se Tasks estão sendo repetidas, preciso saber por quê.

Se Stages estão sendo refeitos, preciso saber por quê.

Se Executors desaparecem, preciso saber por quê.

Resiliência é muito mais útil quando acompanhada de observabilidade.


49. Um modelo mental de Fault Tolerance

Minha forma de enxergar ficou assim:

                   DATA SOURCE
                       │
                       ▼
                 Transformations
                       │
                       ▼
                    Lineage
                       │
                       ▼
                      DAG
                       │
                       ▼
                    Stages
                       │
                       ▼
                     Tasks
                       │
                       ▼
                   Executors
                       │
                       ▼
                    Failure?
                   /         \
                 não          sim
                 │             │
                 ▼             ▼
             Continue      What was lost?
                               │
                               ▼
                           Lineage
                               │
                               ▼
                         Recompute
                               │
                               ▼
                           Continue
Enter fullscreen mode Exit fullscreen mode

Agora acrescente:

Cache
Enter fullscreen mode Exit fullscreen mode

quando queremos evitar recomputação frequente.

E:

Checkpoint
Enter fullscreen mode Exit fullscreen mode

quando queremos estabelecer um novo ponto materializado e truncar uma linhagem problemática.


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

A resiliência do Spark não vem de uma única configuração.

Ela emerge de várias ideias trabalhando juntas:

Immutable Datasets
        +
Transformations
        +
Dependencies
        +
Lineage
        +
DAG
        +
Task Scheduling
        +
Retries
        +
Recomputation
Enter fullscreen mode Exit fullscreen mode

E, quando necessário:

Cache / Persist
Enter fullscreen mode Exit fullscreen mode

ou:

Checkpoint
Enter fullscreen mode Exit fullscreen mode

Essa arquitetura permite que uma falha parcial não precise significar perda completa da computação.


51. O significado de RDD ficou muito mais claro

No começo da série:

RDD
=
Resilient Distributed Dataset
Enter fullscreen mode Exit fullscreen mode

podia parecer apenas um nome.

Agora conseguimos decompor:

DISTRIBUTED
     ↓
Partitions across cluster
Enter fullscreen mode Exit fullscreen mode
DATASET
     ↓
Collection of data
Enter fullscreen mode Exit fullscreen mode

e:

RESILIENT
     ↓
Lineage
     ↓
Recomputation
     ↓
Fault recovery
Enter fullscreen mode Exit fullscreen mode

Essa é uma das ideias fundamentais da arquitetura original do Spark.


52. O que essa ideia me ensinou além do Spark

A lição mais interessante vai além da tecnologia.

Em sistemas distribuídos, não podemos construir arquitetura assumindo:

Everything will always work
Enter fullscreen mode Exit fullscreen mode

Precisamos assumir:

Machines fail
Processes fail
Networks fail
Tasks fail
Containers disappear
Enter fullscreen mode Exit fullscreen mode

Então a arquitetura precisa responder:

What happens next?
Enter fullscreen mode Exit fullscreen mode

Essa pergunta vale para:

  • processamento de dados;
  • microsserviços;
  • bancos distribuídos;
  • streaming;
  • Kubernetes;
  • cloud;
  • Machine Learning distribuído.

Tolerância a falhas não é um recurso opcional adicionado no final.

É uma propriedade arquitetural.


53. Como penso hoje

Quando vejo uma transformação Spark:

resultado = (
    dados
    .filter(...)
    .map(...)
    .reduceByKey(...)
)
Enter fullscreen mode Exit fullscreen mode

não penso apenas:

input
 ↓
transform
 ↓
output
Enter fullscreen mode Exit fullscreen mode

Penso:

Where is the source?
        ↓
What are the dependencies?
        ↓
Where are the shuffle boundaries?
        ↓
What is the lineage?
        ↓
What happens if a partition disappears?
        ↓
How expensive is recomputation?
        ↓
Should something be persisted?
        ↓
Is checkpointing justified?
Enter fullscreen mode Exit fullscreen mode

Essa mudança de mentalidade é muito mais importante do que memorizar APIs.


Próximo artigo

Agora temos uma base sólida sobre:

RDD
 ↓
Partitions
 ↓
Transformations
 ↓
Lineage
 ↓
DAG
 ↓
Stages
 ↓
Tasks
 ↓
Executors
 ↓
Fault Tolerance
Enter fullscreen mode Exit fullscreen mode

Mas existe um tema do material original que merece ser revisitado com bastante cuidado:

variáveis compartilhadas.

Em uma aplicação comum, podemos imaginar:

contador += 1
Enter fullscreen mode Exit fullscreen mode

e esperar que exista apenas um contador.

Mas em Spark temos:

Driver
  +
Executor 1
  +
Executor 2
  +
Executor 3
Enter fullscreen mode Exit fullscreen mode

Então surge uma pergunta:

O que significa "variável compartilhada" em uma aplicação distribuída?

No próximo artigo vamos explorar:

  • closures;
  • serialização de funções;
  • variáveis capturadas;
  • Driver vs. Executors;
  • por que alterar uma variável dentro de uma Task não funciona como muitos esperam;
  • Broadcast Variables;
  • Accumulators;
  • quando utilizar cada mecanismo;
  • armadilhas de side effects;
  • e como pensar em estado em computação distribuída.

Vamos sair disto:

counter = 0

for item:
    counter += 1
Enter fullscreen mode Exit fullscreen mode

para isto:

              DRIVER
                 │
          serialized closure
        ┌────────┼────────┐
        ▼        ▼        ▼
     Executor Executor Executor
        │        │        │
      Task     Task      Task
Enter fullscreen mode Exit fullscreen mode

E entender por que código aparentemente simples muda completamente quando atravessa máquinas.


← Parte 7: Memória — Executors, Driver, Cache, Spill e OutOfMemory

Próximo: Parte 9 — Variáveis compartilhadas no Apache Spark: Broadcast, Accumulators e Closures

Top comments (0)