Curso
Você já tentou depurar um trabalho do Spark que falhou repentinamente e descobriu que estava completamente perdido por causa da profundidade da toca do coelho do Spark?
Quando trabalhei pela primeira vez com o Apache Spark, pensei que bastava escrever algumas transformações do PySpark e que o Spark seria "magicamente" escalonado no cluster. Eu estava errado. O desempenho do Spark depende inteiramente da compreensão do que está acontecendo nos bastidores.
Este guia é para qualquer pessoa que não queira tratar o Spark como uma caixa preta. Veremos como a arquitetura do Spark foi projetada, desde o modelo de mestre-trabalhador e o fluxo de trabalho de execução até o gerenciamento de memória e os mecanismos de tolerância a falhas.
Se você deseja criar aplicativos de Big Data rápidos, tolerantes a falhas e eficientes, você está no lugar certo!
Arquitetura básica do Apache Spark
Antes de você escrever sua primeira linha no PySpark, o Spark já tomou algumas decisões arquitetônicas para você. O Spark não é rápido apenas por causa da computação na memória, mas porque foi desenvolvido em uma arquitetura de mestre-trabalhador que é dimensionada e sobrevive ao caos do mundo real, como falhas de nóshes, problemas com a máquina virtual Java (JVM) e volumes de dados inconsistentes.
Vamos detalhar a arquitetura principal do Spark e por que ele ainda é tão poderoso e presente nos fluxos de trabalho modernos de Big Data.
Paradigma do mestre-trabalhador
No centro do Spark está o modelo de mestre-trabalhador . Pense nisso da seguinte forma:
- Driver (mestre): Esse é o cérebro do Spark. Ele executa sua função
main()
, cria o contexto do Spark, lida com o agendamento do DAG e informa ao cluster o que você deve fazer. - Executores (trabalhadores): Esses são os músculos. Eles executam suas tarefas, mantêm os dados na memória e informam ao driver.
Essa configuração permite que você se concentre na definição das transformações, e o Spark decide onde e como executá-las em paralelo nos executores.
O que eu gosto nesse design é que ele é independente da implantação. O mesmo código é executado, independentemente de você implantá-lo em seu computador local, no Kubernetes ou no Mesos. Isso facilita o desenvolvimento e o teste local e, em seguida, o dimensionamento para clusters sem que você precise reescrever o código.
E aqui está outro benefício poderoso da separação entre motorista e trabalhador da Spark: Ele melhora o isolamento de falhas. Se um nó de trabalho morrer durante a execução de uma tarefa, o Spark poderá reatribuir essa tarefa a outro trabalhador sem que você tenha que interromper o aplicativo.
Componentes principais
Vamos detalhar o que está acontecendo dentro do driver e dos nós.
Arquitetura Spark. Imagem do autor.
Contexto Spark
Quando você chama SparkContext()
ou usa SparkSession.builder.getOrCreate()
, está abrindo a porta de entrada para toda a magia interna do Spark.
O contexto do Spark:
- Conecta-se ao seu gerenciador de cluster
- Atribui executores
- Mantém o controle do status do trabalho e dos planos de execução
O Spark constrói um DAG ( Directed Acyclic Graph, gráfico acíclico dirigido ) de transformações nos bastidores. Esse DAG é dividido em estágios e tarefas e, em seguida, executado em paralelo.
O agendador de DAG descobre quais tarefas podem ser executadas juntas e o agendador de tarefas as atribui aos executores. Enquanto isso, o gerenciador de blocos garante que os dados sejam armazenados em cache, embaralhados ou recarregados conforme necessário.
Esse design em camadas torna o Spark incrivelmente flexível, pois você pode ajustar a memória, o armazenamento e a computação de forma independente.
Se você estiver trabalhando com transformações do Spark ou engenharia de recursos, confira o curso Feature Engineering with PySpark para ver essa arquitetura em ação.
Tempo de execução do executor
Os executores são onde o trabalho é feito.
Cada executor é executado:
- Uma ou mais tarefas (com threads)
- Um pedaço de memória para armazenar dados em cache e embaralhar a saída
- Sua própria instância de JVM, isolada das demais
Você pode configurar a quantidade de memória que cada executor obtém, quantos núcleos ele usa e se deve gravar no disco quando a memória acabar.
Mas tenha cuidado: Se você não alocar memória suficiente, receberá erros de falta de memória o tempo todo. No entanto, você também deve evitar alocar muita memória, pois isso desperdiça recursos. O monitoramento e o ajuste são essenciais aqui.
Fluxo de trabalho de execução: Do código ao cluster
Escrever código PySpark é bastante simples. Você filtra um DataFrame, faz uma junção, agrega alguma coisa e executa. Mas, por trás dessa API limpa, o Spark está desenvolvendo discretamente um mecanismo de execução que pode distribuir o trabalho em vários nós.
Vamos ver o que acontece nos bastidores.
Conversão de plano lógico para físico
Aqui está o que a maioria dos usuários do Spark não percebe no início: Quando você escreve código PySpark, não está executando nada imediatamente. Você está criando um plano, e o Catalyst Optimizer do Spark pega esse plano e o transforma em uma estratégia de execução eficiente.
Ele funciona em quatro fases:
- Análise: O Spark resolve nomes de colunas, tipos de dados e referências a tabelas, garantindo que tudo seja válido.
- Otimização lógica: É aqui que o Spark aplica regras como predicado pushdown e dobramento constante. Ele otimiza os filtros e combina as projeções.
- Planejamento físico: O Spark considera várias estratégias de execução e escolhe a mais eficiente (com base no tamanho dos dados, no particionamento, etc.).
- Geração de código: Por fim, ele usa a geração de código de estágio completo para produzir bytecode JVM.
O otimizador de catalisador do Spark. Imagem da Databricks.
Portanto, essa cadeia de .select()
, .join()
e .groupBy()
não está apenas sendo executada linha por linha. Ele está sendo analisado, otimizado e compilado em algo que é executado rapidamente em um cluster.
Confira esta Folha de dicas do PySpark se você quiser uma folha de dicas para os comandos mais úteis do PySpark.
Agendador de DAG e criação de estágios
Quando o plano é concluído, o agendador DAG assume o controle.
Ele divide o trabalho em estágios com base em limites de embaralhamento, onde o Spark decide o que acontece sequencialmente e o que pode ser executado em paralelo.
Há dois tipos principais de estágios:
- ShuffleMapStage: Isso envolve um embaralhamento, que geralmente é causado por transformações amplas, como
groupBy()
oujoin()
. Os dados são então particionados e enviados pela rede. Esse tipo de estágio é necessário para calcular o ResultStage. - ResultStage: Esses estágios produzem saída, como gravação em disco ou retorno de resultados para o driver.
Uma coisa importante que aprendi é minimizar os embaralhamentos. Um embaralhamento precisa ocorrer antes do término de um estágio e é caro. Você precisa entender onde eles ocorrem no seu DAG e se pode otimizar ainda mais o seu código para reduzir o número de embaralhamentos.
Ciclo de vida da execução da tarefa
Depois que o agendador de DAG tiver criado todos os estágios, eles poderão ser executados nos diferentes executores.
O ciclo de vida da execução da tarefa é mais ou menos assim:
- Serialização de tarefas: O driver serializa as instruções da tarefa e as envia para os executores.
- Embaralhar a fase de gravação: O Spark grava a saída particionada no disco local.
- Fase de busca: Os executores no próximo estágio buscam os arquivos de embaralhamento relevantes de outros no cluster.
- Desserialização e execução: Os executores desserializam os dados, executam sua lógica e, possivelmente, armazenam em cache ou gravam os resultados.
- Coleta de lixo: A JVM recupera automaticamente a memória que não está mais sendo usada pelos aplicativos Spark. Essa etapa é essencial para evitar vazamentos de memória e garantir que os aplicativos Spark sejam executados sem problemas.
Uma pequena dica de minha própria experiência: se o seu trabalho no Spark travar depois de ter funcionado bem antes, isso geralmente se deve a atrasos na coleta de lixo ou na busca aleatória. Sempre verifique seu código e certifique-se de que você entende a arquitetura do Spark para que possa otimizar esses tópicos de forma eficaz.
Arquitetura de gerenciamento de memória
O gerenciamento de memória do Spark é um tópico muito complexo e pode lhe custar horas de depuração se você não o entender.
Vamos, portanto, dar uma olhada em como o Spark gerencia a memória nos bastidores, para que você esteja ciente disso e possa evitar horas de depuração de código lento ou erros de falta de memória.
Modelo de memória unificado
Antes do Spark 1.6, a memória era estritamente dividida entre a execução (para embaralhamentos e junções) e o armazenamento (para armazenamento em cache). Isso mudou com o Spark 1.6, que introduziu o modelo de memória unificada.
No modelo de memória unificada, os dados são divididos em três pools principais:
- Memória reservada: Uma pequena quantidade de memória é usada para os componentes internos do Spark e para o sistema.
- Você tem memória Spark: É usado para armazenar dados de execução e para armazenamento em cache. Ele é compartilhado dinamicamente. Se você precisar de mais memória para embaralhamento e menos para armazenamento em cache (ou vice-versa), o Spark se adapta.
- Memória do usuário: Espaço para estruturas de dados definidas pelo usuário, necessárias para a execução do código do usuário nos aplicativos Spark.
O pool de memória do Spark é dividido em dois pools:
- Memória do executor: Armazena dados temporários necessários durante os estágios das tarefas de processamento (por exemplo, embaralhamentos, junções, agregações, etc.).
- Pool de memória de armazenamento: Usado para armazenar dados em cache e armazenar estruturas de dados internas.
Essa elasticidade permite que o Spark seja mais flexível com volumes de dados imprevisíveis.
No entanto, isso também significa perder um pouco do controle quando você não sabe o que está acontecendo. Por exemplo, se você cache()
um DataFrame grande, mas também tiver agregações caras no mesmo estágio, o Spark poderá despejar seus dados em cache para abrir espaço para o embaralhamento.
Armazenamento fora da pilha e em colunas
No armazenamento fora da pilha e em colunas do Spark, o mecanismo Tungsten entra em ação.
O Tungsten introduziu várias otimizações que melhoraram o desempenho do Spark:
- Gerenciamento de memória fora da pilha: Agora, o Spark armazena alguns dados fora do heap da JVM, reduzindo a sobrecarga da coleta de lixo e tornando o gerenciamento de memória mais previsível.
- Armazenamento em formato binário: Os dados são armazenados em um formato binário compacto e de fácil armazenamento em cache, o que melhora o uso da CPU e permite a execução vetorizada.
- Algoritmos com reconhecimento de cache: Agora, o Spark pode usar os caches da CPU com mais eficiência, evitando leituras desnecessárias da RAM ou do disco.
E se você estiver trabalhando com DataFrames, já estará usando essas otimizações. Esse é um dos motivos pelos quais incentivo as pessoas a usarem DataFrames e APIs SQL em vez de RDDs brutos. Você obtém toda a potência do Catalyst e do Tungsten sem nenhum ajuste extra.
Se estiver trabalhando com pipelines de limpeza de dados, você verá isso em ação em Cleaning Data with PySpark.
Mecanismos de tolerância a falhas
Se você trabalha com sistemas distribuídos, sabe de uma coisa com certeza: Eles falham. Os nós falham. Erros de rede acontecem. Os executores ficam sem memória e são desligados.
Mas o Spark foi desenvolvido para lidar com esses problemas e garantir que você tenha sucesso em seus trabalhos.
Vamos nos aprofundar em como o Spark garante que seus trabalhos ainda sejam bem-sucedidos, mesmo que ocorram algumas instabilidades.
Rastreamento de linhagem de RDD
Os RDDs (Resilient Distributed Datasets, conjuntos de dados distribuídos resilientes) são a estrutura de dados fundamental do Spark. E elas são chamadas de resilientes por um motivo.
O Spark usa a linhagem para garantir que cada RDD possa ser recalculado no caso de falha de um nó e perda de dados.
Assim, quando um nó falha, o Spark simplesmente recomputa os dados perdidos usando o gráfico de linhagem.
Veja como isso funciona na prática:
- Dependências restritas (como
map()
oufilter()
): O Spark só precisa da partição perdida para recomputar. - Dependências amplas (como
groupBy()
oujoin()
): Talvez você precise buscar dados em várias partições, pois o Spark pode exigir a saída de vários estágios.
O Lineage evita a necessidade de lidar com falhas manualmente. No entanto, se o gráfico de linhagem se tornar muito longo, pois pode conter centenas de transformações, a recomputação dos dados perdidos se tornará cara. É aí que o checkpointing entra em ação.
Checkpointing e registros de gravação antecipada
Quando você se depara com fluxos de trabalho complexos ou trabalhos de streaming, o Spark não pode depender apenas da linhagem. É aí que o checkpointing entra em ação.
Você pode chamar rdd.checkpoint()
para manter o estado atual do RDD em um local de armazenamento confiável (como o HDFS).
Em seguida, o Spark trunca a linhagem. Se ocorrer um erro, ele recarrega os dados diretamente em vez de recomputá-los.
No streaming estruturado, o Spark também usa registros de gravação antecipada (WALs) para garantir que os dados não sejam perdidos em trânsito.
É isso que o torna tão estável:
- Receptores confiáveis: Eles gravam os dados recebidos em logs antes do processamento.
- O executor bate o coração: Esses sinais regulares confirmam que os executores estão vivos e saudáveis.
- Diretórios de ponto de verificação: Para trabalhos de streaming, eles mantêm offsets, metadados e estado de saída para que você possa continuar de onde parou.
O checkpointing é opcional para trabalhos de processamento em lote, mas obrigatório para pipelines de streaming.
Suponha que você tenha um trabalho do Spark que falhou após 10 horas de execução, mas pode retomar de onde parou, graças ao checkpointing e aos WALs.
Recursos arquitetônicos avançados
Até agora, você já viu como o Spark processa trabalhos e lida com memória e falhas.
Nesta seção, vamos nos aprofundar em algumas das atualizações arquitetônicas avançadas que tornam o Spark mais dinâmico, mais em tempo real e mais adaptável.
Execução adaptativa de consultas (AQE)
O AQE foi introduzido no Spark 3.0 e melhora o desempenho da consulta, ajustando dinamicamente os planos de execução em tempo de execução com base nas estatísticas coletadas durante a execução.
Os recursos do AQE incluem:
- Alterne dinamicamente as estratégias de união: Se a sua união de transmissão não couber na memória, o AQE mudará para uma união de mesclagem de classificação.
- Juntar partições embaralhadas: Mesclar pequenas partições embaralhadas em partições maiores, o que reduz a sobrecarga.
- Lidar com dados distorcidos: O AQE pode dividir partições distorcidas para equilibrar o tempo de execução.
Esse recurso é um divisor de águas, pois permite que trabalhos que antes exigiam ajuste manual e tentativa e erro se adaptem em tempo real.
Apenas certifique-se de ativá-lo explicitamente por meio da configuração (spark.sql.adaptive.enabled = true
). E se você estiver executando o Spark 3.0+, não há motivo para não fazê-lo.
Arquitetura de streaming estruturada
O Structured Streaming usa o mecanismo do Spark e o estende para o domínio em tempo real, sem exigir que você aprenda uma API totalmente nova.
Nos bastidores, ele ainda aplica o micro-batching. Mas você pode manuseá-lo:
- Gerenciamento de compensações: O programa Spark rastreia exatamente quais dados foram lidos da sua fonte (Kafka, soquete, arquivo, etc.). Isso oferece fortes garantias de exatamente uma vez quando configurado corretamente.
- Marca d'água: Com as agregações baseadas em tempo, o Spark usa marcas d'água para decidir quando os dados atrasados são tarde demais para serem incluídos. Isso é fundamental para o processamento em tempo de evento.
- Lojas estatais: Quando você faz agregações em janelas ou junções de streaming, o Spark mantém o estado em micro-lotes. Esse estado é armazenado no disco e submetido a pontos de controle para evitar a perda de dados.
O que é poderoso aqui é como o streaming se parece com o batching. Você escreve um groupBy()
ou um filter()
e o Spark cuida de todo o resto, tornando a análise de streaming acessível sem uma cadeia de ferramentas especializada.
Arquitetura de segurança
Se você estiver executando o Spark na produção, especialmente em finanças, saúde ou áreas de negócios semelhantes, precisará saber como o Spark lida com a autenticação, a criptografia e a auditabilidade.
Então, vamos nos aprofundar nesses tópicos e em como o Spark cuida deles.
Autenticação e criptografia
O Spark tem muitos recursos de segurança que você deve habilitar primeiro. Mas, uma vez ativado, o Spark oferece uma caixa de ferramentas sólida para comunicação e autenticação seguras:
- Autenticação (SASL): O Spark usa a camada de autenticação e segurança simples (SASL) para verificar se apenas usuários e serviços autorizados podem enviar trabalhos ou se conectar ao cluster.
- Criptografia em trânsito (AES-GCM, SSL/TLS): O Spark criptografa a comunicação entre os nós usando AES-GCM (criptografia autenticada) ou TLS. Isso protege os dados do trabalho de serem detectados, o que é especialmente importante em ambientes multilocatários ou de nuvem.
- Integração do Kerberos: Se você estiver executando no Hadoop/YARN, o Spark se integra ao Kerberos para autenticação segura do usuário. Isso vincula seus trabalhos Spark diretamente aos sistemas de gerenciamento de acesso e identidade corporativos.
- Controle de acesso à interface do usuário: A Spark Web UI pode vazar informações confidenciais (como registros, caminhos de entrada, consultas SQL), portanto, defina
spark.acls.enable=true
espark.ui.view.acls
espark.ui.view.acls.groups
para restringi-las.
Você pode verificar todos os recursos de segurança na documentação oficial do Spark. Confira e garanta que você habilite os recursos necessários para proteger seus aplicativos Spark.
Auditoria e conformidade
O registro de quem fez o quê e quando também é fundamental.
O Spark oferece suporte a você:
- Registro de eventos: Quando ativado (
spark.eventLog.enabled=true
), o Spark registra cada trabalho, estágio e evento de tarefa no disco. Você pode usar esses logs para reproduzir o histórico do trabalho ou atender aos requisitos de auditoria. - Controle de acesso baseado em função (RBAC): O Spark não fornece RBAC, mas se você estiver usando o Spark por meio de uma plataforma como Databricks, EMR ou OpenShift, geralmente terá RBAC na camada de infraestrutura. O Spark envia trabalhos usando uma identidade definida, que controla o acesso aos dados e aos recursos de computação.
- Mascaramento de dados e controle de acesso na origem: O Spark lê a partir de muitas fontes(Parquet, Delta Lake, Hive etc.), e o controle de acesso que você tem deve ser aplicado lá.
Padrões de otimização de desempenho
O Spark é bastante poderoso e rápido, e pode ser otimizado para ser ainda mais rápido se você souber onde fazer os ajustes necessários.
Há várias áreas em que você pode tentar otimizar para obter o máximo do Spark. Então, vamos nos aprofundar em cada área.
Otimização de embaralhamento
Se o Spark tem um ponto fraco, é o shuffle. Os embaralhamentos ocorrem quando os dados precisam ser movidos entre partições, geralmente após transformações amplas como groupByKey()
, distinct()
ou join()
.
E quando os embaralhamentos dão errado, você pode obter E/S de disco em massa, longas pausas na coleta de lixo ou tarefas distorcidas que nunca terminam.
Veja como você pode melhorar os embaralhamentos:
- Prefira
reduceByKey()
agroupByKey()
:reduceByKey()
agrega localmente antes de embaralhar.groupByKey()
envia tudo pela rede. - Repartir de forma inteligente: Use
.repartition(n)
para aumentar o paralelismo ou.coalesce(n)
para reduzi-lo. Não deixe que você dependa do particionamento padrão do Spark. - Use as junções de transmissão (com sabedoria): Se um conjunto de dados for pequeno o suficiente, transmita-o a todos os funcionários. Defina
spark.sql.autoBroadcastJoinThreshold
para controlar o limite de tamanho. - Evitar
collect()
: Evite isso sempre que possível, pois a transferência de dados para o driver prejudica o desempenho.
Diretrizes de configuração de memória
Ajustar a memória do Spark pode ser uma ciência e tanto, mas você pode usar a lista de verificação abaixo para facilitar o processo:
- Alocar memória suficiente: Comece com pelo menos 6 GB de memória para o cluster do Spark e ajuste de acordo com suas necessidades específicas.
- Considere a fração de memória do Spark: Por padrão, 60% é a fração de memória no Spark. Aumente-o se os aplicativos dependerem muito de operações de DataFrame/Dataset ou se você precisar de mais memória de usuário.
- Use o número correto de núcleos por executor: Normalmente, 3-5 é o ideal. Um número insuficiente leva à subutilização, enquanto um número excessivo leva à contenção de tarefas.
- Habilite a alocação dinâmica (se houver suporte): O Spark pode aumentar ou diminuir a escala dos executores com base na carga de trabalho.
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.maxExecutors=20
- Ajuste a fração de armazenamento: Se você precisar de mais armazenamento em cache, aumente o valor de
spark.memory.storageFraction
. - Monitore e trace o perfil de uso da memória: Utilize ferramentas como o Spark UI ou o VisualVM para monitorar o consumo de memória e identificar gargalos.
O ajuste da configuração da memória pode ajudar bastante. Certa vez, reduzi um trabalho de 30 minutos para 8 minutos adaptando a configuração da memória, sem alterar uma única linha de código.
Fórmulas de dimensionamento de clusters
Essa é a parte em que a maioria das equipes erra, pois adivinham o tamanho do cluster em vez de estimá-lo corretamente.
Mas você pode fazer melhor usando as fórmulas abaixo:
- Determine o número de partições:
- Calcule o número de partições necessárias com base no tamanho dos dados e no tamanho desejado da partição.
- Uma diretriz padrão é ter uma partição para cada 128 MB a 256 MB de dados não compactados.
- Fórmula: Número de partições = arredondar para cima (tamanho total dos dados ÷ tamanho da partição).
- Calcule o número total de núcleos:
- O número de núcleos necessários deve ser suficiente para processar todas as partições em paralelo.
- Fórmula: Total de núcleos = arredondar para cima (número de partições ÷ partições por núcleo).
- Determinar a memória por executor:
- Calcule a quantidade de memória que cada executor precisa com base em seus núcleos, tamanho da partição e sobrecarga.
- Fórmula: Memória por executor = memória base × (1 + porcentagem de sobrecarga).
- Calcule o número de executores:
- Determine o número de executores com base no número total de núcleos e núcleos por executor.
- Fórmula: Número de executores = arredondar para cima (total de núcleos ÷ núcleos por executor).
- Calcular a memória total:
- Calcule a memória total necessária para o cluster com base no número de executores e na memória por executor.
- Fórmula: Memória total = número de executores × memória por executor.
Por exemplo:
- Entrada: 500 GB de dados e um tamanho de partição de ~128 MB
- Partições: ~4.000 partições
- Núcleos: 4.000 partições / 4 partições por núcleo = 1.000
- Memória por executor: Suponha que você tenha 8 GB por executor e 20% de sobrecarga. 8 GB * 1,20 = 9,6 GB
- Executores: 1.000 núcleos / 4 núcleos por executor = 250 executores
- Memória total: 250 executores * 9,6 GB = 2.400 GB
Mas lembre-se: Essa é apenas uma estimativa. Você pode usá-lo como ponto de partida e, em seguida, otimizar ainda mais por meio da criação de perfis.
Tendências arquitetônicas emergentes
O Spark já existe há uma década, mas continua bastante atualizado. Ele está evoluindo mais rápido do que nunca, graças às plataformas nativas da nuvem, à aceleração da GPU e à integração mais estreita do ML.
Se você usa o Spark hoje da mesma forma que usava há três anos, provavelmente está deixando o desempenho de lado e perdendo ótimos recursos novos.
Vamos dar uma olhada em alguns dos mais recentes.
Mecanismo Photon (Databricks)
Se você trabalha com a Databricks, provavelmente já trabalhou com o Photon e já ouviu falar dele.
Se você quiser saber mais sobre o Databricks, recomendo o curso Introduction to Databricks.
O Photon é o mecanismo de última geração da plataforma Databricks Lakehouse que oferece desempenho de consulta rápido a baixo custo. Ele é compatível com as APIs do Spark, portanto você não precisa adaptar seu código do Spark para usá-lo.
Ele ajuda a melhorar significativamente seu código SQL e PySpark.
O Photon inclui os seguintes recursos:
- Execução vetorizada: O Photon processa dados em lotes colunares, aproveitando as instruções da CPU SIMD (Single Instruction, Multiple Data) para executar operações em vários valores simultaneamente. O Spark tradicional usa a execução linha por linha e depende muito da JVM para alocação de memória e coleta de lixo.
- Tempo de execução do C++ (sem sobrecarga de JVM): Não há coleta de lixo do Java, que pode ser um gargalo em grandes trabalhos do Spark. A memória é gerenciada com precisão em C++.
- Otimizações de consulta aprimoradas: O Photon se integra profundamente ao Catalyst Optimizer do Spark, mas também inclui suas otimizações durante a execução (como filtragem de tempo de execução, caminhos de código adaptáveis, otimizações de junção e agregação).
- Aceleração de hardware: Suporte a hardware moderno (como GPUs NVIDIA, conjuntos de instruções AVX-512 para CPUs Intel, processadores Graviton (ARM) no AWS).
Spark sem servidor
O sem servidor é fantástico, pois significa que você não precisa gerenciar clusters, pré-provisionar recursos e só paga pelo tempo em que o Spark estiver em execução.
E o serverless para o Spark já está disponível em serviços como o Databricks Serverless, o AWS Glue e o GCP Dataproc Serverless.
E aqui está o motivo pelo qual isso é incrível:
- Dimensionamento automático: A plataforma dimensiona a computação com base nas necessidades reais do seu trabalho, o que significa que você não precisa adivinhar quantos nós são necessários.
- Custo-benefício: Você só paga pelo que usa. Você não precisa mais pagar por servidores ociosos.
- Simplicidade: Não há necessidade de lidar com a instalação, configuração ou manutenção do cluster, pois isso é feito para você.
- Desempenho: É possível obter tempos de execução mais rápidos, pois a configuração e a instalação são otimizadas para você.
O Spark sem servidor é ideal para análises interativas, trabalhos ad-hoc ou cargas de trabalho imprevisíveis.
Mas tenha cuidado: pipelines consistentes e de longa duração ainda podem ser mais baratos em clusters fixos. Sempre meça o custo e a latência.
Integração do MLflow
Se você está fazendo machine learning em escala e pretende colocar os modelos em produção, o Spark sozinho não é suficiente. Você precisa de princípios de MLOps, como rastreamento de experimentos, versão de modelos e reprodutibilidade. É aí que o MLflow se encaixa.
O MLflow agora se integra ao Spark e traz uma pilha completa de MLOPs para seus pipelines.
Você pode:
- Experimentos do programa: Registre parâmetros, métricas e artefatos de trabalhos de ML do Spark usando
mlflow.log_param()
emlflow.log_metric()
. - Modelos de versão: Salve modelos do site
pyspark.ml
ousklearn
diretamente no registro de modelos do MLflow. - Servir modelos: Implemente modelos treinados em pontos de extremidade REST usando o serviço de modelo do MLflow.
Você não precisa trocar de ferramenta. Você continua a usar o Spark para treinamento, engenharia de recursos e pontuação, enquanto utiliza o MLflow para tarefas de MLOPs.
Conclusão
Se você não sabe muito sobre o Spark, ele é como uma caixa preta gigante. Você escreve um pouco de código PySpark, pressiona run e espera que funcione.
Às vezes isso funcionava bem para mim, às vezes levava a longas sessões de depuração e a descobrir o que estava errado.
Foi só quando comecei a olhar os bastidores que as coisas fizeram sentido para mim. E demorou um bom tempo para que eu entendesse o que estava acontecendo.
Aqui está o que eu focaria se estivesse começando do zero novamente:
- Saiba como o Spark divide seu código em trabalhos, estágios e tarefas.
- Entenda a memória.
- Cuidado com os embaralhamentos.
- Comece pequeno e execute as coisas no modo local. Suje suas mãos.
Foi exatamente isso que aprendemos neste artigo.
Se você quiser continuar aprendendo, aqui estão alguns recursos para iniciantes que eu recomendo:
- Introdução ao PySpark: Um ótimo ponto de partida prático se você ainda estiver se sentindo confortável.
- Limpeza de dados com o PySpark: Aprenda a limpar os dados, pois os dados do mundo real são sempre confusos.
- As 20 principais perguntas da entrevista com o Spark: Isso não é apenas para entrevistas, mas para aprofundar seu entendimento.
- As 4 principais certificações do Apache Spark em 2025: Caso você queira que suas habilidades sejam reconhecidas por meio de certificações.
Aprenda PySpark do zero
Perguntas frequentes
Como escolho o gerenciador de cluster certo para minha implantação do Spark?
O Spark oferece suporte a vários gerenciadores de cluster (YARN, Mesos, Kubernetes e autônomo). Sua escolha depende da infraestrutura existente, das necessidades de compartilhamento de recursos e da experiência operacional: O YARN se integra bem aos clusters do Hadoop, o Kubernetes oferece portabilidade em contêineres e o Mesos se destaca no isolamento de vários locatários.
O que é o serviço de embaralhamento externo e como ele melhora o desempenho?
O serviço de embaralhamento externo desacopla o serviço de arquivo de embaralhamento dos ciclos de vida do executor, permitindo a alocação dinâmica e reduzindo a perda de dados durante o despejo do executor. Ele mantém os arquivos de embaralhamento disponíveis mesmo depois que os executores são desligados, o que acelera as novas tentativas de estágio e conserva a E/S do disco sob carga pesada.
Como as uniões de transmissão funcionam internamente e quando devo usá-las?
Para uniões de difusão, o Spark envia uma pequena tabela de pesquisa a cada executor para evitar o embaralhamento total dos dados. Use-os quando um lado da união estiver abaixo do spark.sql.autoBroadcastJoinThreshold
(padrão 10 MB), pois eles reduzem drasticamente a E/S da rede e aceleram as uniões em distribuições de chaves distorcidas.
Quais são as práticas recomendadas para ajustar a coleta de lixo da JVM no Spark?
Monitore as pausas do GC por meio da interface do usuário do Spark ou de ferramentas como o VisualVM e prefira o coletor G1GC por seus baixos tempos de pausa. Aloque a memória do executor com espaço livre para sobrecarga (spark.executor.memoryOverhead
) e ajuste -XX:InitiatingHeapOccupancyPercent
para acionar o GC mais cedo, evitando longas pausas para parar o mundo.
Como posso aproveitar a aceleração de GPU para acelerar os trabalhos do Spark?
Use o acelerador NVIDIA RAPIDS para o Apache Spark para transferir operações SQL e DataFrame de forma transparente para GPUs. Ele se conecta ao mecanismo de execução do Spark, substituindo os operadores baseados em CPU por equivalentes acelerados por GPU e oferecendo um processamento até 10 vezes mais rápido para cargas de trabalho adequadas.
Qual é a diferença entre alocação estática e dinâmica de recursos no Spark?
A alocação estática fixa o número de executores para o tempo de vida do trabalho, oferecendo previsibilidade ao custo de possíveis recursos ociosos. A alocação dinâmica permite que o Spark solicite ou libere executores com base nas tarefas pendentes e na carga de trabalho, melhorando a utilização do cluster para trabalhos flutuantes, o que é ideal para ambientes compartilhados.
Como devo configurar o Spark para obter o desempenho ideal em sistemas de armazenamento em nuvem, como o S3?
Ative a aceleração de transferência S3, ajuste spark.hadoop.fs.s3a.connection.maximum
e use a visualização consistente (S3A v2) para lidar com a consistência eventual. Agrupe arquivos pequenos antes de gravar e considere os committers do S3A para reduzir a sobrecarga da operação de lista e melhorar a taxa de transferência de gravação.
Como posso proteger as comunicações do Spark com Kerberos e TLS?
Ative o TLS para RPC (spark.ssl.enabled
) e configure o SASL/Kerberos (spark.authenticate and spark.kerberos.keytab
) para impor a autenticação mútua. Armazene as credenciais em um keytab seguro e acessível pelo HDFS e restrinja o acesso à interface do usuário do Spark por meio de configurações de ACL para evitar a exposição não autorizada de dados.
O que são UDFs do Pandas e quando elas são mais eficientes do que as UDFs comuns?
Os UDFs do Pandas (UDFs vetorizados) usam o Apache Arrow para trocar dados em lote entre a JVM e o Python, reduzindo drasticamente a sobrecarga de serialização. Eles superam os UDFs tradicionais de linha por linha para operações numéricas complexas, especialmente ao processar grandes lotes colunares no PySpark.
Que benefícios a API DataSource V2 oferece em relação à V1 para fontes de dados personalizadas?
O DataSource V2 oferece uma interface mais limpa e modular que suporta filtros push-down, poda de partição e fontes de streaming nativamente. Ele permite controle de leitura/gravação refinado e melhor integração com o otimizador Catalyst do Spark, resultando em maior desempenho e facilidade de manutenção para conectores personalizados.
Sou um engenheiro de nuvem com sólida base em engenharia elétrica, aprendizado de máquina e programação. Minha carreira começou na área de visão computacional, com foco na classificação de imagens, antes de fazer a transição para MLOps e DataOps. Sou especialista em criar plataformas MLOps, dar suporte a cientistas de dados e fornecer soluções baseadas em Kubernetes para otimizar os fluxos de trabalho de aprendizado de máquina.