BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos A solução Kongo: Criando uma aplicação IoT escalável com Apache Kafka

A solução Kongo: Criando uma aplicação IoT escalável com Apache Kafka

Pontos Principais

  • O Kafka permite o uso de fontes de dados e sinks heterogêneos - um recurso essencial para aplicações de IoT que podem usar o Kafka para combinar fontes heterogêneas em um único sistema;

  • A API Kafka Streams permite que a aplicação atue como um processador de streams, consumindo uma stream de dados de entrada de um ou mais tópicos e, então, produzindo um stream de saída para um ou mais tópicos;

  • Quando usar o Kafka, certifique-se de que o número de chaves é maior que o número de partições. Ao mesmo tempo, o número de partições deve ser maior ou igual ao número de consumidores em um grupo;

  • A aplicação de demonstração inclui operações de IoT com etiquetas RFID nas mercadorias transportadas a partir da origem até o destino.

A "solução Kongo" é um projeto inventado para estudar e experimentar o Apache Kafka. Neste artigo, o autor mostrará sua jornada educacional, desenvolvendo uma aplicação para um projeto realista de demonstração de uma infraestrutura de IoT, contando com os erros e as melhores práticas encontradas nesse processo. Isso inclui mergulhar no projeto da arquitetura da aplicação, alguns designs de sistemas concorrentes, uma extensão Kafka Streams e uma olhada em como maximizar a escalabilidade do Kafka.

Apache Kafka

O Kafka é um sistema distribuído de processamento de stream que permite que os produtores distribuídos enviem mensagens aos consumidores distribuídos por meio de um cluster Kafka. Simplificando, é uma maneira de entregar mensagens onde se deseja que elas sejam enviadas. O Kafka é particularmente vantajoso porque oferece alto rendimento e baixa latência, poderosa escalabilidade horizontal e a alta confiabilidade necessária em ambientes de produção. Ele também permite que não haja perda de dados e traz as vantagens de ser de código aberto, além de ser um projeto Apache amplamente suportado. Ao mesmo tempo, o Kafka permite o uso de fontes de dados e sinks heterogêneos - um recurso essencial para aplicações de IoT que podem aproveitar o Kafka para combinar fontes heterogêneas em um único sistema.

Para obter alto rendimento, baixa latência e escalabilidade horizontal, o Kafka foi projetado como um intermediário "burro" e um consumidor "inteligente". Isso resulta em diferentes trade-offs na funcionalidade e no desempenho em comparação com outras tecnologias de mensagens, como RabbitMQ e Pulsar (por exemplo, o Pulsar possui algumas operações baseadas em broker, como roteamento baseado em conteúdo, consulte o blog do ApacheCon2019 para obter mais informações sobre Kafka vs. Pulsar) .

Como o Kafka funciona

Kafka é um sistema de publicação/subscrição com baixo acoplamento, no qual produtores e consumidores não se conhecem. A filtragem - que determina quais consumidores recebem as mensagens - é baseada em tópicos e funciona assim:

  • Os produtores enviam mensagens para tópicos;
  • Os consumidores assinam tópicos de interesse;
  • Quando os consumidores pesquisam, eles recebem apenas mensagens enviadas para esses tópicos.

De certa forma, o Kafka funciona como um celeiro Amish, na medida em que apresenta simultaneidade compartilhada:

  • Consumidores de um mesmo grupo que estão inscritos em um tópico são partições alocadas para compartilhar trabalho. Quando esses consumidores pesquisam por novas mensagens, recebem apenas aqueles itens que estão alocadas nas suas partições;
  • Quanto mais partições um tópico tem, mais consumidores ele suporta e, portanto, mais trabalho ele pode realizar.

No entanto, Kafka também funciona como um exército de clones, pois suporta a entrega da mesma mensagem a vários grupos de consumidores, permitindo um recurso útil de transmissão:

  • As mensagens são duplicadas entre os grupos, pois cada grupo de consumidores recebe uma cópia de cada mensagem.

