Entendendo os agentes de mensagem. Aprendendo a mecânica das mensagens com ActiveMQ e Kafka. Capítulo 3. Kafka

Continuação da tradução de um pequeno livro:
Entendendo os corretores de mensagens
autor: Jakub Korab, editor: O'Reilly Media, Inc., data de publicação: junho de 2017, ISBN: 9781492049296.

Parte anterior traduzida: Entendendo os agentes de mensagem. Aprendendo a mecânica das mensagens com ActiveMQ e Kafka. Capítulo 1 Introdução

CAPÍTULO 3

Kafka

O Kafka foi desenvolvido pelo LinkedIn para contornar algumas das limitações dos intermediários de mensagens tradicionais e evitar a necessidade de configurar vários intermediários de mensagens para diferentes interações ponto a ponto, o que é descrito neste livro em "Scaling up and out" na página 28 Casos de uso O LinkedIn depende amplamente da ingestão unidirecional de grandes quantidades de dados, como cliques em páginas e logs de acesso, enquanto ainda permite que esses dados sejam usados ​​por vários sistemas sem impactar a produtividade dos produtores ou outros consumidores. Na verdade, a razão pela qual Kafka existe é obter o tipo de arquitetura de mensagens que o Universal Data Pipeline descreve.

Diante desse objetivo final, outras exigências naturalmente surgiram. Kafka deve:

  • Seja extremamente rápido
  • Forneça mais largura de banda ao trabalhar com mensagens
  • Suporte aos modelos Editor-Assinante e Ponto a Ponto
  • Não desacelere ao adicionar consumidores. Por exemplo, o desempenho da fila e do tópico no ActiveMQ diminui à medida que o número de consumidores no destino aumenta.
  • Ser horizontalmente escalável; se um agente que persiste em mensagens só pode fazê-lo na velocidade máxima do disco, faz sentido ir além de uma única instância do agente para aumentar o desempenho
  • Limite o acesso para armazenar e recuperar mensagens

Para conseguir tudo isso, Kafka adotou uma arquitetura que redefiniu as funções e responsabilidades dos clientes e agentes de mensagens. O modelo JMS é muito orientado ao broker, onde o broker é responsável por distribuir mensagens e os clientes só precisam se preocupar em enviar e receber mensagens. Kafka, por outro lado, é centrado no cliente, com o cliente assumindo muitas das características de um corretor tradicional, como distribuição justa de mensagens relevantes aos consumidores, em troca de um corretor extremamente rápido e escalável. Para pessoas que trabalharam com sistemas de mensagens tradicionais, trabalhar com Kafka requer uma mudança fundamental de mentalidade.
Essa direção de engenharia levou à criação de uma infra-estrutura de mensagens capaz de aumentar o throughput em muitas ordens de grandeza em comparação com um broker convencional. Como veremos, essa abordagem vem com compensações, o que significa que o Kafka não é adequado para determinados tipos de cargas de trabalho e software instalado.

Modelo de destino unificado

Para atender aos requisitos descritos acima, o Kafka combinou mensagens de publicação-assinatura e ponto a ponto em um tipo de destino − tema. Isso é confuso para as pessoas que trabalharam com sistemas de mensagens, onde a palavra "tópico" se refere a um mecanismo de transmissão do qual (a partir do tópico) a leitura não é durável. Os tópicos Kafka devem ser considerados um tipo de destino híbrido, conforme definido na introdução deste livro.

No restante deste capítulo, a menos que indiquemos explicitamente o contrário, o termo "tópico" se referirá a um tópico Kafka.

Para entender completamente como os tópicos se comportam e quais garantias eles fornecem, precisamos primeiro ver como eles são implementados no Kafka.
Cada tópico no Kafka tem seu próprio log.
Os produtores que enviam mensagens para o Kafka gravam nesse log e os consumidores leem o log usando ponteiros que avançam constantemente. Periodicamente, Kafka exclui as partes mais antigas do log, independentemente de as mensagens nessas partes terem sido lidas ou não. Uma parte central do design do Kafka é que o corretor não se importa se as mensagens são lidas ou não - isso é responsabilidade do cliente.

Os termos "log" e "ponteiro" não aparecem no Documentação do Kafka. Esses termos bem conhecidos são usados ​​aqui para ajudar na compreensão.

