Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

01 - Ponto a Ponto (Queue)

#competicao-de-consumidores#fila#load-balancing#work-queue
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Ponto a Ponto (Queue)

A comunicação ponto a ponto sobre fila é o padrão de mensageria mais elementar e o mais implantado em produção. O produtor envia cada mensagem para uma fila e exatamente um consumidor processa aquela mensagem, mesmo que dezenas de consumidores estejam conectados à mesma fila. Esse comportamento, conhecido como competição de consumidores, transforma a fila em um distribuidor de carga natural, sem balanceador explícito no desenho. Em produção, o padrão sustenta filas de trabalho como processamento de pagamentos, geração de documentos, envio de notificações e consumo de webhooks, cenários em que cada unidade de trabalho deve ser executada uma vez por alguém, e não por todos. Dominar esse padrão é pré-requisito para a decisão entre fila e tópico, escolha que aparece em praticamente todo desenho de arquitetura orientada a eventos.

Conceito e semântica de entrega

A fila é uma estrutura de retenção ordenada gerenciada pelo broker. O produtor publica e o consumidor consome no ritmo que consegue processar, em geral por meio de pull ou de entrega empurrada em lote. A semântica central do padrão é direta: uma mensagem entregue a um consumidor não é entregue a nenhum outro consumidor da mesma fila.

Essa semântica é o oposto do broadcast do publish-subscribe, no qual cada assinante recebe sua própria cópia do fluxo. Na fila, os consumidores competem entre si por trabalho; no pub-sub, eles observam o mesmo fluxo de forma independente. Nenhuma das duas abordagens é superior, pois respondem a perguntas diferentes: trabalho dividido versus informação distribuída.

Vale distinguir ponto a ponto em mensageria de comunicação ponto a ponto em rede. Aqui o termo descreve a relação entre fila e grupo de consumidores, não a topologia física da rede. Um cluster de brokers por trás da fila não muda a semântica percebida por produtor e consumidor.

O nome formal do comportamento é competing consumers, e a literatura de integração empresarial o descreve desde os primeiros sistemas de filas de mensagens. A ideia é tratar consumidores como um pool anônimo e intercambiável: nenhum deles é dono de mensagem específica, e a perda de qualquer instância individual não afeta o resultado agregado. Essa anonimidade é justamente o que dá ao padrão sua resiliência operacional.

O problema que resolve

O problema clássico é o pico de carga sobre um trabalhador único. Sem fila, o produtor chama o processador diretamente, que vira gargalo e ponto único de falha ao mesmo tempo. Com fila ponto a ponto, o produtor despeja trabalho e segue operando, enquanto o pool de consumidores absorve o volume na capacidade disponível naquele momento.

O segundo problema é a diferença de velocidade entre produtor e consumidor. Um endpoint que recebe webhooks de um parceiro pode receber cinco mil eventos em um minuto, enquanto o processamento real leva cinquenta milissegundos por item. A fila absorve essa diferença como backlog, e o conjunto de consumidores processa no ritmo sustentável, sem recusar o produtor.

O terceiro problema é elasticidade sem configuração. Como as mensagens são distribuídas entre quem estiver disponível, escalar significa subir mais réplicas do consumidor e pronto. Não existe política de balanceamento para ajustar, afinidade para calcular ou sharding manual para manter, porque o próprio mecanismo de entrega faz a distribuição.

Há ainda um benefício silencioso: a fila converte falha do consumidor em atraso, e não em perda. Se o worker cai durante um deploy, o trabalho apenas espera; o produtor não recebe erro e o usuário não vê falha. Essa conversão de indisponibilidade em latência é uma das razões mais fortes para adotar o padrão em integrações com sistemas fora do seu controle.

Funcionamento interno

O ciclo de vida da mensagem no padrão tem quatro passos fundamentais:

  1. O produtor publica a mensagem na fila e o broker grava, confirmando a publicação.
  2. O broker entrega a mensagem a um consumidor disponível, tipicamente em rodízio ou para o menos ocupado.
  3. O consumidor processa o conteúdo e envia o ack, e o broker então remove a mensagem definitivamente.
  4. Sem ack dentro do prazo de visibilidade, o broker devolve a mensagem à fila e a reentrega a outro consumidor.

O passo quatro é o que garante progresso quando um consumidor morre no meio do processamento. A mensagem retorna e outro worker assume a tarefa, e o trabalho não se perde. O preço dessa robustez é a possibilidade de entrega duplicada, tema aprofundado na garantia at-least-once.

O mecanismo de exclusividade no recebimento é o coração técnico do padrão. Brokers implementam essa exclusividade com marcas de entrega: enquanto o ack não chega, a mensagem fica reservada para o consumidor que a recebeu e oculta para os demais. Em RabbitMQ, essa reserva é implícita na entrega não confirmada; em SQS, é o visibility timeout; em Kafka com consumer group, é o offset commitado por partição. O mecanismo muda, a semântica permanece.

O controle de prefetch define quantas mensagens ficam em voo por consumidor antes do ack. Prefetch alto aumenta throughput e reduz latência de ida e volta ao broker, mas concentra carga no worker mais rápido e pode deixar workers lentos com fatia pequena do trabalho. Prefetch baixo distribui melhor, porém multiplica viagens de rede. Em filas de trabalho com tarefas de duração parecida, valores entre dez e cinquenta costumam equilibrar bem.

A distribuição entre consumidores é melhor-esforço, não uma promessa de igualdade perfeita. Workers com processamento mais rápido pedem a próxima mensagem antes, e absorvem mais carga ao longo do tempo; esse viés é desejável, pois maximiza throughput agregado. Em SQS, a entrega em lote de até dez mensagens por chamada reduz custo e latência, e a visibilidade independente por mensagem evita que duas réplicas processem o mesmo item simultaneamente.

# Produtor: publica tarefas sem saber quantos workers existem
for pedido in pedidos_pendentes:
    fila.enviar(
        queue="pedidos.faturamento",
        corpo=pedido.para_json(),
        message_id=pedido.id,          # base da idempotência no consumidor
    )

# Consumidor: N réplicas deste mesmo processo competem pela fila
def processar(mensagem):
    try:
        faturar(mensagem.corpo)        # trabalho real da fila
        mensagem.ack()                 # confirma e remove da fila
    except ErroTransitorio:
        raise                          # sem ack: volta e será reentregue
    except ErroDeContrato:
        mensagem.nack(requeue=False)   # falha permanente: segue para DLQ

# Escalar é apenas subir réplicas, sem mudar configuração de balanceamento
# kubectl scale deployment faturamento-worker --replicas=8

Aplicação prática em produção

Os cenários que mais se beneficiam compartilham uma característica: a mensagem é uma unidade de trabalho independente. Cada item pode ser processado isoladamente, sem necessidade de que outros consumidores vejam o mesmo item.

Cenário Por que fila ponto a ponto
Geração de relatórios sob demanda Cada pedido é processado uma vez por um worker
Ingestão de webhooks de parceiros Desacopla recebimento rápido do processamento lento
Processamento de imagens e vídeos Pool de workers com GPU divide a carga naturalmente
Envio de e-mails em massa Throughput limitado pelo provedor, backlog absorvido
Processamento de arquivos em lote Paralelismo por item com retry isolado por falha

Em microsserviços, o padrão aparece como cola entre etapa síncrona e etapa pesada: a API aceita a requisição, grava o comando e publica na fila; os workers fazem o trabalho caro. A latência percebida pelo usuário fica na casa de milissegundos, e a complexidade fica confinada ao consumidor.

A alocação de capacidade também fica simples. Se o consumo médio é de duzentas mensagens por minuto e cada worker processa quarenta, cinco réplicas sustentam o fluxo com folga; para picos de três vezes, autoscaling por profundidade de fila ou por idade de mensagem cobre o intervalo entre pico e média. Essa métrica de escala é mais direta que CPU, porque reflete o problema real: trabalho esperando.

Comparação com publish-subscribe

Aspecto Fila (ponto a ponto) Pub-Sub
Consumidores por mensagem Exatamente um Todos os assinantes
Papel do consumidor Competição por trabalho Observação do mesmo fluxo
Balanceamento de carga Intrínseco ao padrão Não é objetivo do padrão
Novo consumidor Reduz o backlog mais rápido Recebe cópia do fluxo a partir de então
Escala do produtor Limitada pelo consumo agregado Independente dos assinantes
Caso típico Fila de tarefas e workers Notificação de eventos de domínio
Broker Implementação do padrão
RabbitMQ Fila clássica ou quorum com acks manuais e prefetch
SQS Fila standard ou FIFO com visibility timeout
Kafka Partição única por consumer group, competição limitada por partição
ActiveMQ Fila JMS com dispatch round-robin para consumidores

A regra de decisão prática: se a mensagem representa trabalho a ser feito, use fila; se representa um fato que interessa a várias partes, use pub-sub. Arquiteturas maduras combinam os dois, com um tópico recebendo o evento e cada fila de trabalho assinando o recorte que lhe interessa. Essa composição preserva a entrega independente do pub-sub e a competição saudável da fila dentro de cada time.

Um detalhe que confunde times ao migrar de fila para tópico: no pub-sub, adicionar um assinante não divide a carga dos existentes, apenas soma novo processamento. Se a intenção era escalar, a mudança errada multiplica trabalho; a correta é aumentar réplicas do consumidor da fila. Deixar essa distinção explícita em ADR evita decisões invertidas sob pressão de incidente.

Erros comuns

  • Assumir ordem global de processamento: com múltiplos consumidores competindo, a ordem de processamento não é garantida, e qualquer lógica dependente de sequência quebra silenciosamente sob carga.
  • Prefetch alto sem justificativa: workers seguram centenas de mensagens em memória, e um crash devolve todas para a fila, multiplicando retrys e duplicando esforço.
  • Tratar a fila como banco de dados: consultas, joins e agregações sobre mensagens indicam que o dado deveria estar em um store, e a fila deveria carregar apenas a referência.
  • Ignorar duplicatas: como reentrega existe por desenho, processar sem idempotência gera cobrança dupla, e-mail repetido e estoque descontado duas vezes.
  • Fila única para cargas heterogêneas: tarefas rápidas ficam presas atrás de tarefas lentas, e o backlog cresce sem que nenhum erro apareça nos logs.
  • Reservar mensagem dentro do consumidor por longos períodos: a mensagem invisível bloqueia reentrega para outros workers, e um travamento invisível aparece apenas como fila parada.
  • Publicar trabalho dependente de estado local do worker: o padrão pressupõe consumidores intercambiáveis, e mensagens que só um nó consegue processar destroem a resiliência do pool.

Boas práticas

Trate a fila como contrato de operação, não como detalhe de implementação: nome padrão, política de retenção, DLQ e limites de backlog definidos antes do primeiro deploy.

  • Torne o consumidor idempotente desde o primeiro dia, usando a message_id ou uma chave de negócio como base da deduplicação.
  • Monitore profundidade da fila e idade da mensagem mais antiga; a idade é o sinal mais honesto de saturação, mais que a contagem absoluta.
  • Ajuste o prazo de visibilidade para o pior caso real de processamento, evitando reentrega durante trabalho legítimo em andamento.
  • Separe filas por tipo de tarefa e por classe de criticidade, para que um pico em uma carga não penalize as demais.
  • Prefira mensagens pequenas com referência ao dado, mantendo o payload pesado em object storage ou no banco de origem.
  • Defina DLQ desde a criação da fila, com alerta e runbook de reprocessamento, e não como remendo posterior a incidentes.
  • Documente a semântica de entrega da fila no contrato do time, porque consumidores precisam saber se duplicatas são possíveis.
  • Versione o schema da mensagem e mantenha compatibilidade retroativa, para que deploys de produtor e consumidor sejam independentes.