Você já se perguntou como alguns de seus aplicativos favoritos lidam com atualizações em tempo real? Resultados esportivos ao vivo, tickers do mercado de ações ou até mesmo notificações de mídia social — todos eles dependem da arquitetura orientada a eventos (EDA) para processar dados instantaneamente. EDA é como ter uma conversa onde cada nova informação desencadeia uma resposta imediata. É o que torna um aplicativo mais interativo e responsivo.
Neste passo a passo, orientaremos você na construção de um aplicativo simples baseado em eventos usando Apache Kafka no Heroku. Abordaremos:
Apache Kafka é uma ferramenta poderosa para construção de sistemas EDA. É uma plataforma de código aberto projetada para lidar com feeds de dados em tempo real. Apache Kafka no Heroku é um complemento do Heroku que fornece o Kafka como um serviço. O Heroku facilita muito a implantação e o gerenciamento de aplicativos, e tenho usado mais isso em meus projetos recentemente. Combinar Kafka com Heroku simplifica o processo de configuração quando você deseja executar um aplicativo orientado a eventos.
Ao final deste guia, você terá um aplicativo em execução que demonstra o poder do EDA com Apache Kafka no Heroku. Vamos começar!
Antes de nos aprofundarmos no código, vamos revisar rapidamente alguns conceitos básicos. Depois de entendê-los, será mais fácil acompanhar.
Construiremos um aplicativo Node.js usando a biblioteca KafkaJS . Aqui está uma rápida visão geral de como nosso aplicativo funcionará:
Nossos sensores meteorológicos (os produtores) gerarão dados periodicamente – como temperatura, umidade e pressão barométrica – e enviarão esses eventos para o Apache Kafka. Para fins de demonstração, os dados serão gerados aleatoriamente.
Teremos um consumidor ouvindo os tópicos. Quando um novo evento for recebido, os dados serão gravados em um log.
Implantaremos toda a configuração no Heroku e usaremos os logs do Heroku para monitorar os eventos à medida que eles ocorrem.
Antes de começarmos, certifique-se de ter o seguinte:
A base de código de todo este projeto está disponível neste repositório GitHub . Sinta-se à vontade para clonar o código e acompanhar esta postagem.
Agora que cobrimos o básico, vamos configurar nosso cluster Kafka no Heroku e começar a construir.
Vamos configurar tudo no Heroku. É um processo bastante rápido e fácil.
~/project$ heroku login
~/project$ heroku create weather-eda
(Chamei meu aplicativo Heroku weather-eda
, mas você pode escolher um nome exclusivo para seu aplicativo.)
~/project$ heroku addons:create heroku-kafka:basic-0 Creating heroku-kafka:basic-0 on ⬢ weather-eda... ~$0.139/hour (max $100/month) The cluster should be available in a few minutes. Run `heroku kafka:wait` to wait until the cluster is ready. You can read more about managing Kafka at https://devcenter.heroku.com/articles/kafka-on-heroku#managing-kafka kafka-adjacent-07560 is being created in the background. The app will restart when complete... Use heroku addons:info kafka-adjacent-07560 to check creation progress Use heroku addons:docs heroku-kafka to view documentation
Você pode encontrar mais informações sobre o complemento Apache Kafka no Heroku aqui . Para nossa demonstração, estou adicionando o nível Basic 0 do complemento. O custo do complemento é de US$ 0,139/hora. Durante a construção deste aplicativo de demonstração, usei o complemento por menos de uma hora e depois o desliguei.
O Heroku leva alguns minutos para deixar o Kafka pronto para você. Em breve, isso é o que você verá:
~/project$ heroku addons:info kafka-adjacent-07560 === kafka-adjacent-07560 Attachments: weather-eda::KAFKA Installed at: Mon May 27 2024 11:44:37 GMT-0700 (Mountain Standard Time) Max Price: $100/month Owning app: weather-eda Plan: heroku-kafka:basic-0 Price: ~$0.139/hour State: created
Com nosso cluster Kafka ativado, precisaremos obter credenciais e outras configurações. Heroku cria vários vars de configuração para nosso aplicativo, preenchendo-os com informações do cluster Kafka que acabou de ser criado. Podemos ver todas essas variáveis de configuração executando o seguinte:
~/project$ heroku config === weather-eda Config Vars KAFKA_CLIENT_CERT: -----BEGIN CERTIFICATE----- MIIDQzCCAiugAwIBAgIBADANBgkqhkiG9w0BAQsFADAyMTAwLgYDVQQDDCdjYS1h ... -----END CERTIFICATE----- KAFKA_CLIENT_CERT_KEY: -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAsgv1oBiF4Az/IQsepHSh5pceL0XLy0uEAokD7ety9J0PTjj3 ... -----END RSA PRIVATE KEY----- KAFKA_PREFIX: columbia-68051. KAFKA_TRUSTED_CERT: -----BEGIN CERTIFICATE----- MIIDfzCCAmegAwIBAgIBADANBgkqhkiG9w0BAQsFADAyMTAwLgYDVQQDDCdjYS1h ... F+f3juViDqm4eLCZBAdoK/DnI4fFrNH3YzhAPdhoHOa8wi4= -----END CERTIFICATE----- KAFKA_URL: kafka+ssl://ec2-18-233-140-74.compute-1.amazonaws.com:9096,kafka+ssl://ec2-18-208-61-56.compute-1.amazonaws.com:9096...kafka+ssl://ec2-34-203-24-91.compute-1.amazonaws.com:9096
Como você pode ver, temos diversas variáveis de configuração. Queremos um arquivo na pasta raiz do nosso projeto chamado .env
com todos esses valores var de configuração. Para fazer isso, simplesmente executamos o seguinte comando:
~/project$ heroku config --shell > .env
Nosso arquivo .env
fica assim:
KAFKA_CLIENT_CERT="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----" KAFKA_CLIENT_CERT_KEY="-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----" KAFKA_PREFIX="columbia-68051." KAFKA_TRUSTED_CERT="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----" KAFKA_URL="kafka+ssl://ec2-18-233-140-74.compute-1.amazonaws.com:9096,kafka+ssl://ec2-18-208-61-56.compute-1.amazonaws.com:9096...kafka+ssl://ec2-34-203-24-91.compute-1.amazonaws.com:9096"
Além disso, adicionamos .env ao nosso arquivo .gitignore. Não gostaríamos de submeter esses dados confidenciais ao nosso repositório.
O Heroku CLI não vem com comandos relacionados ao Kafka prontos para uso. Como estamos usando o Kafka, precisaremos instalar o plugin CLI .
~/project$ heroku plugins:install heroku-kafka Installing plugin heroku-kafka... installed v2.12.0
Agora podemos gerenciar nosso cluster Kafka a partir da CLI.
~/project$ heroku kafka:info === KAFKA_URL Plan: heroku-kafka:basic-0 Status: available Version: 2.8.2 Created: 2024-05-27T18:44:38.023+00:00 Topics: [··········] 0 / 40 topics, see heroku kafka:topics Prefix: columbia-68051. Partitions: [··········] 0 / 240 partition replicas (partitions × replication factor) Messages: 0 messages/s Traffic: 0 bytes/s in / 0 bytes/s out Data Size: [··········] 0 bytes / 4.00 GB (0.00%) Add-on: kafka-adjacent-07560 ~/project$ heroku kafka:topics === Kafka Topics on KAFKA_URL No topics found on this Kafka cluster. Use heroku kafka:topics:create to create a topic (limit 40)
Apenas como uma verificação de sanidade, vamos brincar com nosso cluster Kafka. Começamos criando um tópico.
~/project$ heroku kafka:topics:create test-topic-01 Creating topic test-topic-01 with compaction disabled and retention time 1 day on kafka-adjacent-07560... done Use `heroku kafka:topics:info test-topic-01` to monitor your topic. Your topic is using the prefix columbia-68051.. ~/project$ heroku kafka:topics:info test-topic-01 ▸ topic test-topic-01 is not available yet
Dentro de um minuto ou mais, nosso tópico estará disponível.
~/project$ heroku kafka:topics:info test-topic-01 === kafka-adjacent-07560 :: test-topic-01 Topic Prefix: columbia-68051. Producers: 0 messages/second (0 bytes/second) total Consumers: 0 bytes/second total Partitions: 8 partitions Replication Factor: 3 Compaction: Compaction is disabled for test-topic-01 Retention: 24 hours
A seguir, nesta janela do terminal, atuaremos como consumidores, ouvindo este tópico seguindo-o.
~/project$ heroku kafka:topics:tail test-topic-01
A partir daqui, o terminal simplesmente aguarda quaisquer eventos publicados sobre o tema.
Em uma janela de terminal separada, atuaremos como produtores e publicaremos algumas mensagens sobre o tema.
~/project$ heroku kafka:topics:write test-topic-01 "hello world!"
De volta à janela do terminal do nosso consumidor, é isso que vemos:
~/project$ heroku kafka:topics:tail test-topic-01 test-topic-01 0 0 12 hello world!
Excelente! Produzimos e consumimos com sucesso um evento para um tópico em nosso cluster Kafka. Estamos prontos para passar para nosso aplicativo Node.js. Vamos destruir este tópico de teste para manter nosso playground arrumado.
~/project$ heroku kafka:topics:destroy test-topic-01 ▸ This command will affect the cluster: kafka-adjacent-07560, which is on weather-eda ▸ To proceed, type weather-eda or re-run this command with --confirm weather-eda > weather-eda Deleting topic test-topic-01... done Your topic has been marked for deletion, and will be removed from the cluster shortly ~/project$ heroku kafka:topics === Kafka Topics on KAFKA_URL No topics found on this Kafka cluster. Use heroku kafka:topics:create to create a topic (limit 40).
Para preparar nosso aplicativo para usar o Kafka, precisaremos criar duas coisas: um tópico e um grupo de consumidores.
Vamos criar o tópico que nossa aplicação utilizará.
~/project$ heroku kafka:topics:create weather-data
A seguir, criaremos o grupo de consumidores do qual o consumidor da nossa aplicação fará parte:
~/project$ heroku kafka:consumer-groups:create weather-consumers
Estamos prontos para construir nosso aplicativo Node.js!
Vamos inicializar um novo projeto e instalar nossas dependências.
~/project$ npm init -y ~/project$ npm install kafkajs dotenv @faker-js/faker pino pino-pretty
Nosso projeto terá dois processos em execução:
consumer.js
, que está inscrito no tópico e registra todos os eventos publicados.
producer.js
, que publicará alguns dados meteorológicos aleatórios sobre o tópico a cada poucos segundos.
Ambos os processos precisarão usar KafkaJS para se conectar ao nosso cluster Kafka, portanto, modularizaremos nosso código para torná-lo reutilizável.
Na pasta src
do projeto, criamos um arquivo chamado kafka.js
. Se parece com isso:
const { Kafka } = require('kafkajs'); const BROKER_URLS = process.env.KAFKA_URL.split(',').map(uri => uri.replace('kafka+ssl://','' )) const TOPIC = `${process.env.KAFKA_PREFIX}weather-data` const CONSUMER_GROUP = `${process.env.KAFKA_PREFIX}weather-consumers` const kafka = new Kafka({ clientId: 'weather-eda-app-nodejs-client', brokers: BROKER_URLS, ssl: { rejectUnauthorized: false, ca: process.env.KAFKA_TRUSTED_CERT, key: process.env.KAFKA_CLIENT_CERT_KEY, cert: process.env.KAFKA_CLIENT_CERT, }, }) const producer = async () => { const p = kafka.producer() await p.connect() return p; } const consumer = async () => { const c = kafka.consumer({ groupId: CONSUMER_GROUP, sessionTimeout: 30000 }) await c.connect() await c.subscribe({ topics: [TOPIC] }); return c; } module.exports = { producer, consumer, topic: TOPIC, groupId: CONSUMER_GROUP };
Neste arquivo, começamos criando um novo cliente Kafka. Isso requer URLs para os corretores Kafka, que podemos analisar a partir da variável KAFKA_URL
em nosso arquivo .env
(que originalmente veio da chamada de configuração do heroku). Para autenticar a tentativa de conexão, precisamos fornecer KAFKA_TRUSTED_CERT
, KAFKA_CLIENT_CERT_KEY
e KAFKA_CLIENT_CERT
.
Então, a partir do nosso cliente Kafka, criamos um producer
e um consumer
, certificando-nos de inscrever nossos consumidores no tópico weather-data
.
Observe em kafka.js
que acrescentamos KAFKA_PREFIX
ao nosso tópico e nome do grupo de consumidores. Estamos usando o plano Basic 0 para Apache Kafka no Heroku, que é um plano Kafka multilocatário. Isso significa que trabalhamos com um KAFKA_PREFIX
. Embora tenhamos nomeado nosso tópico weather-data
e nosso grupo de consumidores weather-consumers
, seus nomes reais em nosso cluster Kafka multilocatário devem ter o KAFKA_PREFIX
anexado a eles (para garantir que sejam exclusivos).
Então, tecnicamente, para nossa demonstração, o nome real do tópico é columbia-68051.weather-data
, não weather-data
. (O mesmo acontece com o nome do grupo de consumidores.)
Agora, vamos criar nosso processo em segundo plano que atuará como nossos produtores de sensores meteorológicos. Na pasta raiz do nosso projeto, temos um arquivo chamado producer.js
. Se parece com isso:
require('dotenv').config(); const kafka = require('./src/kafka.js'); const { faker } = require('@faker-js/faker'); const SENSORS = ['sensor01','sensor02','sensor03','sensor04','sensor05']; const MAX_DELAY_MS = 20000; const READINGS = ['temperature','humidity','barometric_pressure']; const MAX_TEMP = 130; const MIN_PRESSURE = 2910; const PRESSURE_RANGE = 160; const getRandom = (arr) => arr[faker.number.int(arr.length - 1)]; const getRandomReading = { temperature: () => faker.number.int(MAX_TEMP) + (faker.number.int(100) / 100), humidity: () => faker.number.int(100) / 100, barometric_pressure: () => (MIN_PRESSURE + faker.number.int(PRESSURE_RANGE)) / 100 }; const sleep = (ms) => { return new Promise((resolve) => { setTimeout(resolve, ms); }); }; (async () => { const producer = await kafka.producer() while(true) { const sensor = getRandom(SENSORS) const reading = getRandom(READINGS) const value = getRandomReading[reading]() const data = { reading, value } await producer.send({ topic: kafka.topic, messages: [{ key: sensor, value: JSON.stringify(data) }] }) await sleep(faker.number.int(MAX_DELAY_MS)) } })()
Muito do código no arquivo tem a ver com a geração de valores aleatórios. Vou destacar as partes importantes:
SENSORS
.
temperature
, humidity
ou barometric_pressure
. O objeto getRandomReading
possui uma função para cada uma dessas leituras, para gerar um valor correspondente razoável.
async
com um loop while
infinito.
Dentro do loop while
, nós:
sensor
aleatoriamente.reading
aleatoriamente.value
aleatório para essa leitura.producer.send
para publicar esses dados no tópico. O sensor
serve como key
para o evento, enquanto a reading
e value
formarão a mensagem do evento. O processo em segundo plano em consumer.js
é consideravelmente mais simples.
require('dotenv').config(); const logger = require('./src/logger.js'); const kafka = require('./src/kafka.js'); (async () => { const consumer = await kafka.consumer() await consumer.run({ eachMessage: async ({ topic, partition, message }) => { const sensorId = message.key.toString() const messageObj = JSON.parse(message.value.toString()) const logMessage = { sensorId } logMessage[messageObj.reading] = messageObj.value logger.info(logMessage) } }) })()
Nosso consumer
já está inscrito no tópico weather-data
. Chamamos consumer.run
e configuramos um manipulador para eachMessage
. Sempre que Kafka notifica o consumer
sobre uma mensagem, ele registra a mensagem. Isso é tudo que há para fazer.
Procfile
No arquivo package.json
, precisamos adicionar alguns scripts
que iniciam nossos processos em segundo plano de produtor e consumidor. O arquivo agora deve incluir o seguinte:
... "scripts": { "start": "echo 'do nothing'", "start:consumer": "node consumer.js", "start:producer": "node producer.js" }, ...
Os importantes são start:consumer
e start:producer
. Mas continuamos start
em nosso arquivo (mesmo que ele não faça nada significativo) porque o construtor Heroku espera que ele esteja lá.
A seguir, criamos um Procfile
que dirá ao Heroku como iniciar os vários trabalhadores que precisamos para nosso aplicativo Heroku. Na pasta raiz do nosso projeto, o Procfile
deve ficar assim:
consumer_worker: npm run start:consumer producer_worker: npm run start:producer
Muito simples, certo? Teremos um trabalhador de processo em segundo plano chamado consumer_worker
e outro chamado producer_worker
. Você notará que não temos um web
trabalhador que é o que você normalmente veria no Procfile
para um aplicativo web. Para nosso aplicativo Heroku, precisamos apenas dos dois trabalhadores em segundo plano. Não precisamos web
.
Com isso, todo o nosso código está definido. Confirmamos todo o nosso código no repositório e estamos prontos para implantar.
~/project$ git push heroku main … remote: -----> Build succeeded! … remote: -----> Compressing... remote: Done: 48.6M remote: -----> Launching... … remote: Verifying deploy... done
Após a implantação, queremos ter certeza de que dimensionamos nossos dinamômetros corretamente. Não precisamos de um dinamômetro para um processo web, mas precisaremos de um para consumer_worker
e producer_worker
. Executamos o seguinte comando para definir esses processos com base em nossas necessidades.
~/project$ heroku ps:scale web=0 consumer_worker=1 producer_worker=1 Scaling dynos... done, now running producer_worker at 1:Eco, consumer_worker at 1:Eco, web at 0:Eco
Agora, tudo deve estar instalado e funcionando. Nos bastidores, nosso producer_worker
deve se conectar ao cluster Kafka e começar a publicar dados do sensor meteorológico a cada poucos segundos. Em seguida, nosso consumer_worker
deve se conectar ao cluster Kafka e registrar todas as mensagens que receber do tópico no qual está inscrito.
Para ver o que nosso consumer_worker
está fazendo, podemos consultar nossos logs do Heroku.
~/project$ heroku logs --tail … heroku[producer_worker.1]: Starting process with command `npm run start:producer` heroku[producer_worker.1]: State changed from starting to up app[producer_worker.1]: app[producer_worker.1]: > [email protected] start:producer app[producer_worker.1]: > node producer.js app[producer_worker.1]: … heroku[consumer_worker.1]: Starting process with command `npm run start:consumer` heroku[consumer_worker.1]: State changed from starting to up app[consumer_worker.1]: app[consumer_worker.1]: > [email protected] start:consumer app[consumer_worker.1]: > node consumer.js app[consumer_worker.1]: app[consumer_worker.1]: {"level":"INFO","timestamp":"2024-05-28T02:31:20.660Z","logger":"kafkajs","message":"[Consumer] Starting","groupId":"columbia-68051.weather-consumers"} app[consumer_worker.1]: {"level":"INFO","timestamp":"2024-05-28T02:31:23.702Z","logger":"kafkajs","message":"[ConsumerGroup] Consumer has joined the group","groupId":"columbia-68051.weather-consumers","memberId":"weather-eda-app-nodejs-client-3ee5d1fa-eba9-4b59-826c-d3b924a6e4e4","leaderId":"weather-eda-app-nodejs-client-3ee5d1fa-eba9-4b59-826c-d3b924a6e4e4","isLeader":true,"memberAssignment":{"columbia-68051.test-topic-1":[0,1,2,3,4,5,6,7]},"groupProtocol":"RoundRobinAssigner","duration":3041} app[consumer_worker.1]: [2024-05-28 02:31:23.755 +0000] INFO (21): {"sensorId":"sensor01","temperature":87.84} app[consumer_worker.1]: [2024-05-28 02:31:23.764 +0000] INFO (21): {"sensorId":"sensor01","humidity":0.3} app[consumer_worker.1]: [2024-05-28 02:31:23.777 +0000] INFO (21): {"sensorId":"sensor03","temperature":22.11} app[consumer_worker.1]: [2024-05-28 02:31:37.773 +0000] INFO (21): {"sensorId":"sensor01","barometric_pressure":29.71} app[consumer_worker.1]: [2024-05-28 02:31:54.495 +0000] INFO (21): {"sensorId":"sensor05","barometric_pressure":29.55} app[consumer_worker.1]: [2024-05-28 02:32:02.629 +0000] INFO (21): {"sensorId":"sensor04","temperature":90.58} app[consumer_worker.1]: [2024-05-28 02:32:03.995 +0000] INFO (21): {"sensorId":"sensor02","barometric_pressure":29.25} app[consumer_worker.1]: [2024-05-28 02:32:12.688 +0000] INFO (21): {"sensorId":"sensor04","humidity":0.1} app[consumer_worker.1]: [2024-05-28 02:32:32.127 +0000] INFO (21): {"sensorId":"sensor01","humidity":0.34} app[consumer_worker.1]: [2024-05-28 02:32:32.851 +0000] INFO (21): {"sensorId":"sensor02","humidity":0.61} app[consumer_worker.1]: [2024-05-28 02:32:37.200 +0000] INFO (21): {"sensorId":"sensor01","barometric_pressure":30.36} app[consumer_worker.1]: [2024-05-28 02:32:50.388 +0000] INFO (21): {"sensorId":"sensor03","temperature":104.55}
Funciona! Sabemos que nosso produtor publica mensagens periodicamente para Kafka porque nosso consumidor as recebe e depois as registra.
É claro que, em um aplicativo EDA maior, cada sensor é um produtor. Eles podem publicar sobre vários tópicos para diversos fins ou podem publicar todos sobre o mesmo tópico. E seu consumidor pode estar inscrito em vários tópicos. Além disso, em nosso aplicativo de demonstração, nossos consumidores simplesmente emitiram muito em eachMessage
; mas em uma aplicação EDA, um consumidor pode responder chamando uma API de terceiros, enviando uma notificação por SMS ou consultando um banco de dados.
Agora que você tem um conhecimento básico de eventos, tópicos, produtores e consumidores e sabe como trabalhar com o Kafka, pode começar a projetar e construir seus próprios aplicativos EDA para satisfazer casos de uso de negócios mais complexos.
O EDA é bastante poderoso – você pode desacoplar seus sistemas enquanto desfruta de recursos importantes, como fácil escalabilidade e processamento de dados em tempo real. Para EDA, Kafka é uma ferramenta fundamental, ajudando você a lidar com fluxos de dados de alto rendimento com facilidade. Usar o Apache Kafka no Heroku ajuda você a começar rapidamente. Por ser um serviço gerenciado, você não precisa se preocupar com as partes complexas do gerenciamento de cluster Kafka. Você pode se concentrar apenas na construção de seus aplicativos.
A partir daqui, é hora de experimentar e prototipar. Identifique quais casos de uso se adaptam bem ao EDA. Mergulhe, teste no Heroku e construa algo incrível. Boa codificação!