Perda de Mensagens no Redis Pub/Sub: Causas e Alternativas Confiáveis

Descubra por que o Redis Pub/Sub perde mensagens durante desconexões de rede ou consumidores lentos e explore padrões como Redis Streams e filas baseadas em listas para entrega garantida.

Perda de Mensagens no Redis Pub/Sub: Causas e Alternativas Confiáveis

Lembro-me da primeira vez que o Redis Pub/Sub me queimou. Era tarde, por volta das 23h, e nosso sistema de notificações começou a perder mensagens. Não todas — apenas o suficiente para que os usuários percebessem antes de nós. O engenheiro de plantão (eu, infelizmente) passou duas horas vasculhando logs de aplicação antes que a verdade óbvia surgisse: o Redis Pub/Sub não enfileira nada. Não é um broker de mensagens. É uma mangueira de incêndio, e se você não estiver diretamente na frente dela com a boca aberta, vai perder algo.

Essa é a coisa que ninguém te conta quando você usa o Redis Pub/Sub pela primeira vez. Está lá na documentação, tecnicamente, mas é fácil ignorar quando você está empolgado com a simplicidade da API. Você publica de um lado, assina do outro, e funciona. Até que não funciona.

A realidade do "fire-and-forget"

O Redis Pub/Sub opera em um princípio brutalmente simples: quando você publica uma mensagem, o Redis a envia para cada assinante conectado naquele canal naquele exato momento. Se um assinante não está conectado, ou se está conectado mas não consegue acompanhar, a mensagem evapora. Não há camada de persistência, nenhum mecanismo de confirmação, nenhuma fila de mensagens mortas. A mensagem existe apenas em trânsito.

Deixe-me dar um exemplo concreto. Digamos que você tenha um serviço que publica atualizações de status de pedidos, e outro serviço que assina para enviar e-mails de confirmação. Sob carga normal, tudo funciona bem. Então seu serviço de e-mail tem um soluço — talvez o relay SMTP esteja lento, talvez haja uma pausa de coleta de lixo. Durante esse soluço, o Redis continua enviando mensagens. O buffer TCP do assinante enche. Eventualmente, a conexão cai. Quando o assinante reconecta, ele retoma de agora, não de onde parou. Cada mensagem publicada durante a janela de desconexão se foi.

Eu medi isso na prática com uma configuração de teste simples: um publicador disparando 10.000 mensagens por segundo, e um assinante que ocasionalmente bloqueia por 50 milissegundos. Mesmo com uma única pausa breve, você perderá dezenas de mensagens. O assinante nunca sabe que foram enviadas. O publicador nunca sabe que foram perdidas. O Redis está perfeitamente feliz — fez exatamente o que foi projetado para fazer.

O que realmente causa a perda de mensagens

Existem três cenários principais onde o Pub/Sub perde mensagens, e todos valem a pena entender porque aparecerão de maneiras diferentes.

Instabilidade de rede é o mais óbvio. Qualquer partição de rede temporária entre o assinante e o Redis rompe a conexão. O Redis detecta isso via o timeout do cliente (padrão 60 segundos, mas você pode ter definido mais baixo). Durante essa janela, todas as mensagens publicadas são perdidas para aquele assinante. Outros assinantes podem recebê-las normalmente, o que torna a depuração ainda mais divertida — você verá estado inconsistente entre serviços e se perguntará se está ficando louco.

Consumidores lentos são mais insidiosos porque a conexão permanece aberta. O Redis usa um modelo push, ou seja, escreve nos sockets dos assinantes tão rápido quanto os publicadores produzem. Se um assinante não consegue processar mensagens rápido o suficiente, o buffer de recebimento TCP do kernel enche. Uma vez que esse buffer está cheio, o Redis não consegue escrever mais dados, e a conexão eventualmente falha. O assinante pode nem perceber que está atrasado até a desconexão acontecer.

