Em uma plataforma de ad tech entre as 10 maiores, processando 1,5 bilhão de registros por dia, a conta de nuvem estava se tornando uma preocupação existencial. Não porque os pipelines estivessem quebrados — eles funcionavam. Os dados chegavam no horário, as transformações estavam corretas, os sistemas downstream eram alimentados. O problema era mais simples e mais perigoso: os custos cresciam mais rápido que a receita.

A plataforma rodava centenas de jobs Spark em dezenas de clusters EMR, processando petabytes de dados de impressões de anúncios, cliques e conversões. Todos os dias, esses pipelines ingeriam streams de eventos brutos, os enriqueciam com segmentos de audiência e metadados de campanha, computavam modelos de atribuição e produziam as tabelas de relatório que alimentavam o faturamento e os analytics. A conta de infraestrutura passava de sete dígitos anualmente — e subia 15-20% trimestre a trimestre enquanto a receita crescia 10%.

A liderança fez uma pergunta direta: "Podemos cortar 30% sem quebrar nada?"

Entreguei 40%. Não através de um único avanço, mas por meio de uma abordagem sistemática que tratava cada decisão de pipeline como uma decisão econômica. Foi assim que aconteceu.

Pensando em dólares, não em milissegundos

A primeira coisa que tive que desaprender — e ajudar o time a desaprender — foi o instinto de otimizar para velocidade. Na engenharia de software tradicional, performance significa latência. Mais rápido é melhor. Mas em escala de petabyte, mais rápido nem sempre é mais barato. Às vezes, o pipeline mais rápido é o mais caro, porque atinge a velocidade jogando compute no problema.

Considere um job Spark que processa um dia de dados de impressão. Você pode rodá-lo em 200 nós e terminar em 15 minutos, ou em 50 nós e terminar em 45 minutos. A primeira opção custa 4x mais em compute, mas entrega o mesmo resultado com o mesmo SLA downstream. Se o seu SLA é "dados disponíveis até as 8h" e ambas as opções terminam antes das 8h, você está pagando um prêmio de 4x por velocidade que ninguém precisa.

Essa mudança mental — de "quão rápido isso pode rodar?" para "qual é a forma mais barata de atender o SLA?" — foi a base de tudo que veio depois. Cada decisão de otimização virou uma pergunta de economia: quanto isso custa, quanto custa a alternativa e qual é a restrição real do negócio?

Construí um modelo de custo que atribuía uma cifra em dólares a cada pipeline importante. Não só a conta de compute — o custo total: compute, armazenamento, transferência de dados, desperdício de execuções falhas e o tempo de engenharia gasto babysitting jobs instáveis. Quando você vê que um único join mal particionado é responsável por US$ 14.000/mês em compute relacionado a shuffle, a prioridade de otimização fica óbvia.

As quatro alavancas

Depois de fazer profiling em cada pipeline importante e mapear os custos às causas raiz, surgiram quatro alavancas de otimização. Elas não têm peso igual — a estratégia de particionamento foi a maior alavanca isolada, responsável por quase metade da economia total. Mas todas as quatro se combinam.

As Quatro Alavancas de Custo Ordenadas por impacto — particionamento domina Estratégia de Partição ~45% da economia Chaves de partição erradas causam explosões de shuffle. Rebalancear com base na distribuição real dos dados, não em suposições. Otimização de Armazenamento ~25% da economia Formatos colunares, compressão, políticas de ciclo de vida, armazenamento em camadas. Dimensionamento de Cluster ~20% da economia Instâncias spot, autoscaling, dimensionamento correto de compute vs memória. Refatoração de Pipeline ~10% da economia Dividir monolitos em estágios reinicializáveis. Quatro alavancas, com pesos diferentes. Comece pela partição — é quase metade do ganho.

Alavanca 1: Estratégia de particionamento

Esse foi o maior ganho isolado. Os pipelines da plataforma haviam sido construídos ao longo dos anos por times diferentes, cada um escolhendo chaves de partição com base na intuição em vez de análise da distribuição dos dados. O pipeline de atribuição — o job mais caro da frota — particionava por ID do anunciante. Razoável em teoria: você quer agrupar todos os eventos de um anunciante para o cálculo de atribuição.