Este modelo é completamente diferente do ActiveMQ, onde as mensagens de todas as filas são armazenadas no mesmo log, e o broker marca as mensagens como apagadas após serem lidas.
Vamos agora nos aprofundar um pouco mais e examinar o log do tópico com mais detalhes.
O log Kafka consiste em várias partições (Figura 3-1). Kafka garante ordenação estrita em cada partição. Isso significa que as mensagens gravadas na partição em uma determinada ordem serão lidas na mesma ordem. Cada partição é implementada como um arquivo de log contínuo que contém subconjunto (subconjunto) de todas as mensagens enviadas ao tópico por seus produtores. O tópico criado contém, por padrão, uma partição. A ideia de partições é a ideia central do Kafka para escala horizontal.

Entendendo os agentes de mensagem. Aprendendo a mecânica das mensagens com ActiveMQ e Kafka. Capítulo 3. Kafka
Figura 3-1. Partições Kafka

Quando um produtor envia uma mensagem para um tópico Kafka, ele decide para qual partição enviar a mensagem. Veremos isso com mais detalhes posteriormente.

Lendo mensagens

O cliente que deseja ler as mensagens gerencia um ponteiro nomeado chamado grupo de consumidores, que aponta para desvio mensagens na partição. Um deslocamento é uma posição incremental que começa em 0 no início de uma partição. Esse grupo de consumidores, referenciado na API por meio do group_id definido pelo usuário, corresponde a um consumidor ou sistema lógico.

A maioria dos sistemas de mensagens lê dados do destino usando várias instâncias e threads para processar mensagens em paralelo. Assim, geralmente haverá muitas instâncias de consumidores compartilhando o mesmo grupo de consumidores.

O problema da leitura pode ser representado da seguinte forma:

  • O tópico tem várias partições
  • Vários grupos de consumidores podem usar um tópico ao mesmo tempo
  • Um grupo de consumidores pode ter várias instâncias separadas

Este é um problema não trivial de muitos para muitos. Para entender como o Kafka lida com relacionamentos entre grupos de consumidores, instâncias de consumidores e partições, vamos examinar uma série de cenários de leitura cada vez mais complexos.

Consumidores e grupos de consumidores

Vamos tomar como ponto de partida um tópico com uma partição (Figura 3-2).

Entendendo os agentes de mensagem. Aprendendo a mecânica das mensagens com ActiveMQ e Kafka. Capítulo 3. Kafka
Figura 3-2. O consumidor lê da partição

Quando uma instância do consumidor se conecta com seu próprio group_id a este tópico, é atribuída a ela uma partição de leitura e um deslocamento nessa partição. A posição deste deslocamento é configurável no cliente como um ponteiro para a posição mais recente (mensagem mais recente) ou posição mais antiga (mensagem mais antiga). O consumidor solicita (enquetes) mensagens do tópico, o que faz com que elas sejam lidas sequencialmente do log.
A posição de deslocamento é confirmada regularmente no Kafka e armazenada como mensagens em um tópico interno _consumidor_compensações. As mensagens lidas ainda não são excluídas, ao contrário de um corretor regular, e o cliente pode retroceder o deslocamento para reprocessar as mensagens já visualizadas.

Quando um segundo consumidor lógico se conecta usando um group_id diferente, ele gerencia um segundo ponteiro independente do primeiro (Figura 3-3). Assim, um tópico Kafka age como uma fila onde há um consumidor e como um tópico normal de publicação-assinatura (pub-sub) no qual vários consumidores se inscrevem, com o benefício adicional de que todas as mensagens são armazenadas e podem ser processadas várias vezes.

Entendendo os agentes de mensagem. Aprendendo a mecânica das mensagens com ActiveMQ e Kafka. Capítulo 3. Kafka
Figura 3-3. Dois consumidores em diferentes grupos de consumidores leem da mesma partição

Consumidores em um grupo de consumidores

Quando uma instância do consumidor lê dados de uma partição, ela tem controle total do ponteiro e processa as mensagens conforme descrito na seção anterior.
Se várias instâncias de consumidores foram conectadas com o mesmo group_id a um tópico com uma partição, a instância que se conectou por último receberá o controle sobre o ponteiro e a partir desse momento receberá todas as mensagens (Figura 3-4).

