Você já passou horas debugando um Job do AWS Glue que deveria levar 10 minutos, mas levou 2 horas? Ou viu a fatura da AWS no final do mês e se perguntou "por que esses jobs custaram tanto?".
A verdade é que o AWS Glue é uma ferramenta poderosa quando você entende suas nuances. Mas existe uma armadilha silenciosa que pega até engenheiros experientes: a escolha entre usar DynamicFrames (a abstração nativa do Glue) ou DataFrames do Spark puro.
Essa escolha parece técnica e irrelevante no início. Mas ela pode significar a diferença entre um pipeline que processa petabytes eficientemente e um que queima orçamento enquanto arrasta processamento.
Neste artigo, vou abrir a caixa preta do AWS Glue. Vamos entender exatamente o que acontece por baixo dos panos, quando as abstrações ajudam, quando elas atrapalham, e como construir pipelines que sejam rápidos e econômicos.
Spoiler: A resposta não é "use sempre Spark puro" nem "confie cegamente no Glue". Como em tudo na engenharia, é sobre saber quando usar cada ferramenta.
1O Dilema do Engenheiro de Dados
Quando você abre um Job do AWS Glue pela primeira vez, se depara com duas escolhas aparentemente simples: usar o DynamicFrame nativo do Glue ou trabalhar com DataFrames do Spark puro. Essa decisão, frequentemente ignorada, pode significar a diferença entre um pipeline que processa terabytes em minutos e um que custa 5 vezes mais e leva horas para falhar.
O AWS Glue promete abstrair a complexidade do Spark, lidando automaticamente com esquemas variáveis, evolução de dados e gerenciamento de estado. Mas essa "mágica" tem um preço: performance degradada, perda de otimizações críticas e custos operacionais elevados.
A questão central: Quando a conveniência das abstrações do Glue justifica o custo computacional? E quando você deve "sujar as mãos" com Spark puro para obter desempenho máximo?
2A Arquitetura por Trás: DynamicFrame vs DataFrame
2.1 Spark DataFrame: Performance Bruta
O Apache Spark é um mestre em otimização. Quando você cria um DataFrame Spark, o esquema é fixado antecipadamente. Isso permite que o motor Tungsten armazene dados em formato binário compacto na memória, utilizando instruções SIMD (Single Instruction, Multiple Data) da CPU para processar múltiplos registros simultaneamente.
A mágica está na vetorização: uma única instrução de CPU pode aplicar a mesma operação em dezenas de valores de uma vez. Isso é possível porque o Spark sabe exatamente onde cada campo está na memória e qual seu tipo.
2.2 DynamicFrame: Flexibilidade com Custo
O DynamicFrame foi projetado para a realidade caótica dos Data Lakes: arquivos JSON com esquemas inconsistentes, logs onde o campo id ora é string, ora é inteiro. Cada DynamicRecord é autodescritivo — carrega metadados sobre seu próprio esquema.
Essa flexibilidade impede otimizações de baixo nível. O processador precisa verificar o tipo de cada campo, linha por linha, quebrando a capacidade de vetorização. O resultado? Throughput de CPU significativamente menor para operações simples de filtro ou agregação.
Implicação Real: Em benchmarks, transformações aritméticas simples podem executar até 3-5x mais lentas em DynamicFrames comparadas a DataFrames Spark otimizados, especialmente em datasets com esquema uniforme.
2.3 Comparação Técnica Direta
| Característica | AWS Glue DynamicFrame | Spark DataFrame Nativo |
|---|---|---|
| Representação em Memória | Objetos baseados em linha com overhead de metadados por registro | Formato binário Tungsten (colunar/linha otimizada), off-heap, compacto |
| Verificação de Tipos | Runtime, por registro. Impede vetorização SIMD eficiente | Compile-time (Catalyst Analysis). Habilita vetorização total |
| Gestão de Memória | Conservadora; spilling para disco precoce para evitar OOM | Agressiva; maximiza uso de RAM. Risco de OOM se mal configurado |
| Evolução de Esquema | Nativa (ResolveChoice), lida com tipos mistos no mesmo dataset | Requer normalização prévia ou Schema Merging (custoso) |
3O Custo Oculto: toDF() e fromDF()
Um dos anti-padrões mais perigosos no Glue é o "ping-pong" entre DynamicFrame e DataFrame. Desenvolvedores frequentemente fazem:
# ❌ ANTI-PADRÃO: Conversões repetidas df_dynamic = glueContext.create_dynamic_frame.from_catalog(...) df_spark = df_dynamic.toDF() # Conversão 1 df_filtered = df_spark.filter(...) df_dynamic2 = DynamicFrame.fromDF(df_filtered, glueContext, "df") # Conversão 2 df_spark2 = df_dynamic2.toDF() # Conversão 3 😱
Cada conversão toDF() não é gratuita. Ela pode exigir:
- Resolução de ambiguidades de esquema (Choice Types)
- Serialização e desserialização entre formatos internos
- Materialização parcial de dados para inferir esquemas
- Quebra da avaliação preguiçosa (Lazy Evaluation) do Spark
Em grandes volumes, uma única chamada toDF() pode adicionar vários minutos de latência. Em loops ou pipelines iterativos, o impacto é exponencial.
3.1 Estratégia Otimizada: "Convert Once, Process Native"
# ✅ PADRÃO RECOMENDADO
# 1. Leia com DynamicFrame (aproveite leitores C++ do Glue 4.0+)
df_dynamic = glueContext.create_dynamic_frame.from_catalog(
database="db",
table_name="table",
push_down_predicate="(ano == '2024')"
)
# 2. Resolva tipos ambíguos uma única vez
df_resolved = df_dynamic.resolveChoice(specs=[('price', 'cast:double')])
# 3. Converta para Spark DataFrame uma única vez
df_spark = df_resolved.toDF()
# 4. Faça TODO o processamento em Spark nativo
df_result = (df_spark
.filter(col("amount") > 100)
.groupBy("category")
.agg(sum("amount").alias("total"))
.orderBy("total", ascending=False)
)
# 5. Se necessário, converta de volta apenas na escrita
df_dynamic_final = DynamicFrame.fromDF(df_result, glueContext, "final")
glueContext.write_dynamic_frame.from_catalog(df_dynamic_final, ...)
4O Problema da "Caixa Preta": Catalyst Optimizer Bloqueado
O Otimizador Catalyst do Spark é uma das razões pelas quais o Spark é tão rápido. Ele:
- Reordena filtros para executá-los o mais cedo possível (Filter Pushdown)
- Projeta apenas colunas necessárias (Column Pruning)
- Escolhe algoritmos de Join otimizados (Broadcast vs Sort-Merge)
- Funde operações para reduzir passes sobre os dados
Porém, quando você usa transformações nativas do Glue como ApplyMapping, ResolveChoice ou Map, o Catalyst as enxerga como caixas pretas — funções opacas que ele não consegue otimizar.
Exemplo crítico: Se você aplica um ApplyMapping (renomeação de colunas) antes de um filtro, o Catalyst pode não conseguir empurrar esse filtro para a leitura dos dados do S3. Resultado? Ler terabytes desnecessários.
4.1 A Armadilha Mortal: Partition Pruning
Imagine que você tem dados particionados no S3:
s3://bucket/data/ ano=2022/ ano=2023/ ano=2024/mes=01/ ano=2024/mes=02/ ...
No Spark nativo, o seguinte código automaticamente lista apenas as partições relevantes:
# ✅ Spark puro: Partition Pruning automático
df = spark.read.parquet("s3://bucket/data/")
df_filtered = df.filter((col("ano") == 2024) & (col("mes") == 1))
No AWS Glue, se você usar create_dynamic_frame e aplicar .filter() depois, o Glue pode listar TODAS as partições antes de filtrar. Em um Data Lake com milhares de partições, isso pode:
- Levar minutos ou horas só para listar arquivos
- Causar Out of Memory no Driver devido ao tamanho da lista de metadados
- Ler dados desnecessários, aumentando custos de S3 e tempo de I/O
4.2 Solução Obrigatória: push_down_predicate
# ✅ CORRETO: Filtro aplicado no Catálogo antes da listagem
df_dynamic = glueContext.create_dynamic_frame.from_catalog(
database="db",
table_name="table",
push_down_predicate="(ano == '2024' and mes == '01')"
)
# ❌ ERRADO: Filtro aplicado após listagem completa
df_dynamic = glueContext.create_dynamic_frame.from_catalog(
database="db",
table_name="table"
)
df_filtered = df_dynamic.filter(lambda x: x["ano"] == "2024") # Tarde demais!
Ignorar o push_down_predicate é a causa raiz #1 de Jobs Glue que falham por timeout ou OOM no Driver.
5ResolveChoice: A "Mágica" que Custa Caro
Uma das grandes vantagens do Glue é lidar com esquemas em evolução. Se uma coluna price aparece como double em alguns arquivos e como string em outros, o Glue cria um tipo Choice.
A transformação ResolveChoice força uma decisão:
- make_cols: Divide em
price_doubleeprice_string(aumenta uso de memória) - cast: Tenta converter tudo para um tipo (pode perder dados)
- project: Descarta tipos indesejados (pode perder dados)
Diferente de um cast no Spark SQL (que é vetorizado), ResolveChoice opera inspecionando cada registro individualmente em runtime. Isso é intensivo em CPU e impede otimizações em lote.
Recomendação: Se seu Data Lake tem esquemas inconsistentes, use o Glue e ResolveChoice para sanitizar os dados uma vez e salvá-los em um formato limpo (Parquet com esquema unificado). Todos os processamentos subsequentes devem ler esses dados limpos com Spark puro.
6Job Bookmarks: Estado com Overhead
Para cargas incrementais, os Job Bookmarks do Glue rastreiam quais arquivos já foram processados. Conveniente, mas:
- Em buckets S3 com milhões de objetos, a fase de verificação pode ser proibitivamente lenta
- Relatos indicam que Bookmarks podem tornar a fase de leitura até 3x mais lenta
- Bookmarks são difíceis de resetar manualmente em casos de backfill
- Bookmarks só funcionam com DynamicFrames — usar
spark.readignora completamente essa funcionalidade
6.1 Alternativa: Checkpointing Manual Otimizado
# ✅ Alternativa mais controlável e performática
# Salve o último timestamp processado no DynamoDB
last_processed = get_last_processed_timestamp_from_dynamodb()
# Injete como filtro de partição
df = spark.read.parquet("s3://bucket/data/")
df_incremental = df.filter(col("event_time") > last_processed)
# Processe...
# Atualize o estado
save_last_processed_timestamp_to_dynamodb(max_timestamp)
7Custos: Performance Ruim = Fatura Alta
O AWS Glue cobra por DPU-hora (Data Processing Unit). Código ineficiente não só roda devagar — custa literalmente mais dinheiro.
Exemplo Real: Um job que deveria levar 10 minutos, mas leva 30 minutos devido a conversões toDF() excessivas e falta de Partition Pruning, custará 3x mais em DPUs.
Se você processar 100 Jobs por dia, essa diferença pode significar milhares de dólares extras por mês.
7.1 Glue 4.0 e 5.0: Leitores Vetorizados em C++
As versões mais recentes do Glue introduziram leitores otimizados em C++ para Parquet e CSV, que podem superar o Spark open source. Porém, esses leitores são ativados principalmente ao usar glueContext (DynamicFrames).
Isso cria um paradoxo: para obter a leitura mais rápida, use DynamicFrame. Para obter o processamento mais rápido, use DataFrame. A solução? Híbrido estratégico: ler com Glue, converter imediatamente, processar com Spark.
8Anti-Padrões Mortais em PySpark no Glue
8.1 UDFs em Python
UDFs (User Defined Functions) em Python forçam serialização de dados entre a JVM e o processo Python para cada linha. Isso é um matador de performance.
# ❌ ANTI-PADRÃO: UDF Python
from pyspark.sql.functions import udf
from pyspark.sql.types import IntegerType
def my_function(value):
return value * 2
my_udf = udf(my_function, IntegerType())
df_result = df.withColumn("doubled", my_udf(col("value")))
# ✅ CORRETO: Função nativa do Spark
from pyspark.sql.functions import col
df_result = df.withColumn("doubled", col("value") * 2)
8.2 toPandas(): O Caminho para o OOM
Converter um DataFrame distribuído para Pandas traz todos os dados para a memória do Driver. Com datasets grandes, isso causa Out of Memory invariável.
# ❌ NUNCA FAÇA ISSO com grandes volumes df_pandas = df_spark.toPandas() # Traz tudo para o Driver 💥 # ✅ Use Pandas-on-Spark (Koalas) se precisar da API Pandas import pyspark.pandas as ps df_pandas_spark = ps.DataFrame(df_spark) # Continua distribuído
8.3 Loops em DynamicFrames
Iterar sobre um DynamicFrame com loops Python executa a lógica sequencialmente no Driver, anulando o processamento distribuído.
# ❌ ANTI-PADRÃO: Loop que mata o paralelismo
for record in df_dynamic.toDF().collect(): # collect() traz tudo pro Driver!
process(record)
# ✅ CORRETO: Use transformações distribuídas
df_result = df_spark.map(lambda row: process_row(row))
9Protocolo Operacional para Engenheiros de Dados
Baseado em toda essa análise, o seguinte protocolo maximiza performance e minimiza custos:
1. Ingestão Híbrida: Use DynamicFrames apenas na leitura (Extract) para alavancar leitores C++ do Glue 4.0+, Job Bookmarks e tolerância a esquemas variáveis.
2. Conversão Imediata: Aplique ResolveChoice estritamente se necessário e converta para Spark DataFrame o mais cedo possível.
3. Processamento Nativo: Realize todas transformações pesadas (filtros, joins, agregações) com a API nativa do Spark DataFrame.
4. Pruning Explícito: Sempre use push_down_predicate ao ler do Catálogo para datasets particionados.
5. Evite Serialização Cíclica: Elimine padrões de código que alternam repetidamente entre DynamicFrame e DataFrame.
6. Monitoramento Ativo: Não confie apenas no sucesso do job. Monitore tempo de CPU e uso de memória (DPU-hours) para identificar ineficiências ocultas.
10Quando Usar DynamicFrame vs Spark Puro
Use DynamicFrame quando:
- Ingerir dados com esquemas altamente variáveis (JSON de APIs, logs)
- Precisar de Job Bookmarks para processamento incremental simples
- Lidar com evolução de esquema onde colunas mudam de tipo
- Integrar com o AWS Glue Data Catalog para descoberta automática de esquema
Use Spark DataFrame puro quando:
- Dados têm esquema consistente e conhecido
- Performance é crítica (processamento de terabytes/petabytes)
- Precisa de controle fino sobre otimizações (particionamento, broadcasts)
- Pipeline tem agregações complexas, joins múltiplos ou window functions
- Está migrando código Spark existente de EMR ou Databricks
A Engenharia Está nos Detalhes
O AWS Glue não é um substituto do Apache Spark — é um framework de ingestão e orquestração construído sobre o Spark. A conveniência de suas abstrações (DynamicFrame, ResolveChoice, Job Bookmarks) tem valor real para dados caóticos e esquemas em evolução.
Porém, essa conveniência cobra seu preço em performance e custo operacional. O domínio das nuances entre essas tecnologias é o que separa pipelines de dados frágeis e dispendiosos de arquiteturas resilientes e de alto desempenho.
A estratégia não é escolher um ou outro — é saber quando usar cada um. Use o Glue onde ele brilha (ingestão resiliente), e o Spark onde ele domina (processamento otimizado). E sempre, sempre, meça e monitore.
Porque no final, performance é feature. E custo operacional é responsabilidade.