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
Vimos:
Partitions
Tasks
Executors
Shuffle
Memory
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
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
Agora imagine que estamos processando:
10 TB
durante horas.
Nesse período, alguma coisa pode acontecer:
Executor failure
Machine failure
Process crash
Network problem
Disk problem
Container termination
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
Distribuídas assim:
Executor 1
├── P1
└── P2
Executor 2
├── P3
└── P4
Agora o Executor 2 desaparece.
Executor 1
├── P1
└── P2
Executor 2
└── X
Perdemos:
P3
P4
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)
não estamos modificando rdd1.
Estamos descrevendo outro RDD:
RDD1
│
│ map(x * 2)
▼
RDD2
Depois:
rdd3 = rdd2.filter(lambda x: x > 100)
temos:
RDD1
│
│ map
▼
RDD2
│
│ filter
▼
RDD3
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
)
Conceitualmente:
textFile
↓
RDD1
↓
map
↓
RDD2
↓
filter
↓
RDD3
↓
map
↓
RDD4
↓
reduceByKey
↓
RDD5
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
Não precisamos interpretar isso como:
Memory
RDD1 complete
RDD2 complete
RDD3 complete
RDD4 complete
RDD5 complete
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
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(...)
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
Quando uma Action aparece:
rdd4.count()
temos:
Lineage
↓
Action
↓
Job
↓
Stages
↓
Tasks
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
Isso forma um:
Directed Acyclic Graph — DAG
ou:
Grafo Direcionado Acíclico
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
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
Mas isso não significa necessariamente:
5 transformations
=
5 stages
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
Portanto:
Lineage / DAG
↓
Stage boundaries
↓
Tasks
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
)
Podemos ter:
RDD1 P1 ───► RDD2 P1
RDD1 P2 ───► RDD2 P2
RDD1 P3 ───► RDD2 P3
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
Spark pode precisar reconstruir apenas o caminho relevante:
RDD1 P2
↓
map
↓
RDD2 P2
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'
Agora:
P3'
é perdida.
Spark conhece sua dependência:
Source P3
↓
map
↓
P3'
Então pode reconstruí-la.
Não precisa necessariamente recalcular:
P1'
P2'
P4'
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
Mas isso possui custos:
Storage
Network
Replication
Coordination
RDDs introduziram uma ideia poderosa para datasets derivados deterministicamente:
Don't replicate every intermediate result
↓
Keep lineage
↓
Recompute lost partitions
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
)
Temos:
TEXT FILE
↓
raw
↓
parse
↓
parsed
↓
filter
↓
errors
↓
map
↓
by_service
↓
reduceByKey
↓
counts
Agora imagine uma partição de errors sendo perdida.
Spark conhece:
textFile
↓
parse
↓
filter
↓
errors partition
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
)
Como vimos na Parte 4:
Map-side work
↓
Shuffle Write
↓
Network
↓
Shuffle Read
↓
Reduce-side work
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 ───┘
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'
Perdeu P2'?
Recompute P2
↓
P2'
Agora:
Wide
P1 ───┐
P2 ───┼──► P'
P3 ───┤
P4 ───┘
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
Agora utilizamos Dataset X cinco vezes:
Dataset X
/ | \
▼ ▼ ▼
Job A Job B Job C
Sem persistência, determinadas partes da linhagem podem precisar ser recomputadas.
Se Dataset X for caro de produzir, podemos decidir:
dataset_x.cache()
Agora trocamos:
Repeated Computation
por:
Storage
Esse é um trade-off.
16. Cache vs. Recomputation
Podemos pensar assim:
Dataset
│
┌─────────┴─────────┐
▼ ▼
Recompute Cache
│ │
▼ ▼
More CPU More Memory
More Time Storage Cost
│ │
└─────────┬─────────┘
▼
Trade-off
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
Executor 2 falha:
P3 X
P4 X
Spark ainda conhece:
Lineage
e pode reconstruir essas partições quando necessário.
Então:
Cache
e:
Lineage
não são mecanismos concorrentes.
Eles se complementam.
18. persist()
Como vimos na Parte 7, podemos usar:
rdd.persist()
para controlar estratégias de persistência.
Conceitualmente:
Persistence
│
├── Memory
├── Disk
├── Serialized forms
└── Replication options
A escolha depende do workload.
Por exemplo:
Dataset pequeno
+
reutilizado frequentemente
+
caro de recomputar
pode justificar uma estratégia diferente de:
Dataset enorme
+
utilizado uma vez
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
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
Se E precisar ser reconstruído:
Source → A → B → C → D → E
Agora imagine um checkpoint em C:
Source
↓
A
↓
B
↓
C
↓
CHECKPOINT
↓
D
↓
E
A recuperação posterior pode partir do checkpoint:
CHECKPOINT
↓
D
↓
E
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"
)
Depois:
rdd.checkpoint()
Quando o RDD for materializado por uma Action, o checkpoint poderá ser efetivamente produzido.
Por exemplo:
rdd.checkpoint()
rdd.count()
Conceitualmente:
checkpoint()
↓
mark RDD
↓
Action
↓
compute
↓
write checkpoint
Assim como cache, existe interação com lazy evaluation.
22. Checkpoint não é Cache
Essa distinção é fundamental.
Cache
Objetivo principal:
Reuse
↓
Performance
Checkpoint
Objetivo principal:
Materialize reliable state
↓
Truncate lineage
↓
Recovery / lineage management
Simplificando:
CACHE
"Evite recomputar algo que vou reutilizar."
versus:
CHECKPOINT
"Crie um novo ponto materializado e corte a dependência da linhagem anterior."
Os dois podem aparecer juntos.
Mas resolvem problemas diferentes.
23. Cache não corta Lineage
Imagine:
A
↓
B
↓
C
↓
D
Fazemos:
d.cache()
Ainda existe conceitualmente:
A
↓
B
↓
C
↓
D [cached]
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
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
Isso envolve:
- I/O;
- armazenamento;
- tempo;
- gerenciamento.
Portanto, não queremos:
checkpoint everything
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
Se cada iteração depende da anterior, podemos formar uma linhagem extensa.
Conceitualmente:
Initial
↓
I1
↓
I2
↓
I3
↓
...
↓
I100
Em determinadas situações, podemos estabelecer checkpoints intermediários:
Initial
↓
I1
↓
...
↓
I20
↓
CHECKPOINT
↓
I21
↓
...
↓
I40
↓
CHECKPOINT
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
Um desaparece:
Executor 1
Executor 2
Executor 3 X
Executor 4
Isso não implica necessariamente:
APPLICATION FAILED
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
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 ✓
Spark pode tentar executar novamente a Task.
Task 3
↓
Retry
↓
Task 3 ✓
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
porque uma máquina teve um problema temporário.
Retry pode funcionar:
Attempt 1 → FAIL
Attempt 2 → SUCCESS
Agora imagine código:
def transform(x):
return 100 / x
e temos:
x = 0
A Task falha.
Spark tenta novamente.
Attempt 1 → divide by zero
Attempt 2 → divide by zero
Attempt 3 → divide by zero
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
A partição falha.
Spark tenta novamente:
read
↓
fail
↓
retry
↓
read same corrupted data
↓
fail
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
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
não deve ser confundida automaticamente com:
Driver high availability
São problemas relacionados, mas diferentes.
31. Lineage não substitui durabilidade da fonte
Imagine que nosso RDD começa em:
/data/events
e depois:
Source
↓
Map
↓
Filter
↓
RDD
Se perdemos uma partição intermediária, podemos tentar reconstruí-la a partir da fonte.
Mas e se a fonte original desaparecer?
Source X
Agora lineage sabe:
como
reconstruir.
Mas não possui necessariamente:
de onde
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
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")
)
continuamos tendo:
Source
↓
Transformations
↓
Dependencies
↓
Stages
↓
Tasks
A diferença é que agora também existe a camada estruturada que vimos na Parte 6:
DataFrame
↓
Logical Plan
↓
Catalyst
↓
Physical Plan
↓
Distributed Execution
Os fundamentos não desapareceram.
Eles ganharam novas abstrações.
34. Lineage vs. Query Plan
Também não devemos tratar:
Lineage
e:
Query Plan
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
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
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
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
podemos preferir, quando a arquitetura permite:
Move computation
↓
to data
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
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
Agora entendemos por que lineage continua relevante mesmo depois de falarmos sobre shuffle.
39. Recomputation tem custo
Fault tolerance não significa:
failure = free
Imagine:
2-hour computation
↓
lost intermediate data
↓
recompute
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?
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
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"
)
)
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")
)
Temos:
RAW
│
▼
Filter + Select
│
▼
BASE
┌─────┼─────┐
▼ ▼ ▼
Customer Event Revenue
Se base for caro de produzir e reutilizado várias vezes:
base.cache()
pode fazer sentido.
A decisão nasce do DAG.
Não de uma regra genérica de que:
cache = performance
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)
A linhagem pode crescer:
Source
↓
T1
↓
T2
↓
T3
↓
...
↓
T100
Podemos decidir estabelecer checkpoints em pontos apropriados.
Conceitualmente:
Source
↓
T1
↓
...
↓
T25
↓
CHECKPOINT
↓
T26
↓
...
↓
T50
↓
CHECKPOINT
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
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?
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
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
Spark pode identificar trabalho perdido.
Depois:
Lost partition
↓
Find lineage
↓
Find required ancestors
↓
Schedule recomputation
↓
Execute elsewhere
↓
Rebuild data
↓
Continue
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?
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
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
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
com:
Failures
Retries
Recomputation
Shuffle
Memory
GC
Spill
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
Agora acrescente:
Cache
quando queremos evitar recomputação frequente.
E:
Checkpoint
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
E, quando necessário:
Cache / Persist
ou:
Checkpoint
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
podia parecer apenas um nome.
Agora conseguimos decompor:
DISTRIBUTED
↓
Partitions across cluster
DATASET
↓
Collection of data
e:
RESILIENT
↓
Lineage
↓
Recomputation
↓
Fault recovery
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
Precisamos assumir:
Machines fail
Processes fail
Networks fail
Tasks fail
Containers disappear
Então a arquitetura precisa responder:
What happens next?
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(...)
)
não penso apenas:
input
↓
transform
↓
output
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?
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
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
e esperar que exista apenas um contador.
Mas em Spark temos:
Driver
+
Executor 1
+
Executor 2
+
Executor 3
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
para isto:
DRIVER
│
serialized closure
┌────────┼────────┐
▼ ▼ ▼
Executor Executor Executor
│ │ │
Task Task Task
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)