Voltar para Insights

Padrões de Data Pipeline Que Realmente Escalam em Produção

ELT vs ETL, anti-patterns de orquestração, e a realidade operacional de manter pipelines que processam milhões de registros diariamente sem falhas silenciosas.

A comunidade de engenharia de dados gastou anos debatendo ETL vs ELT, batch vs streaming, e Airflow vs todo o resto. A maioria desses debates produz talks de conferência e blog posts que ficam ótimos no papel mas colapsam sob o peso da realidade de produção.

Depois de construir e manter pipelines que processam milhões de registros diariamente em múltiplos ambientes de clientes, estabelecemos padrões que sobrevivem ao contato com dados reais. Este artigo documenta o que funciona, o que falha silenciosamente, e o que todo time aprende da maneira difícil.

ELT Sobre ETL: O Argumento Estrutural

O padrão tradicional ETL (Extract, Transform, Load) transforma dados antes de chegarem ao warehouse. O padrão moderno ELT (Extract, Load, Transform) carrega dados brutos primeiro, depois transforma dentro do warehouse.

Optamos por ELT por uma razão estrutural: preservação de dados brutos.

Quando você transforma antes de carregar, perde o dado original. Se sua lógica de transformação tem um bug — e terá — você precisa re-extrair do sistema fonte. Dependendo da fonte (APIs com rate limits, bancos legado sem change tracking, plataformas SaaS de terceiros), re-extração varia de “irritante” a “impossível.”

Com ELT, o dado bruto fica em uma camada de staging. Quando a lógica de transformação muda, você reprocessa do staging. Sem necessidade de re-extração. O sistema fonte nunca é tocado duas vezes.

A Exceção Prática

ELT quebra quando o volume de dados brutos é genuinamente enorme e o modelo de custo do warehouse torna armazenar dados brutos proibitivo. Se você está ingerindo 500GB/dia de telemetria IoT bruta e precisa apenas de 5GB de output agregado, transformar antes de carregar faz sentido econômico.

O framework de decisão:

FatorFavorece ELTFavorece ETL
Custo de re-extração da fonteAltoBaixo
Volume bruto vs outputSimilar10x+ diferença
Estabilidade da lógica de transformaçãoEvoluindoEstável
Custo de armazenamento warehouseBaixo (ClickHouse, S3)Alto (BigQuery on-demand)
Requisitos de auditoria regulatóriaSimNão

Orquestração: Mage.ai em Produção

Usamos Mage.ai como nosso orquestrador primário para pipelines de produção client-facing. A decisão foi deliberada e veio após avaliar Airflow, Prefect e Dagster em múltiplas dimensões.

Por Que Não Airflow

Apache Airflow é o padrão da indústria, e com razão. É battle-tested, tem suporte massivo de comunidade, e todo engenheiro de dados o conhece. Mas Airflow tem três custos operacionais que se acumulam em ambientes de consultoria:

  1. Overhead de infraestrutura: Airflow requer banco de metadados, web server, scheduler e workers. Para um deploy de cliente único, são 4+ containers antes de rodar um único pipeline.
  2. Debug de DAG: Quando uma task falha, debug requer navegar logs em múltiplos componentes. O feedback loop entre “mudança no código” e “ver resultado” é medido em minutos, não segundos.
  3. Somente Python: Toda transformação precisa ser Python. Para transformações SQL-heavy (que a maioria dos pipelines analíticos são), você empacota SQL em strings Python — um desastre ergonômico.

O Que Mage.ai Acerta

Mage.ai endereça esses pontos de fricção:

  • Blocks como unidades first-class: Cada passo do pipeline (data loader, transformer, exporter) é um bloco isolado com seu próprio contexto de execução. Blocos SQL executam SQL nativamente. Blocos Python executam Python. Sem wrapping.
  • Desenvolvimento interativo: Blocos podem ser executados individualmente durante desenvolvimento com preview imediato do output. O feedback loop cai de minutos para segundos.
  • Footprint menor: Uma única instância Mage serve como UI, scheduler e executor. Para deploys de clientes onde orçamento de infraestrutura é restrito, isso importa.

Padrão de Estrutura de Pipeline

Todo pipeline de produção segue uma arquitetura consistente de blocos:

Pipeline: client_sales_ingestion
├── [Loader] api_extract_sales      # Extração API paginada
├── [Transformer] validate_schema    # Enforcement de schema + null handling
├── [Transformer] enrich_dimensions  # Join de dados de referência (regiões, categorias)
├── [Transformer] compute_metrics    # Cálculos de negócio
├── [Exporter] load_clickhouse       # Batch insert no store analítico
└── [Exporter] notify_status         # Slack/webhook em sucesso ou falha

A disciplina chave: loaders nunca transformam, transformers nunca carregam. Cada bloco tem exatamente uma responsabilidade. Quando um pipeline falha em enrich_dimensions, você sabe que a extração sucedeu e o problema está na lógica de enriquecimento. Sem adivinhação.

O Problema da Falha Silenciosa

As falhas de pipeline mais perigosas são as que não levantam exceções. O pipeline completa com sucesso, reporta status verde, mas os dados de output estão errados.

Falhas Silenciosas Comuns

1. Schema drift sem detecção

A API fonte adiciona um novo campo ou muda um tipo de campo. Seu loader sucede porque está lendo JSON. Seu transformer sucede porque só acessa campos conhecidos. Seu exporter sucede porque ClickHouse aceita o insert. Mas o novo campo — que contém dados de negócio críticos — é silenciosamente descartado.

Correção: Validação explícita de schema no boundary do loader:

from pydantic import BaseModel, ConfigDict

