ResiliênciaIntermediário7 min de leitura5 seçõesanos 2010, Data aproximada

Backpressure

Quando o produtor é mais rápido que o consumidor, alguém tem que ceder. Backpressure é o sinal de 'vá mais devagar' subindo a cadeia: o consumidor dita o ritmo, o produtor respeita, e o buffer entre eles não cresce até estourar a memória. Sem ele, a diferença de velocidade não some — ela vira um buffer infinito, latência crescente e, no fim, um out-of-memory.

Em 10 segundos

Quando o produtor é mais rápido que o consumidor, a diferença de velocidade não desaparece — ela vira buffer. Backpressure é o consumidor dizendo 'vá mais devagar' e o produtor obedecendo: em vez de empurrar, o produtor só envia o que o consumidor pediu. Sem isso, o buffer cresce sem limite, a latência sobe e o processo morre de out-of-memory. Quando o buffer enche mesmo assim, as opções são pausar, descartar ou recusar.

Antes de ter nome

É o controle de fluxo do TCP (a janela deslizante, de 1981) generalizado: o receptor dita o ritmo para o emissor não o afogar de pacotes.

  • #mensageria
  • #fluxo
  • #streams
  • #reactive
  • #capacidade
Analogia

A pia que transborda

Se a torneira despeja água mais rápido do que o ralo escoa, a pia começa a encher. Você pode aumentar a pia (um buffer maior) — mas se a torneira continuar mais rápida que o ralo, mais cedo ou mais tarde transborda no chão. A única solução real é a torneira sentir que a pia está cheia e diminuir o fluxo. Backpressure é exatamente esse aviso subindo o cano até a torneira: o ralo dita o ritmo, não a torneira.

01Conceito

Quem dita o ritmo

A raiz do problema é o modelo push: o produtor empurra no ritmo dele e assume que o consumidor dá conta. Quando não dá, o excesso vai para um buffer, e o buffer é uma bomba-relógio — cresce silenciosamente até a memória acabar.

Backpressure inverte para pull: o consumidor sinaliza quanto aguenta e o produtor só envia isso. O ritmo passa a ser ditado por quem processa, não por quem gera, e o buffer para de crescer porque a fonte desacelera junto.

Quando o buffer enche mesmo com o sinal, existe uma decisão de negócio a tomar, e ela depende do dado. Pausar/bloquear o produtor preserva tudo, mas propaga a lentidão para trás (bom para dados que não podem ser perdidos, como pagamentos). Descartar — dropar o mais antigo, ou amostrar — mantém o ritmo às custas de perder itens (aceitável para métricas, telemetria, posições de mouse). Recusar novas entradas com um erro claro empurra a decisão para o cliente (o território do rate limiting). Não existe opção universal: existe a que combina com o valor do dado.

O sinal precisa atravessar a cadeia inteira para funcionar. Se um pipeline tem fonte → transformação → gravação, e a gravação está lenta, o backpressure só ajuda se a transformação e a fonte respeitarem o pedido de desacelerar. Um único elo que ignora o sinal — que bufferiza sem limite 'para não travar' — quebra a corrente: o buffer volta a crescer nele, e o out-of-memory apenas mudou de lugar. É por isso que APIs como o Reactive Streams padronizam o request(n) de ponta a ponta, em vez de deixar cada elo inventar o seu.

02No mundo real

Como isso aparece na prática

A ingestão que derrubava o serviço à noite

Cenário

Um serviço recebia eventos por webhook e os gravava num banco. Em picos noturnos, os eventos chegavam mais rápido do que o banco gravava; o buffer em memória crescia até o processo morrer por falta de memória.

Como o padrão entra

A ingestão passou a ler do stream com backpressure: enquanto o banco não confirmava a gravação, a leitura de novos eventos pausava, e o webhook passava a responder mais devagar sob pressão em vez de acumular tudo.

Trade-off: Sob pico, o produtor (o emissor do webhook) passou a ver respostas mais lentas e a precisar de retry. Em troca, o serviço parou de morrer — degradou o ritmo em vez de cair inteiro.