O problema é que a distribuição dos dados entre anunciantes seguia uma lei de potência severa. Os 50 maiores anunciantes geravam 40% de todo o volume de impressões. Isso significava que, durante a fase de shuffle, um punhado de partições era 100x maior que a mediana. Essas partições superdimensionadas afunilavam o job inteiro. O Spark terminava 95% das tasks em 10 minutos e depois gastava outros 40 minutos esperando os poucos stragglers que processavam as partições gigantes. O cluster ficava em grande parte ocioso durante essa cauda, queimando compute para nada.

A correção foi uma chave de partição composta: ID do anunciante combinado com um bucket de tempo. Para anunciantes grandes, subdividimos os dados em blocos horários. Para anunciantes pequenos, diário bastava. O volume de shuffle caiu 60%, e o runtime do job foi de 50 minutos para 18 minutos no mesmo cluster — ou, alternativamente, podíamos rodá-lo em um cluster menor em 30 minutos e cortar os custos de compute em 55%.

Alavanca 2: Otimização de armazenamento

A plataforma armazenava dados intermediários em formatos orientados a linhas em vários pipelines — um resquício de uma era em que os jobs foram originalmente escritos. Mudar para armazenamento colunar com compressão agressiva reduziu o footprint de armazenamento em 70% para esses datasets. Mas o maior ganho não foi a conta de armazenamento em si — foi a performance de leitura. Formatos colunares com partition pruning permitiam que jobs downstream lessem apenas as colunas necessárias, reduzindo o I/O em 3-5x. Menos I/O significa runtimes menores, o que significa menos compute.

Também implementamos políticas de ciclo de vida que moviam os dados por camadas de armazenamento: armazenamento quente para os últimos 7 dias (acessados com frequência para debug e reprocessamento), armazenamento morno para 8-90 dias, e arquivo frio depois disso. Só os dados brutos de eventos consumiam gasto significativo em armazenamento quente quando 95% deles nunca eram acessados depois da primeira semana.

Alavanca 3: Dimensionamento de cluster

A maioria dos times superprovisiona em 2-3x. É um comportamento racional: se um job falha porque o cluster era pequeno demais, você é paginado às 3 da manhã. Se um job tem sucesso porque o cluster era grande demais, ninguém nota o desperdício. A estrutura de incentivos recompensa o superprovisionamento.

Consertamos isso fazendo profiling da utilização real de recursos de cada job importante — CPU, memória, I/O de disco, rede — ao longo de duas semanas de execuções. Os dados eram condenatórios. O cluster mediano usava 35% da memória provisionada e 45% da CPU. Redimensionamos agressivamente, substituindo instâncias on-demand por instâncias spot (com tratamento gracioso para interrupções de spot), e implementamos políticas de autoscaling que casavam o tamanho do cluster com a carga real.

O insight chave: instâncias otimizadas para compute para jobs CPU-bound (agregações, joins), otimizadas para memória para jobs com tabelas grandes de broadcast ou requisitos de cache. A configuração anterior usava um único tipo de instância para tudo — a opção general-purpose mais cara.

Alavanca 4: Refatoração de pipeline

Vários pipelines críticos eram monolíticos: uma única aplicação Spark que lia dados brutos, aplicava uma dúzia de transformações e escrevia a saída final. Quando esses jobs falhavam no passo 10 de 12, o pipeline inteiro tinha que reiniciar do zero. Nos volumes de dados que processávamos, um único restart podia desperdiçar dezenas de milhares de dólares em compute.

Quebramos os piores ofensores em estágios discretos com saídas intermediárias com checkpoint. Cada estágio podia ser reiniciado independentemente. Isso não tornou o caminho feliz mais rápido, mas reduziu dramaticamente o desperdício com falhas. Quando um timeout de API downstream causava uma falha de job no estágio de enriquecimento, reiniciávamos do checkpoint de enriquecimento em vez de reler e reprocessar petabytes de dados brutos.

Os ganhos específicos do Spark

Além das quatro alavancas estruturais, várias otimizações específicas do Spark produziram retornos desproporcionais. Essas são as técnicas que uso primeiro em qualquer engajamento de otimização de custo em Spark.