Em teoria, pode-se usar quantos grupos de consumidores desejar. Dito isso, o artigo aborda algumas complicações associadas ao aumento do número de grupos de consumidores sob uma perspectiva de escalabilidade.

A solução Kongo

Para explorar uma aplicação Kafka realista, este autor criou a solução Kongo, que trata da implementação de uma aplicação de logística de IoT para supervisionar a movimentação segura de várias mercadorias estocadas em armazéns e viajando em caminhões. A palavra Kongo é um nome antigo para o rio Congo, um importante canal de comércio que apresenta córregos e corredeiras onde a água pode se mover tão dinamicamente quanto o Kafka move dados, daí o nome.

Para este projeto de demonstração, foram criados dados para imitar uma operação de IoT com tags RFID em cada uma das muitas mercadorias transportadas. Cada item de mercadoria possui atributos importantes que determinam seu manuseio seguro. Para alguns exemplos, digamos que temos:

  • Galinhas: perecíveis, frágeis, comestíveis;
  • Resíduos tóxicos: perigosos, volumosos;
  • Legumes: perecíveis, comestíveis;
  • Arte: frágil.

O objetivo da aplicação é verificar regras interessantes em tempo real. Por exemplo, galinhas e vegetais comestíveis não devem viajar no mesmo caminhão com resíduos tóxicos. (Nota: as regras do problema de Kongo foram inspiradas pelas regulamentações reais de transporte na Austrália, que são responsáveis por 97 categorias de mercadorias e incluem uma matriz muito complexa que define o que pode viajar junto.) Assumimos que os armazéns podem estocar todos os produtos com segurança em áreas separadas e que as etiquetas RFID informam ao sistema toda vez que ocorre um evento de carga ou descarga (dentro e fora dos caminhões). A simulação também inclui algumas informações de sensores associadas a cada armazém e caminhão, como temperatura, vibração e outros dados para chegar a cerca de 20 métricas no total.

Arquitetura e simulação da aplicação

A arquitetura da aplicação é mostrada na Figura 1 abaixo.

Figure 1: Simulação da solução Kongo - Passos Lógicos

Para criar a simulação do problema de Kongo, primeiro foi criado um sistema inteiro com um certo número de mercadorias, armazéns, caminhões e todas as métricas e parâmetros detalhados. Em seguida, avançamos a simulação em um loop repetitivo, movendo mercadorias de armazéns para caminhões para outros armazéns aleatoriamente e verificando as regras de localização e sensor para reconhecer quando ocorrem violações que podem estragar as mercadorias.

Inicialmente, a aplicação foi construída como uma arquitetura monolítica, como mostra a Figura 2.

Figura 2: Kongo - Arquitetura Monolítica Inicial

A parte de simulação do sistema possui um conhecimento perfeito do mundo e gera os eventos de simulação (por exemplo, valores do sensor e eventos de RFID de carga/descarga de caminhão). Os eventos são passados internamente na aplicação monolítica inicial para as regras de verificação (as duas caixas laranja), que produzem violações. Isso funciona bem, mas não é escalável, e não demonstra nada de interessante.

Em seguida, serão separados o lado da simulação do lado de verificação usando event streams.

Figura 3: Kongo - arquitetura com baixo acomplamento para Event Streams.

A seguir, pode-se observar uma arquitetura real distribuída usando o Kafka:

Figura 4: Kongo - Arquitetura Distribuída com Kafka

Aqui, são apresentados produtores e consumidores de Kafka separados. Os produtores agora executam a simulação, enquanto os consumidores realizam a verificação das regras.

Objetivos do projeto e escolhas

Os objetivos de projeto incluíam a implementação de "eventos de entrega", em que cada local (um armazém ou caminhão) entrega eventos a cada mercadoria naquele local e apenas a esses produtos.

Figura 5: Objetivo do projeto - Eventos entregues às mercadorias no mesmo local.

