o problema
MapReduce e Dryad deram ao programador uma abstração para a CPU do cluster e nenhuma para a memória do cluster. A consequência aparece na hora de reusar um resultado intermediário: o único caminho entre dois jobs é o sistema de arquivos distribuído, com replicação, I/O de disco e serialização em cada passagem. Para uma classe grande de aplicações — regressão logística, k-means, PageRank, qualquer coisa que itere sobre o mesmo dataset — esse custo domina o tempo de execução. Mineração interativa é ainda pior: cada query ad-hoc relê tudo do disco.
O problema já tinha sido reconhecido, e a resposta da época foi construir um framework por padrão de acesso. Pregel mantém o estado dos vértices em memória entre superpassos; HaLoop dá uma interface de MapReduce iterativo. Mas cada um compartilha dado implicitamente, só para o padrão que implementa: ninguém consegue carregar dois datasets na memória e decidir depois o que perguntar. As abstrações genéricas de memória distribuída — DSM, key-value stores, Piccolo, bancos — oferecem escrita fina em endereços arbitrários, e com escrita fina só existem dois jeitos de tolerar falha: replicar os dados ou logar as atualizações pela rede. As duas coisas empurram volumes grandes por um link muito mais lento que a RAM.
a ideia
A saída foi restringir a interface em vez de generalizá-la. Um RDD é uma coleção read-only e particionada de registros, que só pode ser criada por operações determinísticas sobre storage estável ou sobre outros RDDs. Como toda transformação é grossa — a mesma operação aplicada a muitos registros — o sistema pode logar a receita em vez do resultado. Esse log é a linhagem: um grafo de quem veio de quem.
A propriedade que sustenta o resto é simples de enunciar: um programa não consegue referenciar um RDD que ele não saiba reconstruir. Perdeu uma partição, o sistema sabe qual transformação a produziu e a partir de qual partição do pai, e recomputa só ela, em paralelo com as outras, sem rollback do programa. Checkpoint deixa de ser obrigatório e vira otimização.
como funciona
Cada RDD expõe cinco coisas: a lista de partições, as dependências nos pais, uma função que computa uma partição a partir dos iteradores dos pais, as localizações preferidas e o particionador. Foi o suficiente para implementar a maioria das transformações em menos de 20 linhas cada.
val lines = spark.textFile("hdfs://...")
val errors = lines.filter(_.startsWith("ERROR"))
errors.persist()
errors.count()
Nada roda até o count: as transformações são preguiçosas e só uma action dispara trabalho. persist marca o que vale guardar — lines nunca entra na RAM.
A distinção que organiza o scheduler é entre dependência estreita (cada partição do pai serve no máximo uma partição do filho: map, filter) e larga (várias filhas dependem da mesma partição do pai: groupByKey, join sem co-particionamento). Numa action, o scheduler percorre a linhagem e monta um DAG de estágios, cortando exatamente nas dependências largas e nas partições já materializadas em memória; dentro de um estágio, as transformações estreitas viram pipeline num nó só. Tarefa é colocada por localidade com delay scheduling. Saída de shuffle é materializada nos nós pais, como no MapReduce. A memória usa LRU no nível de RDD, com a regra de nunca despejar partição do mesmo RDD que está sendo escrito. São 14.000 linhas de Scala rodando sobre Mesos, lendo de qualquer input source do Hadoop, mais duas mudanças no interpretador Scala — servir os bytecodes das linhas por HTTP e mudar a geração de código — para o shell interativo funcionar no cluster.
o que isso custou
A restrição é real e os autores a declaram: RDD não serve para atualização fina e assíncrona de estado compartilhado. Backend de aplicação web, crawler incremental — isso é território de log de updates e checkpoint, e o paper entrega esses casos para bancos, RAMCloud, Percolator e Piccolo.
Linhagem também não é grátis em toda topologia. Com cadeias longas e dependências largas, como o ranks do PageRank, a queda de um nó pode custar uma fatia de cada RDD ancestral e forçar recomputação completa; a resposta é checkpoint, e a escolha de o que salvar ficou com o usuário (automatizar era trabalho futuro). Falha do scheduler não é tolerada.
O ganho também é proporcional ao quanto do tempo era I/O. Em regressão logística, 25,3× sobre o Hadoop com 100 máquinas; em k-means, onde a computação domina, 1,9× a 3,2×. E memória insuficiente degrada de forma gradual, não catastrófica, mas degrada: com metade do dataset em RAM a iteração levou 40,7 s, contra 11,5 s com tudo em memória.
onde isso aparece hoje
O paper mostra que a abstração engole os frameworks especializados que a antecederam: Pregel e HaLoop viraram bibliotecas de 200 linhas sobre Spark, e MapReduce é flatMap mais reduceByKey. Essa foi a evidência de generalidade — e o motivo de o Spark ter substituído a camada de execução de boa parte do ecossistema Hadoop depois de aberto.
O shuffle, que aqui já era o limite dos estágios, continuou sendo o ponto caro e virou objeto de trabalho próprio, como o Magnet. E a ideia de tratar armazenamento aberto e processamento como camadas separadas, com o motor mantendo o que importa em memória, reaparece na arquitetura de Lakehouse.