A telemetria que preferiu descartar

Cenário

Um pipeline de métricas de UI enviava cada movimento e clique. Sob carga, o buffer enchia, e pausar o produtor travava a interface do usuário — inaceitável para dado que é só telemetria.

Como o padrão entra

Em vez de pausar, o pipeline passou a amostrar: sob pressão, descartava parte dos eventos de baixo valor e mantinha os importantes, preservando a fluidez da interface.

Trade-off: As métricas ficaram amostradas, não completas, sob carga. Foi a escolha certa para o tipo de dado: perder alguns pontos de telemetria é aceitável; travar a interface do usuário para não perdê-los, não.

03Cuidado

Onde isto dá errado

  1. Resolver com um buffer maior

    Aumentar o buffer não elimina a diferença de velocidade, só adia o estouro e piora a latência no caminho. Enquanto o produtor for mais rápido, qualquer buffer finito enche — a solução é desacelerar a fonte, não guardar mais.

  2. Um elo da cadeia que ignora o sinal

    Backpressure só funciona se toda a cadeia respeita o 'vá mais devagar'. Um único estágio que bufferiza sem limite para 'não travar' quebra a corrente, e o out-of-memory apenas se muda para dentro dele.

  3. Pausar dado que não podia esperar, ou descartar o que não podia sumir

    A resposta ao buffer cheio depende do dado: pausar um pagamento é seguro, pausar a interface do usuário não; descartar telemetria é aceitável, descartar uma transação não. Escolher a estratégia errada para o tipo de dado troca um problema por outro.

04Implementação

Do push sem limite ao pull com ritmo

// Sem backpressure: o produtor despeja no ritmo dele, o consumidor
// nao acompanha, e o que sobra vai para um buffer que so cresce.
for (const item of fonteRapida) {
  fila.push(item);        // fila cresce sem limite -> memoria estoura
  processarLento(item);   // nao vaza no ritmo da entrada
}

// Com backpressure: o consumidor DITA o ritmo. Ele pede o que
// aguenta, e o produtor so manda quando ha demanda.
async function consumir(stream: Readable) {
  for await (const item of stream) {   // for await respeita o fluxo:
    await processarLento(item);         // enquanto processa, o stream
  }                                     // PAUSA a leitura da fonte.
}

// O sinal "va mais devagar" sobe a cadeia inteira, ate a origem.
// Quando o buffer enche, as opcoes sao: PAUSAR o produtor,
// DESCARTAR o excesso (drop/sample), ou RECUSAR (rejeitar novos).
05Decisão

Quando usar × quando evitar

Use quando

3
  • Pipelines de streaming onde a fonte pode superar o destino.
  • Ingestão que grava em um sistema mais lento.
  • Sempre que um buffer entre produtor e consumidor puder crescer sem limite.

Evite quando

2
  • Quando o consumidor sempre acompanha o produtor com folga.
  • Em lotes pequenos e limitados, sem ameaça à memória.

Testar o que ficou

3 armadilhas de Backpressure viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • request(n) do Reactive StreamsA API em que o consumidor pede exatamente quantos itens aguenta, impedindo o produtor de despejar mais do que ele processa.
  • highWaterMark dos streams do NodeO limite do buffer que faz o stream pausar a leitura da fonte quando o consumidor não vaza rápido o bastante.
  • a fila que enche e recusaQuando o buffer lota e o sistema passa a recusar ou bloquear novas mensagens, é backpressure pedindo à origem para desacelerar.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Produtor para quando o consumidor não aguenta.
if (fila.length > LIMITE) await esperarDrenar(fila);

O que ele cobra

1 salto a mais entre a chamada e o efeito.

  • O produtor precisa saber lidar com 'vá mais devagar' — pausar, bufferizar ou descartar, cada opção com seu custo
  • Propagar o sinal por toda a cadeia exige que cada elo o respeite, senão ele para no primeiro que o ignora

Não vale se: o consumidor sempre acompanha o produtor com folga — sem risco de afogamento, controlar o fluxo é cerimônia sem retorno.

Onde isto aparece

Continue por aqui