Já vi isso acontecer com assinantes que fazem escritas síncronas no banco de dados para cada mensagem. Em baixo volume, está tudo bem. No pico, o banco de dados se torna o gargalo, o assinante fica para trás, e as mensagens se acumulam no buffer TCP. Quando esse buffer transborda, a conexão é reiniciada, e o assinante perde tudo que ainda não leu do socket.

Desconexões de cliente durante implantações ou reinicializações são a terceira grande categoria. Se você está fazendo implantações contínuas e uma instância de assinante cai, ela perde tudo que foi publicado durante sua ausência. Não há mecanismo de "me atualize". Quando ela volta online, começa do zero.

Uma coisa que me surpreendeu: mesmo um desligamento limpo não ajuda. Se seu assinante cancela a assinatura graciosamente antes de sair, ele ainda perde mensagens publicadas entre o cancelamento e o retorno. O cancelamento é instantâneo — não há opção de "segure minhas mensagens por um minuto".

Quando o Pub/Sub é realmente bom

Não quero dar a impressão de que o Redis Pub/Sub é inútil. Ele é excelente para casos de uso específicos, e ainda o uso regularmente. A chave é entender quais são esses casos de uso.

Notificações em tempo real onde a perda ocasional é aceitável funcionam perfeitamente. Pense em placares de esportes ao vivo, cotações de ações ou indicadores de digitação em um chat. Se um usuário perde uma atualização de placar, a próxima vem em alguns segundos de qualquer forma. Os dados têm uma vida útil curta e nenhum requisito de durabilidade.

Descoberta de serviços e transmissão de configuração são outro ponto ideal. Quando você altera uma flag de funcionalidade e publica para todas as instâncias da aplicação, não há problema se uma instância que está reiniciando perder a atualização — ela pegará o estado atual quando voltar online ou na próxima atualização periódica.

Também usei Pub/Sub com sucesso para invalidação de cache em vários servidores de aplicação. Publique uma chave de cache para invalidar, e cada servidor limpa seu cache local. Se um servidor perde a mensagem, o pior caso é servir dados obsoletos até que a entrada de cache expire naturalmente. Não é ideal, mas também não é catastrófico.

O fio condutor aqui: Pub/Sub funciona quando as mensagens são efêmeras por natureza, quando a perda é recuperável através de outros mecanismos, e quando você não precisa de garantias de ordenação ou entrega exatamente uma vez.

Redis Streams: a alternativa nativa

Redis Streams, introduzido no Redis 5.0, é o que uso agora quando preciso de entrega confiável de mensagens. Não é Pub/Sub com persistência adicionada — é um modelo fundamentalmente diferente, mais próximo de um log distribuído como Kafka do que de um mecanismo de broadcast.

Com Streams, as mensagens são anexadas a um log e permanecem lá até serem explicitamente confirmadas. Consumidores podem desconectar, reiniciar, ficar para trás e ainda assim recuperar. O stream retém mensagens com base em um comprimento máximo ou um período de retenção, então você controla quanto histórico manter.

Aqui está como o modelo mental difere. No Pub/Sub, você assina um canal e as mensagens fluem para você. No Streams, você puxa mensagens no seu próprio ritmo. Um grupo de consumidores rastreia quais mensagens cada consumidor confirmou, então você pode ter múltiplos consumidores lendo do mesmo stream sem duplicação (ou com duplicação intencional, se quiser fan-out).

Uma configuração básica de Streams se parece com isso:

XADD orders * status confirmed order_id 12345

Isso anexa uma mensagem ao stream orders. O * diz ao Redis para gerar um ID automaticamente. Então seu consumidor lê com:

XREADGROUP GROUP email-processor worker-1 COUNT 10 STREAMS orders >

O > significa "me dê mensagens que não foram entregues a nenhum consumidor neste grupo ainda." Após o processamento, o consumidor confirma:

XACK orders email-processor <message-id>

Se o consumidor falhar antes de confirmar, a mensagem permanece pendente. Outro consumidor no grupo pode reivindicá-la com XCLAIM após um timeout. Este é o mecanismo de confirmação e reentrega que o Pub/Sub não tem.

