Cinco armadilhas de escalabilidade a serem evitadas com a sua aplicação do Kafka

Empresário com óculos usando notebook em escritório moderno

O Apache Kafka é uma plataforma de fluxo de eventos altamente escalável de alto desempenho. Para liberar todo o potencial do Kafka, você precisa considerar cuidadosamente o projeto de sua aplicação. É muito fácil escrever aplicações Kafka que funcionam mal ou que acabam enfrentando um obstáculo de escalabilidade. Desde 2015, a IBM fornece o serviço IBM® Event Streams, que é um serviço Apache Kafka totalmente gerenciado executado no IBM Cloud. Desde então, o serviço ajudou muitos clientes, bem como equipes da IBM, a resolver problemas de escalabilidade e desempenho com as aplicações Kafka que eles escreveram.

Este artigo descreve alguns dos problemas comuns do Kafka e fornece algumas recomendações sobre como evitar problemas de escalabilidade em suas aplicações.

1. Minimize a espera pelas viagens de ida e volta da rede

Algumas operações do Kafka funcionam pelo cliente enviando dados ao broker e aguardando uma resposta. Uma viagem de ida e volta completa pode levar 10 milissegundos, o que parece rápido, mas limita você a no máximo 100 operações por segundo. Por esse motivo, é recomendável que você tente evitar esses tipos de operações sempre que possível. Felizmente, os clientes do Kafka oferecem maneiras para que você evite esperar esses tempos de ida e volta. Você só precisa garantir que está aproveitando elas.

Dicas para maximizar o rendimento:

  1. Não verifique todas as mensagens enviadas se teve sucesso. A API do Kafka permite desacoplar o envio de uma mensagem da verificação se a mensagem foi recebida com sucesso pelo broker. Aguardar a confirmação de que uma mensagem foi recebida pode introduzir latência de ida e volta da rede em sua aplicação, portanto, procure minimizar isso sempre que possível. Isso pode significar enviar o maior número possível de mensagens antes de verificar se todas foram recebidas. Ou pode significar delegar a verificação da entrega bem-sucedida das mensagens para outra thread de execução dentro da sua aplicação para que ela possa ser executada em paralelo com o envio de mais mensagens.
  2. Não siga o processamento de cada mensagem com uma confirmação de deslocamento. O envio de offsets (de forma síncrona) é implementado como uma viagem de ida e volta da rede com o servidor. Confirme os offsets com menos frequência ou use a função assíncrona de confirmação de offset para evitar pagar o preço dessa viagem de ida e volta para cada mensagem que você processar. Lembre-se de que comprometer compensações com menos frequência pode significar que mais dados precisam ser reprocessados se sua aplicação falhar.

Se você leu o texto acima e pensou: "Ah, isso não vai deixar minha aplicação mais complexa?", a resposta é sim, provavelmente sim. Há um equilíbrio entre rendimento e complexidade da aplicação. O que torna o tempo de ida e volta da rede uma armadilha particularmente insidiosa é que, uma vez que você atingiu esse limite, pode ser necessário alterar extensamente a aplicação para alcançar melhorias adicionais na taxa de transferência.

2. Não permita que o aumento dos tempos de processamento seja confundido com falhas do consumidor

Uma funcionalidade útil do Kafka é que ele monitora a "vivacidade" das aplicações de consumo e desconecta qualquer uma que possa ter falhado. Isso funciona fazendo com que o broker acompanhe quando cada cliente consumidor fez a última "pesquisa" (a terminologia do Kafka para solicitar mais mensagens). Se um cliente não fizer pesquisas com frequência suficiente, o broker ao qual está conectado conclui que ele deve ter falhado e o desconecta. Isso foi projetado para permitir que os clientes que não estão enfrentando problemas entrem e assumam o trabalho do cliente com problemas.

Infelizmente, com esse esquema, o broker do Kafka não consegue distinguir entre um cliente que está demorando muito para processar as mensagens recebidas e um cliente que realmente falhou. Considere uma aplicação que executa ciclos: 1) Chama pesquisas e recebe um lote de mensagens; ou 2) processa cada mensagem em lote, levando 1 segundo para processar cada mensagem.

