
Preparamos a tradução da próxima parte do artigo em série, que compara a funcionalidade do Apache Kafka e RabbitMQ. Esta publicação trata de semântica e garantias de entrega de mensagens. Observe que o autor levou em conta o Kafka até a versão 0.10 inclusive, e na versão 0.11 apareceu exatamente uma vez. No entanto, o artigo permanece relevante e é cheio de pontos úteis do ponto de vista prático.
Peças anteriores:
primeiro ,
segundo .
O RabbitMQ e o Kafka oferecem garantias confiáveis de entrega de mensagens. Ambas as plataformas oferecem garantias nos princípios de "entrega no máximo única" e "pelo menos entrega única", mas com o princípio de "entrega única e rigorosa", as garantias da Kafka se aplicam de acordo com um cenário muito limitado.
Primeiro, vamos descobrir o que essas garantias significam:
- Entrega no máximo. Isso significa que a mensagem não pode ser entregue mais de uma vez. No entanto, a mensagem pode ser perdida.
- Pelo menos uma vez entrega. Isso significa que a mensagem nunca será perdida. Nesse caso, a mensagem pode ser entregue mais de uma vez.
- Entrega exatamente uma vez. O Santo Graal dos sistemas de mensagens. Todas as mensagens são entregues estritamente uma vez.
A palavra "entrega" aqui provavelmente não será um termo exato. Seria mais preciso dizer "processamento". De qualquer forma, o que nos interessa agora é se o consumidor pode processar mensagens e por que princípio isso acontece: “não mais que um”, “pelo menos um” ou “estritamente uma vez”. Mas a palavra “processamento” complica a percepção, e a expressão “entrega pelo princípio de 'estritamente uma vez'” neste caso não será uma definição precisa, porque pode ser necessário entregar a mensagem duas vezes para processá-la adequadamente uma vez. Se o destinatário desconectado durante o processamento, a mensagem deverá ser enviada novamente para o novo destinatário.
O segundo Discutindo a questão do processamento de mensagens, chegamos ao tópico de falhas parciais, que é uma dor de cabeça para os desenvolvedores. No processo de processamento da mensagem, há várias etapas. Consiste em sessões de comunicação entre o aplicativo e o sistema de mensagens no início e no final e o próprio aplicativo trabalhando com os dados no meio. Os cenários de falha parcial do aplicativo devem ser tratados pelo próprio aplicativo. Se as operações executadas forem completamente transacionais e os resultados forem formulados com base no princípio de "tudo ou nada", poderão ser evitadas falhas parciais na lógica do aplicativo. Mas muitas vezes, muitos estágios incluem interação com outros sistemas, onde a transacionalidade é impossível. Se incluirmos na interação os relacionamentos entre sistemas de mensagens, aplicativos, cache e banco de dados, podemos garantir o processamento “apenas uma vez”? A resposta é não.
A estratégia "estritamente uma vez" é limitada a um cenário em que o único destinatário das mensagens processadas é a própria plataforma de mensagens, e essa plataforma fornece transações completas. Nesse cenário limitado, você pode processar mensagens, gravá-las, enviar sinais de que foram processadas como parte de uma transação feita com base no princípio de "tudo ou nada". É fornecido pela biblioteca Kafka Streams.
Mas se o processamento de mensagens for sempre idempotente, você poderá evitar a necessidade de implementar a estratégia "estritamente uma vez" por meio de transações. Se o processamento final de mensagens for idempotente, você pode se preocupar em aceitar duplicatas. Mas nem todas as ações podem ser implementadas idempotentemente.
Alerta de ponta a pontaO que não está representado em nenhum dispositivo de todos os sistemas de mensagens com os quais trabalhei é a confirmação de ponta a ponta. Dado que uma mensagem pode ser enfileirada no RabbitMQ, a notificação de ponta a ponta não faz sentido. No Kafka, da mesma forma, vários grupos diferentes de destinatários podem ler informações simultaneamente de um tópico. Na minha experiência, alertas de ponta a ponta são o que as pessoas que são novas no conceito de mensagens geralmente pedem. Nesses casos, é melhor explicar imediatamente que isso não é possível.
Cadeia de responsabilidadeEm geral, as fontes de mensagens não podem saber que suas mensagens são entregues aos destinatários. Eles só podem saber que o sistema de mensagens recebeu suas mensagens e assumiu a responsabilidade de garantir seu armazenamento e entrega seguros. Há uma cadeia de responsabilidades, que começa com a fonte, passa pelo sistema de mensagens e termina no destinatário. Todos devem cumprir corretamente seus deveres e transmitir claramente a mensagem para a próxima. Isso significa que você, como desenvolvedor, deve projetar corretamente seus aplicativos para evitar a perda ou uso indevido de mensagens enquanto elas estão sob seu controle.
Procedimento de transferência de mensagensEste artigo é dedicado principalmente a como cada plataforma fornece estratégias de envio “pelo menos uma” e “não mais que uma”. Mas ainda há uma ordem de mensagens. Nas partes anteriores desta série, escrevi sobre a ordem em que as mensagens são enviadas e a ordem em que são processadas, e aconselho que você consulte essas partes.
Em suma, o RabbitMQ e o Kafka fornecem uma garantia de primeiro a entrar, primeiro a sair (FIFO). O RabbitMQ mantém esse pedido no nível da fila e Kafka no nível de segmentação. As implicações de tais decisões de design foram discutidas em artigos anteriores.
Garantias de entrega no RabbitMQAs garantias de entrega são fornecidas:
- confiabilidade da mensagem - eles não desaparecerão enquanto armazenados no RabbitMQ;
- notificações de mensagem - o RabbitMQ troca sinais com remetentes e destinatários.
Elementos de confiabilidade
Espelhamento de FilasAs filas podem ser espelhadas (replicadas) em muitos nós (servidores). Cada fila possui uma fila principal em um dos nós. Por exemplo, existem três nós, 10 filas e duas réplicas por fila. 10 filas de controle e 20 réplicas serão distribuídas por três nós. A distribuição das filas de controle por nós pode ser configurada. No caso de um nó congelar:
- em vez de cada fila principal no nó travado, uma réplica dessa fila é fornecida em outro nó;
- novas réplicas são criadas em outros nós para substituir as réplicas perdidas no nó de saída, dando suporte ao fator de replicação.
A questão da tolerância a falhas será discutida na próxima parte do artigo.
Filas confiáveisExistem dois tipos de filas no RabbitMQ: confiáveis e não confiáveis. Filas confiáveis são gravadas no disco e salvas no caso de uma reinicialização do nó. Quando o nó inicia, eles são substituídos.
Postagens persistentesSe a fila for confiável, isso não significa que suas mensagens serão salvas quando o nó for reinicializado. Somente as mensagens marcadas como persistentes pelo remetente serão recuperadas.
Ao trabalhar no RabbitMQ, quanto mais confiável a mensagem, menor o desempenho possível. Se houver um fluxo de eventos em tempo real e não for crítico perder vários deles ou um pequeno intervalo de tempo do fluxo, é melhor não usar a replicação de fila e transmitir todas as mensagens como instáveis. Porém, se não for desejável perder mensagens devido ao mau funcionamento de um nó, é melhor usar filas confiáveis com replicação e mensagens estáveis.
Notificações de mensagem
MensagensAs mensagens podem ser perdidas ou duplicadas durante a transmissão. Depende do comportamento do remetente.
“Atirou e esqueceu”A fonte pode decidir não solicitar a confirmação do destinatário (notificação de recebimento de uma mensagem ao remetente) e simplesmente enviar a mensagem automaticamente. As mensagens não serão duplicadas, mas poderão ser perdidas (o que satisfaz a estratégia de "entrega única máxima").
Confirmações ao remetenteQuando o remetente abre um canal para o broker de filas, ele pode usar o mesmo canal para enviar confirmações. Agora, em resposta à mensagem recebida, o broker da fila deve fornecer uma das duas coisas:
- basic.ack. Confirmação positiva. A mensagem é recebida, a responsabilidade por ela agora cabe ao RabbitMQ;
- basic.nack. Confirmação negativa. Algo aconteceu e a mensagem não foi processada. A responsabilidade por isso permanece na fonte. Se desejar, ele pode enviar uma mensagem uma segunda vez.
Além das notificações de entrega positiva e negativa, é fornecida uma mensagem basic.return. Às vezes, o remetente precisa saber não apenas que a mensagem chegou no RabbitMQ, mas também que realmente caiu em uma ou mais filas. Pode acontecer que a origem envie uma mensagem para o sistema de troca de tópicos, no qual a mensagem não é roteada para nenhuma das filas de entrega. Nessa situação, o broker simplesmente descarta a mensagem. Em alguns cenários, isso é normal; em outros, a fonte deve saber se a mensagem foi descartada e prosseguir de acordo com isso. Você pode definir o sinalizador "Obrigatório" para mensagens individuais e, se a mensagem não tiver sido definida em nenhuma fila de entrega, o basic.return será retornado ao remetente.
A fonte pode aguardar confirmação após o envio de cada mensagem, mas isso reduzirá bastante seu desempenho. Em vez disso, as fontes podem enviar um fluxo constante de mensagens, definindo um limite para o número de mensagens não reconhecidas. Quando o limite de mensagens provisórias for atingido, o envio será interrompido até que todas as confirmações sejam recebidas.
Agora que existem muitas mensagens em trânsito do remetente para o RabbitMQ, as confirmações são agrupadas usando o sinalizador múltiplo para melhorar o desempenho. Todas as mensagens enviadas pelo canal recebem um valor inteiro monotonicamente crescente, o “Número de Sequência”. A notificação de uma mensagem inclui o número de sequência da mensagem correspondente. E se, ao mesmo tempo, o valor for múltiplo = verdadeiro, o remetente deverá rastrear os números de sequência de suas mensagens para saber quais mensagens foram entregues com êxito e quais não. Eu escrevi um artigo detalhado sobre este tópico.
Graças às confirmações, evitamos a perda de mensagens das seguintes maneiras:
- reenviar mensagens em caso de notificação negativa;
- armazenamento contínuo de mensagens em algum lugar em caso de notificação negativa ou retorno básico.
TransaçõesAs transações raramente são usadas no RabbitMQ pelos seguintes motivos:
- Fracas garantias. Se as mensagens forem roteadas para várias filas ou tiverem um ícone obrigatório, a continuidade da transação não será suportada;
- Baixa produtividade.
Sinceramente, nunca as usei, elas não dão garantias adicionais, exceto as confirmações ao remetente, e apenas aumentam a incerteza na questão de como interpretar o aviso de recebimento de mensagens decorrentes da conclusão das transações.
Erros de comunicação / canalAlém das notificações de recebimento de mensagens, o remetente deve ter em mente as falhas das ferramentas de comunicação e dos intermediários. Ambos esses fatores levam à perda do canal de comunicação. Com a perda de canais, a oportunidade de receber notificações ainda não recebidas de recebimento de mensagens desaparece. Aqui, o remetente deve escolher entre o risco de perda de mensagem e o risco de duplicação.
A falha do intermediário pode ocorrer quando a mensagem estava no buffer do sistema operacional ou pré-processada e a mensagem será perdida. Ou, talvez a mensagem estivesse na fila, mas o intermediário da mensagem morreu antes de enviar uma confirmação. Nesse caso, a mensagem será entregue com sucesso.
Da mesma forma, a falha dos meios de comunicação afeta a situação. Ocorreu uma falha durante a transmissão da mensagem? Ou depois que a mensagem foi colocada na fila, mas antes de receber uma notificação positiva?
O remetente não pode determinar isso; portanto, ele deve escolher uma das seguintes opções:
- Não encaminhe a mensagem, criando um risco de perda;
- reenvie a mensagem e crie um risco de duplicação.
Se muitas mensagens de remetente estiverem em trânsito, o problema ficará mais complicado. A única coisa que o remetente pode fazer é dar uma dica aos destinatários adicionando um cabeçalho especial à mensagem, indicando que a mensagem está sendo enviada pela segunda vez. Os destinatários podem decidir verificar as mensagens quanto à presença desses cabeçalhos e, se forem encontrados, verificar adicionalmente as mensagens recebidas em busca de duplicatas (se essa verificação não tiver sido executada anteriormente).
Destinatários
Existem duas opções disponíveis para os destinatários para receber notificações:
- sem modo de notificação;
- modo de notificação manual.
Sem modo de notificaçãoEle é um modo de notificações automáticas. E ele é perigoso. Primeiro de tudo, porque quando uma mensagem entra no seu aplicativo, ela é removida da fila. Isso pode levar à perda de mensagens se:
- a conexão foi interrompida antes que a mensagem fosse recebida;
- a mensagem ainda está no buffer interno e o aplicativo foi desativado;
- O processamento da mensagem falhou.
Além disso, estamos perdendo mecanismos de contrapressão como forma de monitorar a qualidade da entrega de mensagens. Ao definir o modo de envio de notificações manualmente, você pode definir uma pré-busca (ou definir o nível de serviços fornecidos, QoS) para limitar o número único de mensagens que o sistema ainda não confirmou o recebimento. Sem isso, o RabbitMQ envia mensagens o mais rápido que a conexão permite, e isso pode ser mais rápido do que o receptor pode processá-las. Como resultado, os buffers estão cheios e ocorrem erros de memória.
Modo de notificação manualO destinatário deve enviar manualmente uma notificação de recebimento de cada mensagem. Ele pode definir uma pré-busca caso o número de mensagens seja mais de uma e processar muitas mensagens ao mesmo tempo. Ele pode decidir enviar uma notificação para cada mensagem ou aplicar o sinalizador múltiplo e enviar várias notificações por vez. O agrupamento de notificações melhora o desempenho.
Quando o destinatário abre o canal, as mensagens que passam por ele contêm o parâmetro Tag de entrega, cujo valor é um número inteiro, aumentando monotonicamente. Ele está incluído em todas as notificações de recebimento e é usado como identificador da mensagem.
As notificações podem ser as seguintes:
- basic.ack. Depois disso, o RabbitMQ remove a mensagem da fila. O sinalizador múltiplo pode ser aplicado aqui.
- basic.nack. O destinatário deve definir um sinalizador para informar ao RabbitMQ se deve enfileirar a mensagem novamente. Ao redefinir a mensagem vai para o início da fila. A partir daí, ele é enviado ao destinatário novamente (mesmo para o mesmo destinatário). A notificação basic.nack suporta o sinalizador múltiplo.
- basic.reject. O mesmo que basic.nack, mas não suporta o sinalizador múltiplo.
Assim, semanticamente basic.ack e basic.nack com requeue = false são os mesmos. Ambos os operadores significam remover uma mensagem da fila.
A próxima pergunta é quando enviar notificações de recebimento. Se a mensagem foi processada rapidamente, convém enviar uma notificação imediatamente após a conclusão desta operação (com ou sem êxito). Mas se a mensagem estava na fila RabbitMQ e o processamento leva muitos minutos? Enviar uma notificação depois disso será problemático, porque se o canal fechar, todas as mensagens para as quais não houve notificações serão retornadas para a fila e o envio será feito uma segunda vez.
Erro de conexão / agente de mensagensSe a conexão foi desconectada ou ocorreu um erro no broker, após o qual o canal deixa de funcionar, todas as mensagens cujo recebimento não foi confirmado serão novamente enfileiradas e encaminhadas. Isso é bom porque evita a perda de dados, mas ruim porque causa duplicação excessiva.
Quanto mais tempo o destinatário receber mensagens por um longo período, cuja confirmação ele não confirmou, maior será o risco de encaminhamento. Quando uma mensagem é reenviada, RabbitMQ para o sinalizador de encaminhamento é definido como true. Por esse motivo, o destinatário tem pelo menos uma indicação de que a mensagem já pode ter sido processada.
IdempotênciaSe a idempotência for necessária e garantir que nenhuma mensagem seja perdida, alguma verificação duplicada ou outros esquemas idempotentes devem ser incorporados. Se a verificação de mensagens duplicadas for muito cara, você poderá aplicar uma estratégia na qual o remetente sempre adicione um cabeçalho especial para reenviar mensagens e o destinatário verificará as mensagens recebidas quanto à presença de um cabeçalho e um sinalizador de reenvio.
Conclusão
O RabbitMQ fornece garantias confiáveis e de longo prazo para mensagens, mas há muitas situações em que elas não ajudam.
Aqui está uma lista de pontos a serem lembrados:
- Você deve usar espelhamento de fila, filas confiáveis, mensagens persistentes, confirmações para o remetente, um sinalizador de confirmação e notificação forçada do destinatário se forem necessárias garantias confiáveis na estratégia de "pelo menos entrega única".
- Se o envio for realizado como parte da estratégia de "pelo menos entrega única", pode ser necessário adicionar um mecanismo para deduplicação ou idempotência ao duplicar os dados que estão sendo enviados.
- Se o problema da perda de mensagens não for tão importante quanto o da velocidade de entrega e alta escalabilidade, pense em sistemas sem redundância, sem mensagens persistentes e sem reconhecimento no lado da fonte. No entanto, eu preferiria deixar notificações forçadas do destinatário para controlar o fluxo de mensagens recebidas alterando as restrições de pré-busca. Nesse caso, você precisará enviar notificações em lotes e usar o sinalizador "múltiplo".
Garantias de entrega em KafkaAs garantias de entrega são fornecidas:
- durabilidade da mensagem - as mensagens armazenadas em um segmento não são perdidas;
- Notificações de mensagens - a troca de sinais entre o Kafka (e possivelmente o repositório Apache Zookeeper), por um lado, e a fonte / receptor, por outro.
Duas palavras sobre o empacotamento de mensagensUma das diferenças entre o RabbitMQ e o Kafka é o uso de pacotes para mensagens.
O RabbitMQ fornece algo semelhante à embalagem, graças a:
- Suspenda o envio de todas as mensagens X até que todas as notificações sejam recebidas. O RabbitMQ geralmente agrupa notificações usando o sinalizador "múltiplo".
- «prefetch» «multiple».
. “multiple”. TCP.
Kafka . , . RabbitMQ, , , . , .
Kafka , , . , . RabbitMQ API , . RabbitMQ .
,Kafka - , , . . , , , , , .
Kafka (In Sync Replicas, ISR). . , , ( 10 ). , . - , .. . .
, Kafka , , , Kafka .
, Kafka, , :
- , . Acks=0.
- . Acks=1
- . Acks=All
, RabbitMQ. , , ( , ). , .
Kafka . :
- enable.idempotence “true”,
- max.in.flight.requests.per.connection 5 ,
- retries 1 ,
- acks “all”.
, acks=0/1 , .
, , . ZooKeeper Kafka.
(), , :
- . . . . , .
- , . “ ”. , ; , . , 10 , 4 , , , ;
- , . “ ”. , , , . , 10 , , 4 ;
- . , .
“ ” Kafka Streams, Java. Java . “ ”, , , . , , “ ” . , , () .
, Kafka Streams, , , “ ”. Kafka: . , . , , , ( ), .
Kafka “--”. . , , .
“ ”, , (, , ). “ ”, , . .
: “ ” ? . , , . . (Last Stable Offset, LSO) — ; “ ” .
Conclusões. , , . , Kafka , .
Resumir
- “ ” “ ”.
- .
- , . Kafka , .
- , , .
- .
- Kafka , “--”. .
- Kafka, - , ( ). RabbitMQ .
- O Kafka pode aumentar os benefícios da pacote devido aos seus recursos de distribuição de pacotes, e o RabbitMQ não possui pacote devido a um modelo de recebimento passivo que não impede conflitos de destinatários.