O modelo de grupo de consumidores na prática

Grupos de consumidores são o que torna os Streams genuinamente úteis para processamento confiável. Cada grupo mantém sua própria posição no stream, então você pode ter um grupo para notificações por e-mail, outro para análises, e outro para logging de auditoria — todos lendo o mesmo stream independentemente.

Dentro de um grupo, as mensagens são distribuídas entre os consumidores. Isso oferece escalabilidade horizontal: adicione mais instâncias de consumidor, e elas compartilharão a carga. Se uma instância morre, suas mensagens pendentes ficam disponíveis para outras instâncias reivindicarem.

Descobri que a lista de entradas pendentes é inestimável para monitoramento. Você pode executar XPENDING para ver quais mensagens não foram confirmadas e há quanto tempo estão pendentes. Isso revela consumidores lentos imediatamente — muito melhor do que descobrir perda de mensagens dias depois através de reclamações de usuários.

Um detalhe sobre Streams: os IDs das mensagens são timestamps monotonicamente crescentes, o que significa que você não pode facilmente inserir mensagens fora de ordem. Se você precisa de ordenação estrita dentro de um stream, isso é na verdade uma funcionalidade. Se você precisa priorizar certas mensagens, precisará de múltiplos streams ou uma abordagem diferente.

Filas baseadas em listas para necessidades mais simples

Antes dos Streams existirem, o padrão para mensagens confiáveis com Redis eram filas baseadas em listas com pops bloqueantes. Esse padrão ainda é perfeitamente viável, especialmente se você está em uma versão mais antiga do Redis ou quer algo extremamente simples.

A ideia é direta: produtores usam LPUSH ou RPUSH para colocar mensagens em uma lista, e consumidores usam BLPOP ou BRPOP para bloquear até que uma mensagem chegue. O pop bloqueante é crucial — sem ele, você estaria fazendo polling, o que desperdiça CPU e adiciona latência.

A confiabilidade vem de uma lista secundária de "processamento". O consumidor move atomicamente uma mensagem da fila pendente para uma fila de processamento usando BRPOPLPUSH (ou LMOVE no Redis 6.2+). Após o processamento, ele remove a mensagem da fila de processamento. Se o consumidor falhar, a fila de processamento retém a mensagem, e um processo monitor pode mover itens obsoletos de volta para a fila pendente.

Já construí esse padrão várias vezes, e funciona, mas é mais código do que você esperaria. Você precisa lidar com timeouts, decidir quanto tempo uma mensagem pode ficar na fila de processamento antes de ser considerada abandonada, e lidar com casos extremos de processamento duplicado. Streams essencialmente formalizam tudo isso, e é por isso que me afastei principalmente de filas de lista artesanais.

O único lugar onde ainda uso filas baseadas em listas é para filas de trabalho onde a ordem de processamento não importa e quero a implementação mais simples possível. Às vezes, uma lista e um loop BLPOP são tudo que você precisa, e adicionar Streams seria overengineering.

Pub/Sub fragmentado no Redis 7

O Redis 7 introduziu Pub/Sub fragmentado, que vale a pena mencionar porque resolve um problema diferente da perda de mensagens. Com Pub/Sub regular, cada mensagem é transmitida para todos os nós em um cluster, mesmo que nenhum assinante em um determinado nó se importe com aquele canal. Isso desperdiça largura de banda de interconexão do cluster.

Pub/Sub fragmentado vincula canais a slots específicos do cluster, então as mensagens só se propagam para nós que realmente têm assinantes para aquele canal. É uma otimização de desempenho, não uma funcionalidade de confiabilidade. Você ainda perderá mensagens na desconexão. Mas se você está executando Pub/Sub em escala em um ambiente clusterizado, vale a pena saber.

Fazendo a escolha: Pub/Sub vs Streams vs listas

Depois de anos convivendo com esses padrões, meu processo de decisão se simplificou para algumas perguntas.

Primeiro: você pode tolerar perda de mensagens? Se sim, e se os dados são efêmeros, Pub/Sub provavelmente está bom. Você terá a menor latência e o modelo operacional mais simples.

Segundo: você precisa de persistência e reprodução de mensagens? Se sim, Streams é a resposta. A capacidade de reprocessar mensagens após uma correção de bug no consumidor já me salvou mais de uma vez. Com Pub/Sub, se seu consumidor teve um bug que fez com que ele manipulasse mal as mensagens por uma hora, essas mensagens se foram para sempre. Com Streams, você pode redefinir a posição do grupo de consumidores e reproduzi-las.

Terceiro: você precisa de múltiplos grupos de consumidores independentes lendo os mesmos dados? Streams lida com isso nativamente. Com Pub/Sub, cada assinante recebe todas as mensagens, o que pode ser o que você quer, mas não há como ter diferentes grupos de assinantes mantendo posições independentes.

Quarto: qual é a sua versão do Redis? Se você está preso em algo anterior ao 5.0, Streams não está disponível, e você está olhando para filas baseadas em listas ou um broker de mensagens externo. Já estive nessa situação e, honestamente, se você precisa de mensagens confiáveis e não pode usar Streams, consideraria se o Redis é a ferramenta certa. RabbitMQ ou NATS podem ser mais adequados.

O lado operacional que ninguém menciona

Aqui está algo que aprendi da maneira mais difícil: monitorar Pub/Sub é enganosamente difícil. Você pode monitorar contagens de conexão e assinaturas de canal com PUBSUB NUMSUB, mas não pode ver quantas mensagens estão sendo perdidas. Não há métrica para "mensagens publicadas mas não recebidas" porque o Redis não rastreia isso.

Com Streams, você obtém visibilidade. XINFO GROUPS mostra o lag do consumidor. XPENDING mostra mensagens não confirmadas. Você pode configurar alertas quando o lag excede um limite. Essa visibilidade operacional por si só já valeu a pena a mudança para Streams.

Gerenciamento de memória é outra consideração. Mensagens Pub/Sub existem apenas em memória e apenas enquanto estão em trânsito, então o uso de memória é limitado pela sua taxa de publicação e velocidade do consumidor. Streams armazenam mensagens até serem truncadas, então você precisa pensar em políticas de retenção. Normalmente defino um comprimento máximo de stream (MAXLEN) com base na taxa de transferência esperada e memória disponível, e monitoro o comprimento do stream para detectar acúmulos inesperados.

O que realmente faço agora

Hoje em dia, uso Redis Streams como padrão para qualquer caso de uso de mensagens que exija confiabilidade. A API é ligeiramente mais complexa que Pub/Sub, mas não muito, e as garantias de confiabilidade valem a pena. Mantenho Pub/Sub para coisas efêmeras — invalidação de cache, presença em tempo real, esse tipo de coisa.

Para mensagens particularmente críticas (processamento de pagamentos, atendimento de pedidos), me afastei completamente do Redis e uso brokers de mensagens dedicados. Redis é fantástico em muitas coisas, mas não é otimizado para persistência em disco de filas de mensagens de alto volume. Se você precisa que as mensagens sobrevivam a uma reinicialização completa do Redis com perda zero, precisa configurar persistência AOF com appendfsync always, o que prejudica o desempenho de escrita. Nesse ponto, algo como Kafka ou Pulsar faz mais sentido.

Mas para o vasto meio-termo — onde a perda de mensagens seria irritante ou custosa, mas não catastrófica, e onde você quer permanecer no ecossistema Redis que já conhece — Streams atinge um ponto ideal. Tem sido confiável o suficiente para mim em produção, e a simplicidade operacional de não introduzir um novo componente de infraestrutura tem valor real.

O erro original que cometi com Pub/Sub não foi realmente sobre a tecnologia. Foi sobre não ler as letras miúdas, sobre assumir que "mensagens" implicava "garantias de entrega de mensagens". Redis Pub/Sub não faz tais garantias, e não finge fazer. Uma vez que você entende isso, pode usá-lo apropriadamente e recorrer a Streams quando precisar de mais.