Se esse consumidor estiver recebendo lotes de 10 mensagens, serão aproximadamente 10 segundos entre as chamadas para a pesquisa. Por padrão, o Kafka permite até 300 segundos (5 minutos) entre as pesquisas antes de desconectar o cliente, então tudo funcionaria bem nesse cenário. Mas o que acontece em um dia realmente movimentado quando um backlog de mensagens começa a se formar sobre o tópico que a aplicação está consumindo? Em vez de apenas receber 10 mensagens de cada chamada de pesquisa, sua aplicação recebe 500 mensagens (por padrão, esse é o número máximo de registros que podem ser retornados por uma chamada para pesquisa). Isso resultaria em tempo de processamento suficiente para o Kafka decidir que a instância da aplicação falhou e desconectá-la. Isso é uma má notícia.

Você ficará encantado em saber que a situação pode piorar. É possível que ocorra uma espécie de ciclo de feedback. À medida que o Kafka começa a desconectar clientes porque eles não estão chamando pesquisas com frequência suficiente, há menos instâncias da aplicação para processar as mensagens. A probabilidade de haver um grande backlog de mensagens sobre o tópico aumenta, levando a uma maior probabilidade de que mais clientes recebam grandes lotes de mensagens e demorem muito tempo para processá-las. Eventualmente, todas as instâncias da aplicação de consumo entram em um ciclo de reinicialização, e nenhum trabalho útil é feito.

Que medidas você pode tomar para evitar que isso aconteça com você?

  1. A quantidade máxima de tempo entre as chamadas de pesquisas pode ser configurada usando a configuração de consumidor do Kafka "max.poll.interval.ms". O número máximo de mensagens que podem ser retornadas por uma única pesquisa também é configurável usando a configuração "max.poll.records". Como regra geral, procure reduzir o "max.poll.records" nas preferências para aumentar o "max.poll.interval.ms", pois a definição de um intervalo máximo de pesquisas grande fará com que o Kafka demore mais para identificar os consumidores que realmente falharam.
  2. Os consumidores do Kafka também podem ser instruídos a pausar e retomar o fluxo de mensagens. Pausar o consumo impede que o método de pesquisa retorne qualquer mensagem, mas ainda redefine o timer usado para determinar se o cliente falhou. Pausar e retomar é uma tática útil se você: a) esperar que as mensagens individuais possam levar muito tempo para serem processadas; e b) desejam que o Kafka seja capaz de detectar uma falha do cliente no meio do processamento de uma mensagem individual.
  3. Não perca a utilidade das métricas do cliente do Kafka. O tópico métricas poderia preencher um artigo inteiro por si só, mas, nesse contexto, o consumidor expõe métricas para o tempo médio e máximo entre as pesquisas. O monitoramento dessas métricas pode ajudar a identificar situações em que um sistema posterior é a razão pela qual cada mensagem recebida do Kafka está demorando mais do que o esperado para ser processada.

Retornaremos ao tópico das falhas do consumidor mais adiante neste artigo, quando analisarmos como elas podem desencadear o reequilíbrio do grupo de consumidores e o efeito disruptivo que isso pode ter.

3. Minimize o custo de consumidores ociosos

Detalhes técnicos, o protocolo usado pelo consumidor do Kafka para receber mensagens funciona enviando uma solicitação de “busca” a um broker do Kafka. Como parte dessa solicitação, o cliente indica o que o broker deve fazer se não houver mensagens para devolver, incluindo quanto tempo o broker deve esperar antes de enviar uma resposta vazia. Por padrão, os consumidores do Kafka instruem os brokers a esperar até 500 milissegundos (controlados pelo método "fetch.max.wait.ms" configuração do consumidor) para que pelo menos 1 byte de dados da mensagem se torne disponível (controlado com o comando "fetch.min.bytes" configuração).

Esperar 500 milissegundos não parece ser razoável, mas se a sua aplicação tiver consumidores que estão em sua maioria ociosos e escalar para 5.000 instâncias, isso representa potencialmente 2.500 solicitações por segundo para não fazer absolutamente nada. Cada uma dessas solicitações leva tempo da CPU no broker para ser processada e, em casos extremos, pode afetar o desempenho e a estabilidade dos clientes do Kafka que desejam realizar um trabalho útil.