Estratégia de Partição: Antes vs. Depois Antes: Partições Desbalanceadas Particionado apenas por advertiser_id A1 A2 A3 A4 A5 A6 A7 Tasks stragglers afunilam o job inteiro Rebalancear Depois: Partições Balanceadas Chave composta: advertiser_id + time_bucket A1h1 A1h2 A2h1 A2h2 A3 A4 A5+ Distribuição uniforme Sem stragglers Shuffle: 2,4 TB • Runtime: 50 min Shuffle: 960 GB • Runtime: 18 min

Broadcast joins para tabelas de dimensão

O pipeline de enriquecimento fazia join do stream massivo de impressões (bilhões de linhas) com várias tabelas de dimensão: metadados de campanha (~500 mil linhas), segmentos de audiência (~2 milhões de linhas) e configuração de anunciante (~50 mil linhas). Esses joins eram implementados como shuffle joins padrão, o que significa que os dois lados do join eram redistribuídos pelo cluster. Para cada join, o dataset inteiro de impressões era shuffled — centenas de gigabytes de I/O de rede para um join contra uma tabela que cabia em memória em um único executor.

Mudar para broadcast joins nessas tabelas de dimensão eliminou o shuffle por completo. As tabelas pequenas eram transmitidas a cada executor, e o join acontecia localmente. O runtime do pipeline de enriquecimento caiu 40%, e o I/O de rede caiu mais de 70%. A economia total de compute dessa única mudança foi substancial — ela rodava a cada hora em toda a frota.

Partition pruning via reestruturação no momento da escrita

Vários jobs downstream liam de uma grande tabela de eventos, mas só precisavam dos dados para intervalos de datas e tipos de evento específicos. A tabela era particionada por data, mas não por tipo de evento. Isso significava que cada job tinha que ler todos os dados do dia e filtrar em memória — escaneando terabytes para usar gigabytes.

Reestruturamos o caminho de escrita para adicionar o tipo de evento como dimensão de partição. Jobs downstream que precisavam apenas de eventos de clique agora podiam ler só a partição de cliques, pulando completamente os dados de impressão e conversão. O volume de leitura para alguns jobs caiu 80%. O caminho de escrita ficou um pouco mais complexo, mas a economia agregada de leitura superou em muito o overhead de escrita, já que os dados eram escritos uma vez e lidos dezenas de vezes.

Adaptive query execution

O adaptive query execution (AQE) do Spark estava disponível, mas desabilitado na plataforma — o time original havia encontrado um bug em uma versão inicial e nunca reabilitou depois que foi corrigido. Habilitar o AQE com thresholds bem ajustados nos deu coalescing automático de partições (mesclagem de partições pequenas), otimização de skew join (divisão de partições superdimensionadas no momento do join) e dimensionamento dinâmico de shuffle partitions. Essas eram melhorias "de graça" — sem mudanças de código, só configuração. Elas cortaram de 8 a 12% dos runtimes em geral.

Cache de resultados intermediários

O pipeline de atribuição computava um dataset intermediário — eventos enriquecidos com pesos de atribuição — que era consumido por três computações downstream separadas: agregação de faturamento, rollups de relatórios e detecção de anomalias. Cada job downstream rederivava esse intermediário a partir dos dados brutos independentemente. Três jobs fazendo o mesmo trabalho caro.

Refatoramos isso em uma computação compartilhada: o dataset intermediário era materializado uma vez e consumido pelos três jobs downstream. A economia de compute foi próxima de 3x nessa parte do pipeline, e simplificou o debug porque havia uma versão da verdade em vez de três versões computadas independentemente.

O que não funciona

Nem toda otimização vale a pena. Algumas das melhorias mais tentadoras, na verdade, pioram as coisas quando você considera o custo total — incluindo complexidade, carga de manutenção e o risco de quebrar um pipeline que está funcionando.

Micro-otimizações que economizam 2% mas adicionam complexidade. Tínhamos engenheiros ansiosos para otimizar a performance de UDFs reescrevendo lógica Python em Scala. A melhoria era real — cerca de 3% naquele estágio específico — mas introduzia um codebase multilinguagem que a maior parte do time não conseguia debugar. O custo de manutenção excedeu a economia em três meses. A regra que estabeleci: nenhuma otimização que exija conhecimento especializado para manter, a menos que a economia seja grande o suficiente para justificar ownership dedicado.

