Motores de processamento de dados: Polars, DataFusion, Ray e Spark
Tradução automática Este artigo foi traduzido automaticamente a partir da versão original em inglês.
Durante anos, o Pandas tratou do processamento tabular em memória e o Apache Spark da variante distribuída. Esta divisão funcionava bem quando os dados eram estruturados.
Os pipelines modernos também processam imagens, áudio e vídeo. Nestes workloads, a descodificação no CPU pode deixar a inferência no GPU sem dados, enquanto a recolha de lixo da JVM e o Global Interpreter Lock do Python limitam o throughput. Os motores mais recentes usam Rust e Apache Arrow para reduzir esses custos.
Para comparar as opções, fiz benchmark ao Pandas, Polars, DataFusion, Daft e Rust nativo em dois datasets reais. O Spark e o Ray são abordados em notebooks distribuídos separados. As viagens de táxi de Nova Iorque representam processamento tabular; as imagens Food-101 representam um pipeline multimodal.
O repositório engine-comparison-demo contém o código. Execute-o no seu próprio hardware, porque o desempenho do motor depende da máquina, do dataset e do workload.
Em resumo: comece pelo formato do workload, não pelo vencedor de um benchmark. Polars é uma boa opção por defeito para pipelines locais com DataFrames; DataFusion é adequado para equipas que incorporam um motor de consultas; Ray Data e Daft foram concebidos para pipelines mistos de CPU/GPU; e Spark continua a ser uma escolha madura para SQL e ETL distribuídos. Os ganhos de velocidade descritos neste guia aplicam-se a workloads específicos; não se transferem automaticamente para os seus. Volte a executar os benchmarks complementares nos seus dados e hardware antes de escolher.
Tipos de workloads de processamento de dados
Os motores especializam-se, por isso a primeira pergunta é que tipo de workload tem realmente. A principal distinção é entre dados estruturados e multimodais.
Os dados estruturados ou tabulares envolvem filtragem, agregação e joins. Normalmente são limitados pelo CPU e cabem frequentemente em memória. Muitas destas consultas podem ser executadas numa única máquina moderna, mas a memória, o I/O, a concorrência, a recuperação e o formato do workload continuam a determinar se um cluster é adequado.
A AI multimodal processa imagens, áudio ou vídeo. A inferência pode correr num GPU enquanto a descodificação no CPU, as leituras remotas ou o batching limitam o pipeline. A etapa que limita o throughput varia consoante o tipo de instância e os operadores; por isso, meça a utilização em todo o percurso, em vez de dimensionar o GPU isoladamente.
Os motores mais recentes visam diferentes partes deste espaço. A execução nativa pode reduzir o overhead do Python, as interfaces compatíveis com Arrow podem tornar o intercâmbio de dados mais barato e a execução em streaming pode limitar o uso de memória em datasets maiores do que a RAM. Nenhum destes benefícios é automático para todos os operadores ou conversões.
Parte 1: processamento num único nó
O Pandas privilegia um modelo de programação eager e em memória. Muitas operações comuns criam resultados intermédios, e a execução paralela não é coordenada através de um otimizador de consultas. Este compromisso mantém o código exploratório direto, mas pode tornar-se dispendioso em scans analíticos maiores. No benchmark PDS-H do Polars, com scale factor 10, o Pandas demorou cerca de 365 segundos para a suite, enquanto o motor de streaming do Polars demorou 3,89 segundos. O resultado de aproximadamente 94x descreve esse benchmark e essa configuração; não é um fator geral de conversão de Pandas para Polars.
Polars para pipelines tabulares locais
O Polars é uma boa opção por defeito quando um pipeline local com DataFrames ultrapassa a execução eager num único processo. A sua API lazy constrói um plano de consultas antes da execução, permitindo otimizações como predicate pushdown e eliminação de projeções. O motor pode então executar os operadores em paralelo e, quando suportado, em batches de streaming.
A diferença aumenta à medida que os dados crescem. Com scale factor 100 (~100 GB), o motor de streaming do Polars terminou em 23,94 segundos, contra 152,27 segundos do seu próprio motor em memória — cerca de 6x mais rápido, com dados maiores do que a RAM.
O exemplo seguinte é um esboço ilustrativo da API, não uma transcrição literal da versão atual de engine_comparison_examples.ipynb ou tabular.py:
import polars as pl
# scan_parquet reads only the schema — no data loaded yet
q = (
pl.scan_parquet("yellow_tripdata_2024-01.parquet")
.filter(
(pl.col("trip_distance") > 5.0)
& (pl.col("total_amount") > 30.0)
)
.group_by("payment_type")
.agg(
pl.col("total_amount").mean().alias("avg_fare"),
pl.col("trip_distance").mean().alias("avg_distance"),
pl.len().alias("trip_count"),
)
)
# The entire plan is optimized and executed here, in parallel
result = q.collect()
A diferença importante está no modelo de execução. Na consulta lazy acima, o Polars pode aplicar o filtro e a seleção de colunas diretamente no scan de Parquet. Isto pode permitir ignorar row groups e evitar a leitura de colunas não utilizadas. Um pipeline eager típico em Pandas não dispõe de um plano para a consulta completa que possa ser otimizado, embora o uso cuidadoso de filtros Parquet, colunas selecionadas e backends alternativos possa recuperar parte da diferença.
DataFusion para motores de consultas embebidos
Enquanto o Polars é uma biblioteca que utiliza diretamente, o DataFusion é o motor sobre o qual se constroem outros motores. É utilizado pelo InfluxDB 3.0, pelo GreptimeDB e pelo Comet Spark accelerator da Apple.
Num ClickBench de novembro de 2024 publicado pelo projeto DataFusion, o DataFusion liderou as configurações Parquet testadas num único nó. Mais tarde, a equipa da Embucket publicou um estudo de caso do fornecedor para TPC-H scale factor 1000 usando datafusion-cli em r6gd.metal, com 64 vCPUs, 512 GB de RAM, NVMe local e medições com cache quente. Q18 e Q21 falharam ou demoraram excessivamente na execução original, sendo depois reescritas estruturalmente para obter os resultados completos. Isto demonstra scale-up num único nó nessa configuração, não um resultado geral de TPC-H.
O exemplo seguinte é um esboço ilustrativo da API, não uma transcrição literal da versão atual de engine_comparison_examples.ipynb ou tabular.py:
from datafusion import SessionContext
ctx = SessionContext()
ctx.register_parquet("taxi", "yellow_tripdata_2024-01.parquet")
# SQL executed directly against Parquet — no intermediate copies
df = ctx.sql("""
SELECT payment_type,
COUNT(*) AS trip_count,
AVG(trip_distance) AS avg_distance,
AVG(total_amount) AS avg_fare
FROM taxi
WHERE trip_distance > 5.0 AND total_amount > 30.0
GROUP BY payment_type
ORDER BY trip_count DESC
""")
result = df.to_pandas()
A força do DataFusion está na sua modularidade. As APIs de extensão abrangem catálogos personalizados, table providers, regras do otimizador e planos de execução. É por isso que surge sempre que alguém está a construir uma plataforma de dados personalizada ou a incorporar um motor de consultas no seu próprio produto.
Daft para dados multimodais
O Daft integra imagens, áudio, vídeo e embeddings no workflow de DataFrame. Disponibiliza expressões nativas para operações como descodificação de imagens e downloads a partir de URLs, pelo que os percursos comuns de preprocessing não exigem um loop Python linha a linha. Essa integração é o principal motivo para o considerar em vez de um motor tabular genérico.
O exemplo seguinte é um esboço ilustrativo da API, não uma transcrição literal da versão atual de engine_comparison_examples.ipynb ou multimodal.py:
import daft
df = daft.read_parquet("yellow_tripdata_2024-01.parquet")
result = (
df.where(
(daft.col("trip_distance") > 5.0)
& (daft.col("total_amount") > 30.0)
)
.groupby("payment_type")
.agg(
daft.col("total_amount").mean().alias("avg_fare"),
daft.col("trip_distance").mean().alias("avg_distance"),
daft.col("trip_distance").count().alias("trip_count"),
)
.collect()
)
A API será familiar se conhece o Pandas, mas o motor utiliza a mesma stack Rust + Arrow presente no Polars e no DataFusion. O Daft compensa sobretudo quando as “linhas” do DataFrame são imagens, PDFs ou tensores.
Benchmark num único nó
Executei benchmarks de Pandas, Polars, DataFusion e Daft, além de Rust nativo através de Polars-rs, sobre cerca de 41 milhões de viagens de táxi amarelo de Nova Iorque relativas a todo o ano de 2024. O Spark e o Ray surgem nos exemplos distribuídos, não neste benchmark de nó único. Os resultados completos estão no repositório de demonstração:
O gráfico serve de contexto para o benchmark complementar, não constitui um ranking justo dos motores em condições equivalentes: o Pandas carrega os dados das viagens e das zonas antes da função medida, enquanto o Polars, o DataFusion, o Daft e o Rust incluem diferentes operações de leitura de ficheiros, registo ou configuração da consulta dentro da secção medida. O script repete cada operação três vezes e reporta a mediana depois de os imports estarem carregados; o estado da cache do sistema de ficheiros, o aquecimento, o CPU e outras condições de execução podem alterar os valores. Alinhe o limite da medição e volte a executar em condições controladas antes de interpretar a ordenação como um resultado de desempenho.
Neste tamanho (~660 MB de Parquet), os três motores Python mais recentes terminam rapidamente. O valor de 94x entre Polars e Pandas acima vem do PDS-H; o meu workload de táxis com 41 milhões de linhas produziu uma diferença menor. Nesta execução com âmbitos mistos, Polars, DataFusion, Daft e Polars-rs ficaram no mesmo intervalo geral, tendo o DataFusion registado o menor tempo reportado. Esta ordenação pertence a esta consulta e a este limite de medição, não aos motores em geral. Quando os motores ficam tão próximos, a adequação da API e medições repetidas com dados representativos devem orientar a decisão.
Parte 2: dados multimodais
O ETL tabular normalmente reduz os dados: filtra, agrega e escreve menos do que lê. Os pipelines de AI multimodal fazem o oposto. Um único documento pode gerar dezenas de segmentos de texto e vetores de embedding.
Num pipeline PySpark convencional, as imagens e o áudio entram frequentemente como dados binários e passam para bibliotecas Python para descodificação ou transformação. A travessia da fronteira JVM/Python e a serialização dos resultados podem representar uma parte significativa do tempo total. O Spark pode usar percursos baseados em Arrow, UDFs vetorizadas e integrações com aceleradores, mas essas opções exigem design e medição explícitos.
Execução em pipeline e utilização do GPU
O Spark agenda o trabalho em etapas separadas por fronteiras de shuffle. Uma implementação direta pode colocar o download, a descodificação e a inferência numa sequência que deixa a capacidade do CPU ou do GPU à espera. O Spark suporta scheduling de recursos GPU e plugins, pelo que o tempo de inatividade não é inevitável. No entanto, a sobreposição e o backpressure não são propriedades automáticas de um job comum de DataFrame PySpark.
A execução em pipeline é a alternativa. Em vez de executar as etapas consecutivamente, o motor sobrepõe I/O, trabalho no CPU e inferência no GPU. Quando as etapas e os buffers estão equilibrados, essa sobreposição pode reduzir o tempo de inatividade e melhorar o throughput; o arranque e o escoamento, o backpressure ou uma etapa mais lenta podem ainda deixar recursos à espera.
Benchmark de processamento de imagens
Fiz um benchmark de processamento de imagens com 500 fotografias reais do dataset Food-101. Resultados do repositório de demonstração:
O Polars e o DataFusion não aparecem porque este teste visa operações nativas sobre imagens, e não expressões tabulares. Nesta execução com 500 imagens, o percurso de imagens do Daft foi cerca de 3,7x mais rápido do que a baseline Pandas + Pillow. O valor exato de multimodal_results.json 1.0148517909983639s dividido pelo valor de rust_multimodal_results.json 0.235865209s é 4.30267692, pelo que a implementação Rust foi 4,3x mais rápida. O README complementar apresenta os valores arredondados 1.01s e 0.24s, cuja razão é 4,2x; trata-se de arredondamento na apresentação, enquanto os valores JSON são autoritativos. O exemplo é útil para expor overhead de orquestração, mas é demasiado pequeno para prever, por si só, o throughput em produção.
Parte 3: processamento distribuído
Quando uma única máquina já não chega, é preciso decidir como distribuir o trabalho.
Apache Spark
O Spark é uma opção madura para ETL tabular de grande escala, joins com muito shuffle e organizações que já operam o seu ecossistema. A recuperação baseada em lineage e o amplo suporte de plataformas são importantes quando a fiabilidade e a familiaridade operacional pesam mais do que a velocidade local. Os pipelines mistos de CPU/GPU exigem uma configuração de recursos e um design mais deliberados do que o percurso SQL, área em que Ray Data e Daft oferecem uma abstração mais direcionada.
O exemplo seguinte é um esboço ilustrativo da API do Spark, não uma transcrição literal da versão atual de distributed_spark.ipynb ou spark_etl.py:
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = SparkSession.builder \
.appName("TaxiETL") \
.config("spark.sql.adaptive.enabled", "true") \
.getOrCreate()
orders = spark.read.parquet("s3a://lake/taxi/*.parquet")
zones = spark.read.csv("s3a://lake/taxi_zones.csv", header=True)
# Spark excels at this: joining massive tables with a shuffle
result = (
orders
.filter(F.col("trip_distance") > 5.0)
.join(zones, orders.PULocationID == zones.LocationID, "inner")
.groupBy("Borough")
.agg(
F.sum("total_amount").alias("total_revenue"),
F.avg("total_amount").alias("avg_fare"),
)
.orderBy(F.desc("total_revenue"))
)
Ray Data para computação heterogénea
O Ray Data foi concebido para workloads de AI. Em vez das barreiras entre etapas do Spark, utiliza um modelo de streaming que mantém os GPUs alimentados. A funcionalidade mais importante para pipelines de AI é o scheduling de recursos mistos: pode declarar que um actor pretende “1 GPU, 4 CPUs” e que outro necessita apenas de CPUs, e o Ray trata do resto.
A Amazon reportou mais de $120 milhões de dólares em poupanças anuais depois de migrar workloads internos selecionados de processamento de dados do Spark para o Ray. O relatório cita uma melhoria de 91% na eficiência de custos na prova de conceito e de 82% em produção por GiB de input do S3. A escala é relevante, mas trata-se de um estudo de caso de migração para workloads específicos, não de uma poupança esperada numa adoção típica do Ray.
O exemplo seguinte é um esboço ilustrativo da API do Ray Data, não uma transcrição literal da versão atual de distributed_ray.ipynb ou ray_inference.py:
import ray
ray.init()
ds = ray.data.read_images("s3://my-bucket/food101/")
class ImageClassifier:
def __init__(self):
import torch
from torchvision.models import resnet18, ResNet18_Weights
self.model = resnet18(weights=ResNet18_Weights.DEFAULT).cuda()
self.model.eval()
self.preprocess = ResNet18_Weights.DEFAULT.transforms()
def __call__(self, batch):
import torch
tensors = torch.stack([
self.preprocess(img) for img in batch["image"]
]).cuda()
with torch.no_grad():
preds = self.model(tensors)
return {
"prediction": preds.argmax(dim=1).cpu().numpy(),
"confidence": preds.max(dim=1).values.cpu().numpy(),
}
# ActorPoolStrategy creates persistent GPU workers
predictions = ds.map_batches(
ImageClassifier,
compute=ray.data.ActorPoolStrategy(size=4),
num_gpus=1,
batch_size=64,
)
predictions.write_parquet("s3://output/predictions/")
A proporção CPU/GPU merece atenção. Nos benchmarks da Anyscale, o Ray Data continuou a escalar à medida que aumentava o número de CPUs por GPU, enquanto os outros motores testados estabilizaram. A fonte reporta ganhos de velocidade até 3x na inferência de imagens à medida que diminui a privação de CPU, porque os feeders no CPU conseguem acompanhar o GPU.
Daft distribuído
O Daft escala horizontalmente através do seu motor Flotilla: um worker Swordfish por nó, com o Flotilla responsável pelo scheduling em todo o cluster. O Swordfish trata da execução local em Rust e sobrepõe o I/O à computação através de streaming em batches pequenos, mantendo cada nó ocupado sem esperar pela etapa seguinte.
Nos benchmarks publicados pelo Daft, o Flotilla foi 4–18x mais rápido do que as implementações Spark testadas em quatro workloads multimodais. A maior diferença ocorreu num caso de deteção de objetos em vídeo. Considere o resultado como evidência de que o design da execução é importante nestes pipelines; depois compare-o com os resultados concorrentes da Anyscale abaixo e com um teste local.
A Anyscale, empresa responsável pelo Ray, publicou um benchmark concorrente no qual o Ray Data eliminou ou inverteu a diferença em instâncias com muitos CPUs após tuning. Em conjunto, os dois estudos de fornecedores justificam testar operadores representativos e proporções CPU/GPU, e não inferir uma tabela classificativa duradoura.
O exemplo seguinte é um esboço ilustrativo da API do Daft, não uma transcrição literal da versão atual de distributed_daft.ipynb ou daft_pipeline.py:
import daft
import numpy as np
from daft import col
# Daft's Flotilla engine distributes work across the cluster
df = daft.read_parquet("s3://data-lake/pdf_metadata/*.parquet")
# Download PDFs — Daft parallelizes downloads in Rust
df = df.with_column("pdf_bytes", col("pdf_url").download())
# Define a GPU class UDF for batched embedding generation
@daft.cls(gpus=1)
class TextEmbedder:
def __init__(self):
from sentence_transformers import SentenceTransformer
self.model = SentenceTransformer("all-MiniLM-L6-v2", device="cuda")
@daft.method.batch(
return_dtype=daft.DataType.fixed_size_list(daft.DataType.float32(), 384)
)
def __call__(self, text_col):
texts = text_col.to_pylist()
embeddings = self.model.encode(texts, batch_size=32)
return np.asarray(embeddings, dtype=np.float32)
# Daft schedules CPU downloads and GPU embeddings simultaneously
embedder = TextEmbedder()
df = df.with_column("embedding", embedder(col("text")))
df.write_parquet("s3://output/embeddings/")
Comparação distribuída
| Funcionalidade | Spark | Ray Data | Daft (Flotilla) |
|---|---|---|---|
| Modelo de execução | Uma task por core, baseada em partições | Tasks e actors de streaming | Um Swordfish por nó, batches de streaming |
| Pontos fortes | SQL/ETL massivo, tolerância a falhas | Computação heterogénea, saturação de GPUs | Pipelines multimodais, memória limitada |
| Controlos de GPU | Scheduling de recursos e plugins do ecossistema | Recursos explícitos por task e actor | Scheduling integrado do pipeline CPU/GPU |
| Tuning habitual | Executors, memória, partições, aceleradores | Batches, actors, object store | Batches, recursos, concorrência de I/O |
| Bom caso de avaliação | Jobs SQL/ETL grandes e com muito shuffle | Training ou inferência com recursos mistos | Ingestão e transformação multimodais |
Parte 4: o papel do Rust e do Arrow
Por baixo da competição, a convergência é a história mais interessante. Polars, Daft e DataFusion usam Rust extensivamente, enquanto o Ray inclui componentes nativos juntamente com as suas APIs Python. Todos podem trocar dados através de partes do ecossistema Apache Arrow.
A Arrow PyCapsule Interface (protocolo __arrow_c_stream__) disponibiliza às bibliotecas compatíveis uma forma normalizada de trocar streams Arrow. As transferências podem evitar a serialização linha a linha e reutilizar buffers quando os schemas e os layouts de memória são compatíveis. Materialização, rechunking, conversão de tipos ou transferência entre dispositivos podem ainda copiar dados; por isso, confirme a transferência com profiling em vez de assumir que é gratuita.
O exemplo seguinte é um esboço ilustrativo da API, não um script autónomo: pressupõe que events.parquet já existe. O engine_comparison_examples.ipynb complementar cria .data/events.parquet nas suas células de configuração antes do exemplo Arrow.
from datafusion import SessionContext
import polars as pl
# Compute in DataFusion (Rust-native execution)
ctx = SessionContext()
ctx.register_parquet("events", "events.parquet")
df = ctx.sql("""
SELECT user_id, COUNT(*) as event_count
FROM events WHERE event_type = 'purchase'
GROUP BY user_id HAVING COUNT(*) > 5
""")
# Convert through Arrow; compatible buffers may be reused
arrow_table = df.to_arrow_table()
df_polars = pl.from_arrow(arrow_table)
# Continue analysis in Polars
result = df_polars.with_columns(
pl.col("event_count").rank().alias("rank")
).sort("rank")
Por que razão estes motores mais recentes usam Rust para execução nativa?
-
Ausência de um garbage collector de tracing no código Rust. Ownership proporciona aos operadores nativos um controlo mais direto sobre a alocação e desalocação. Isto pode reduzir a latência relacionada com GC, mas não evita pressão de memória nem falhas out-of-memory.
-
Representações nativas compactas. As estruturas Rust não incluem cabeçalhos de objetos Java. O benefício prático depende do layout do motor: os sistemas JVM colunares também evitam representar cada valor como um objeto individual.
-
Verificações de concorrência mais fortes. As regras de Rust seguro excluem muitas data races em tempo de compilação. O código do motor pode ainda conter blocos unsafe e bugs lógicos de concorrência, mas a linguagem reduz a superfície de falha.
Combinadas com o layout de memória colunar do Arrow, estas escolhas podem reduzir o overhead de alocação e serialização. Não o eliminam em todas as fronteiras; objetos Python, transporte de rede, schemas incompatíveis e movimentação entre dispositivos continuam a ser relevantes.
Parte 5: adotar vários motores
Não tem de forçar toda a plataforma a usar um único motor. A composição é útil quando o custo da transferência entre motores é inferior ao custo de fazer um único sistema suportar todos os workloads.
Uma possibilidade é usar Spark para joins de lakehouse já estabelecidos, escrever numa tabela ou num formato de ficheiro aberto e entregar o resultado ao Ray Data ou ao Daft para inferência no GPU ou transformações multimodais. Polars ou DuckDB podem servir para análise local sobre os mesmos ficheiros. Uma equipa pequena pode precisar apenas de um destes motores; adicione outro quando um bottleneck medido justificar a fronteira operacional.
O que torna esta composição possível é a interoperabilidade. Parquet é o formato comum de transferência em ficheiro nestes exemplos, enquanto o Arrow pode fornecer intercâmbio em memória. O suporte para Delta e Iceberg depende do motor e do connector, por isso confirme cada fronteira. Uma transferência por ficheiro também tem custos de I/O e conversão; não é automaticamente gratuita.
Seleção do motor por caso de avaliação
| Caso de avaliação | Shortlist | Validar |
|---|---|---|
| DataFrames analíticos locais | Polars / DuckDB | Adequação da API, memória, mistura de consultas |
| SQL/ETL distribuído existente | Apache Spark | Comportamento do shuffle, operações, custo |
| Trabalho paralelo nativo em Python | Dask / Ray | Overhead do scheduler, modelo de falhas |
| Training ou inferência CPU/GPU mistos | Ray Data / Daft | Utilização do acelerador, backpressure |
| Ingestão multimodal | Daft / Ray Data | Operadores nativos, comportamento dos retries |
| Motor de consultas embebido | DataFusion | APIs de extensão, cobertura SQL |
Escala diagonal
Durante muito tempo, a escolha era fazer scale-up (uma máquina maior) ou scale-out (mais máquinas). A Polars Cloud está a fazer algo intermédio, a que chama escala diagonal. Faça scale horizontal enquanto lê do cloud storage para maximizar o I/O e, depois, concentre o trabalho num único nó grande quando os filtros e as agregações tiverem reduzido os dados. O shuffle distribuído nunca chega a ocorrer.
A lição mais abrangente é comparar o custo por job concluído com sucesso, não o preço horário da instância. Um nó maior pode ser mais barato quando a redução do runtime compensa a tarifa superior, mas o resultado depende do I/O, da pressão de memória e da quantidade de trabalho eliminada antes de um shuffle.
Reproduzir a comparação
O repositório de demonstração complementar contém os scripts, notebooks e o ambiente utilizados nas comparações. O comando rápido abaixo usa o dataset de táxis predefinido do repositório, relativo a todo o ano de 2024, correspondente à execução maior apresentada anteriormente.
# Install dependencies
uv sync
# Run the tabular benchmark (~41M NYC taxi trips from full-year 2024)
uv run python -m engine_comparison.benchmarks.tabular
# Run the multimodal benchmark (500 real food photos)
uv run python -m engine_comparison.benchmarks.multimodal
# Run native Rust benchmarks (Polars-rs + image crate)
cd rust_benchmark && cargo run --release && cd ..
O repositório também inclui notebooks para comparações lado a lado das APIs e configurações Docker Compose para execuções distribuídas locais de Spark, Ray e Daft.
Principais conclusões
-
Demonstre que uma máquina não é suficiente. Os resultados de scale-up mostram que alguns scans analíticos grandes cabem num único nó. Meça os requisitos de memória, I/O, spill e recuperação antes de aceitar o overhead de um cluster.
-
Os modelos de execução são mais importantes do que a sintaxe familiar. Planeamento lazy, pushdown, streaming e operadores paralelos explicam grande parte da diferença entre um script eager de DataFrame e um motor analítico.
-
Meça todo o pipeline do acelerador. Descodificação, I/O de rede, batching, serialização e backpressure determinam se um GPU se mantém ocupado. Ray Data e Daft tornam esta sobreposição uma abstração central; o Spark pode suportá-la com design e tooling adicionais.
-
Faça a shortlist por workload e depois execute benchmarks. Use a adequação ao ecossistema para reduzir as opções e um job representativo end-to-end para escolher. Os resultados de benchmarks de fornecedores são hipóteses, não garantias.
-
Os formatos abertos mantêm a escolha reversível. Interfaces compatíveis com Arrow, Parquet e formatos de tabela abertos podem reduzir os custos de transferência. Confirme se uma determinada conversão reutiliza os buffers ou os copia.
Se precisar de um ponto de partida, experimente Polars para um pipeline tabular local e Daft ou Ray Data para um pipeline misto de CPU/GPU. Escolha Spark quando a sua execução distribuída, o ecossistema ou a base operacional existente resolverem um requisito. O tamanho do dataset, por si só, é uma justificação fraca.
Referências
Benchmarks e dados de desempenho
- Benchmarks PDS-H do Polars - streaming do Polars vs. Pandas em SF-10 (94x) e SF-100 (6,4x face ao motor em memória)
- Resultados do ClickBench do DataFusion (novembro de 2024) - Resultados Parquet num único nó publicados pelo projeto
- Embucket: TPC-H SF-1000 no DataFusion - Estudo de caso do fornecedor sobre
r6gd.metalcom medições de cache quente; Q18 e Q21 foram reescritas depois de a execução original ter falhado ou demorado excessivamente - Migração da Amazon de Spark para Ray - $120M/ano em poupanças, 82% de eficiência de custos em produção
- Benchmarks do Daft Flotilla - 4–18x mais rápido do que o Spark em workloads multimodais
- Benchmarks Anyscale Ray vs. Daft - Benchmark concorrente de fornecedor para workloads multimodais
Motores e frameworks
- Polars - Biblioteca Rust de DataFrames com execução lazy
- Apache DataFusion - Motor de consultas Rust embebível (GitHub)
- Daft - DataFrame distribuído nativo para multimodalidade
- Ray - framework de computação distribuída para AI (Data Internals)
- Apache Spark - Motor distribuído de ETL e SQL
- DuckDB - Motor analítico SQL in-process
- Dask - Computação paralela nativa em Python
Arquitetura e ecossistema
- Polars Cloud: escala diagonal - Scale vertical/horizontal dinâmico
- Arquitetura Daft Flotilla - Motor distribuído Swordfish + Flotilla
- Apache DataFusion Comet - acelerador Spark originalmente desenvolvido pela Apple
- Apache Arrow - Formato colunar em memória (Flight RPC, PyCapsule Interface)
- InfluxDB 3.0 + DataFusion - Base de dados de séries temporais construída sobre DataFusion
- GreptimeDB - Base de dados de observabilidade que usa DataFusion
Repositório de demonstração
- engine-comparison-demo - Código complementar deste artigo: benchmarks, notebooks e Docker Compose para Spark/Ray/Daft
Datasets
- NYC Taxi Trip Records - Dados de táxis amarelos da NYC TLC (Parquet, ~2,9M linhas/mês)
- Food-101 Dataset - 101 mil imagens de comida da ETH Zurich (Bossard et al., ECCV 2014)