Normalmente, a abordagem do Kafka para o dimensionamento é adicionar mais brokers e, em seguida, reequilibrar uniformemente as partições de tópicos em todos os brokers, tanto os antigos quanto os novos. Infelizmente, essa abordagem pode não ajudar se seus clientes estiverem bombardeando o Kafka com solicitações de busca desnecessárias. Cada cliente enviará solicitações de busca a todos os brokers que lideram uma partição de tópico da qual o cliente está consumindo mensagens. Portanto, é possível que, mesmo depois de dimensionar o cluster do Kafka e redistribuir as partições, a maioria de seus clientes envie solicitações de busca para a maioria dos brokers.

Então, o que você pode fazer?

  1. Alterar a configuração do consumidor do Kafka pode ajudar a reduzir esse efeito. Se você quiser receber mensagens assim que elas chegarem, o "fetch.min.bytes" deverá permanecer no seu padrão de 1; no entanto, o endereço "fetch.max.wait.ms" a configuração pode ser aumentada para um valor maior, o que reduzirá o número de solicitações feitas por consumidores ociosos.
  2. Em um escopo mais amplo, sua aplicação precisa ter potencialmente milhares de instâncias, cada uma delas consumindo raramente do Kafka? Pode haver boas razões para isso, mas talvez existam maneiras de projetar ela para fazer um uso mais eficiente do Kafka. Abordaremos algumas dessas considerações na próxima seção.

4. Escolha o número apropriado de tópicos e partições

Se você chega ao Kafka vindo de um histórico com outros sistemas de publicação-assinatura (por exemplo, Message Queuing Telemetry Transport, ou MQTT para abreviar), então pode você esperar que os tópicos do Kafka sejam muito leves, quase efêmeros. Mas não são. O Kafka fica muito mais confortável com um número de tópicos medidos em milhares. Também espera-se que os tópicos do Kafka tenham uma vida relativamente longa. Práticas como criar um tópico para receber uma única mensagem de resposta e, em seguida, excluir o tópico, são incomuns com o Kafka e não aproveitam os pontos fortes do Kafka.

Em vez disso, planeje tópicos de longa duração. Talvez eles compartilhem o tempo de vida de uma aplicação ou de uma atividade. Também procure limitar o número de tópicos a centenas ou talvez milhares. Isso pode exigir uma perspectiva diferente sobre quais mensagens são intercaladas sobre um determinado tópico.

Uma pergunta relacionada que surge com frequência é: "Quantas partições meu tópico deve ter?" Tradicionalmente, o conselho é superestimar, porque adicionar partições após a criação de um tópico não altera o particionamento dos dados existentes mantidos no tópico (e, portanto, pode afetar os consumidores que dependem do particionamento para oferecer ordenação de mensagens dentro de uma partição). Este é um bom conselho; no entanto, gostaríamos de sugerir algumas considerações adicionais:

  1. Para tópicos que podem esperar uma taxa de transferência medida em MB/segundo, ou onde a taxa de transferência pode crescer à medida que você aumenta sua aplicação, recomendamos ter mais de uma partição, para que a carga possa ser distribuída por vários brokers. O serviço Event Streams sempre executa o Kafka com um múltiplo de três brokers. No momento em que este artigo foi escrito, ele tinha no máximo 9 corretores, mas talvez isso aumente no futuro. Se você escolher um múltiplo de três para o número de partições em seu tópico, ele poderá ser equilibrado igualmente em todos os brokers.
  2. O número de partições em um tópico é o limite de quantos consumidores do Kafka podem compartilhar mensagens de consumo do tópico com grupos de consumidores do Kafka (falaremos sobre isso posteriormente). Se você incluir mais consumidores em um grupo de consumidores do que o número de partições no tópico, alguns consumidores ficarão ociosos, não consumindo dados de mensagens.
  3. Não há nada de inerentemente errado em ter tópicos de partição única, desde que você tenha certeza absoluta de que eles nunca receberão tráfego de mensagens significativo, ou você não dependerá da classificação em um tópico e ficará feliz em adicionar mais partições mais tarde.

5. O reequilíbrio do grupo de consumidores pode ser surpreendentemente disruptivo

A maioria das aplicações do Kafka que consomem mensagens aproveite os recursos do grupo de consumidores do Kafka para coordenar quais clientes consomem de quais partições de tópico. Se sua memória sobre os grupos de consumidores é um pouco imprecisa, aqui está uma atualização rápida sobre os pontos principais:

  • Grupos de consumidores coordenam um grupo de clientes Kafka de modo que apenas um cliente receba mensagens de uma partição de tópico específica em um determinado momento. Isso é útil se você precisar compartilhar as mensagens sobre um tópico entre várias instâncias de uma aplicação.
  • Quando um cliente do Kafka ingressa em um grupo de consumidores ou sai de um grupo de consumidores ao qual ele ingressou anteriormente, o grupo de consumidores é reequilibrado. Normalmente, os clientes ingressam em um grupo de consumidores quando a aplicação da qual fazem parte é iniciada e saem porque a aplicação é desligada, reiniciada ou trava.
  • Quando um grupo é reequilibrado, as partições de tópicos são redistribuídas entre os membros do grupo. Assim, por exemplo, se um cliente entrar para um grupo, alguns dos clientes que já estão no grupo podem ter partições de tópico retiradas deles (ou “revogadas”, na terminologia do Kafka) para serem dadas ao cliente que acabou de ingressar. O inverso também vale: quando um cliente deixa um grupo, as partições de tópico atribuídas a ele são redistribuídas entre os membros restantes.

À medida que o Kafka amadureceu, algoritmos de reequilíbrio cada vez mais sofisticados foram (e continuam sendo) desenvolvidos. Nas versões iniciais do Kafka, quando um grupo de consumidores era reequilibrado, todos os clientes do grupo tinham que parar de consumir, as partições de tópicos eram redistribuídas entre os novos membros do grupo e todos os clientes começavam a consumir novamente. Essa abordagem tem duas desvantagens (não se preocupe, elas foram aprimoradas desde então):

  1. Todos os clientes do grupo param de consumir mensagens enquanto ocorre o reequilíbrio. Isso tem repercussões óbvias sobre o rendimento.
  2. Os clientes do Kafka normalmente tentam manter um buffer de mensagens que ainda não foram entregues à aplicação e buscar mais mensagens do broker antes que o buffer seja esgotado. A intenção é evitar que a entrega de mensagens para a aplicação seja interrompida enquanto mais mensagens são buscadas no broker do Kafka (sim, conforme anteriormente neste artigo, o cliente do Kafka também está tentando evitar esperar em viagens de ida e volta da rede). Infelizmente, quando um reequilíbrio faz com que as partições sejam revogadas de um cliente, todos os dados em buffer da partição devem ser descartados. Da mesma forma, quando o reequilíbrio faz com que uma nova partição seja atribuída a um cliente, o cliente começará a armazenar os dados a partir do último deslocamento confirmado para a partição, podendo causar um pico na taxa de transferência de rede do broker para o cliente. Isso é causado pelo cliente ao qual a partição foi atribuída recentemente aos dados da mensagem de releitura que haviam sido armazenados em buffer pelo cliente do qual a partição foi revogada.

Algoritmos de reequilíbrio mais recentes fizeram melhorias significativas, para usar a terminologia de Kafka, adicionando "aderência" e "cooperação":

  • Os algoritmos "aderentes" tentam garantir que, após um reequilíbrio, o maior número possível de membros do grupo mantenha as mesmas partições que tinham antes do reequilíbrio. Isso minimiza a quantidade de dados de mensagens em buffer que são descartados ou relidos do Kafka quando o reequilíbrio ocorre.
  • Algoritmos "cooperativos" permitem que os clientes continuem consumindo mensagens enquanto ocorre um reequilíbrio. Quando um cliente tem uma partição atribuída a ele antes de um reequilíbrio e mantém a partição após o reequilíbrio, ele pode continuar consumindo de partições ininterruptas pelo reequilíbrio. Isso é sinérgico com a "aderência", que age para manter as partições atribuídas ao mesmo cliente.