Over-engineering de compressão que atrasa leituras. Experimentamos compressão de nível máximo em datasets intermediários. Os custos de armazenamento caíram, mas o tempo de descompressão aumentou tanto que o efeito líquido foi mais caro — o custo de compute das leituras mais lentas excedeu a economia de armazenamento. O ponto ideal era compressão moderada: redução substancial de armazenamento com overhead de descompressão desprezível.

Cache prematuro. Cache parece uma vitória grátis, mas cada dataset em cache consome memória do executor. Faça cache das coisas erradas e você reduz a memória disponível para a computação real, causando spills para disco que atrasam tudo. Descobrimos que apenas datasets lidos mais de duas vezes dentro da mesma aplicação se beneficiavam do cache. Todo o resto era mais barato recomputar.

O Efeito Composto do Custo Pequenos percentuais viram números gigantes em escala 5% de economia por execução do job × 24 execuções/dia (por hora) × 30 clusters na frota × 365 dias por ano 5% × 24 × 30 × 365 = 1.314.000 execuções de job otimizadas por ano Se cada execução custa US$ 8, uma economia de 5% = US$ 525.600/ano Em escala de petabyte, "só 5%" nunca é "só 5%"

O efeito composto

A coisa mais importante de entender sobre otimização de custo em escala de petabyte é que pequenas melhorias se multiplicam. Uma melhoria de 5% em uma única execução de job é pouco notável. Mas quando esse job roda a cada hora, em 30 clusters, 365 dias por ano, esses 5% se compõem em mais de um milhão de execuções otimizadas — e centenas de milhares de dólares em economia anual.

Deixe eu mostrar a matemática real do engajamento. O pipeline de atribuição rodava a cada hora em 8 clusters. Antes da otimização, cada execução custava aproximadamente US$ 12 em compute. Depois do rebalanceamento de partições (redução de 40%), broadcast joins (redução de 15% além disso) e dimensionamento de cluster (outros 20%), o custo por execução caiu para cerca de US$ 5. Aquele único pipeline — um entre cerca de 40 pipelines importantes — economizou US$ 490.000 anualmente.

O pipeline de enriquecimento era parecido: execuções horárias, 12 clusters, US$ 18 por execução antes da otimização. Broadcast joins e partition pruning o levaram para US$ 9. Economia anual: mais US$ 570.000. E assim por diante em toda a frota.

A redução total de 40% veio de dezenas de otimizações individuais, cada uma modesta por si só, compondo-se através da frequência e do tamanho da frota. Nenhum truque isolado economizou 40%. O sistema economizou.

É por isso que sou cético em relação a histórias de otimização do tipo "um truque estranho". Em escala, a redução sustentável de custo vem da aplicação sistemática da alavanca certa em cada pipeline, não de encontrar uma bala de prata.

O playbook de otimização: por onde começar

Quando recebo uma frota de pipelines Spark caros, sigo uma ordem diagnóstica específica. A ordem importa porque é classificada por impacto — você quer consertar os problemas de maior alavancagem primeiro, e os achados de cada passo informam o próximo.

Árvore de Decisão de Otimização Siga esta ordem — comece pela alavanca de maior impacto Pipeline caro demais? 1. Verifique skew de partição Maior alavanca — quase metade da economia Rebalancear chaves de partição Corrigir 2. Verifique volume de shuffle Broadcast joins, partition pruning Broadcast + pruning Corrigir 3. Verifique dimensionamento Utilização de CPU/memória, tipos de instância Redimensionar + spot Corrigir 4. Considere refatoração Dividir monolitos, estágios com checkpoint Estágios + checkpoint Corrigir Por que esta ordem? Correções de partição reduzem o shuffle, o que muda as necessidades de cluster e as prioridades de refatoração. Os achados de cada passo mudam a abordagem ideal para o próximo. Corrija as partições primeiro — tudo downstream muda.