Entendendo os agentes de mensagem. Aprendendo a mecânica das mensagens com ActiveMQ e Kafka. Capítulo 3. Kafka
Figura 3-4. Dois consumidores no mesmo grupo de consumidores leem da mesma partição

Esse modo de processamento, no qual o número de instâncias do consumidor excede o número de partições, pode ser pensado como uma espécie de consumidor exclusivo. Isso pode ser útil se você precisar de agrupamento "ativo-passivo" (ou "quente-quente") de suas instâncias de consumidor, embora a execução de vários consumidores em paralelo ("ativo-ativo" ou "quente-quente") seja muito mais comum do que consumidores. Em espera.

Esse comportamento de distribuição de mensagens descrito acima pode ser surpreendente em comparação com o comportamento de uma fila JMS normal. Nesse modelo, as mensagens enviadas para a fila serão distribuídas igualmente entre os dois consumidores.

Na maioria das vezes, quando criamos várias instâncias de consumidores, fazemos isso para processar mensagens em paralelo, aumentar a velocidade de leitura ou aumentar a estabilidade do processo de leitura. Como apenas uma instância do consumidor pode ler dados de uma partição por vez, como isso é feito no Kafka?

Uma maneira de fazer isso é usar uma única instância de consumidor para ler todas as mensagens e passá-las para o pool de encadeamentos. Embora essa abordagem aumente a taxa de processamento, ela aumenta a complexidade da lógica do consumidor e não aumenta a robustez do sistema de leitura. Se uma cópia do consumidor cair devido a uma falha de energia ou evento semelhante, a subtração será interrompida.

A maneira canônica de resolver esse problema no Kafka é usar bОmais partições.

Particionamento

As partições são o principal mecanismo para paralelizar a leitura e dimensionar um tópico além da largura de banda de uma única instância do broker. Para entender melhor, vamos considerar uma situação em que há um tópico com duas partições e um consumidor assina esse tópico (Figura 3-5).

Entendendo os agentes de mensagem. Aprendendo a mecânica das mensagens com ActiveMQ e Kafka. Capítulo 3. Kafka
Figura 3-5. Um consumidor lê de várias partições

Nesse cenário, o consumidor recebe controle sobre os ponteiros correspondentes ao seu group_id em ambas as partições e começa a ler as mensagens de ambas as partições.
Quando um consumidor adicional para o mesmo group_id é adicionado a este tópico, o Kafka realoca uma das partições do primeiro para o segundo consumidor. Depois disso, cada instância do consumidor lerá de uma partição do tópico (Figura 3-6).

Para garantir que as mensagens sejam processadas em paralelo em 20 threads, você precisa de pelo menos 20 partições. Se houver menos partições, você ficará com consumidores que não têm nada para trabalhar, conforme descrito anteriormente na discussão de consumidores exclusivos.

Entendendo os agentes de mensagem. Aprendendo a mecânica das mensagens com ActiveMQ e Kafka. Capítulo 3. Kafka
Figura 3-6. Dois consumidores no mesmo grupo de consumidores lêem partições diferentes

Esse esquema reduz bastante a complexidade do intermediário Kafka em comparação com a distribuição de mensagens necessária para manter a fila JMS. Aqui você não precisa se preocupar com os seguintes pontos:

  • Qual consumidor deve receber a próxima mensagem, com base na alocação de round-robin, capacidade atual de buffers de pré-busca ou mensagens anteriores (como para grupos de mensagens JMS).
  • Quais mensagens são enviadas para quais consumidores e se devem ser reenviadas em caso de falha.

Tudo o que o broker Kafka precisa fazer é passar as mensagens sequencialmente para o consumidor quando este as solicitar.

No entanto, os requisitos para paralelizar a revisão e reenviar as mensagens com falha não desaparecem - a responsabilidade por eles simplesmente passa do corretor para o cliente. Isso significa que eles devem ser levados em consideração em seu código.

Enviando mensagens

É responsabilidade do produtor dessa mensagem decidir para qual partição enviar uma mensagem. Para entender o mecanismo pelo qual isso é feito, primeiro precisamos considerar o que exatamente estamos enviando.