Apesar dessas melhorias em algoritmos de reequilíbrio mais recentes, se suas aplicações estiverem frequentemente sujeitas a reequilíbrios de grupo de consumidores, você ainda verá um impacto na taxa de transferência geral das mensagens e desperdiçará largura de banda de rede à medida que os clientes descartam e buscam dados de mensagens em buffer. Veja aqui algumas sugestões sobre o que você pode fazer:

  1. Certifique-se de que você possa identificar quando o reequilíbrio estiver ocorrendo. Em escala, coletar e visualizar métricas é sua melhor opção. Esta é uma situação em que uma variedade de fontes de métricas ajuda a construir o quadro completo. O broker do Kafka tem métricas para a quantidade de bytes de dados enviados aos clientes e também para o número de reequilíbrio de grupos de consumidores. Se você estiver coletando métricas da sua aplicação, ou seu tempo de execução, que são exibidas quando ocorrem reinicializações, a correlação com as métricas do corretor pode fornecer uma confirmação adicional de que o reequilíbrio é um problema para você.
  2. Evite reinicializações desnecessárias de aplicações quando, por exemplo, uma aplicação falhar. Se você estiver enfrentando problemas de estabilidade em sua aplicação, isso pode levar a reequilíbrios muito mais frequentes do que o previsto. Pesquisar logs de aplicações em busca de mensagens de erro comuns emitidas por uma falha de aplicação, por exemplo, rastreamentos de stack, pode ajudar a identificar com que frequência os problemas estão ocorrendo e fornecer informações úteis para depurar o problema subjacente.
  3. Você está usando o melhor algoritmo de reequilíbrio para sua aplicação? No momento em que este artigo foi escrito, o padrão ouro é o "CooperativeStickyAssignor"; no entanto, o padrão (a partir do Kafka 3.0) é usar o "RangeAssignor" (e algoritmo de atribuição anterior) em vez do atribuidor fixo cooperativo. A documentação do Kafka descreve as etapas de migração necessárias para que seus clientes utilizem o atribuidor fixo cooperativo. Também vale a pena notar que, embora o atribuidor fixo cooperativo seja uma boa escolha geral, há outros atribuidores adaptados a casos de uso específicos.
  4. Os membros de um grupo de consumidores são fixos? Por exemplo, talvez você sempre execute quatro instâncias distintas e altamente disponíveis de uma aplicação. Você pode aproveite a funcionalidade de associação de grupo estático do Kafka. Ao atribuir IDs exclusivos a cada instância de sua aplicação, a participação em grupos estáticos permite que você evite o reequilíbrio por completo.
  5. Confirme o deslocamento atual quando uma partição for revogada da instância da aplicação. O cliente consumidor do Kafka fornece um listener para eventos de reequilíbrio. Se uma instância da sua aplicação estiver prestes a ter uma partição revogada dela, o listener oferece a oportunidade de confirmar um deslocamento para a partição que está prestes a ser retirada. A vantagem de confirmar um deslocamento no ponto em que a partição é revogada é que isso garante que qualquer membro do grupo atribuído à partição atenda a partir desse ponto, em vez de potencialmente reprocessar algumas das mensagens da partição.

O que está por vir?

Agora você é especialista em escalar aplicações do Kafka. Você está convidado a colocar esses pontos em prática e experimentar a oferta totalmente gerenciada do Kafka na IBM Cloud. Para quaisquer desafios na configuração, consulte o Guia de Introdução e as Perguntas Frequentes.

 
Soluções relacionadas
IBM Event Streams

O IBM Event Streams é um software de fluxo de eventos construído no Apache Kafka de código aberto. Está disponível como um serviço totalmente gerenciado na IBM Cloud ou para auto-hospedagem.

Explore o Event Streams
Softwares e soluções de integração

Libere o potencial dos negócios com as soluções de integração da IBM, interligando aplicações e sistemas para acessar dados críticos com rapidez e segurança.

Explore as soluções de integração
Serviços de consultoria em nuvem

Libere novos recursos e aumente a agilidade dos negócios com os serviços de consultoria em nuvem da IBM.

Explore serviços de consultoria em nuvem
Dê o próximo passo

O IBM® Event Streams é um software de fluxo de eventos construído no Apache Kafka de código aberto. Está disponível como um serviço totalmente gerenciado na IBM® Cloud ou para auto-hospedagem.

  1. Explore o Event Streams
  2. Veja mais informações