Do ponto de vista do Kafka, isso significa que desejamos a garantia da entrega do evento e não desejamos que o evento seja enviado para a mercadoria errada. Dado que o Kafka usa tópicos para entregar eventos a um destino, havia duas possibilidades extremas de design para escolher.

Uma era usar apenas um tópico para todos os locais. A outra abordagem - que, inicialmente, parecia sensata - era ter um tópico por local. Essa também parecia uma idéia inteligente em termos de consumidores.

Figura 6: Variáveis de design - um ou muitos tópicos e consumidores.

Por fim, testou-se a possibilidade indicada no canto inferior direito deste gráfico, em que todo bem é um grupo de consumidores. Isso significava que o número de grupos de consumidores Kafka era realmente igual ao número de mercadorias na simulação. Infelizmente, ter um grande número de tópicos ou grupos de consumidores Kafka leva a problemas de escalabilidade, como foi verificado no experimento.

Vamos analisar os dois casos extremos de design e depois ver como eles funcionam.

Possible design #1

Figura 7: Design 1 - Muitos tópicos e muitos grupos de consumidores

Nesse design, foram usados múltiplos tópicos e a mesma quantidade de consumidores e mercadorias. Isso significa ter muitos grupos de consumidores. Parece uma solução elegante e se encaixa razoavelmente bem no modelo de dados.

Projeto possível # 2

Figura 8: Um Tópico e um Consumer Group

Esse design, usando um único tópico e um único grupo de consumidores para todos os dados de localização, parece desnecessariamente simplista. Um componente extra separa as mercadorias e o sistema dos consumidores, decidindo quais eventos vão para quais mercadorias com base nos dados de localização.

Verificação do projeto

Estes projetos foram testados para ver o que realmente funcionava bem. Para o benchmarking inicial, foi usado um pequeno cluster Kafka com 100 localizações e 100.000 mercadorias. A transmissão do Kafka para distribuiu os 100.000 produtos, o que significa que cada evento deveria ser entregue a 1.000 consumidores (100.000 / 100).

Aqui está o resultado em termos de taxa de transferência relativa:

O primeiro design é muito ruim em termos de taxa de transferência, enquanto o segundo design, usando um único tópico e grupo de consumidores, fornece a taxa de transferência máxima. Lição aprendida!

Kafka Streams

O Kafka Streams é uma parte poderosa da pilha de tecnologia Kafka que foi explorada criando uma extensão para a solução Kongo. A API do Streams permite que uma aplicação atue como um processador de fluxo, consumindo um fluxo de entrada de um ou mais tópicos e produzindo um fluxo de saída para um ou mais tópicos também. Ao fazer isso, ele pode transformar fluxos de entrada em fluxos de saída.

Existe uma DSL do Kafka Streams disponível (recomendada para novos usuários). O DSL do Streams inclui abstrações integradas para streams e tabelas, e suporta um estilo de programação funcional declarativo.

Há dois tipos de transformações de stream:

  • Stateless: Por exemplo, uma operação map e filter;
  • Stateful: Transformações como agregações, incluindo count reduced joins e windowing.

Para o iniciante, o diagrama a seguir é essencial para aprender a compor essas operações. Essa é essencialmente uma máquina de estado que informa quais operações funcionam juntas, qual saída é produzida por cada tipo de operação e o que se pode fazer com o que é produzido:

Foi construída uma extensão da aplicação Kongo Streams capaz de verificar se os caminhões estão sobrecarregados. Foram adicionados à simulação os limites máximos de carga do caminhão e pesos para cada mercadoria. Então, foi desenvolvida uma aplicação Streams que pode verificar esses valores para caminhões sobrecarregados. Os primeiros esforços nessa área criaram dois problemas. Um: foram disparadas algumas exceções da topologia do Streams, e dois: pesos com valores negativos que significavam que a simulação agora incluía caminhões flutuantes.