Essa ordem não é arbitrária. Correções de partição reduzem o volume de shuffle, o que muda o perfil de recursos do seu cluster, o que muda se a refatoração vale a pena. Se você redimensionar o cluster antes de corrigir o skew de partição, vai ter que redimensionar de novo depois. Se você refatorar o pipeline em estágios antes de corrigir os joins, pode dividir nos lugares errados. Comece no topo do funil.

Documentando a economia

Aqui está a coisa que ninguém te conta sobre otimização de custo em escala: a economia é frágil. Toda decisão de otimização existe por uma razão — uma distribuição específica de dados, um padrão de acesso particular, um perfil de recursos medido. Quando essas condições mudam (e vão mudar), a otimização pode se tornar contraproducente.

A estratégia de partição que economizou 55% no pipeline de atribuição foi calibrada para uma distribuição específica de tamanhos de anunciantes. Se a plataforma onboarda três novos anunciantes massivos, os limites da chave composta precisam mudar. Os thresholds de broadcast join foram definidos com base nos tamanhos das tabelas de dimensão — se essas tabelas crescerem 10x, os broadcasts vão causar falhas de out-of-memory.

Documentei cada decisão de otimização com três coisas: o que mudamos, por que a abordagem original era cara (com dados), e quando a otimização deve ser revisada. Para o rebalanceamento de partições: "Revisar quando qualquer anunciante isolado exceder 8% do volume diário total." Para os broadcast joins: "Revisar se qualquer tabela de dimensão exceder 2 GB." Para o dimensionamento de cluster: "Refazer o profiling trimestralmente ou após qualquer mudança importante de carga."

Essa documentação não era verniz opcional — era uma parte crítica da própria otimização. Sem ela, o próximo engenheiro que encontrar um problema de performance não vai entender por que a chave de partição parece estranha, não vai saber os thresholds, e provavelmente vai "consertar" a otimização de volta para algo que custa 40% mais.

Desde então formalizei isso em uma abordagem estruturada de base de conhecimento — onde cada decisão de infraestrutura, sua razão e seus gatilhos de revisão são capturados em um sistema cruzado e mantido. Você pode ler sobre essa abordagem em A Estratégia de Base de Conhecimento. O princípio é o mesmo: conhecimento institucional que não é capturado é conhecimento institucional que já está sendo perdido.

Conclusão Principal

Otimização de custo em escala de petabyte é uma disciplina de engenharia, não um conjunto de truques. As ferramentas são diretas — rebalanceamento de partições, broadcast joins, dimensionamento de cluster, decomposição de pipeline. A parte difícil é abordar isso sistematicamente: medir antes de otimizar, entender a economia de cada decisão, seguir a ordem diagnóstica que maximiza o impacto e documentar tudo para que a economia persista.

Aqui está o que eu diria a qualquer time olhando para uma conta de nuvem que cresce mais rápido que a receita:

  • Comece pelo modelo de custo. Você não pode otimizar o que não consegue medir. Associe valores em dólares a cada pipeline importante antes de tocar em qualquer código.
  • Corrija as partições primeiro. Em cada engajamento que fiz, skew de partição é a maior fonte isolada de desperdício. Também é a mais satisfatória de corrigir — os números de antes/depois são dramáticos.
  • Pense em SLAs, não em velocidade. O pipeline mais barato que atende à restrição de negócio é o pipeline certo. Mais rápido que o necessário é desperdício.
  • Pequenas melhorias se compõem. Não busque balas de prata. Uma melhoria sistemática de 5% em 40 pipelines rodando a cada hora produz mais economia que uma melhoria heroica de 50% em um único job.
  • Documente a economia. Toda otimização tem prazo de validade. Capture as premissas para que engenheiros futuros saibam quando revisar.

A liderança pediu 30%. Entregamos 40%. Não porque achamos um truque inteligente, mas porque tratamos cada pipeline como um sistema econômico e otimizamos o sistema, não as partes.

Custos de pipeline crescendo mais rápido que o seu negócio?

Já otimizei pipelines em escala de petabyte e posso ajudar você a encontrar os ganhos escondidos na sua infraestrutura.

Agendar Conversa de Descoberta Leia o Estudo de Caso