class SalesRecord(BaseModel):
    model_config = ConfigDict(extra='forbid')  # Rejeitar campos desconhecidos
    
    transaction_id: str
    amount: Decimal
    currency: str
    customer_id: int
    created_at: datetime

def validate_batch(raw_records: list[dict]) -> list[SalesRecord]:
    valid, rejected = [], []
    for record in raw_records:
        try:
            valid.append(SalesRecord(**record))
        except ValidationError as e:
            rejected.append({'record': record, 'error': str(e)})
    
    if rejected:
        log_rejected_records(rejected)  # Não perca visibilidade
        if len(rejected) / len(raw_records) > 0.05:
            raise SchemaViolationThreshold(
                f"{len(rejected)}/{len(raw_records)} registros falharam validação"
            )
    return valid

A configuração extra='forbid' é crítica. Força o pipeline a falhar ruidosamente quando o schema fonte muda, ao invés de silenciosamente ignorar novos campos.

2. Registros duplicados por falhas de idempotência

Paginação de API com offsets baseados em cursor pode produzir duplicatas quando registros são inseridos na fonte durante extração. Seu pipeline roda diariamente, extrai páginas 1-100, mas a página 50 desloca porque 200 novos registros foram adicionados no meio da extração.

Correção: Deduplicação no exporter com chaves determinísticas:

-- ClickHouse ReplacingMergeTree lida com isso nativamente
CREATE TABLE analytics.sales_events
(
    transaction_id String,
    amount Decimal(18, 2),
    -- ... outros campos
    _loaded_at DateTime DEFAULT now()
)
ENGINE = ReplacingMergeTree(_loaded_at)
ORDER BY transaction_id;

ReplacingMergeTree mantém apenas a versão mais recente de cada linha (por transaction_id). Duplicatas são automaticamente colapsadas durante merges de background.

3. Incompatibilidades de timezone

A API fonte retorna timestamps em UTC. Sua transformação assume hora local. Revenue atribuído a “segunda-feira” é na verdade domingo à noite. Seus relatórios semanais estão consistentemente errados por 8-16 horas dependendo do timezone do cliente.

Correção: Todos os timestamps internos são UTC. Conversão de timezone acontece exclusivamente na camada de apresentação (Superset/Flutter). Nunca no pipeline.

Padrões de Retry e Dead Letter Queues

Pipelines de produção falham. Redes dão timeout. APIs fazem rate-limit. Bancos travam. A questão não é se falhas acontecem — é como o sistema recupera.

Exponential Backoff com Jitter

Para falhas transientes (HTTP 429, connection timeouts), retentamos com exponential backoff mais jitter aleatório:

import random
import time

def retry_with_backoff(func, max_retries=5, base_delay=1.0):
    for attempt in range(max_retries):
        try:
            return func()
        except TransientError:
            if attempt == max_retries - 1:
                raise
            delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
            time.sleep(delay)

O jitter previne problemas de thundering herd quando múltiplos pipelines retentam contra a mesma API simultaneamente.

Padrão Dead Letter

Para registros que falham validação ou transformação, nunca os descartamos silenciosamente. Registros falhados são roteados para uma tabela dead letter:

CREATE TABLE staging.dead_letters
(
    pipeline_name String,
    block_name String,
    record String,          -- JSON serializado do registro original
    error_message String,
    failed_at DateTime DEFAULT now(),
    resolved Boolean DEFAULT false
)
ENGINE = MergeTree()
ORDER BY (pipeline_name, failed_at);

Uma query de monitoramento diária expõe dead letters não resolvidas. Engenheiros revisam, corrigem a causa raiz e reprocessam. Nenhum dado é perdido.

Monitoramento: As Três Métricas Que Importam

Monitoramento de pipelines tipicamente gera ruído. Centenas de métricas, dashboards com 30 painéis, alertas disparando a cada hora. Reduzimos monitoramento operacional a três métricas core:

1. Freshness

Quão antigo é o registro mais recente na tabela destino? Se o pipeline roda a cada hora e o registro mais novo tem 3 horas, algo está errado.

SELECT
    dateDiff('minute', max(created_at), now()) AS freshness_minutes
FROM analytics.sales_events
WHERE tenant_id = {tenant_id}

Threshold de alerta: 2x o intervalo esperado do pipeline.

2. Anomalia de Volume

O pipeline de hoje processou um número razoável de registros comparado à média móvel?

SELECT
    today_count,
    avg_7d,
    abs(today_count - avg_7d) / avg_7d AS deviation_pct
FROM (
    SELECT
        countIf(event_date = today()) AS today_count,
        countIf(event_date >= today() - 7) / 7 AS avg_7d
    FROM analytics.sales_events
)

Se volume cai >50% ou dispara >200%, investigue. Ou a fonte mudou ou o pipeline está filtrando incorretamente.

3. Taxa de Dead Letter

Que porcentagem dos registros entrantes falhou processamento?

Se a taxa de dead letter excede 5%, o pipeline está quebrado. Entre 1-5%, há um problema de qualidade de dados na fonte. Abaixo de 1% é ruído operacional normal.

A Realidade Operacional

Engenharia de pipeline é 20% construção e 80% manutenção. O pipeline inicial leva um dia para escrever. O monitoramento, lógica de retry, validação de schema, tratamento de dead letter e gestão de timezone levam uma semana. Mas essa semana de engenharia defensiva previne meses de investigações “por que os números não batem?” que consomem tempo de analistas, erodem confiança de stakeholders, e eventualmente levam ao pipeline sendo substituído por uploads manuais de Excel.

Construa a infraestrutura chata primeiro. O trabalho empolgante de data science se torna possível somente quando o encanamento é confiável.