Trilha recomendada

Use este insight em tres movimentos

Leia o enquadramento, conecte-o a prova de implementacao e depois mantenha vivo o loop semanal de sinais para que esta pagina vire uma relacao mais longa com o site.

Spills de Memória Out-of-Core no DuckDB com Parquet S3
Arquitetura de Engines de Consulta

Spills de Memória Out-of-Core no DuckDB com Parquet S3

Spills de memória out-of-core no DuckDB com Parquet S3 travam pods sob carga. Ajuste limites de buffer para eliminar erros OOM e cortar custos em nuvem.

2026-08-06 • 8 min

CompartilharLinkedInX

Spills de Memória Out-of-Core no DuckDB com Parquet S3

Spills de Memória Out-of-Core no DuckDB com Parquet S3 derrubaram nossos nós de processamento em lote durante horários de pico. Ao escanear arquivos Parquet de múltiplos gigabytes diretamente de buckets S3 em pods Kubernetes leves, o consumo de memória ultrapassou os limites do contêiner, disparando o sinal 137 do Linux OOM Killer. A falha interrompeu a disponibilidade analítica e expôs limitações graves nas estratégias padrão de alocação de memória de threads ao lidar com armazenamento de objetos remoto.

Engines OLAP embarcadas como o DuckDB oferecem altíssimo throughput de consulta em arrays de disco locais. No entanto, executar agregações complexas e junções sobre armazenamento de objetos remoto exige um gerenciamento rigoroso de buffers. Por padrão, o DuckDB tenta maximizar o paralelismo de CPU alocando threads de varredura concorrentes para cada bloco de arquivo Parquet remoto. Ao consultar clusters de objetos não particionados de 400 GB dentro de um worker Kubernetes com 16 GB de RAM, a contenção de threads combinada com buffers de descompactação de row-groups sobrecarrega a memória disponível muito antes que os mecanismos de spill externo entrem em ação.

Diagnosticando Alocações de Thread em Scans Vetoriais no S3

Quando o DuckDB executa uma instrução SELECT sobre referências de objetos S3 via extensão httpfs, ele solicita metadados e limites de row-groups usando requisições HTTP range. O motor de consulta calcula a concorrência com base no número de núcleos de CPU do sistema, e não na memória disponível. Em uma instância de contêiner de 16 cores com limite de 16 GB de RAM, o DuckDB inicia 16 threads paralelas. Cada thread puxa row-groups Parquet de 128 MB para a memória simultaneamente.

A descompactação expande colunas codificadas por dicionário em vetores colunares em memória. Um bloco Parquet físico de 128 MB frequentemente expande para 1,2 GB ou mais na RAM quando tipos de dados string ou aninhados estão presentes. Multiplicado por 16 threads ativas, a alocação total atinge 19,2 GB em segundos—ultrapassando o limite do contêiner antes que o DuckDB ative seu mecanismo interno de spill out-of-core.

Para evitar isso, engenheiros de plataforma devem inspecionar o plano de execução usando métricas de profiling. A inspeção dos dados de execução revela se a pressão de memória surge da descompactação de row-groups ou de hash joins em streaming:

import duckdb

con = duckdb.connect(database=":memory:")
con.execute("PRAGMA enable_profiling='json'")
con.execute("PRAGMA profiling_output='profile_results.json'")

# Configura perfil de consulta com baixo consumo de memória no S3
con.execute("""
    SET s3_region='us-east-1';
    SET max_memory='12GB';
    SET preserve_insertion_order=false;
    SET threads=4;
""")

query = """
    SELECT 
        customer_id,
        COUNT(order_id) AS total_orders,
        SUM(order_amount) AS revenue
    FROM read_parquet('s3://analytics-warehouse/raw_events/*/*.parquet')
    WHERE event_date >= '2026-01-01'
    GROUP BY customer_id
    HAVING SUM(order_amount) > 1000;
"""

con.execute(query).fetchall()

Ao limitar as threads ativas para 4, a descompactação concorrente máxima cai de 19,2 GB para 4,8 GB, deixando uma folga substancial para o estado de execução e tabelas de hash.

Configurando Parâmetros de Max Memory e Preservação de Ordem

Definir max_memory no DuckDB cria um teto rígido para alocações internas. Quando a memória alocada atinge esse limite, o DuckDB altera a execução dos operadores da RAM para o armazenamento em disco externo. No entanto, definir apenas max_memory é insuficiente se a preservação da ordem de inserção permanecer ativada.

Por padrão, o DuckDB preserva a ordem de inserção das linhas durante a execução para que os resultados coincidam com a sequência física dos arquivos de origem. Para manter essa garantia durante junções ou agregações em streaming, o motor armazena vetores intermediários na RAM em vez de gravá-los no disco fora de ordem. Desativar essa exigência com SET preserve_insertion_order=false; permite que o DuckDB processe e grave resultados intermediários diretamente em arquivos de spill em disco assim que a pressão de memória aumenta.

Restrições arquiteturais semelhantes ocorrem ao configurar camadas de armazenamento empresarial. Times que migram workloads de motores tradicionais frequentemente encontram degradação de performance quando os índices não correspondem aos padrões de acesso, como documentado em nossa análise do ClickHouse MergeTree Primary Key Index Tuning Guide.

Newsletter

Quer o proximo sinal antes de ele virar backlog?

Uma nota semanal curta: pressao de mercado, padrao de entrega e um link de prova que voce pode reutilizar.

Um email por semana. Sem spam. Apenas conteudo de alto sinal para tomadores de decisao.

Ajustando Buffers Parquet para Requisições HTTP Range