Os Streams têm topologias de processador que, como foi aprendido no experimento, adicionam algumas complexidades. Quando começaram a aparecer "erros de topologias inválidas", não era claro o que eles significavam, mas o uso de uma ferramenta de terceiros para visualizar a topologia era bastante útil para entender e depurar topologias do Streams. Foi possível descobrir os erros - por exemplo, no diagrama de visualização abaixo, no qual estava sendo usado o mesmo nó de uma fonte para duas operações, o que não é permitido:

Quanto à questão do caminhão antigravitacional, as configurações do Kafka devem ser cuidadosamente consideradas e não simplesmente deixadas nas posições predefinidas. Ao ativar a configuração transacional "exatamente uma vez", o produtor transacional começou a permitir que a aplicação envie mensagens para várias requisições atomicamente, e os pesos não foram mais negativos. Lembre-se disto: Kafka não dá um caminhão voador se o programador acertar as configurações (ou seja, isso importa muito).

Escalando

Por fim, foi testada a escalabilidade de toda a aplicação e descobriu-se algumas práticas recomendadas. Usando uma simulação com 100 armazéns, 200 caminhões (300 localizações) e 10.000 mercadorias, foram realizados experimentos com escalabilidade horizontal e vertical em vários clusters Kafka.

(Esses resultados demonstram o uso de três nós com dois cores cada, etc.)

A aplicação de demonstração tem boa escalabilidade. Além disso, esses testes usavam clusters relativamente pequenos: na produção, o céu é o limite. Um truque útil é dividir a aplicação e colocar tópicos diferentes e em diferentes clusters Kafka dimensionados independentemente - grandes usuários Kafka como o Netflix fazem isso.

Além disso, tamanhos de instância maiores têm um enorme impacto no desempenho, provavelmente porque os fornecedores (como a AWS) alocam diferentes velocidades de rede. Quanto maior a instância, menor a latência:

Lições de escalabilidade

Um problema de escalabilidade que a aplicação encontrou foi o resultado de colisões de hash, que produziram uma exceção dizendo que havia muitos arquivos abertos. Essa foi outra lição aprendida: se um consumidor não recebe nenhum evento em um determinado período, ele excede o tempo limite. O sistema ativa automaticamente outro consumidor, agravando o problema.

A simulação tinha 300 locais, mas imprudentemente eu só tinha 300 partições e apenas 200 valores únicos. Portanto, apenas 200 dos 300 participantes estavam realmente recebendo eventos; o restante expirou devido a colisões de hash. Se algumas partições receberam mais de um dos valores de localização, outras obtiveram zero. No Kafka, deve-se garantir que o número de chaves seja muito maior que o número de partições (pelo menos 20 vezes maior é uma regra geral). Ao mesmo tempo, o número de partições deve ser maior ou igual ao número de consumidores em um grupo.

Se houver muitos consumidores, a escalabilidade sofre. No entanto, se os clientes Kafka demorarem muito para ler e processar eventos, a aplicação precisará de mais threads do consumidor e, portanto, de mais partições, o que também afeta a escalabilidade do cluster Kafka. A solução é minimizar o tempo de resposta do consumidor. Para maximizar a escalabilidade, use os consumidores Kafka apenas para ler eventos do cluster Kafka. Para qualquer processamento, como gravar em um banco de dados ou executar algoritmos complexos ou verificações complicadas, faça o processamento de forma assíncrona em um conjunto de encadeamentos separado ou em algum outro mecanismo escalável. Isso leva à regra nº 1 do Kafka: o Kafka é fácil de dimensionar quando você usa o menor número de consumidores possível.

Recursos Adicionais

Se você quiser experimentar Kongo e Kafka, aqui estão os códigos base e de streams. Você também precisará de um cluster Kafka e poderá obter uma avaliação gratuita do Instaclustr Managed Kafka aqui.

Sobre o Autor

Paul Brebner é o Evangelista Tecnológico Chefe da Instaclustr, que fornece uma plataforma de serviços gerenciados de tecnologias de código aberto, como Apache Cassandra, Apache Spark, Elasticsearch e Apache Kafka.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT