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

02 - Comunicação Assíncrona (Vantagens)

#assincronia#backlog#consistencia-eventual#desacoplamento#filas
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Comunicação Assíncrona (Vantagens)

A comunicação assíncrona é o modelo em que o produtor envia uma mensagem a uma infraestrutura intermediária, tipicamente uma fila ou um log de eventos, e continua sua execução sem esperar o processamento pelo destinatário. O consumidor retira a mensagem quando tiver capacidade, em um momento que pode ser distante do envio. É o padrão que sustenta processamentos em segundo plano, pipelines de dados, integrações entre domínios e arquiteturas orientadas a eventos. Sua relevância em produção é central porque resolve o problema estrutural do modelo síncrono: o acoplamento temporal, que força disponibilidade e capacidade a serem idênticas nos dois lados da chamada. O ganho tem preço, e o engenheiro precisa conhecer os dois lados: complexidade operacional adicional, consistência eventual e dificuldade maior de depuração.

Desacoplamento temporal

A vantagem fundamental da assincronia é que produtor e consumidor não precisam coexistir. O produtor pode publicar às 14h e o consumidor processar às 14h02, ou às 15h, ou depois de um deploy que reiniciou todas as instâncias. Nenhuma das partes depende da disponibilidade da outra no instante da interação.

Esse desacoplamento tem três efeitos práticos imediatos. Primeiro, falha do consumidor não falha o produtor: se o serviço de e-mail cai, os pedidos continuam sendo criados, e os e-mails apenas atrasam. Segundo, deploy e restart do consumidor não interrompem o fluxo de negócio, porque as mensagens ficam retidas na infraestrutura até o retorno. Terceiro, a evolução técnica dos lados fica livre: o consumidor pode ser reescrito em outra linguagem, migrar de versão gradualmente ou ser substituído, sem contrato de chamada em tempo real com quem publica.

O desacoplamento também vale para o conhecimento mútuo. Em arquiteturas orientadas a eventos, o produtor publica um fato, como PedidoCriado, e não sabe quem consome; novos consumidores podem ser adicionados sem alterar o produtor. Essa é a base da extensibilidade que filas e logs oferecem sobre RPC, onde cada novo interessado exigiria modificação no chamador.

Resiliência com backlog

O backlog é o estoque de mensagens retidas na fila esperando processamento. Ele funciona como amortecedor entre taxas de produção e taxas de consumo que nunca coincidem exatamente em produção. Quando o tráfego dobra por trinta minutos, um sistema síncrono começa a rejeitar requisições; um sistema assíncrono acumula o excedente na fila e o consome no ritmo possível, esticando o tempo de processamento em vez de falhar.

Esse amortecedor transforma falhas em atrasos. Um consumidor derrubado por um bug não perde mensagens: elas esperam na fila e são processadas quando a correção entra em produção. Compare com o modelo síncrono, em que as requisições recebidas durante a queda simplesmente falharam, e quem as originou precisa descobrir isso e reexecutar por conta própria. O backlog também permite reprocessamento intencional: manter mensagens retidas por horas ou dias dá a capacidade de reconsumir o fluxo após um bug de dados, reconstruindo o estado derivado de todos os eventos.

O custo do amortecedor é a latência variável. O SLA deixa de ser tempo de resposta individual e passa a ser tempo de processamento total, medido do envio à conclusão. Monitorar o tamanho da fila e a idade da mensagem mais antiga, o chamado backlog age ou time-to-process, é o equivalente operacional de monitorar latência p99 em sistemas síncronos.

# Pseudocódigo: fluxo assíncrono com amortecimento de pico e idempotência

# Produtor, dentro da operação de negócio, após gravar o pedido:
pedido = banco.gravar(novo_pedido)
fila.publicar(topico = "pedidos", chave = pedido.cliente_id,
              mensagem = EventoPedidoCriado(id = pedido.id))
responder_ao_usuario(202, {pedido_id: pedido.id, status: "em processamento"})

# Consumidor, com escala horizontal, processa no próprio ritmo:
mensagem = fila.receber(visibilidade = 60s)
if tabela_processados.existe(mensagem.id):
    fila.confirmar(mensagem)            # duplicata já processada: só confirma
    return
processar(mensagem)                     # e-mail, provisionamento, cobrança
tabela_processados.gravar(mensagem.id)  # marca ANTES do confirmar
fila.confirmar(mensagem)                # só agora a mensagem sai da fila

# Sob pico: taxa de entrada multiplica, a de consumo segue estável,
# o backlog cresce e depois decresce; nenhuma requisição é rejeitada.

Throughput e elasticidade

Consumidores escalam horizontalmente de forma independente do produtor. Em uma fila com consumidores concorrentes, cada consumidor retira mensagens em paralelo, e a taxa total é a soma das taxas individuais; adicionar instâncias aumenta o throughput sem alterar o produtor. Em um log particionado como Kafka, o paralelismo é dado pelo número de partições, e a elasticidade é limitada por essa decisão de desenho tomada na criação do tópico.

O ganho de throughput também vem do batching natural da mensageria. Produtores agrupam mensagens em lotes por conexão, brokers persistem em blocos sequenciais em disco, e consumidores processam em janelas; cada uma dessas amortizações multiplica a eficiência em comparação com RPC individual por requisição. É por isso que pipelines de ingestão de dados de alto volume são assíncronos por padrão, e que o custo marginal por mensagem tende a ser centavos em vez de custo de thread e conexão.

A elasticidade resulta da combinação. Em um pico de vendas, o número de consumidores é aumentado, o backlog é drenado mais rápido, e ao final do pico as instâncias extra são removidas. O produtor nunca precisou saber que houve pico além de publicar mais mensagens, e o banco de dados do consumidor, que é o gargalo real, recebe carga na taxa que aguenta, controlada pelo ritmo de consumo.

Mecânica interna: vida de uma mensagem

Entender o ciclo completo da mensagem evita a maior parte dos erros operacionais. O produtor publica e recebe confirmação de persistência do broker; nessa etapa existem trade-offs de durabilidade, como confirmar escrita em réplica antes de responder ou aceitar perda mínima por velocidade. A mensagem fica retida até ser consumida ou expirar.

O consumidor recebe a mensagem e precisa confirmar o processamento, o ack; enquanto não confirma, o broker considera a entrega em aberto. Se o consumidor cai antes de confirmar, a mensagem volta para a fila e é entregue novamente a outro consumidor, o que é exatamente o comportamento desejado na falha e a fonte da duplicidade no sucesso parcial.

Quando uma mensagem falha repetidamente, ela precisa sair do fluxo principal para não bloquear o consumo: é a fila de mortos, com a mensagem original e o motivo da falha preservados para investigação. Sem esse caminho de escape, uma mensagem venenosa trava a fila inteira atrás dela, e o backlog cresce indefinidamente enquanto o consumidor tenta a mesma falha em loop.

# Política de consumo ilustrativa: retry escalonado e destino de falha
consumer:
  prefetch: 10                    # mensagens em processamento por instância
  concurrency: 8                  # workers paralelos por instância
  visibility_timeout: 60s         # tempo para concluir antes de nova entrega
  retry:
    max_attempts: 5               # após a quinta falha, vai para DLQ
    backoff: [10s, 30s, 2m, 10m]  # escalonado, evita martelar dependência caída
dead_letter:
  target: "pedidos-dlq"           # fila isolada, com retenção maior
  include: [motivo, tentativas, correlation_id]  # contexto para investigação
alerts:
  backlog_size: "> 10000 por 10m" # volume anômalo acumulado
  message_age: "> 15m"            # dado velho chegando ao consumidor
  dlq_rate: "> 0 por 5m"          # qualquer falha persistente merece olhar

O preço: complexidade operacional e consistência eventual

A assincronia muda o modelo mental de execução para um modelo distribuído, e isso aparece em vários pontos. A depuração deixa de ter um stack trace completo: um fluxo espalhado entre produtor, broker e três consumidores exige correlação por id de mensagem e rastreamento distribuído para reconstruir o caminho. O teste local exige broker rodando ou contêiner, e a reprodução de bugs depende de sequência e timing.

A consistência vira eventual por construção. Entre a gravação do pedido e o envio do e-mail de confirmação existe uma janela em que o estado percebido pelos sistemas diverge. A interface precisa comunicar isso, geralmente com estado de processamento, e o negócio precisa aceitar que a resposta imediata é um compromisso de processamento, não a conclusão. Quando a operação exige leitura da própria escrita, como o usuário verificar o pedido recém-criado, o desenho precisa garantir leitura do mesmo serviço que escreveu ou sincronização explícita.

A lista de novas responsabilidades é concreta: ordem de processamento passa a exigir decisão explícita, mensagens podem ser entregues mais de uma vez e exigem idempotência, falhas de processamento precisam de política de retry e fila de mortos, e o schema das mensagens precisa de versionamento compatível, porque produtor e consumidor são implantados em momentos diferentes.

Quando a assincronia vale o custo

A assincronia é a escolha correta quando a tarefa não precisa de resposta imediata para o fluxo continuar. Os casos clássicos em produção: envio de notificações, geração de documentos e relatórios, processamento de mídia, sincronização com sistemas externos, cobrança e faturamento, atualização de índices de busca e projeções de leitura em CQRS. Em todos, o usuário não espera o resultado na tela, e o custo do modelo assíncrono é menor que o benefício de desacoplar.

Ela também é a resposta correta para absorver picos. Quando a taxa de entrada é espinhosa, como emissão de bilhetes na abertura de vendas, a fila permite aceitar tudo imediatamente e processar em minutos, em vez de recusar parte do tráfego. O negócio decide se atraso é aceitável; se for, a fila é a ferramenta certa.

O contraponto é igualmente claro: operações de leitura que compõem a resposta da tela, autorizações e validações de unicidade permanecem síncronas. Misturar os dois modelos na mesma operação é comum e recomendado: o cadastro é síncrono até gravar o registro e responder o usuário, e assíncrono para tudo que vem depois.

Cenários reais de produção

Cenário de geração de relatórios: uma área corporativa pede exportações de dados que chegam a processar por minutos. Implementado síncronamente, o endpoint trava workers do serviço principal e derruba a latência do restante do sistema. Com fila, o endpoint apenas registra o pedido e devolve um identificador; workers dedicados processam a fila, e o usuário acompanha ou recebe a notificação de arquivo pronto. O pico de pedidos às segundas pela manhã vira apenas um backlog temporário.

Cenário de integração com terceiro instável: um ERP legado responde em janelas irregulares e fica indisponível várias vezes ao dia. Sincronamente, cada indisponibilidade falha pedidos; assincronamente, as mensagens esperam na fila durante a janela de queda e são processadas quando o ERP volta. A taxa de sucesso do fluxo passa a depender da taxa de sucesso agregada do dia, e não do instante exato de cada chamada.

Cenário de fanout para múltiplos destinos: um evento de pedido precisa alimentar faturamento, estoque, analytics e e-mail. Com chamadas síncronas, o serviço de pedidos conhece e depende dos quatro; com publicação assíncrona, ele publica PedidoCriado uma vez, e cada interesseiro consome independentemente. O quinto sistema, incluído depois, entra sem nenhuma mudança no produtor.

Padrões de consumo assíncrono

O padrão de consumidores concorrentes é o mais comum: múltiplas instâncias consomem a mesma fila, cada mensagem vai para um consumidor só, e a ordem entre mensagens não é garantida. É o padrão de uma fila de tarefas, onde mensagens são independentes entre si.

O padrão de publicação-assinatura entrega a mesma mensagem a todos os assinantes, cada um com sua própria cópia ou fila de destino. Serve para eventos que interessam a vários sistemas, e é a base do fanout gerenciado, como SNS distribuindo para múltiplas filas SQS.

O padrão de log com partições, tipificado pelo Kafka, combina os dois mundos: ordenação garantida dentro de cada partição por chave, paralelismo entre partições e possibilidade de múltiplos grupos independentes lendo o mesmo fluxo. A escolha do padrão é uma decisão de arquitetura, não de biblioteca, porque define garantias de ordem e de entrega do fluxo inteiro.

Comparação de garantias e custos

Aspecto Síncrono (RPC) Assíncrono (fila ou log)
Acoplamento Temporal: destino precisa estar de pé Nenhum: as partes não coexistem
Resposta ao chamador Resultado imediato Compromisso de processamento
Falha do destino Falha a chamada Mensagem retida, atraso
Pico de tráfego Rejeições ou latência em cascata Backlog absorve, processa no ritmo possível
Ordenação Natural por chamada Requer decisão: partição por chave ou consumidor único
Duplicidade Rara, controlável por retry Comum em at-least-once, exige idempotência
Consistência Forte no escopo da chamada Eventual entre produtor e consumidor
Depuração Stack trace e trace contínuo Correlação por id, rastreamento distribuído
SLA típico Latência p99 da requisição Tempo de processamento e idade do backlog

Boas práticas e erros comuns

  • Tratar a fila como depósito de todas as integrações: tarefa que exige resposta imediata ao usuário não pertence a fila; forçar assincronia onde há dependência de leitura cria telas eternamente em processamento.
  • Não definir idempotência do consumidor: com entrega at-least-once, duplicidade é questão de tempo; chave de idempotência na mensagem e verificação de processamento anterior são obrigatórias.
  • Ignorar a idade das mensagens: uma fila com backlog constante e mensagens de 6 horas processa dados obsoletos; alarme deve existir para tamanho e idade, não apenas para taxa de erro.
  • Usar a fila como canal de comando entre sistemas que precisam de resposta: se o fluxo precisa de confirmação para continuar, o desenho correto é RPC síncrono ou padrão de requisição-resposta sobre mensageria, com correlação explícita.
  • Deixar a ordenação implícita: consumir em paralelo quebra a ordem de eventos; quando a ordem importa, particionar por chave de entidade e consumir sequencialmente dentro da partição.
  • Não versionar o schema das mensagens: produtor atualizado e consumidor antigo coexistem durante deploys; o contrato de mensagem precisa de regras de compatibilidade, como campos novos opcionais.
  • Faltar com observabilidade ponta a ponta: cada mensagem deve carregar correlation id, e o consumidor deve logar tempo de fila, tempo de processamento e resultado, senão o fluxo se torna caixa preta.
  • Esquecer o destino final dos dados: o consumidor escreve em banco com capacidade finita; drenar a fila mais rápido do que o banco aguenta só desloca o gargalo e gera timeout em cascata.
  • Omitir a fila de mortos: sem ela, mensagem venenosa trava o consumo; com ela, a falha fica isolada, investigável e reprocessável depois da correção.
  • Medir apenas a taxa de mensagens por segundo: taxa alta com idade de backlog crescendo é processamento atrasado, não saúde; o par taxa e idade é que descreve a realidade.
  • Construir cadeias de filas sem limites de profundidade: fila A alimenta fila B que alimenta fila C, e uma falha no final multiplica backlog em toda a cadeia; cada elo precisa de política de retenção, alarme e escoamento.
  • Tratar confirmação de publicação como garantia de processamento: o produtor recebe ack do broker, não a conclusão do trabalho; monitorar o lado do consumidor é parte do SLA do fluxo.