Ler arquivos Parquet em conexões de rede de alta latência introduz um tempo de espera de E/S significativo. O DuckDB atenua a latência de rede fazendo pré-busca de row-groups através de buffers governados por s3_uploader_max_filesize e configurações internas do leitor HTTP. Se esses buffers forem sobredimensionados em relação à RAM do pod, ocorre um inchaço de memória antes mesmo que os operadores processem uma única tupla.

Para otimizar o throughput de rede sem arriscar o esgotamento da memória, ajuste as configurações do leitor Parquet. O parâmetro parquet_metadata_cache deve ser ativado para buscas frequentes de metadados, enquanto os buffers de pré-busca de objetos devem ser rigidamente limitados:

-- Configura parâmetros de armazenamento para limites de memória ideais
SET s3_region='us-west-2';
SET max_memory='10GB';
SET temp_directory='/tmp/duckdb_spills.tmp';
SET preserve_insertion_order=false;
SET threads=4;

-- Varredura em streaming com pushdown de filtros
SELECT 
    t.merchant_id,
    DATE_TRUNC('day', t.transaction_timestamp) AS txn_date,
    AVG(t.amount) AS avg_ticket
FROM read_parquet(
    's3://financial-logs/transactions/*/*.parquet',
    hive_partitioning=true
) t
WHERE t.status = 'COMPLETED'
  AND t.transaction_timestamp >= '2026-03-01'
GROUP BY 1, 2;

Quando o temp_directory é explicitamente configurado em um volume NVMe local de alta velocidade, o DuckDB grava perfeitamente as tabelas de hash intermediárias no disco assim que a alocação de memória ultrapassa 10 GB. Sem um temp_directory explícito, o DuckDB tenta realizar o spill na memória do sistema ou em locais temporários padrão que podem não ter o espaço necessário, provocando falhas no job.

Implementando Agregações em Streaming com Diretórios de Spill Externos

Em implantações Kubernetes contêinerizadas, o sistema de arquivos que sustenta o /tmp é frequentemente um emptyDir apoiado pela memória RAM ou por armazenamento compartilhado do host. Se o DuckDB realizar o spill de arquivos out-of-core em um volume baseado em RAM, a gravação em disco não oferece alívio de memória e acelera a eliminação do pod.

Engenheiros devem montar volumes persistentes dedicados (EBS/PD) ou armazenamento local NVMe no caminho do contêiner designado para o temp_directory. O manifesto abaixo demonstra um padrão de implantação em produção para workers de lote:

apiVersion: batch/v1
kind: Job
metadata:
  name: duckdb-parquet-aggregation
  namespace: data-pipelines
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: worker
        image: python:3.11-slim
        resources:
          limits:
            cpu: "8"
            memory: "16Gi"
          requests:
            cpu: "4"
            memory: "8Gi"
        volumeMounts:
        - name: scratch-space
          mountPath: /mnt/scratch
        env:
        - name: DUCKDB_TEMP_DIR
          value: "/mnt/scratch/spill"
      volumes:
      - name: scratch-space
        ephemeral:
          volumeClaimTemplate:
            spec:
              accessModes: [ "ReadWriteOnce" ]
              storageClassName: "gp3-nvme"
              resources:
                requests:
                  storage: 100Gi

Essa estrutura garante que, quando o DuckDB atingir seu limite de max_memory, as operações out-of-core gravem diretamente no SSD local sem consumir o limite de RAM do contêiner nem inflar o uso de memória do nó host.

Benchmarking de Perfis de Memória em Grandes Varreduras de Objetos

Para estabelecer métricas operacionais claras, avaliamos o consumo de memória em quatro configurações distintas ao consultar um conjunto de dados Parquet não compactado de 400 GB residente no S3. O ambiente de teste foi um pod Kubernetes limitado a 16 GB de RAM e 8 cores de CPU.

Na configuração padrão (threads=8, max_memory=16GB, preserve_insertion_order=true, diretório temp em RAM), o pico de memória atingiu 17,8 GB, resultando no encerramento do contêiner por OOM em 42 segundos de execução.

Na Configuração A (threads=8, max_memory=12GB, preserve_insertion_order=true, diretório temp em NVMe), a memória atingiu 12,4 GB, mas a preservação da ordem exigiu retenções massivas de buffers intermediários, gerando tempos de execução de 380 segundos devido à contenção de gravação em disco.

Na Configuração B (threads=4, max_memory=12GB, preserve_insertion_order=false, diretório temp em NVMe), o pico de consumo de memória se estabilizou em 11,2 GB. O DuckDB concluiu a varredura completa em 142 segundos sem travamentos ou degradação no contêiner.

Para arquiteturas de dados em produção que exigem transformações ETL complexas sobre grandes volumes na nuvem, combinar engines de consulta otimizadas com a infraestrutura correta é essencial. Integrar padrões de lakehouse modulares, como os implementados no projeto AWS Databricks Lakehouse, ajuda a manter a previsibilidade em pipelines analíticos de grande escala.

Ajustando limites de threads, impondo a gravação de resultados sem ordenação estrita e montando volumes scratch NVMe dedicados, as equipes de plataforma podem eliminar travamentos por falta de memória out-of-core, garantir SLOs rigorosos e maximizar a eficiência de hardware de engines OLAP embarcadas.

CompartilharLinkedInX

Cluster do tema

Explore este tema entre prova e sinais vivos

Permaneça no mesmo tema mudando apenas o formato: saia do enquadramento estrategico e avance para prova de implementacao ou para um sinal fresco de mercado que mantenha a sessao em movimento.

Newsletter

Receba o proximo sinal estrategico antes do mercado assimilar.

Cada nota semanal conecta uma mudanca de mercado, um padrao de execucao e uma prova pratica que vale estudar.

Um email por semana. Sem spam. Apenas conteudo de alto sinal para tomadores de decisao.