Enquanto no JMS utilizamos uma estrutura de mensagem com metadados (cabeçalhos e propriedades) e um corpo contendo o payload (payload), no Kafka a mensagem é par "valor-chave". A carga útil da mensagem é enviada como um valor. A chave, por outro lado, é usada principalmente para particionamento e deve conter chave específica de lógica de negóciospara colocar mensagens relacionadas na mesma partição.

No Capítulo 2, discutimos o cenário de apostas online em que eventos relacionados precisam ser processados ​​em ordem por um único consumidor:

  1. A conta do usuário está configurada.
  2. O dinheiro é creditado na conta.
  3. É feita uma aposta que retira dinheiro da conta.

Se cada evento for uma mensagem postada em um tópico, a chave natural seria o ID da conta.
Quando uma mensagem é enviada usando a API Kafka Producer, ela é passada para uma função de particionamento que, dada a mensagem e o estado atual do cluster Kafka, retorna o ID da partição para a qual a mensagem deve ser enviada. Esse recurso é implementado em Java por meio da interface Partitioner.

Essa interface se parece com isso:

interface Partitioner {
    int partition(String topic,
        Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
}

A implementação do Partitioner usa o algoritmo de hash de propósito geral padrão sobre a chave para determinar a partição ou round-robin se nenhuma chave for especificada. Esse valor padrão funciona bem na maioria dos casos. No entanto, no futuro, você desejará escrever o seu próprio.

Escrevendo sua própria estratégia de particionamento

Vejamos um exemplo em que você deseja enviar metadados junto com a carga útil da mensagem. A carga em nosso exemplo é uma instrução para fazer um depósito na conta do jogo. Uma instrução é algo que gostaríamos de ter a garantia de não ser modificada na transmissão e queremos ter certeza de que apenas um sistema upstream confiável pode iniciar essa instrução. Nesse caso, os sistemas emissor e receptor concordam com o uso de uma assinatura para autenticar a mensagem.
No JMS normal, simplesmente definimos uma propriedade de "assinatura de mensagem" e a incluímos na mensagem. No entanto, Kafka não nos fornece um mecanismo para passar metadados, apenas uma chave e um valor.

Como o valor é um payload de transferência bancária cuja integridade queremos preservar, não temos escolha a não ser definir a estrutura de dados a ser usada na chave. Supondo que precisamos de um ID de conta para particionamento, já que todas as mensagens relacionadas a uma conta devem ser processadas em ordem, chegaremos à seguinte estrutura JSON:

{
  "signature": "541661622185851c248b41bf0cea7ad0",
  "accountId": "10007865234"
}

Como o valor da assinatura varia dependendo da carga útil, a estratégia de hash padrão da interface do Particionador não agrupará de forma confiável as mensagens relacionadas. Portanto, precisaremos escrever nossa própria estratégia que analisará essa chave e particionará o valor de accountId.

O Kafka inclui somas de verificação para detectar corrupção de mensagens na loja e possui um conjunto completo de recursos de segurança. Mesmo assim, requisitos específicos do setor, como o acima, às vezes aparecem.

A estratégia de particionamento do usuário deve garantir que todas as mensagens relacionadas acabem na mesma partição. Embora isso pareça simples, o requisito pode ser complicado pela importância de ordenar postagens relacionadas e como o número de partições em um tópico é fixo.

O número de partições em um tópico pode mudar com o tempo, pois elas podem ser adicionadas se o tráfego ultrapassar as expectativas iniciais. Assim, as chaves de mensagem podem ser associadas à partição para a qual foram originalmente enviadas, implicando um pedaço de estado a ser compartilhado entre as instâncias do produtor.

Outro fator a ser considerado é a distribuição uniforme de mensagens pelas partições. Normalmente, as chaves não são distribuídas uniformemente pelas mensagens e as funções de hash não garantem uma distribuição justa de mensagens para um pequeno conjunto de chaves.
É importante observar que, independentemente de como você optar por dividir as mensagens, o próprio separador pode precisar ser reutilizado.

Considere o requisito para replicar dados entre clusters Kafka em diferentes localizações geográficas. Para isso, o Kafka vem com uma ferramenta de linha de comando chamada MirrorMaker, que é usada para ler mensagens de um cluster e transferi-las para outro.

O MirrorMaker deve entender as chaves do tópico replicado para manter a ordem relativa entre as mensagens ao replicar entre os clusters, pois o número de partições para esse tópico pode não ser o mesmo em dois clusters.

As estratégias de particionamento personalizadas são relativamente raras, pois o hash padrão ou o round robin funcionam bem na maioria dos cenários. No entanto, se você precisar de garantias de pedidos fortes ou precisar extrair metadados de cargas úteis, o particionamento é algo que você deve examinar mais de perto.

Os benefícios de escalabilidade e desempenho do Kafka vêm da transferência de algumas das responsabilidades do corretor tradicional para o cliente. Neste caso, é tomada a decisão de distribuir mensagens potencialmente relacionadas entre vários consumidores trabalhando em paralelo.

Os corretores JMS também precisam lidar com esses requisitos. Curiosamente, o mecanismo de envio de mensagens relacionadas para o mesmo consumidor, implementado por meio de Grupos de Mensagens JMS (uma variação da estratégia de balanceamento de carga permanente (SLB)), também exige que o remetente marque as mensagens como relacionadas. No caso do JMS, o broker é responsável por enviar esse grupo de mensagens relacionadas a um consumidor entre muitos e transferir a propriedade do grupo se o consumidor cair.

Acordos do Produtor

O particionamento não é a única coisa a considerar ao enviar mensagens. Vamos dar uma olhada nos métodos send() da classe Producer na API Java:

Future < RecordMetadata > send(ProducerRecord < K, V > record);
Future < RecordMetadata > send(ProducerRecord < K, V > record, Callback callback);

Deve-se notar imediatamente que ambos os métodos retornam Future, o que indica que a operação de envio não é executada imediatamente. O resultado é que uma mensagem (ProducerRecord) é gravada no buffer de envio para cada partição ativa e enviada ao broker como um encadeamento de segundo plano na biblioteca do cliente Kafka. Embora isso torne as coisas incrivelmente rápidas, significa que um aplicativo inexperiente pode perder mensagens se seu processo for interrompido.

Como sempre, há uma maneira de tornar a operação de envio mais confiável em detrimento do desempenho. O tamanho desse buffer pode ser definido como 0, e a thread da aplicação de envio será forçada a aguardar até que a transferência da mensagem para o broker seja concluída, conforme a seguir:

RecordMetadata metadata = producer.send(record).get();

Mais sobre a leitura de mensagens

A leitura de mensagens tem complexidades adicionais que precisam ser especuladas. Ao contrário da API JMS, que pode executar um ouvinte de mensagem em resposta a uma mensagem, o Consumidores Kafka apenas vota. Vamos dar uma olhada mais de perto no método enquete()utilizado para este fim:

ConsumerRecords < K, V > poll(long timeout);

O valor de retorno do método é uma estrutura de contêiner contendo vários objetos registro do consumidor de potencialmente várias partições. registro do consumidor é em si um objeto detentor de um par chave-valor com metadados associados, como a partição da qual é derivado.

Conforme discutido no Capítulo 2, devemos ter em mente o que acontece com as mensagens após terem sido processadas com ou sem sucesso, por exemplo, se o cliente não conseguir processar a mensagem ou se ela abortar. No JMS, isso era tratado por meio de um modo de confirmação. O corretor excluirá a mensagem processada com sucesso ou reenviará a mensagem bruta ou falsa (supondo que as transações foram usadas).
Kafka funciona de maneira muito diferente. As mensagens não são apagadas no broker após a revisão, e o que acontece em caso de falha é de responsabilidade do próprio código de revisão.

Como dissemos, o grupo de consumidores está associado ao deslocamento no log. A posição do log associada a este deslocamento corresponde à próxima mensagem a ser emitida em resposta a enquete(). O momento em que esse deslocamento aumenta é decisivo para a leitura.

Voltando ao modelo de leitura discutido anteriormente, o processamento de mensagens consiste em três estágios:

  1. Recuperar uma mensagem para leitura.
  2. Processe a mensagem.
  3. Confirme a mensagem.

O consumidor Kafka vem com uma opção de configuração ativar.auto.commit. Esta é uma configuração padrão usada com frequência, como é comum em configurações que contêm a palavra "auto".

Antes do Kafka 0.10, um cliente que usasse essa opção enviaria o deslocamento da última mensagem lida na próxima chamada enquete() após o processamento. Isso significava que qualquer mensagem que já tivesse sido buscada poderia ser reprocessada se o cliente já as tivesse processado, mas foi destruída inesperadamente antes de chamar enquete(). Como o intermediário não mantém nenhum estado sobre quantas vezes uma mensagem foi lida, o próximo consumidor que recuperar essa mensagem não saberá que algo de ruim aconteceu. Esse comportamento era pseudotransacional. O offset só era confirmado se a mensagem fosse processada com sucesso, mas se o cliente abortasse, a corretora enviaria a mesma mensagem novamente para outro cliente. Esse comportamento era consistente com a garantia de entrega da mensagem "pelo menos uma vez".

No Kafka 0.10, o código do cliente foi alterado para que o commit seja acionado periodicamente pela biblioteca do cliente, conforme configurado auto.commit.interval.ms. Esse comportamento está em algum lugar entre os modos JMS AUTO_ACKNOWLEDGE e DUPS_OK_ACKNOWLEDGE. Ao usar o autocommit, as mensagens podem ser confirmadas independentemente de terem sido realmente processadas - isso pode acontecer no caso de um consumidor lento. Se um consumidor abortasse, as mensagens seriam buscadas pelo próximo consumidor, começando na posição confirmada, o que poderia resultar em uma mensagem perdida. Nesse caso, o Kafka não perdeu as mensagens, o código de leitura simplesmente não as processou.

Este modo tem a mesma promessa da versão 0.9: as mensagens podem ser processadas, mas se falhar, o deslocamento pode não ser confirmado, podendo causar a duplicação da entrega. Quanto mais mensagens você buscar ao executar enquete(), mais este problema.

Conforme discutido em “Lendo mensagens de uma fila” na página 21, não existe uma entrega única de uma mensagem em um sistema de mensagens quando os modos de falha são levados em consideração.

No Kafka, existem duas maneiras de confirmar (confirmar) um deslocamento (deslocamento): automaticamente e manualmente. Em ambos os casos, as mensagens podem ser processadas várias vezes se a mensagem foi processada, mas falhou antes da confirmação. Você também pode optar por não processar a mensagem se a confirmação ocorreu em segundo plano e seu código foi concluído antes de poder ser processado (talvez no Kafka 0.9 e anterior).

Você pode controlar o processo de confirmação de deslocamento manual na API do consumidor Kafka definindo o parâmetro ativar.auto.commit para false e chamando explicitamente um dos seguintes métodos:

void commitSync();
void commitAsync();

Se você deseja processar a mensagem "pelo menos uma vez", deve confirmar o deslocamento manualmente com commitSync()executando este comando imediatamente após o processamento das mensagens.

Esses métodos não permitem que as mensagens sejam confirmadas antes de serem processadas, mas não fazem nada para eliminar possíveis atrasos no processamento, embora pareçam ser transacionais. Não há transações no Kafka. O cliente não tem a capacidade de fazer o seguinte:

  • Reverta automaticamente uma mensagem falsificada. Os próprios consumidores devem lidar com exceções decorrentes de cargas problemáticas e interrupções de back-end, pois não podem confiar no intermediário para reenviar mensagens.
  • Envie mensagens para vários tópicos em uma operação atômica. Como veremos em breve, o controle sobre diferentes tópicos e partições pode residir em diferentes máquinas no cluster Kafka que não coordenam as transações quando enviadas. No momento em que este livro foi escrito, algum trabalho foi feito para tornar isso possível com o KIP-98.
  • Associe a leitura de uma mensagem de um tópico com o envio de outra mensagem para outro tópico. Novamente, a arquitetura do Kafka depende de muitas máquinas independentes rodando como um barramento e nenhuma tentativa é feita para esconder isso. Por exemplo, não há componentes de API que permitam vincular consumidor и Produtor em uma transação. No JMS, isso é fornecido pelo objeto Número daa partir do qual são criados Produtores de mensagens и MensagemConsumidores.

Se não podemos confiar em transações, como podemos fornecer uma semântica mais próxima daquela fornecida pelos sistemas de mensagens tradicionais?

Se houver a possibilidade de que o deslocamento do consumidor possa aumentar antes que a mensagem seja processada, como durante uma falha do consumidor, o consumidor não terá como saber se seu grupo de consumidores perdeu a mensagem ao receber uma partição. Portanto, uma estratégia é rebobinar o deslocamento para a posição anterior. A API do consumidor Kafka fornece os seguintes métodos para isso:

void seek(TopicPartition partition, long offset);
void seekToBeginning(Collection < TopicPartition > partitions);

método procurar() pode ser usado com método
offsetsForTimes(Map timestampsToSearch) para voltar a um estado em algum ponto específico no passado.

Implicitamente, usar essa abordagem significa que é muito provável que algumas mensagens que foram processadas anteriormente sejam lidas e processadas novamente. Para evitar isso, podemos usar a leitura idempotente, conforme descrito no Capítulo 4, para acompanhar as mensagens visualizadas anteriormente e eliminar duplicatas.

Como alternativa, seu código de consumidor pode ser mantido simples, desde que a perda ou duplicação de mensagens seja aceitável. Quando consideramos os casos de uso para os quais o Kafka é comumente usado, como manipulação de eventos de log, métricas, rastreamento de cliques etc., entendemos que é improvável que a perda de mensagens individuais tenha um impacto significativo nos aplicativos ao redor. Nesses casos, os valores padrão são perfeitamente aceitáveis. Por outro lado, se seu aplicativo precisar enviar pagamentos, você deve cuidar cuidadosamente de cada mensagem individual. Tudo se resume ao contexto.

Observações pessoais mostram que à medida que a intensidade das mensagens aumenta, o valor de cada mensagem individual diminui. Mensagens grandes tendem a ser valiosas quando visualizadas de forma agregada.

Alta disponibilidade

A abordagem do Kafka para alta disponibilidade é muito diferente da abordagem do ActiveMQ. Kafka é projetado em torno de clusters escaláveis ​​onde todas as instâncias do agente recebem e distribuem mensagens ao mesmo tempo.

Um cluster Kafka consiste em várias instâncias de agente em execução em servidores diferentes. O Kafka foi projetado para rodar em hardware autônomo comum, onde cada nó possui seu próprio armazenamento dedicado. O uso de armazenamento conectado à rede (SAN) não é recomendado porque vários nós de computação podem competir por tempo.Ыe intervalos de armazenamento e criar conflitos.

Kafka é sempre sistema. Muitos grandes usuários do Kafka nunca encerram seus clusters e o software sempre é atualizado com uma reinicialização sequencial. Isso é conseguido garantindo a compatibilidade com a versão anterior para mensagens e interações entre corretores.

Agentes conectados a um cluster de servidor Funcionário do zoológico, que atua como um registro de dados de configuração e é usado para coordenar as funções de cada broker. O próprio ZooKeeper é um sistema distribuído que fornece alta disponibilidade por meio da replicação de informações estabelecendo quorum.

No caso base, um tópico é criado em um cluster Kafka com as seguintes propriedades:

  • O número de partições. Conforme discutido anteriormente, o valor exato usado aqui depende do nível desejado de leitura paralela.
  • O fator de replicação (fator) determina quantas instâncias do broker no cluster devem conter logs para esta partição.

Usando ZooKeepers para coordenação, Kafka tenta distribuir de forma justa novas partições entre os agentes no cluster. Isso é feito por uma única instância que atua como um Controlador.

Em tempo de execução para cada partição de tópico Controlador atribuir funções a um corretor o líder (líder, mestre, apresentador) e seguidores (seguidores, escravos, subordinados). O broker, atuando como líder desta partição, é responsável por receber todas as mensagens enviadas a ele pelos produtores e distribuir as mensagens aos consumidores. Quando as mensagens são enviadas para uma partição de tópico, elas são replicadas para todos os nós intermediários que atuam como seguidores dessa partição. Cada nó contendo logs para uma partição é chamado réplica. Um broker pode atuar como líder para algumas partições e como seguidor para outras.

Um seguidor contendo todas as mensagens mantidas pelo líder é chamado réplica sincronizada (uma réplica que está em um estado sincronizado, réplica em sincronização). Se um broker que atua como líder para uma partição ficar inativo, qualquer broker atualizado ou sincronizado para essa partição pode assumir a função de líder. É um design incrivelmente sustentável.

Parte da configuração do produtor é o parâmetro reconhecer, que determina quantas réplicas devem reconhecer (reconhecer) o recebimento de uma mensagem antes que o encadeamento do aplicativo continue enviando: 0, 1 ou todos. Se definido para todos os, então, quando uma mensagem é recebida, o líder enviará uma confirmação de volta ao produtor assim que receber as confirmações (reconhecimentos) do registro de várias sugestões (incluindo ele mesmo) definidas pela configuração do tópico min.insync.replicas (padrão 1). Se a mensagem não puder ser replicada com sucesso, o produtor lançará uma exceção de aplicativo (NotEnoughReplicas ou NotEnoughReplicasAfterAppend).

Uma configuração típica cria um tópico com um fator de replicação de 3 (1 líder, 2 seguidores por partição) e o parâmetro min.insync.replicas é configurado como 2. Nesse caso, o cluster permitirá que um dos intermediários que gerenciam a partição de tópico fique inativo sem afetar os aplicativos clientes.

Isso nos traz de volta ao já conhecido trade-off entre desempenho e confiabilidade. A replicação ocorre à custa de tempo de espera adicional para confirmações (reconhecimentos) dos seguidores. Porém, por ser executado em paralelo, a replicação para pelo menos três nós tem o mesmo desempenho que dois (ignorando o aumento no uso da largura de banda da rede).

Ao usar esse esquema de replicação, o Kafka evita habilmente a necessidade de gravar fisicamente cada mensagem no disco com a operação sincronizar(). Cada mensagem enviada pelo produtor será gravada no log da partição, mas conforme discutido no Capítulo 2, a gravação em um arquivo é inicialmente feita no buffer do sistema operacional. Se esta mensagem for replicada para outra instância do Kafka e estiver em sua memória, a perda do líder não significa que a mensagem em si foi perdida - ela pode ser assumida por uma réplica sincronizada.
Recusa em realizar a operação sincronizar() significa que Kafka pode receber mensagens tão rápido quanto pode gravá-las na memória. Por outro lado, quanto mais você puder evitar a liberação de memória para o disco, melhor. Por esse motivo, não é incomum que os agentes Kafka sejam alocados com 64 GB ou mais de memória. Esse uso de memória significa que uma única instância do Kafka pode ser facilmente executada em velocidades milhares de vezes mais rápidas do que um agente de mensagens tradicional.

Kafka também pode ser configurado para aplicar a operação sincronizar() aos pacotes de mensagens. Como tudo no Kafka é orientado a pacotes, ele realmente funciona muito bem para muitos casos de uso e é uma ferramenta útil para usuários que exigem garantias muito fortes. Grande parte do desempenho puro do Kafka vem das mensagens que são enviadas ao intermediário como pacotes e que essas mensagens são lidas do intermediário em blocos sequenciais usando cópia zero operações (operações durante as quais a tarefa de copiar dados de uma área de memória para outra não é executada). O último é um grande ganho de desempenho e recursos e só é possível por meio do uso de uma estrutura de dados de log subjacente que define o esquema de partição.

Um desempenho muito melhor é possível em um cluster Kafka do que com um único agente Kafka, porque as partições de tópico podem ser expandidas em muitas máquinas separadas.

Resultados de

Neste capítulo, vimos como a arquitetura Kafka reinventa o relacionamento entre clientes e agentes para fornecer um pipeline de mensagens incrivelmente robusto, com taxa de transferência muitas vezes maior do que a de um agente de mensagens convencional. Discutimos a funcionalidade que ele usa para conseguir isso e analisamos brevemente a arquitetura dos aplicativos que fornecem essa funcionalidade. No próximo capítulo, veremos problemas comuns que os aplicativos baseados em mensagens precisam resolver e discutiremos estratégias para lidar com eles. Terminaremos o capítulo descrevendo como falar sobre tecnologias de mensagens em geral para que você possa avaliar sua adequação para seus casos de uso.

Parte anterior traduzida: Entendendo os agentes de mensagens. Aprendendo a mecânica das mensagens com ActiveMQ e Kafka. Capítulo 1

Tradução feita: tele.gg/middle_java

Para ser continuado ...

Apenas usuários registrados podem participar da pesquisa. Entrarpor favor

Kafka é usado em sua organização?

  • Sim

  • Não

  • Anteriormente usado, agora não

  • Nós planejamos usar

38 usuários votaram. 8 usuários se abstiveram.

Fonte: habr.com

Adicionar um comentário