Apache Kafka

Apache Kafka

Apache Kafka: A Distributed Streaming Platform.
Jade Bird

Jade Bird

1659928800

Building Event-Driven Microservices with Spring Boot & Apache Kafka

In this tutorial, you'll learn how to build a simple Event-Driven Microservices with Spring Boot and Apache Kafka.

Basically, you will learn how to use Apache Kafka as a message broker for Asynchronous communication between multiple microservices.

Event-driven architecture (EDA) is a software design pattern in which decoupled applications can asynchronously publish and subscribe to events via an event broker/message broker.

Course content: 15 Lectures:
1 - Course Overview [00:00]
2 - What is Event Driven Architecture? [02:49]
3 - How Event-Driven Architecture Works and Its Advantages [06:40]
4 - Install and Setup Apache Kafka on Mac and Windows [11:44]
5 - Create 4 Microservices [34:47]
6 - Import and Setup 4 Microservices in IntelliJ IDEA [40:26]
7 - Create Order and OrderEvent DTO Classes in Base-Domains [47:23]
8 - Configure Kafka Producer in OrderService Microservice [50:40]
9 - Configure Kafka Topic in OrderService Microservice [57:48]
10 - Create Kafka Producer in OrderService Microservice [01:01:46]
11 - Create REST API and Test Kafka Producer in OrderService Microservice [01:09:22]
12 - Configure Kafka Consumer in StockService Microservice [01:16:30]
13 - Create Kafka Consumer in StockService Microservice [01:24:34]
14 - Configure and Create Kafka Consumer in EmailService Microservice [01:36:10]
15 - Run 3 Microservices Together and Have a Demo [01:40:43]

Blog post and source code at https://www.javaguides.net/2022/07/event-driven-microservices-using-spring-boot-and-apache-kafka.html 

#microservices #springboot #kafka

Building Event-Driven Microservices with Spring Boot & Apache Kafka
Thierry  Perret

Thierry Perret

1659792979

Configuration Rapide De L'application Java à L'aide De Kafka Messaging

Apache Kafka est un projet open source qui prend en charge le streaming de données de qualité industrielle en temps réel. Kafka peut traiter plus de cent mille messages par seconde. Certaines entreprises signalent la capacité de traiter des millions de messages par seconde.

Kafka est bien adapté aux applications qui coordonnent les activités de covoiturage, diffusent des vidéos et fournissent des technologies financières en temps réel. Si votre application nécessite un flux continu de données sensibles au facteur temps, Kafka répondra à vos besoins et plus encore.

Comme pour toute technologie complexe, il y a une courbe d'apprentissage. Cette technologie nécessite une connaissance générale de la construction de conteneurs et de la configuration de Kafka. De plus, il est essentiel d'apprendre les spécificités du langage de programmation car vous pouvez utiliser Kafka avec une variété de langages.

Cet article se concentre sur la création d'un client Java capable de produire et de consommer des données vers et depuis un flux OpenShift Kafka.

Cet article est le troisième d'une série qui se penche sur les utilisations de Kafka.  Un guide du développeur pour l'utilisation de Kafka avec Java, partie 1,  couvrait les concepts de base de Kafka tels que l'installation de Kafka et l'utilisation du client en ligne de commande. Le deuxième article,  Comment créer des consommateurs et des producteurs Kafka en Java , décrit comment créer du code Java qui interagit directement avec un courtier Kafka hébergé sur votre machine locale.

Allons maintenant un peu plus loin dans la programmation de Kafka au niveau de l'entreprise. Nous adapterons le code créé dans le deuxième article pour qu'il fonctionne avec Red Hat OpenShift Streams pour Apache Kafka. Cette technologie permet aux entreprises de travailler de manière productive avec Kafka en évitant le travail fastidieux et détaillé nécessaire à la prise en charge d'un courtier Kafka fonctionnant à l'échelle du Web.

Tout d'abord, je vais donner un aperçu des flux OpenShift pour Apache Kafka. Ensuite, je décrirai les mécanismes de configuration d'un flux et démontrerai le code Java/Maven qui se lie à une instance Kafka. Ce code Java inclut des tests qui produisent et consomment des messages vers et depuis un flux.

Vous pouvez télécharger ce code depuis  le référentiel GitHub sur votre ordinateur local.

Comment OpenShift Streams pour Apache Kafka simplifie le processus Kafka

OpenShift Streams pour Apache Kafka est un service fourni par Red Hat qui permet aux développeurs de créer des flux Kafka entièrement fonctionnels sans avoir à configurer les serveurs Kafka et à les lier à des URL nommées. Au lieu de cela, un développeur peut se connecter à la console OpenShift, créer une instance de Kafka et un sujet Kafka qui produit et consomme des messages.

Le développeur crée un compte de service qui se lie à un sujet Kafka particulier. Ensuite, le développeur configure l'instance Kafka pour accorder des autorisations d'accès au compte de service en fonction d'un sujet particulier ou de plusieurs sujets. Les développeurs peuvent accorder au compte de service l'autorisation de lire à partir d'un sujet en tant que consommateur ou d'écrire dans un sujet en tant que producteur. Ces étapes requises pour utiliser OpenShift Streams pour Apache Kafka sont résumées dans la figure 1.

Un schéma de quatre étapes pour accéder à une instance Kafka.

Figure 1 : Le workflow pour accéder à une instance Kafka se compose de quatre étapes.

 

Dans les coulisses, OpenShift Streams pour Apache Kafka sert d'intermédiaire pour un fournisseur de cloud natif ou un centre de données sur site. OpenShift Streams pour Apache Kafka fournit la configuration et l'accès aux serveurs et au stockage hébergé en externe. Le fournisseur de cloud, tel qu'AWS, stocke les messages Kafka et les serveurs Kafka gèrent ces messages. (Voir Figure 2.)

Une illustration de la façon dont les instances de Kafka réduisent le travail.

Figure 2 : L'utilisation d'OpenShift Streams pour Apache Kafka réduit la main-d'œuvre avec les instances Kafka.

 

Les développeurs accèdent au serveur Kafka via une URL publiée par OpenShift Streams pour Apache Kafka. Sur le backend, OpenShift Streams pour Apache Kafka fonctionne avec le fournisseur de cloud natif pour enregistrer, stocker et récupérer les données de flux. En fin de compte, OpenShift Streams pour Apache Kafka réduit le travail complexe et banal de gestion de Kafka, permettant aux développeurs de se concentrer sur la réalisation de leurs objectifs de programmation.

Gérer un courtier Kafka

Vous pouvez obtenir un flux opérationnel via la console Web OpenShift ou l' interface de ligne de commande (CLI) OpenShift oc . Pour les besoins de cet article, nous travaillerons avec la console Web OpenShift.

La meilleure façon d'apprendre les bases de la création d'un flux OpenShift est de suivre la leçon de démarrage rapide : Mise en route avec les flux OpenShift pour Apache Kafka .

L'un des avantages de la leçon de démarrage rapide est qu'elle est interactive. Vous pouvez vous connecter à la console Web OpenShift et créer un serveur Kafka sous OpenShift Streams pour Apache Kafka. La console Web fournit des instructions étape par étape, comme illustré à la figure 3.

Une capture d'écran de la console OpenShift Quickstart.

Figure 3 : Le démarrage rapide OpenShift vous permet de travailler directement dans la console OpenShift.

 

Pour exécuter l'application de démonstration, procédez comme suit à partir des instructions de démarrage rapide :

  • Créez l'instance Kafka.
  • Créez le compte de service.
  • Créez le sujet.
  • Configurez l'accès à la rubrique en fonction de la rubrique de service.

Créez quatre éléments d'information requis au fur et à mesure que vous parcourez le démarrage rapide. Ces éléments clés sont :

  • Le nom du sujet Kafka que vous avez créé dans OpenShift.
  • L'URL du serveur d'amorçage Kafka.
  • L'ID client associé au compte de service que vous lierez à l'instance Kafka que vous avez créée dans OpenShift.
  • Le secret client associé au compte de service que vous lierez à l'instance Kafka que vous avez créée dans OpenShift.

Les instructions de démarrage rapide expliquent également quand créer chaque élément. Copiez les champs dans un éditeur de texte afin de pouvoir les coller .envultérieurement dans un fichier de variable d'environnement. Le .envfichier (détaillé dans la section suivante) émule les variables d'environnement et leurs valeurs.

Une fois le démarrage rapide terminé, vous disposerez d'une instance entièrement fonctionnelle de Kafka exécutée sous OpenShift Streams pour Apache Kafka. L'instance de démarrage rapide s'exécute sans frais pour vous. L'instance fonctionne pendant 48 heures jusqu'à ce qu'elle soit détruite. Cependant, si vous souhaitez expérimenter davantage, vous pouvez créer un autre flux Kafka pour le remplacer.

Produire et consommer des messages sous OpenShift en 6 étapes

Comme mentionné précédemment, le projet de démonstration stocké sur GitHub est une application Java qui utilise le client Apache Kafka pour produire et consommer des messages de Kafka.

Le client Apache Kafka est agnostique, ce qui signifie qu'il peut fonctionner avec une instance locale de Kafka ainsi qu'avec une exécution dans le cloud. Dans la démonstration, le client Kafka se lie à l'instance de Kafka que vous avez créée dans la leçon de démarrage rapide.

1. Cloner le code de GitHub

Téléchargez le code depuis GitHub en exécutant la commande suivante :

$ git clone https://github.com/redhat-developer-demos/kafka-openshift-streams-demo.git

Pour exécuter le code localement, créez des variables d'environnement pour obtenir un accès sécurisé à votre instance Kafka. Utilisez le package Maven java-dotenv pour créer ces variables d'environnement. Le package java-dotenv vous permet de créer des variables d'environnement à partir des paramètres d'un fichier texte.

2. Créez des variables d'environnement avec java-dotenv

L'application doit fournir les spécificités de votre instance Kafka, y compris un nom d'utilisateur et un mot de passe, pour y accéder. Mettre les identifiants nom d'utilisateur/mot de passe directement dans le code ou même dans un fichier de configuration est une mauvaise affaire. Il suffirait d'un seul piratage néfaste du système de gestion du contrôle des sources pour qu'un attaquant ait accès à ces informations d'identification et à ce code. Une meilleure façon de fournir ces informations de connexion consiste à injecter des informations d'identification de sécurité dans les variables d'environnement au moment de l'exécution, puis le code vérifie la disponibilité de la variable d'environnement requise. Cette méthode ne stocke pas les données d'accès dans le code, éliminant ainsi ce type de vulnérabilité.

Cependant, le provisionnement continu des variables d'environnement est fastidieux. C'est là que java-dotenv entre en jeu.

Lorsque vous utilisez java-dotenv, vous placez les informations d'identification du nom d'utilisateur et du mot de passe dans un fichier .env sous la forme d'une paire clé-valeur. Ensuite, java-dotenv prend ces informations d'identification et les injecte dans la mémoire en tant que variables d'environnement. Si les informations d'identification sont déjà stockées dans des variables d'environnement en mémoire, java-dotenv ignore les données du fichier .env et signale uniquement les valeurs des variables d'environnement en mémoire.

Soyez avisé. Bien que java-dotenv facilite les choses, il existe un risque. Ne validez jamais le fichier .env local dans le contrôle de code source. Vous pouvez éviter cette erreur en ajoutant la ligne suivante au fichier .gitignore du code source   comme suit :

# add this line the .gitignore to prevent an accidental commit

.env

3. Installez java-dotenv

Installez java-dotenv en ajoutant la <dependency>configuration suivante à la <dependencies>section du fichier du projet Maven pom.xml(l'entrée a déjà été ajoutée au fichier du projet de démonstration pom.xml) :

<dependency>
     <groupId>io.github.cdimascio</groupId>
     <artifactId>java-dotenv</artifactId>
     <version>5.2.2</version>
 </dependency>

4. Configurez le fichier .env

Configurez le .envfichier pour déclarer les variables d'environnement à injecter dans la mémoire du projet de démonstration.

REMARQUE : le projet de démonstration n'est pas livré avec le .envfichier. Vous devez en créer un et placer le fichier à la racine du répertoire de travail du projet de démonstration.

La structure du .envfichier est la suivante :

DOTENV_TEST_MESSAGE=PING
KAFKA_STREAM_TEST_TOPIC=<TOPIC_NAME_ON_KAFKA_STREAMS>
KAFKA_STREAM_USER_NAME=<SERVICE_ACCOUNT_CLIENT_ID>
KAFKA_STREAM_PWD=<SERVICE_ACCOUNT_CLIENT_SECRET>
KAFKA_STREAM_BOOTSTRAP_SERVER=<KAFKA_STREAMS_BOOTSTRAP_SERVER_URL>

Le texte entre crochets doit être complété comme suit :

  • <TOPIC_NAME_ON_KAFKA_STREAMS>est le nom du sujet qui produira et consommera des messages. Comme expliqué précédemment, vous créez la rubrique dans OpenShift Streams pour Apache Kafka.
  • <SERVICE_ACCOUNT_CLIENT_ID>est l'ID client créé lorsque vous avez ouvert le compte de service OpenShift lié au flux.
  • <SERVICE_ACCOUNT_CLIENT_SECRET>est le secret client que vous avez créé.
  • <KAFKA_STREAMS_BOOTSTRAP_SERVER_URL>est l'URL du serveur d'amorçage Kafka, produit lorsque vous avez créé l'instance d'OpenShift Streams pour Apache Kafka.

Voici un exemple de .envfichier valide :

DOTENV_TEST_MESSAGE=PING
KAFKA_STREAM_TEST_TOPIC=mytopic
KAFKA_STREAM_USER_NAME=srvc-acct-7a36b530-e8fe-471c-bb3d-aa005020d911
KAFKA_STREAM_PWD=6823f7bd-649b-4029-8a1c-8713a828918b
KAFKA_STREAM_BOOTSTRAP_SERVER=my-first-k-c-skrukt--m--nhc-fna.bf2.kafka.rhcloud.com:443

5. Configurer une connexion sécurisée à OpenShift Streams pour Apache Kafka

La connexion du client Java à OpenShift Streams pour Apache Kafka nécessite des paramètres de configuration spéciaux pour la création d'une connexion sécurisée. Ces paramètres de configuration, qui utilisent la norme SASL (Simple Authentication and Security Layer), sont les suivants (chaque paramètre est lié à une page Web qui fournit des détails) :

L'application de démonstration attribue des valeurs aux paramètres comme suit :

  • security.protocol="SASL_SSL"
  • sasl.mechanism="PLAIN"
  • sasl.jaas.config='org.apache.kafka.common.security.plain.PlainLoginModule required username="<USERNAME>" password="<PASSWORD>"'

Essentiellement, les paramètres signifient que la connexion utilisera le protocole de sécurité SASL_SSL et que le mécanisme de sécurité SASL sera PLAIN . Pour les besoins de cette démonstration, le PLAINmécanisme de sécurité simplifie la configuration en vous permettant de soumettre le nom d'utilisateur et le mot de passe sous forme de texte brut. Cependant, la chaîne de connexion est soumise de manière confidentielle par un chiffrement de niveau inférieur spécifié dans RFC4616 .

Étant donné que l'application de démonstration de cet article est dédiée à la connexion à une instance de Kafka exécutée à distance sous OpenShift Streams pour Apache Kafka, les paramètres de configuration requis sont placés directement dans le code de la addSecurityPropertiesméthode. La méthode se trouve dans le src/main/java/com/demo/kafka/PropertiesHelper.javafichier du code source de l'application de démonstration.

Jetez un œil au code pour voir comment implémenter la configuration. Notez que les valeurs du nom d'utilisateur et du mot de passe sont stockées respectivement dans les variables d'environnement KAFKA_STREAM_USER_NAMEet . KAFKA_STREAM_PWDAinsi, les informations de connexion sont protégées des intrusions dans le système de fichiers du serveur :

static Properties addSecurityProperties(Properties props) throws
                                PropertiesConfigurationException {
  // use the private method that ensures the required environment variables are present
  testForSecurityProperties();

  // create an instance of java-dotenv to retrieve the environment variables
  Dotenv env = getDotEnv();

  //override the local Kafka instance settings with the Kafka Stream instance
  props.put("bootstrap.servers", env.get("KAFKA_STREAM_BOOTSTRAP_SERVER"));

  // add the security settings
  props.put("security.protocol", "SASL_SSL");
  props.put("sasl.mechanism", "PLAIN");
  // create the connection string, getting username and pwd from env vars
  props.put("sasl.jaas.config", PlainLoginModule.class.getName() + " required username=\"" + env.get("KAFKA_STREAM_USER_NAME") + "\" password=\"" + env.get("KAFKA_STREAM_PWD") + "\";");

  // return the amended properties collection
  return props;
}

Maintenant que nous avons configuré le client Java pour utiliser l'instance Kafka exécutée sur OpenShift Streams pour Apache Kafka, vous pouvez exécuter les tests unitaires qui produisent et consomment des messages vers et depuis OpenShift.

6. Exécutez les tests

Pour exécuter les tests unitaires qui produisent et consomment des messages, exécutez la commande à partir du niveau supérieur du répertoire dans lequel vous avez installé le code de démonstration. Mais avant cela, assurez-vous d'avoir correctement configuré le fichier .env avec les valeurs du nom d'utilisateur, du mot de passe, de l'URL du serveur d'amorçage et de la rubrique spécifique à votre instance Kafka et exécutée sous OpenShift Streams pour Apache Kafka.

En supposant que le .envfichier est créé et configuré, exécutez la commande suivante :

$ mvn test

En cas de succès, le résultat doit ressembler à ce qui suit :

Results :

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 19.510 s
[INFO] Finished at: 2022-05-09T19:08:02Z
[INFO] -----------------------------------------------------------------------

Une exécution de test réussie crée de nombreux messages stockés dans l'instance Kafka sur OpenShift Streams pour Apache Kafka. (Rappelez-vous que Kafka conserve tous les messages qu'il reçoit.)

Vous pouvez vérifier que les messages sont bien stockés sur OpenShift Streams pour Apache Kafka en accédant à votre instance de Kafka exécutée dans OpenShift. Sélectionnez l'instance Kafka que vous avez créée, puis sélectionnez l'  onglet Messages  sur la page Instance Kafka, comme illustré à la figure 4.

OpenShift Streams pour Apache Kafka affiche les messages stockés sous l'instance Kafka associée.

Figure 4 : OpenShift Streams pour Apache Kafka affiche les messages stockés sous l'instance Kafka associée.

Vous verrez les messages générés par le test sur la page des rubriques de la console Web OpenShift.

Nous avons terminé la démonstration !

OpenShift Streams pour Apache Kafka vous aide à vous concentrer sur le développement d'applications

Dans cet article, vous avez appris à utiliser Red Hat OpenShift Streams pour Apache Kafka afin de créer et d'utiliser une instance entièrement fonctionnelle de Kafka. Après avoir couvert les concepts et les avantages d'OpenShift Streams pour Apache Kafka, vous avez été initié au processus de création d'une instance Kafka et d'un sujet Kafka sur cette instance. Vous avez suivi le démarrage rapide dans OpenShift pour créer une instance Kafka, une rubrique Kafka et un compte de service OpenShift. Vous avez attribué des autorisations d'accès au compte de service pour le sujet que vous avez créé.

De plus, vous avez téléchargé le projet de démonstration à partir de GitHub et configuré un .envfichier dans le répertoire de travail du projet de démonstration. Le .envfichier définit les variables d'environnement qui ont les informations d'identification pour accéder à l'instance Kafka créée sous OpenShift Streams pour Apache Kafka. Vous avez tout réuni en exécutant les tests unitaires fournis avec le code de démonstration pour produire et consommer des messages vers et depuis OpenShift Streams pour Apache Kafka.

Espérons que les concepts et les techniques que vous avez appris dans cet article vous inciteront à étudier davantage Kafka et à l'essayer sur OpenShift Streams pour Apache Kafka. Il s'agit d'une technologie puissante et efficace qui facilitera le travail avec Kafka à mesure que vous avancerez dans votre parcours pour créer des programmes axés sur les messages à l'aide de Java et d'OpenShift.

Lien : https://developers.redhat.com/articles/2022/08/04/quick-java-application-setup-using-kafka-messaging#openshift_streams_for_apache_kafka_helps_you_focus_on_application_development

#java #kafka 

Configuration Rapide De L'application Java à L'aide De Kafka Messaging

Karafka: Ruby and Rails Efficient Kafka Processing Framework

Note: All of the documentation here refers to Karafka 2.0.0.rc5 or higher. If you are looking for the documentation for Karafka 1.4, please click here.

About Karafka

Karafka is a Ruby and Rails multi-threaded efficient Kafka processing framework that:

  • Supports parallel processing in multiple threads (also for a single topic partition work)
  • Has ActiveJob backend support (including ordered jobs)
  • Automatically integrates with Ruby on Rails
  • Supports in-development code reloading
  • Is powered by librdkafka (the Apache Kafka C/C++ client library)
  • Has an out-of the box StatsD/DataDog monitoring with a dashboard template.
# Define what topics you want to consume with which consumers in karafka.rb
Karafka::App.routes.draw do
  topic 'system_events' do
    consumer EventsConsumer
  end
end

# And create your consumers, within which your messages will be processed
class EventsConsumer < ApplicationConsumer
  # Example that utilizes ActiveRecord#insert_all and Karafka batch processing
  def consume
    # Store all of the incoming Kafka events locally in an efficient way
    Event.insert_all messages.payloads
  end
end

Karafka uses threads to handle many messages simultaneously in the same process. It does not require Rails but will integrate tightly with any Ruby on Rails applications to make event processing dead simple.

Getting started

If you're entirely new to the subject, you can start with our "Kafka on Rails" articles series, which will get you up and running with the terminology and basic ideas behind using Kafka:

If you want to get started with Kafka and Karafka as fast as possible, then the best idea is to visit our Getting started guides and the example apps repository.

We also maintain many integration specs illustrating various use-cases and features of the framework.

TL;DR (1 minute from setup to publishing and consuming messages)

Prerequisites: Kafka running. You can start it by following instructions from here.

  1. Add and install Karafka:
bundle add karafka -v 2.0.0.rc5

bundle exec karafka install
  1. Dispatch a message to the example topic using the Rails or Ruby console:
Karafka.producer.produce_sync(topic: 'example', payload: { 'ping' => 'pong' }.to_json)
  1. Run Karafka server and see the consumption magic happen:
bundle exec karafka server

[7616dc24-505a-417f-b87b-6bf8fc2d98c5] Polled 1 message in 1000ms
[dcf3a8d8-0bd9-433a-8f63-b70a0cdb0732] Consume job for ExampleConsumer on example started
{"ping"=>"pong"}
[dcf3a8d8-0bd9-433a-8f63-b70a0cdb0732] Consume job for ExampleConsumer on example finished in 0ms

Want to Upgrade? LGPL is not for you? Want to help?

I also sell Karafka Pro subscriptions. It includes a commercial-friendly license, priority support, architecture consultations, and high throughput data processing-related features (virtual partitions, long-running jobs, and more).

20% of the income will be distributed back to other OSS projects that Karafka uses under the hood.

Help me provide high-quality open-source software. Please see the Karafka homepage for more details.

Support

Karafka has Wiki pages for almost everything and a pretty decent FAQ. It covers the installation, setup, and deployment, along with other useful details on how to run Karafka.

If you have questions about using Karafka, feel free to join our Slack channel.

Karafka has priority support for technical and architectural questions that is part of the Karafka Pro subscription.


Author: karafka
Source code: https://github.com/karafka/karafka
License:  Unknown and 2 other licenses found

#ruby #kafka #ruby-on-rails 

Karafka: Ruby and Rails Efficient Kafka Processing Framework
高橋  陽子

高橋 陽子

1659774807

如何使用 Kafka Messaging 設置快速 Java 應用程序

Apache Kafka 是一個開源項目,可實時支持工業級數據流。Kafka 每秒可以處理超過 10 萬條消息。一些公司報告說每秒處理數百萬條消息的能力。

Kafka 非常適合用於協調拼車活動、流式傳輸視頻和提供實時金融科技的應用程序。如果您的應用程序需要持續的時間敏感數據流,那麼 Kafka 將滿足您的需求,然後再滿足一些需求。

與任何復雜的技術一樣,都有一個學習曲線。這項技術需要有關構建容器和配置 Kafka 的一般知識。此外,學習編程語言細節也很重要,因為您可以將 Kafka 與多種語言一起使用。

本文的重點是構建一個 Java 客戶端,該客戶端可以在 OpenShift Kafka 流中生成和使用數據。

本文是深入研究 Kafka 使用的系列文章的第三篇。  將 Kafka 與 Java 結合使用的開發人員指南,第 1 部分 涵蓋了基本的 Kafka 概念,例如安裝 Kafka 和使用命令行客戶端。第二篇文章, 如何在 Java 中創建 Kafka 消費者和生產者,描述瞭如何創建與本地機器上託管的 Kafka 代理直接交互的 Java 代碼。

現在讓我們更深入地研究企業級的 Kafka 編程。我們將調整第二篇文章中創建的代碼,以使用 Red Hat OpenShift Streams for Apache Kafka。這項技術使企業能夠通過避免支持以 Web 規模運行的 Kafka 代理的繁瑣、詳細的工作來高效地使用 Kafka。

首先,我將概述 OpenShift Streams for Apache Kafka。然後我將描述設置流的機制並演示綁定到 Kafka 實例的 Java/Maven 代碼。此 Java 代碼包括在流中生成和使用消息的測試。

您可以從GitHub 存儲庫將此代碼下載 到本地計算機。

OpenShift Streams for Apache Kafka 如何簡化 Kafka 流程

OpenShift Streams for Apache Kafka 是 Red Hat 提供的一項服務,它允許開發人員創建功能齊全的 Kafka 流,而無需配置 Kafka 服務器並將它們綁定到命名 URL。相反,開發人員可以登錄到 OpenShift 控制台,創建一個 Kafka 實例,以及一個產生和消費消息的 Kafka 主題。

開發人員創建一個綁定到特定 Kafka 主題的服務帳戶。然後開發者配置Kafka實例,根據特定主題或多個主題授予服務賬號訪問權限。開發人員可以授予服務帳戶以消費者身份讀取主題或以生產者身份寫入主題的權限。圖 1 總結了使用 OpenShift Streams for Apache Kafka 所需的這些步驟。

獲得對 Kafka 實例的訪問權限的四個步驟的圖表。

圖 1:獲取 Kafka 實例訪問權限的工作流程包括四個步驟。

 

在幕後,OpenShift Streams for Apache Kafka 充當原生雲提供商或本地數據中心的中間人。OpenShift Streams for Apache Kafka 提供對服務器和外部託管存儲的配置和訪問。雲提供商(例如 AWS)存儲 Kafka 消息,而 Kafka 服務器管理這些消息。(參見圖 2。)

Kafka 實例如何減少勞動力的說明。

圖 2:使用 OpenShift Streams for Apache Kafka 減少了 Kafka 實例的勞動力。

 

開發人員可以通過 OpenShift Streams 為 Apache Kafka 發布的 URL 訪問 Kafka 服務器。在後端,OpenShift Streams for Apache Kafka 與本地雲提供商合作,以保存、存儲和檢索流數據。最終,用於 Apache Kafka 的 OpenShift Streams 減少了管理 Kafka 的複雜而平凡的工作,讓開發人員能夠專注於實現他們的編程目標。

運行 Kafka 代理

您可以通過 OpenShift Web 控制台或OpenShift oc命令行界面 (CLI) 啟動並運行流。就本文而言,我們將使用 OpenShift Web 控制台。

學習創建 OpenShift 流的基礎知識的最佳方法是參加快速入門課程 OpenShift Streams for Apache Kafka 入門。

快速入門課程的優點之一是它是交互式的。您可以登錄到 OpenShift Web 控制台並在 OpenShift Streams for Apache Kafka 下創建一個 Kafka 服務器。Web 控制台提供分步說明,如圖 3 所示。

OpenShift 快速入門控制台的屏幕截圖。

圖 3:OpenShift 快速入門允許您直接在 OpenShift 控制台中工作。

 

要運行演示應用程序,請按照快速入門說明完成以下步驟:

  • 創建 Kafka 實例。
  • 創建服務帳戶。
  • 創建主題。
  • 根據服務主題配置對主題的訪問。

在完成快速入門時創建四個必需的信息。這些關鍵項目是:

  • 您在 OpenShift 中創建的 Kafka 主題名稱。
  • Kafka 引導服務器的 URL。
  • 與您將綁定到您在 OpenShift 中創建的 Kafka 實例的服務帳戶關聯的客戶端 ID。
  • 與您將綁定到您在 OpenShift 中創建的 Kafka 實例的服務帳戶關聯的客戶端密碼。

快速入門說明還解釋了何時創建每個項目。將字段複製到文本編輯器中,以便稍後將它們粘貼到環境變量.env文件中。該.env文件(在下一節中詳細介紹)模擬環境變量及其值。

完成快速入門後,您將擁有一個在 OpenShift Streams for Apache Kafka 下運行的功能齊全的 Kafka 實例。快速入門實例免費運行。實例運行 48 小時,直到銷毀。然而,如果你想做更多的實驗,你可以啟動另一個 Kafka 流來代替它。

在 OpenShift 下生產和消費消息的 6 個步驟

如前所述,存儲在 GitHub 上的演示項目是一個 Java 應用程序,它使用Apache Kafka 客戶端來生成和使用來自 Kafka 的消息。

Apache Kafka 客戶端是不可知的,這意味著它可以與 Kafka 的本地實例一起工作,也可以在雲中運行。在演示中,Kafka 客戶端綁定到您在快速入門課程中創建的 Kafka 實例。

1. 從 GitHub 克隆代碼

通過運行以下命令從 GitHub 下載代碼:

$ git clone https://github.com/redhat-developer-demos/kafka-openshift-streams-demo.git

要在本地運行代碼,請創建環境變量以獲得對 Kafka 實例的安全訪問。使用 Maven java-dotenv 包來創建這些環境變量。java-dotenv包使您能夠從文本文件中的設置創建環境變量。

2.用java-dotenv創建環境變量

該應用程序需要提供您的 Kafka 實例的詳細信息,包括用戶名和密碼,才能獲得訪問權限。將用戶名/密碼憑據直接放入代碼甚至配置文件中是一件壞事。攻擊者只需對源代碼控制管理系統進行一次惡意黑客攻擊,即可獲得對這些憑據和代碼的訪問權限。提供此登錄信息的更好方法是在運行時將安全憑證注入環境變量,然後代碼驗證所需環境變量的可用性。該方法不在代碼中存儲訪問數據,從而消除了此類漏洞。

然而,不斷地提供環境變量是乏味的。這就是 java-dotenv 發揮作用的地方。

使用 java-dotenv 時,您將用戶名和密碼憑據作為鍵值對放在 .env 文件中。然後 java-dotenv 獲取這些憑據並將它們作為環境變量注入內存。如果憑據已經存儲在內存中的環境變量中,java-dotenv 將忽略 .env 文件中的數據並僅報告內存中環境變量的值。

被告知。儘管 java-dotenv 使事情變得更容易,但也存在風險。永遠不要將本地 .env 文件提交到源代碼管理中。您可以通過將以下行添加到源代碼的 .gitignore 文件中來防止此錯誤, 如下所示:

# add this line the .gitignore to prevent an accidental commit

.env

3.安裝java-dotenv

通過將以下<dependency>配置添加到<dependencies>Maven 項目pom.xml文件的部分來安裝 java-dotenv(該條目已添加到演示項目的pom.xml文件中):

<dependency>
     <groupId>io.github.cdimascio</groupId>
     <artifactId>java-dotenv</artifactId>
     <version>5.2.2</version>
 </dependency>

4.配置.env文件

配置.env文件以聲明環境變量以注入到演示項目的內存中。

注意:演示項目不附帶該.env文件。您必須創建一個並將文件放在演示項目工作目錄的根目錄中。

.env文件的結構如下:

DOTENV_TEST_MESSAGE=PING
KAFKA_STREAM_TEST_TOPIC=<TOPIC_NAME_ON_KAFKA_STREAMS>
KAFKA_STREAM_USER_NAME=<SERVICE_ACCOUNT_CLIENT_ID>
KAFKA_STREAM_PWD=<SERVICE_ACCOUNT_CLIENT_SECRET>
KAFKA_STREAM_BOOTSTRAP_SERVER=<KAFKA_STREAMS_BOOTSTRAP_SERVER_URL>

尖括號內的文字應填寫如下:

  • <TOPIC_NAME_ON_KAFKA_STREAMS>是將產生和使用消息的主題的名稱。如前所述,您在 OpenShift Streams for Apache Kafka 中創建主題。
  • <SERVICE_ACCOUNT_CLIENT_ID>是您打開綁定到流的 OpenShift 服務帳戶時創建的客戶端 ID。
  • <SERVICE_ACCOUNT_CLIENT_SECRET>是您創建的客戶端密碼。
  • <KAFKA_STREAMS_BOOTSTRAP_SERVER_URL>是 Kafka 引導服務器的 URL,在您為 Apache Kafka 創建 OpenShift Streams 實例時生成。

這是一個有效.env文件的示例:

DOTENV_TEST_MESSAGE=PING
KAFKA_STREAM_TEST_TOPIC=mytopic
KAFKA_STREAM_USER_NAME=srvc-acct-7a36b530-e8fe-471c-bb3d-aa005020d911
KAFKA_STREAM_PWD=6823f7bd-649b-4029-8a1c-8713a828918b
KAFKA_STREAM_BOOTSTRAP_SERVER=my-first-k-c-skrukt--m--nhc-fna.bf2.kafka.rhcloud.com:443

5. 配置與 OpenShift Streams for Apache Kafka 的安全連接

讓 Java 客戶端連接到 OpenShift Streams for Apache Kafka 需要特殊的配置設置來創建安全連接。這些使用簡單身份驗證和安全層 (SASL) 標準的配置設置如下(每個設置都鏈接到提供詳細信息的網頁):

演示應用程序為設置分配值,如下所示:

  • security.protocol="SASL_SSL"
  • sasl.mechanism="PLAIN"
  • sasl.jaas.config='org.apache.kafka.common.security.plain.PlainLoginModule required username="<USERNAME>" password="<PASSWORD>"'

本質上,這些設置意味著連接將使用 SASL_SSL 安全協議,而 SASL 安全機制將是PLAIN。出於本演示的目的,PLAIN安全機制通過讓您以純文本形式提交用戶名和密碼憑據來簡化配置。但是,連接字符串是通過RFC4616中指定的低級加密秘密提交的。

由於本文的演示應用程序專門連接到在 OpenShift Streams for Apache Kafka 下遠程運行的 Kafka 實例,因此所需的配置設置直接放入addSecurityProperties方法中的代碼中。該方法位於src/main/java/com/demo/kafka/PropertiesHelper.java演示應用程序的源代碼文件中。

看一下代碼,看看如何實現配置。請注意,用戶名和密碼的值分別存儲在KAFKA_STREAM_USER_NAMEKAFKA_STREAM_PWD環境變量中。因此,連接信息受到保護,不會侵入服務器的文件系統:

static Properties addSecurityProperties(Properties props) throws
                                PropertiesConfigurationException {
  // use the private method that ensures the required environment variables are present
  testForSecurityProperties();

  // create an instance of java-dotenv to retrieve the environment variables
  Dotenv env = getDotEnv();

  //override the local Kafka instance settings with the Kafka Stream instance
  props.put("bootstrap.servers", env.get("KAFKA_STREAM_BOOTSTRAP_SERVER"));

  // add the security settings
  props.put("security.protocol", "SASL_SSL");
  props.put("sasl.mechanism", "PLAIN");
  // create the connection string, getting username and pwd from env vars
  props.put("sasl.jaas.config", PlainLoginModule.class.getName() + " required username=\"" + env.get("KAFKA_STREAM_USER_NAME") + "\" password=\"" + env.get("KAFKA_STREAM_PWD") + "\";");

  // return the amended properties collection
  return props;
}

現在我們已將 Java 客戶端配置為使用在 OpenShift Streams for Apache Kafka 上運行的 Kafka 實例,您可以運行單元測試來生成和使用來自 OpenShift 的消息。

6. 運行測試

要運行生成和使用消息的單元測試,請從安裝演示代碼的目錄的頂層運行命令。但在此之前,請確保您已使用用戶名、密碼、引導服務器 URL 和特定於 Kafka 實例並在 OpenShift Streams for Apache Kafka 下運行的主題的值正確配置了 .env 文件。

假設.env文件已創建並配置,請運行以下命令:

$ mvn test

成功後,輸出應類似於以下內容:

Results :

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 19.510 s
[INFO] Finished at: 2022-05-09T19:08:02Z
[INFO] -----------------------------------------------------------------------

成功的測試運行會創建大量消息,這些消息存儲在 OpenShift Streams for Apache Kafka 上的 Kafka 實例中。(請記住,Kafka 會保留它收到的所有消息。)

您可以通過轉到在 OpenShift 中運行的 Kafka 實例來驗證消息是否確實存儲在 OpenShift Streams for Apache Kafka 上。選擇您創建的 Kafka 實例,然後選擇 Kafka Instance 頁面上的 Messages 選項卡,如圖 4 所示。

OpenShift Streams for Apache Kafka 顯示存儲在關聯 Kafka 實例下的消息。

圖 4:OpenShift Streams for Apache Kafka 顯示存儲在關聯 Kafka 實例下的消息。

您將在 OpenShift Web 控制台的主題頁面上看到測試產生的消息。

我們已經完成了演示!

OpenShift Streams for Apache Kafka 可幫助您專注於應用程序開發

在本文中,您了解瞭如何使用 Red Hat OpenShift Streams for Apache Kafka 創建和使用功能齊全的 Kafka 實例。在我們介紹了 OpenShift Streams for Apache Kafka 的概念和優勢之後,我們向您介紹了在該實例上創建 Kafka 實例和 Kafka 主題的過程。您通過 OpenShift 中的快速入門創建了一個 Kafka 實例、一個 Kafka 主題和一個 OpenShift 服務帳戶。您為創建的主題分配了對服務帳戶的訪問權限。

此外,您從 GitHub 下載了演示項目,並.env在演示項目的工作目錄中配置了一個文件。該.env文件定義了具有訪問在 OpenShift Streams for Apache Kafka 下創建的 Kafka 實例的憑據的環境變量。您通過運行與演示代碼一起提供的單元測試來生成和使用來自 OpenShift Streams for Apache Kafka 的消息,從而將所有這些結合在一起。

希望您在本文中學到的概念和技術能夠激發您進一步研究 Kafka 並在 OpenShift Streams for Apache Kafka 上進行嘗試。這是一項強大而高效的技術,當您在使用 Java 和 OpenShift 創建消息驅動程序的過程中前進時,它將使您更輕鬆地使用 Kafka。

鏈接:https ://developers.redhat.com/articles/2022/08/04/quick-java-application-setup-using-kafka-messaging#openshift_streams_for_apache_kafka_helps_you_focus_on_application_development

#java #kafka 

如何使用 Kafka Messaging 設置快速 Java 應用程序

Как настроить быстрое Java-приложение с помощью Kafka Messaging

Apache Kafka — это проект с открытым исходным кодом, поддерживающий потоковую передачу данных промышленного уровня в режиме реального времени. Kafka может обрабатывать более ста тысяч сообщений в секунду. Некоторые компании сообщают о способности обрабатывать миллионы сообщений в секунду.

Kafka хорошо подходит для приложений, которые координируют действия по совместному использованию, транслируют видео и предоставляют финтех в реальном времени. Если вашему приложению требуется непрерывный поток срочных данных, Kafka удовлетворит ваши потребности, а затем и некоторые другие.

Как и в случае с любой сложной технологией, существует кривая обучения. Эта технология требует общих знаний о создании контейнеров и настройке Kafka. Кроме того, важно изучить специфику языка программирования, потому что вы можете использовать Kafka с различными языками.

В этой статье основное внимание уделяется созданию клиента Java, который может создавать и потреблять данные в поток OpenShift Kafka и из него.

Эта статья является третьей в серии, посвященной использованию Кафки.  Руководство разработчика по использованию Kafka с Java, часть 1,  охватывает основные концепции Kafka, такие как установка Kafka и использование клиента командной строки. Во второй статье «  Как создать потребителей и производителей Kafka в Java » описывается, как создать код Java, который напрямую взаимодействует с брокером Kafka, размещенным на вашем локальном компьютере.

Теперь давайте немного углубимся в программирование Kafka на уровне предприятия. Мы адаптируем код, созданный во второй статье, для работы с Red Hat OpenShift Streams для Apache Kafka. Эта технология позволяет предприятиям продуктивно работать с Kafka, избегая утомительной и детальной работы, связанной с поддержкой брокера Kafka, работающего в веб-масштабе.

Сначала я представлю обзор OpenShift Streams для Apache Kafka. Затем я опишу механику настройки потока и продемонстрирую код Java/Maven, который привязывается к экземпляру Kafka. Этот код Java включает тесты, которые создают и потребляют сообщения в поток и из потока.

Вы можете загрузить этот код из  репозитория GitHub на свой локальный компьютер.

Как OpenShift Streams для Apache Kafka упрощает процесс Kafka

OpenShift Streams для Apache Kafka — это служба, предоставляемая Red Hat, которая позволяет разработчикам создавать полнофункциональные потоки Kafka без необходимости конфигурировать серверы Kafka и привязывать их к именованным URL-адресам. Вместо этого разработчик может войти в консоль OpenShift, создать экземпляр Kafka и тему Kafka, которая создает и использует сообщения.

Разработчик создает учетную запись службы, которая привязывается к определенной теме Kafka. Затем разработчик настраивает экземпляр Kafka для предоставления разрешений на доступ к учетной записи службы в соответствии с определенной темой или несколькими темами. Разработчики могут предоставить служебной учетной записи разрешение на чтение темы в качестве потребителя или запись в тему в качестве производителя. Эти шаги, необходимые для использования OpenShift Streams для Apache Kafka, показаны на рисунке 1.

Схема из четырех шагов для получения доступа к экземпляру Kafka.

Рисунок 1. Рабочий процесс получения доступа к экземпляру Kafka состоит из четырех шагов.

 

За кулисами OpenShift Streams для Apache Kafka выступает в качестве посредника между собственным облачным провайдером или локальным центром обработки данных. OpenShift Streams для Apache Kafka обеспечивает настройку и доступ к серверам и внешнему хранилищу. Облачный провайдер, такой как AWS, хранит сообщения Kafka, а серверы Kafka управляют этими сообщениями. (См. рис. 2.)

Иллюстрация того, как экземпляры Kafka сокращают трудозатраты.

Рис. 2. Использование OpenShift Streams для Apache Kafka снижает трудоемкость экземпляров Kafka.

 

Разработчики получают доступ к серверу Kafka через URL-адрес, опубликованный OpenShift Streams для Apache Kafka. На серверной части OpenShift Streams для Apache Kafka работает с собственным облачным провайдером для сохранения, хранения и извлечения потоковых данных. В конечном итоге OpenShift Streams для Apache Kafka сокращает сложную и рутинную работу по управлению Kafka, позволяя разработчикам сосредоточиться на достижении целей программирования.

Запуск брокера Kafka

Вы можете запустить поток через веб-консоль OpenShift или интерфейс командной строки (CLI) OpenShift oc . В этой статье мы будем работать с веб-консолью OpenShift.

Лучший способ изучить основы создания потока OpenShift — пройти урок быстрого старта: Начало работы с потоками OpenShift для Apache Kafka .

Одна из приятных особенностей урока быстрого старта заключается в том, что он интерактивен. Вы можете войти в веб-консоль OpenShift и создать сервер Kafka в OpenShift Streams для Apache Kafka. Веб-консоль предоставляет пошаговые инструкции, как показано на рис. 3.

Скриншот консоли OpenShift Quickstart.

Рисунок 3: Быстрый запуск OpenShift позволяет вам работать непосредственно в консоли OpenShift.

 

Чтобы запустить демонстрационное приложение, выполните следующие действия из кратких инструкций:

  • Создайте экземпляр Кафки.
  • Создайте учетную запись службы.
  • Создайте тему.
  • Настройте доступ к теме в соответствии с темой службы.

Создайте четыре обязательных фрагмента информации по мере прохождения краткого руководства. Вот эти ключевые элементы:

  • Название темы Kafka, которую вы создали в OpenShift.
  • URL-адрес загрузочного сервера Kafka.
  • Идентификатор клиента, связанный с учетной записью службы, которую вы привяжете к экземпляру Kafka, созданному вами в OpenShift.
  • Секрет клиента, связанный с учетной записью службы, которую вы привяжете к экземпляру Kafka, созданному вами в OpenShift.

В кратких инструкциях также объясняется, когда создавать каждый элемент. Скопируйте поля в текстовый редактор, чтобы .envпозже вставить их в файл переменных среды. Файл .env(подробно описанный в следующем разделе) эмулирует переменные среды и их значения.

После выполнения краткого руководства у вас будет полнофункциональный экземпляр Kafka, работающий под управлением OpenShift Streams для Apache Kafka. Экземпляр быстрого запуска запускается бесплатно. Экземпляр работает 48 часов, пока не будет уничтожен. Однако, если вы хотите больше экспериментировать, вы можете запустить другой поток Kafka, который займет его место.

Создание и использование сообщений в OpenShift за 6 шагов

Как упоминалось ранее, демонстрационный проект, хранящийся на GitHub, представляет собой приложение Java, которое использует клиент Apache Kafka для создания и использования сообщений от Kafka.

Клиент Apache Kafka независим, то есть он может работать как с локальным экземпляром Kafka, так и с одним запуском в облаке. В демонстрации клиент Kafka привязывается к экземпляру Kafka, созданному вами на уроке быстрого запуска.

1. Клонируйте код с GitHub

Загрузите код с GitHub, выполнив следующую команду:

$ git clone https://github.com/redhat-developer-demos/kafka-openshift-streams-demo.git

Чтобы запустить код локально, создайте переменные среды, чтобы получить безопасный доступ к вашему экземпляру Kafka. Используйте пакет Maven java-dotenv для создания этих переменных среды. Пакет java-dotenv позволяет создавать переменные среды из настроек в текстовом файле.

2. Создайте переменные среды с помощью java-dotenv

Приложение должно указать особенности вашего экземпляра Kafka, включая имя пользователя и пароль, чтобы получить доступ. Вводить учетные данные имени пользователя/пароля непосредственно в код или даже в файл конфигурации — плохой бизнес. Злоумышленнику потребуется всего один гнусный взлом системы управления исходным кодом, чтобы получить доступ к этим учетным данным и коду. Лучший способ предоставить эту информацию для входа — ввести учетные данные безопасности в переменные среды во время выполнения, а затем код проверит доступность требуемой переменной среды. Этот метод не хранит данные доступа в коде, что устраняет этот тип уязвимости.

Однако непрерывная подготовка переменных среды утомительна. Здесь в игру вступает java-dotenv.

При использовании java-dotenv вы помещаете учетные данные имени пользователя и пароля в файл .env в виде пары ключ-значение. Затем java-dotenv берет эти учетные данные и вводит их в память как переменные среды. Если учетные данные уже сохранены в переменных среды в памяти, java-dotenv игнорирует данные в файле .env и сообщает только значения переменных среды в памяти.

Будьте советуем. Хотя java-dotenv упрощает задачу, существует риск. Никогда не передайте локальный файл .env в систему управления версиями. Вы можете предотвратить эту ошибку, добавив следующую строку в файл исходного кода  .gitignore  следующим образом:

# add this line the .gitignore to prevent an accidental commit

.env

3. Установите java-dotenv

Установите java-dotenv, добавив следующую <dependency>конфигурацию в <dependencies>раздел файла проекта Maven pom.xml(эта запись уже добавлена ​​в pom.xmlфайл демонстрационного проекта):

<dependency>
     <groupId>io.github.cdimascio</groupId>
     <artifactId>java-dotenv</artifactId>
     <version>5.2.2</version>
 </dependency>

4. Настройте файл .env

Настройте .envфайл, чтобы объявить переменные среды для внедрения в память демонстрационного проекта.

ПРИМЕЧАНИЕ. Демонстрационный проект не поставляется с .envфайлом. Вы должны создать его и поместить файл в корень рабочего каталога демонстрационного проекта.

Структура .envфайла следующая:

DOTENV_TEST_MESSAGE=PING
KAFKA_STREAM_TEST_TOPIC=<TOPIC_NAME_ON_KAFKA_STREAMS>
KAFKA_STREAM_USER_NAME=<SERVICE_ACCOUNT_CLIENT_ID>
KAFKA_STREAM_PWD=<SERVICE_ACCOUNT_CLIENT_SECRET>
KAFKA_STREAM_BOOTSTRAP_SERVER=<KAFKA_STREAMS_BOOTSTRAP_SERVER_URL>

Текст в угловых скобках следует заполнить следующим образом:

  • <TOPIC_NAME_ON_KAFKA_STREAMS>это имя темы, которая будет производить и потреблять сообщения. Как объяснялось ранее, вы создаете тему в OpenShift Streams для Apache Kafka.
  • <SERVICE_ACCOUNT_CLIENT_ID>— это идентификатор клиента, созданный при открытии служебной учетной записи OpenShift, привязанной к потоку.
  • <SERVICE_ACCOUNT_CLIENT_SECRET>это секрет клиента, который вы создали.
  • <KAFKA_STREAMS_BOOTSTRAP_SERVER_URL>— это URL-адрес сервера начальной загрузки Kafka, созданный при создании экземпляра OpenShift Streams для Apache Kafka.

Это пример действительного .envфайла:

DOTENV_TEST_MESSAGE=PING
KAFKA_STREAM_TEST_TOPIC=mytopic
KAFKA_STREAM_USER_NAME=srvc-acct-7a36b530-e8fe-471c-bb3d-aa005020d911
KAFKA_STREAM_PWD=6823f7bd-649b-4029-8a1c-8713a828918b
KAFKA_STREAM_BOOTSTRAP_SERVER=my-first-k-c-skrukt--m--nhc-fna.bf2.kafka.rhcloud.com:443

5. Настройте безопасное соединение с OpenShift Streams для Apache Kafka.

Для подключения клиента Java к OpenShift Streams для Apache Kafka требуются специальные параметры конфигурации для создания безопасного соединения. Эти параметры конфигурации, в которых используется стандарт Simple Authentication and Security Layer (SASL), следующие (каждый параметр связан с веб-страницей, содержащей подробные сведения):

Демонстрационное приложение присваивает значения параметрам следующим образом:

  • security.protocol="SASL_SSL"
  • sasl.mechanism="PLAIN"
  • sasl.jaas.config='org.apache.kafka.common.security.plain.PlainLoginModule required username="<USERNAME>" password="<PASSWORD>"'

По сути, настройки означают, что соединение будет использовать протокол безопасности SASL_SSL, а механизм безопасности SASL будет PLAIN . Для целей этой демонстрации PLAINмеханизм безопасности упрощает настройку, позволяя вам отправлять учетные данные имени пользователя и пароля в виде обычного текста. Однако строка подключения передается конфиденциально с помощью низкоуровневого шифрования, указанного в RFC4616 .

Поскольку демонстрационное приложение в этой статье посвящено подключению к экземпляру Kafka, работающему удаленно под OpenShift Streams для Apache Kafka, необходимые параметры конфигурации помещаются непосредственно в код addSecurityPropertiesметода. Метод находится в src/main/java/com/demo/kafka/PropertiesHelper.javaфайле исходного кода демонстрационного приложения.

Взгляните на код, чтобы увидеть, как реализовать конфигурацию. Обратите внимание, что значения для имени пользователя и пароля хранятся в переменных среды и соответственно KAFKA_STREAM_USER_NAME. KAFKA_STREAM_PWDТаким образом, информация о подключении защищена от вторжений в файловую систему сервера:

static Properties addSecurityProperties(Properties props) throws
                                PropertiesConfigurationException {
  // use the private method that ensures the required environment variables are present
  testForSecurityProperties();

  // create an instance of java-dotenv to retrieve the environment variables
  Dotenv env = getDotEnv();

  //override the local Kafka instance settings with the Kafka Stream instance
  props.put("bootstrap.servers", env.get("KAFKA_STREAM_BOOTSTRAP_SERVER"));

  // add the security settings
  props.put("security.protocol", "SASL_SSL");
  props.put("sasl.mechanism", "PLAIN");
  // create the connection string, getting username and pwd from env vars
  props.put("sasl.jaas.config", PlainLoginModule.class.getName() + " required username=\"" + env.get("KAFKA_STREAM_USER_NAME") + "\" password=\"" + env.get("KAFKA_STREAM_PWD") + "\";");

  // return the amended properties collection
  return props;
}

Теперь, когда мы настроили клиент Java для использования экземпляра Kafka, работающего в OpenShift Streams, для Apache Kafka, вы можете запустить модульные тесты, которые создают и потребляют сообщения в OpenShift и из него.

6. Запустите тесты

Чтобы запустить модульные тесты, которые создают и потребляют сообщения, запустите команду из верхнего уровня каталога, в котором вы установили демонстрационный код. Но прежде чем вы это сделаете, убедитесь, что вы правильно настроили файл .env со значениями имени пользователя, пароля, URL-адреса сервера начальной загрузки и темы, специфичной для вашего экземпляра Kafka и работающей в OpenShift Streams для Apache Kafka.

Предполагая, что .envфайл создан и настроен, выполните следующую команду:

$ mvn test

В случае успеха вывод должен выглядеть примерно так:

Results :

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 19.510 s
[INFO] Finished at: 2022-05-09T19:08:02Z
[INFO] -----------------------------------------------------------------------

Успешный тестовый запуск создает множество сообщений, хранящихся в экземпляре Kafka в OpenShift Streams для Apache Kafka. (Помните, Kafka сохраняет все полученные сообщения.)

Вы можете убедиться, что сообщения действительно хранятся в OpenShift Streams для Apache Kafka, перейдя к вашему экземпляру Kafka, работающему в OpenShift. Выберите созданный вами экземпляр Kafka, затем выберите  вкладку Messages  на странице Kafka Instance, как показано на рис. 4.

OpenShift Streams для Apache Kafka отображает сообщения, хранящиеся в соответствующем экземпляре Kafka.

Рис. 4. OpenShift Streams для Apache Kafka отображает сообщения, хранящиеся в соответствующем экземпляре Kafka.

Вы увидите сообщения, созданные тестом, на странице тем в веб-консоли OpenShift.

Мы завершили демонстрацию!

OpenShift Streams для Apache Kafka помогает вам сосредоточиться на разработке приложений

В этой статье вы узнали, как работать с Red Hat OpenShift Streams для Apache Kafka, чтобы создать и использовать полнофункциональный экземпляр Kafka. После того, как мы рассмотрели концепции и преимущества OpenShift Streams для Apache Kafka, вы познакомились с процессом создания экземпляра Kafka и темы Kafka для этого экземпляра. Вы прошли краткое руководство по OpenShift, чтобы создать экземпляр Kafka, тему Kafka и учетную запись службы OpenShift. Вы назначили права доступа служебной учетной записи для созданной вами темы.

Кроме того, вы загрузили демонстрационный проект с GitHub и настроили .envфайл в рабочем каталоге демонстрационного проекта. Файл .envопределяет переменные среды, которые имеют учетные данные для доступа к экземпляру Kafka, созданному в OpenShift Streams для Apache Kafka. Вы собрали все это вместе, запустив модульные тесты, которые поставляются с демонстрационным кодом, для создания и использования сообщений в OpenShift Streams и из них для Apache Kafka.

Надеемся, что концепции и методы, которые вы изучили в этой статье, вдохновят вас на дальнейшее изучение Kafka и опробование OpenShift Streams для Apache Kafka. Это мощная и эффективная технология, которая упростит работу с Kafka по мере того, как вы продвигаетесь вперед в создании программ, управляемых сообщениями, с использованием Java и OpenShift.

Ссылка: https://developers.redhat.com/articles/2022/08/04/quick-java-application-setup-using-kafka-messaging#openshift_streams_for_apache_kafka_helps_you_focus_on_application_development

#java #kafka 

Как настроить быстрое Java-приложение с помощью Kafka Messaging
Hoang  Kim

Hoang Kim

1659760275

Cách Thiết Lập ứng Dụng Java Nhanh Bằng Kafka Messaging

Apache Kafka là một dự án mã nguồn mở hỗ trợ truyền dữ liệu công nghiệp theo thời gian thực. Kafka có thể xử lý hơn một trăm nghìn tin nhắn mỗi giây. Một số công ty báo cáo khả năng xử lý hàng triệu tin nhắn mỗi giây.

Kafka rất phù hợp cho các ứng dụng điều phối hoạt động chia sẻ video, phát trực tuyến video và cung cấp fintech thời gian thực. Nếu ứng dụng của bạn yêu cầu một luồng dữ liệu nhạy cảm về thời gian liên tục, Kafka sẽ đáp ứng nhu cầu của bạn và sau đó là một số.

Đối với bất kỳ công nghệ phức tạp nào cũng vậy, có một đường cong học tập. Công nghệ này yêu cầu kiến ​​thức chung về xây dựng các thùng chứa và cấu hình Kafka. Ngoài ra, điều cần thiết là tìm hiểu các thông tin cụ thể về ngôn ngữ lập trình vì bạn có thể sử dụng Kafka với nhiều ngôn ngữ khác nhau.

Trọng tâm của bài viết này là xây dựng một ứng dụng khách Java có thể sản xuất và tiêu thụ dữ liệu đến và đi từ luồng OpenShift Kafka.

Bài viết này là bài thứ ba trong loạt bài đi sâu vào các công dụng của Kafka.  Hướng dẫn của nhà phát triển để sử dụng Kafka với Java, Phần 1  bao gồm các khái niệm Kafka cơ bản như cài đặt Kafka và sử dụng máy khách dòng lệnh. Bài viết thứ hai,  Cách tạo người tiêu dùng và nhà sản xuất Kafka trong Java , mô tả cách tạo mã Java tương tác trực tiếp với nhà môi giới Kafka được lưu trữ trên máy cục bộ của bạn.

Bây giờ chúng ta hãy đi sâu hơn một chút về lập trình Kafka ở cấp độ doanh nghiệp. Chúng tôi sẽ điều chỉnh mã được tạo trong bài viết thứ hai để làm việc với Red Hat OpenShift Streams cho Apache Kafka. Công nghệ này cho phép các doanh nghiệp làm việc hiệu quả với Kafka bằng cách tránh lao động chi tiết, tẻ nhạt mà chuyển sang hỗ trợ một nhà môi giới Kafka chạy trên quy mô web.

Đầu tiên, tôi sẽ cung cấp tổng quan về OpenShift Streams cho Apache Kafka. Sau đó, tôi sẽ mô tả cơ chế thiết lập một luồng và trình bày mã Java / Maven liên kết với một cá thể Kafka. Mã Java này bao gồm các bài kiểm tra tạo và sử dụng các thông điệp đến và đi từ một luồng.

Bạn có thể tải mã này từ  repo GitHub về máy cục bộ của mình.

Cách OpenShift Streams cho Apache Kafka đơn giản hóa quy trình Kafka

OpenShift Streams cho Apache Kafka là một dịch vụ được cung cấp bởi Red Hat cho phép các nhà phát triển tạo các luồng Kafka đầy đủ chức năng mà không phải định cấu hình máy chủ Kafka và ràng buộc chúng với các URL được đặt tên. Thay vào đó, một nhà phát triển có thể đăng nhập vào bảng điều khiển OpenShift, tạo một phiên bản của Kafka và một chủ đề Kafka tạo và sử dụng thông báo.

Nhà phát triển tạo một tài khoản dịch vụ liên kết với một chủ đề Kafka cụ thể. Sau đó, nhà phát triển định cấu hình cá thể Kafka để cấp quyền truy cập vào tài khoản dịch vụ theo một chủ đề cụ thể hoặc nhiều chủ đề. Các nhà phát triển có thể cấp quyền cho tài khoản dịch vụ để đọc từ một chủ đề với tư cách là người tiêu dùng hoặc ghi vào một chủ đề với tư cách là nhà sản xuất. Các bước cần thiết này để sử dụng OpenShift Streams cho Apache Kafka được tóm tắt trong Hình 1.

Sơ đồ gồm bốn bước để có được quyền truy cập vào một phiên bản Kafka.

Hình 1: Quy trình làm việc để đạt được quyền truy cập vào một cá thể Kafka bao gồm bốn bước.

 

Đằng sau hậu trường, OpenShift Streams cho Apache Kafka hoạt động như một trung gian cho một nhà cung cấp đám mây bản địa hoặc một trung tâm dữ liệu tại chỗ. OpenShift Streams cho Apache Kafka cung cấp cấu hình và quyền truy cập vào các máy chủ và bộ nhớ được lưu trữ bên ngoài. Nhà cung cấp đám mây, chẳng hạn như AWS, lưu trữ các tin nhắn Kafka và máy chủ Kafka quản lý các tin nhắn đó. (Xem Hình 2.)

Một minh họa về cách các trường hợp Kafka giảm bớt sức lao động.

Hình 2: Sử dụng OpenShift Streams cho Apache Kafka giảm bớt lao động với các phiên bản Kafka.

 

Các nhà phát triển có quyền truy cập vào máy chủ Kafka thông qua URL do OpenShift Streams xuất bản cho Apache Kafka. Về phần phụ trợ, OpenShift Streams cho Apache Kafka làm việc với nhà cung cấp đám mây gốc để lưu, lưu trữ và truy xuất dữ liệu luồng. Cuối cùng, OpenShift Streams cho Apache Kafka giảm bớt công việc phức tạp và nhàm chán trong việc quản lý Kafka, cho phép các nhà phát triển tập trung vào việc đạt được các mục tiêu lập trình của họ.

Điều hành một nhà môi giới Kafka

Bạn có thể tải và chạy qua bảng điều khiển web OpenShift hoặc giao diện dòng lệnh OpenShift oc (CLI). Với mục đích của bài viết này, chúng tôi sẽ làm việc với bảng điều khiển web OpenShift.

Cách tốt nhất để tìm hiểu kiến ​​thức cơ bản về tạo luồng OpenShift là tham gia bài học bắt đầu nhanh: Bắt đầu với OpenShift Streams cho Apache Kafka .

Một trong những điều thú vị về bài học bắt đầu nhanh là nó có tính tương tác. Bạn có thể đăng nhập vào bảng điều khiển web OpenShift và tạo máy chủ Kafka trong OpenShift Streams cho Apache Kafka. Bảng điều khiển web cung cấp hướng dẫn từng bước như trong Hình 3.

Ảnh chụp màn hình của bảng điều khiển OpenShift Quickstart.

Hình 3: OpenShift Quickstart cho phép bạn làm việc trực tiếp trong bảng điều khiển OpenShift.

 

Để chạy ứng dụng trình diễn, hãy hoàn thành các bước sau từ hướng dẫn bắt đầu nhanh:

  • Tạo cá thể Kafka.
  • Tạo tài khoản dịch vụ.
  • Tạo chủ đề.
  • Định cấu hình quyền truy cập vào chủ đề theo chủ đề dịch vụ.

Tạo bốn phần thông tin bắt buộc khi bạn bắt đầu nhanh. Các mục chính này là:

  • Tên chủ đề Kafka mà bạn đã tạo trong OpenShift.
  • URL của máy chủ khởi động Kafka.
  • ID khách hàng được liên kết với tài khoản dịch vụ mà bạn sẽ liên kết với phiên bản Kafka mà bạn đã tạo trong OpenShift.
  • Bí mật khách hàng được liên kết với tài khoản dịch vụ mà bạn sẽ liên kết với phiên bản Kafka mà bạn đã tạo trong OpenShift.

Hướng dẫn bắt đầu nhanh cũng giải thích thời điểm tạo từng mục. Sao chép các trường vào trình soạn thảo văn bản để sau này bạn có thể dán chúng vào tệp biến môi trường .env. Tệp .env(được trình bày chi tiết trong phần tiếp theo) mô phỏng các biến môi trường và giá trị của chúng.

Sau khi hoàn tất quá trình bắt đầu nhanh, bạn sẽ có một phiên bản đầy đủ chức năng của Kafka đang chạy trong OpenShift Streams cho Apache Kafka. Phiên bản khởi động nhanh chạy miễn phí cho bạn. Phiên bản này chạy trong 48 giờ cho đến khi bị phá hủy. Tuy nhiên, nếu bạn muốn thử nghiệm nhiều hơn, bạn có thể quay một dòng Kafka khác để thay thế nó.

Tạo và sử dụng thông điệp trong OpenShift trong 6 bước

Như đã đề cập trước đây, dự án trình diễn được lưu trữ trên GitHub là một ứng dụng Java sử dụng ứng dụng khách Apache Kafka để tạo và sử dụng các thông điệp từ Kafka.

Ứng dụng khách Apache Kafka là bất khả tri, có nghĩa là nó có thể hoạt động với một phiên bản cục bộ của Kafka cũng như một phiên bản chạy trên đám mây. Trong phần trình diễn, ứng dụng khách Kafka liên kết với phiên bản Kafka mà bạn đã tạo trong bài học bắt đầu nhanh.

1. Sao chép mã từ GitHub

Tải xuống mã từ GitHub bằng cách chạy lệnh sau:

$ git clone https://github.com/redhat-developer-demos/kafka-openshift-streams-demo.git

Để chạy mã cục bộ, hãy tạo các biến môi trường để có quyền truy cập an toàn vào phiên bản Kafka của bạn. Sử dụng gói Maven java-dotenv để tạo các biến môi trường này. Gói java-dotenv cho phép bạn tạo các biến môi trường từ cài đặt trong tệp văn bản.

2. Tạo biến môi trường với java-dotenv

Ứng dụng cần cung cấp các chi tiết cụ thể của phiên bản Kafka của bạn, bao gồm tên người dùng và mật khẩu, để có quyền truy cập. Đưa thông tin đăng nhập tên người dùng / mật khẩu trực tiếp vào mã hoặc thậm chí là tệp cấu hình là hành vi xấu. Chỉ cần một lần đột nhập bất chính vào hệ thống quản lý kiểm soát nguồn để kẻ tấn công có thể truy cập vào các thông tin đăng nhập và mã đó. Cách tốt hơn để cung cấp thông tin đăng nhập này là đưa thông tin xác thực bảo mật vào các biến môi trường trong thời gian chạy và sau đó mã xác minh tính khả dụng của biến môi trường được yêu cầu. Phương pháp này không lưu trữ dữ liệu truy cập trong mã, do đó loại bỏ loại lỗ hổng này.

Tuy nhiên, liên tục cung cấp các biến môi trường là điều tẻ nhạt. Đây là lúc java-dotenv phát huy tác dụng.

Khi sử dụng java-dotenv, bạn đặt thông tin đăng nhập tên người dùng và mật khẩu vào tệp .env dưới dạng một cặp khóa-giá trị. Sau đó, java-dotenv lấy các thông tin xác thực này và đưa chúng vào bộ nhớ dưới dạng các biến môi trường. Nếu thông tin xác thực đã được lưu trữ trong các biến môi trường trong bộ nhớ, java-dotenv sẽ bỏ qua dữ liệu trong tệp .env và chỉ báo cáo giá trị của các biến môi trường trong bộ nhớ.

Được tư vấn. Mặc dù java-dotenv làm cho mọi thứ dễ dàng hơn nhưng vẫn có rủi ro. Không bao giờ chuyển tệp .env cục bộ vào kiểm soát nguồn. Bạn có thể ngăn chặn sai lầm này bằng cách thêm dòng sau vào  tệp .gitignore của mã nguồn  như sau:

# add this line the .gitignore to prevent an accidental commit

.env

3. Cài đặt java-dotenv

Cài đặt java-dotenv bằng cách thêm <dependency>cấu hình sau vào <dependencies>phần của pom.xmltệp dự án Maven (mục đã được thêm vào pom.xmltệp của dự án trình diễn):

<dependency>
     <groupId>io.github.cdimascio</groupId>
     <artifactId>java-dotenv</artifactId>
     <version>5.2.2</version>
 </dependency>

4. Định cấu hình tệp .env

Cấu hình .envtệp để khai báo các biến môi trường để đưa vào bộ nhớ của dự án trình diễn.

LƯU Ý: Dự án trình diễn không được gửi cùng với .envtệp. Bạn phải tạo một tệp và đặt tệp vào thư mục gốc của thư mục làm việc của dự án trình diễn.

Cấu trúc của .envtệp như sau:

DOTENV_TEST_MESSAGE=PING
KAFKA_STREAM_TEST_TOPIC=<TOPIC_NAME_ON_KAFKA_STREAMS>
KAFKA_STREAM_USER_NAME=<SERVICE_ACCOUNT_CLIENT_ID>
KAFKA_STREAM_PWD=<SERVICE_ACCOUNT_CLIENT_SECRET>
KAFKA_STREAM_BOOTSTRAP_SERVER=<KAFKA_STREAMS_BOOTSTRAP_SERVER_URL>

Văn bản trong dấu ngoặc nhọn phải được điền như sau:

  • <TOPIC_NAME_ON_KAFKA_STREAMS>là tên của chủ đề sẽ tạo ra và tiêu thụ thông điệp. Như đã giải thích trước đó, bạn tạo chủ đề trong OpenShift Streams cho Apache Kafka.
  • <SERVICE_ACCOUNT_CLIENT_ID>là ID khách hàng được tạo khi bạn mở tài khoản dịch vụ OpenShift được liên kết với luồng.
  • <SERVICE_ACCOUNT_CLIENT_SECRET>là bí mật khách hàng mà bạn đã tạo.
  • <KAFKA_STREAMS_BOOTSTRAP_SERVER_URL>là URL của máy chủ khởi động Kafka, được tạo khi bạn tạo phiên bản OpenShift Streams cho Apache Kafka.

Đây là một ví dụ về một .envtệp hợp lệ:

DOTENV_TEST_MESSAGE=PING
KAFKA_STREAM_TEST_TOPIC=mytopic
KAFKA_STREAM_USER_NAME=srvc-acct-7a36b530-e8fe-471c-bb3d-aa005020d911
KAFKA_STREAM_PWD=6823f7bd-649b-4029-8a1c-8713a828918b
KAFKA_STREAM_BOOTSTRAP_SERVER=my-first-k-c-skrukt--m--nhc-fna.bf2.kafka.rhcloud.com:443

5. Định cấu hình kết nối an toàn tới OpenShift Streams cho Apache Kafka

Để ứng dụng khách Java kết nối với OpenShift Streams cho Apache Kafka yêu cầu cài đặt cấu hình đặc biệt để tạo kết nối an toàn. Các cài đặt cấu hình này, sử dụng tiêu chuẩn Lớp Bảo mật và Xác thực Đơn giản (SASL), như sau (mỗi cài đặt được liên kết với một trang web cung cấp thông tin chi tiết):

Ứng dụng trình diễn gán các giá trị cho các cài đặt như sau:

  • security.protocol="SASL_SSL"
  • sasl.mechanism="PLAIN"
  • sasl.jaas.config='org.apache.kafka.common.security.plain.PlainLoginModule required username="<USERNAME>" password="<PASSWORD>"'

Về cơ bản, cài đặt có nghĩa là kết nối sẽ sử dụng giao thức bảo mật SASL_SSL và cơ chế bảo mật SASL sẽ là PLAIN . Với mục đích của phần trình diễn này, PLAINcơ chế bảo mật đơn giản hóa cấu hình bằng cách cho phép bạn gửi thông tin đăng nhập tên người dùng và mật khẩu dưới dạng văn bản thuần túy. Tuy nhiên, chuỗi kết nối được gửi một cách bí mật bằng mã hóa cấp thấp hơn được chỉ định trong RFC4616 .

Vì ứng dụng trình diễn của bài viết này được dành riêng để kết nối với một phiên bản của Kafka chạy từ xa trong OpenShift Streams cho Apache Kafka, các cài đặt cấu hình bắt buộc được đưa trực tiếp vào mã trong addSecurityPropertiesphương thức. Phương thức này nằm trong src/main/java/com/demo/kafka/PropertiesHelper.javatệp mã nguồn của ứng dụng trình diễn.

Hãy xem mã để biết cách triển khai cấu hình. Lưu ý rằng các giá trị cho tên người dùng và mật khẩu được lưu trữ tương ứng trong các biến môi trường KAFKA_STREAM_USER_NAMEKAFKA_STREAM_PWDbiến. Do đó, thông tin kết nối được bảo vệ khỏi sự xâm nhập vào hệ thống tệp của máy chủ:

static Properties addSecurityProperties(Properties props) throws
                                PropertiesConfigurationException {
  // use the private method that ensures the required environment variables are present
  testForSecurityProperties();

  // create an instance of java-dotenv to retrieve the environment variables
  Dotenv env = getDotEnv();

  //override the local Kafka instance settings with the Kafka Stream instance
  props.put("bootstrap.servers", env.get("KAFKA_STREAM_BOOTSTRAP_SERVER"));

  // add the security settings
  props.put("security.protocol", "SASL_SSL");
  props.put("sasl.mechanism", "PLAIN");
  // create the connection string, getting username and pwd from env vars
  props.put("sasl.jaas.config", PlainLoginModule.class.getName() + " required username=\"" + env.get("KAFKA_STREAM_USER_NAME") + "\" password=\"" + env.get("KAFKA_STREAM_PWD") + "\";");

  // return the amended properties collection
  return props;
}

Bây giờ chúng ta đã cấu hình máy khách Java để sử dụng phiên bản Kafka chạy trên OpenShift Streams cho Apache Kafka, bạn có thể chạy các bài kiểm tra đơn vị tạo và sử dụng các thông báo đến và đi từ OpenShift.

6. Chạy các bài kiểm tra

Để chạy các bài kiểm tra đơn vị tạo và sử dụng thông báo, hãy chạy lệnh từ cấp cao nhất của thư mục mà bạn đã cài đặt mã trình diễn. Nhưng trước khi thực hiện, hãy đảm bảo rằng bạn đã định cấu hình đúng cách tệp .env với các giá trị của tên người dùng, mật khẩu, URL máy chủ bootstrap và chủ đề cụ thể cho phiên bản Kafka của bạn và chạy trong OpenShift Streams cho Apache Kafka.

Giả sử .envtệp được tạo và định cấu hình, hãy chạy lệnh sau:

$ mvn test

Khi thành công, đầu ra sẽ giống như sau:

Results :

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 19.510 s
[INFO] Finished at: 2022-05-09T19:08:02Z
[INFO] -----------------------------------------------------------------------

Một lần chạy thử nghiệm thành công sẽ tạo ra nhiều thông báo được lưu trữ trong phiên bản Kafka trên OpenShift Streams cho Apache Kafka. (Hãy nhớ rằng, Kafka giữ lại tất cả các tin nhắn mà nó nhận được.)

Bạn có thể xác minh rằng các thông báo thực sự được lưu trữ trên OpenShift Streams cho Apache Kafka bằng cách chuyển đến phiên bản Kafka của bạn đang chạy trong OpenShift. Chọn cá thể Kafka bạn đã tạo, sau đó chọn  tab Tin nhắn  trên trang Cá thể Kafka như thể hiện trong Hình 4.

OpenShift Streams cho Apache Kafka hiển thị các thông báo được lưu trữ trong phiên bản Kafka được liên kết.

Hình 4: OpenShift Streams cho Apache Kafka hiển thị các thông báo được lưu trữ trong phiên bản Kafka được liên kết.

Bạn sẽ thấy các thông báo do thử nghiệm tạo ra trên trang chủ đề trong bảng điều khiển web OpenShift.

Chúng tôi đã hoàn thành cuộc biểu tình!

OpenShift Streams cho Apache Kafka giúp bạn tập trung vào phát triển ứng dụng

Trong bài viết này, bạn đã học cách làm việc với Red Hat OpenShift Streams cho Apache Kafka để tạo và sử dụng một phiên bản đầy đủ chức năng của Kafka. Sau khi chúng tôi đề cập đến các khái niệm và lợi ích của OpenShift Streams cho Apache Kafka, bạn đã được giới thiệu về quy trình tạo phiên bản Kafka và chủ đề Kafka trên trường hợp đó. Bạn đã bắt đầu nhanh trong OpenShift để tạo một phiên bản Kafka, một chủ đề Kafka và một tài khoản dịch vụ OpenShift. Bạn đã chỉ định quyền truy cập vào tài khoản dịch vụ cho chủ đề bạn đã tạo.

Ngoài ra, bạn đã tải xuống dự án trình diễn từ GitHub và định cấu hình một .envtệp trong thư mục làm việc của dự án trình diễn. Tệp .envxác định các biến môi trường có thông tin xác thực để truy cập cá thể Kafka được tạo trong OpenShift Streams cho Apache Kafka. Bạn đã kết hợp tất cả lại với nhau bằng cách chạy các bài kiểm tra đơn vị có mã trình diễn để tạo và sử dụng các thông báo đến và đi từ OpenShift Streams cho Apache Kafka.

Hy vọng rằng các khái niệm và kỹ thuật bạn đã học được trong bài viết này sẽ truyền cảm hứng cho bạn để nghiên cứu sâu hơn về Kafka và thử nó trên OpenShift Streams cho Apache Kafka. Đây là một công nghệ mạnh mẽ và hiệu quả sẽ giúp làm việc với Kafka dễ dàng hơn khi bạn tiến lên trong hành trình tạo các chương trình hướng tin nhắn bằng Java và OpenShift.

Liên kết: https://developers.redhat.com/articles/2022/08/04/quick-java-application-setup-using-kafka-messaging#openshift_streams_for_apache_kafka_helps_you_focus_on_application_development

#java #kafka 

Cách Thiết Lập ứng Dụng Java Nhanh Bằng Kafka Messaging
Royce  Reinger

Royce Reinger

1659714540

Ruby-kafka: A Ruby Client Library for Apache Kafka

ruby-kafka

A Ruby client library for Apache Kafka, a distributed log and message bus. The focus of this library will be operational simplicity, with good logging and metrics that can make debugging issues easier.

Installation

Add this line to your application's Gemfile:

gem 'ruby-kafka'

And then execute:

$ bundle

Or install it yourself as:

$ gem install ruby-kafka

Compatibility

 Producer APIConsumer API
Kafka 0.8Full support in v0.4.xUnsupported
Kafka 0.9Full support in v0.4.xFull support in v0.4.x
Kafka 0.10Full support in v0.5.xFull support in v0.5.x
Kafka 0.11Full support in v0.7.xLimited support
Kafka 1.0Limited supportLimited support
Kafka 2.0Limited supportLimited support
Kafka 2.1Limited supportLimited support
Kafka 2.2Limited supportLimited support
Kafka 2.3Limited supportLimited support
Kafka 2.4Limited supportLimited support
Kafka 2.5Limited supportLimited support
Kafka 2.6Limited supportLimited support
Kafka 2.7Limited supportLimited support

This library is targeting Kafka 0.9 with the v0.4.x series and Kafka 0.10 with the v0.5.x series. There's limited support for Kafka 0.8, and things should work with Kafka 0.11, although there may be performance issues due to changes in the protocol.

  • Kafka 0.8: Full support for the Producer API in ruby-kafka v0.4.x, but no support for consumer groups. Simple message fetching works.
  • Kafka 0.9: Full support for the Producer and Consumer API in ruby-kafka v0.4.x.
  • Kafka 0.10: Full support for the Producer and Consumer API in ruby-kafka v0.5.x. Note that you must run version 0.10.1 or higher of Kafka due to limitations in 0.10.0.
  • Kafka 0.11: Full support for Producer API, limited support for Consumer API in ruby-kafka v0.7.x. New features in 0.11.x includes new Record Batch format, idempotent and transactional production. The missing feature is dirty reading of Consumer API.
  • Kafka 1.0: Everything that works with Kafka 0.11 should still work, but so far no features specific to Kafka 1.0 have been added.
  • Kafka 2.0: Everything that works with Kafka 1.0 should still work, but so far no features specific to Kafka 2.0 have been added.
  • Kafka 2.1: Everything that works with Kafka 2.0 should still work, but so far no features specific to Kafka 2.1 have been added.
  • Kafka 2.2: Everything that works with Kafka 2.1 should still work, but so far no features specific to Kafka 2.2 have been added.
  • Kafka 2.3: Everything that works with Kafka 2.2 should still work, but so far no features specific to Kafka 2.3 have been added.
  • Kafka 2.4: Everything that works with Kafka 2.3 should still work, but so far no features specific to Kafka 2.4 have been added.
  • Kafka 2.5: Everything that works with Kafka 2.4 should still work, but so far no features specific to Kafka 2.5 have been added.
  • Kafka 2.6: Everything that works with Kafka 2.5 should still work, but so far no features specific to Kafka 2.6 have been added.
  • Kafka 2.7: Everything that works with Kafka 2.6 should still work, but so far no features specific to Kafka 2.7 have been added.

This library requires Ruby 2.1 or higher.

Usage

Please see the documentation site for detailed documentation on the latest release. Note that the documentation on GitHub may not match the version of the library you're using – there are still being made many changes to the API.

Setting up the Kafka Client

A client must be initialized with at least one Kafka broker, from which the entire Kafka cluster will be discovered. Each client keeps a separate pool of broker connections. Don't use the same client from more than one thread.

require "kafka"

# The first argument is a list of "seed brokers" that will be queried for the full
# cluster topology. At least one of these *must* be available. `client_id` is
# used to identify this client in logs and metrics. It's optional but recommended.
kafka = Kafka.new(["kafka1:9092", "kafka2:9092"], client_id: "my-application")

You can also use a hostname with seed brokers' IP addresses:

kafka = Kafka.new("seed-brokers:9092", client_id: "my-application", resolve_seed_brokers: true)

Producing Messages to Kafka

The simplest way to write a message to a Kafka topic is to call #deliver_message:

kafka = Kafka.new(...)
kafka.deliver_message("Hello, World!", topic: "greetings")

This will write the message to a random partition in the greetings topic. If you want to write to a specific partition, pass the partition parameter:

# Will write to partition 42.
kafka.deliver_message("Hello, World!", topic: "greetings", partition: 42)

If you don't know exactly how many partitions are in the topic, or if you'd rather have some level of indirection, you can pass in partition_key instead. Two messages with the same partition key will always be assigned to the same partition. This is useful if you want to make sure all messages with a given attribute are always written to the same partition, e.g. all purchase events for a given customer id.

# Partition keys assign a partition deterministically.
kafka.deliver_message("Hello, World!", topic: "greetings", partition_key: "hello")

Kafka also supports message keys. When passed, a message key can be used instead of a partition key. The message key is written alongside the message value and can be read by consumers. Message keys in Kafka can be used for interesting things such as Log Compaction. See Partitioning for more information.

# Set a message key; the key will be used for partitioning since no explicit
# `partition_key` is set.
kafka.deliver_message("Hello, World!", key: "hello", topic: "greetings")

Efficiently Producing Messages

While #deliver_message works fine for infrequent writes, there are a number of downsides:

  • Kafka is optimized for transmitting messages in batches rather than individually, so there's a significant overhead and performance penalty in using the single-message API.
  • The message delivery can fail in a number of different ways, but this simplistic API does not provide automatic retries.
  • The message is not buffered, so if there is an error, it is lost.

The Producer API solves all these problems and more:

# Instantiate a new producer.
producer = kafka.producer

# Add a message to the producer buffer.
producer.produce("hello1", topic: "test-messages")

# Deliver the messages to Kafka.
producer.deliver_messages

#produce will buffer the message in the producer but will not actually send it to the Kafka cluster. Buffered messages are only delivered to the Kafka cluster once #deliver_messages is called. Since messages may be destined for different partitions, this could involve writing to more than one Kafka broker. Note that a failure to send all buffered messages after the configured number of retries will result in Kafka::DeliveryFailed being raised. This can be rescued and ignored; the messages will be kept in the buffer until the next attempt.

Read the docs for Kafka::Producer for more details.

Asynchronously Producing Messages

A normal producer will block while #deliver_messages is sending messages to Kafka, possibly for tens of seconds or even minutes at a time, depending on your timeout and retry settings. Furthermore, you have to call #deliver_messages manually, with a frequency that balances batch size with message delay.

In order to avoid blocking during message deliveries you can use the asynchronous producer API. It is mostly similar to the synchronous API, with calls to #produce and #deliver_messages. The main difference is that rather than blocking, these calls will return immediately. The actual work will be done in a background thread, with the messages and operations being sent from the caller over a thread safe queue.

# `#async_producer` will create a new asynchronous producer.
producer = kafka.async_producer

# The `#produce` API works as normal.
producer.produce("hello", topic: "greetings")

# `#deliver_messages` will return immediately.
producer.deliver_messages

# Make sure to call `#shutdown` on the producer in order to avoid leaking
# resources. `#shutdown` will wait for any pending messages to be delivered
# before returning.
producer.shutdown

By default, the delivery policy will be the same as for a synchronous producer: only when #deliver_messages is called will the messages be delivered. However, the asynchronous producer offers two complementary policies for automatic delivery:

  1. Trigger a delivery once the producer's message buffer reaches a specified threshold. This can be used to improve efficiency by increasing the batch size when sending messages to the Kafka cluster.
  2. Trigger a delivery at a fixed time interval. This puts an upper bound on message delays.

These policies can be used alone or in combination.

# `async_producer` will create a new asynchronous producer.
producer = kafka.async_producer(
  # Trigger a delivery once 100 messages have been buffered.
  delivery_threshold: 100,

  # Trigger a delivery every 30 seconds.
  delivery_interval: 30,
)

producer.produce("hello", topic: "greetings")

# ...

When calling #shutdown, the producer will attempt to deliver the messages and the method call will block until that has happened. Note that there's no guarantee that the messages will be delivered.

Note: if the calling thread produces messages faster than the producer can write them to Kafka, you'll eventually run into problems. The internal queue used for sending messages from the calling thread to the background worker has a size limit; once this limit is reached, a call to #produce will raise Kafka::BufferOverflow.

Serialization

This library is agnostic to which serialization format you prefer. Both the value and key of a message is treated as a binary string of data. This makes it easier to use whatever serialization format you want, since you don't have to do anything special to make it work with ruby-kafka. Here's an example of encoding data with JSON:

require "json"

# ...

event = {
  "name" => "pageview",
  "url" => "https://example.com/posts/123",
  # ...
}

data = JSON.dump(event)

producer.produce(data, topic: "events")

There's also an example of encoding messages with Apache Avro.

Partitioning

Kafka topics are partitioned, with messages being assigned to a partition by the client. This allows a great deal of flexibility for the users. This section describes several strategies for partitioning and how they impact performance, data locality, etc.

Load Balanced Partitioning

When optimizing for efficiency, we either distribute messages as evenly as possible to all partitions, or make sure each producer always writes to a single partition. The former ensures an even load for downstream consumers; the latter ensures the highest producer performance, since message batching is done per partition.

If no explicit partition is specified, the producer will look to the partition key or the message key for a value that can be used to deterministically assign the message to a partition. If there is a big number of different keys, the resulting distribution will be pretty even. If no keys are passed, the producer will randomly assign a partition. Random partitioning can be achieved even if you use message keys by passing a random partition key, e.g. partition_key: rand(100).

If you wish to have the producer write all messages to a single partition, simply generate a random value and re-use that as the partition key:

partition_key = rand(100)

producer.produce(msg1, topic: "messages", partition_key: partition_key)
producer.produce(msg2, topic: "messages", partition_key: partition_key)

# ...

You can also base the partition key on some property of the producer, for example the host name.

Semantic Partitioning

By assigning messages to a partition based on some property of the message, e.g. making sure all events tracked in a user session are assigned to the same partition, downstream consumers can make simplifying assumptions about data locality. In this example, a consumer can keep process local state pertaining to a user session knowing that all events for the session will be read from a single partition. This is also called semantic partitioning, since the partition assignment is part of the application behavior.

Typically it's sufficient to simply pass a partition key in order to guarantee that a set of messages will be assigned to the same partition, e.g.

# All messages with the same `session_id` will be assigned to the same partition.
producer.produce(event, topic: "user-events", partition_key: session_id)

However, sometimes it's necessary to select a specific partition. When doing this, make sure that you don't pick a partition number outside the range of partitions for the topic:

partitions = kafka.partitions_for("events")

# Make sure that we don't exceed the partition count!
partition = some_number % partitions

producer.produce(event, topic: "events", partition: partition)

Compatibility with Other Clients

There's no standardized way to assign messages to partitions across different Kafka client implementations. If you have a heterogeneous set of clients producing messages to the same topics it may be important to ensure a consistent partitioning scheme. This library doesn't try to implement all schemes, so you'll have to figure out which scheme the other client is using and replicate it. An example:

partitions = kafka.partitions_for("events")

# Insert your custom partitioning scheme here:
partition = PartitioningScheme.assign(partitions, event)

producer.produce(event, topic: "events", partition: partition)

Another option is to configure a custom client partitioner that implements call(partition_count, message) and uses the same schema as the other client. For example:

class CustomPartitioner
  def call(partition_count, message)
    ...
  end
end
  
partitioner = CustomPartitioner.new
Kafka.new(partitioner: partitioner, ...)

Or, simply create a Proc handling the partitioning logic instead of having to add a new class. For example:

partitioner = -> (partition_count, message) { ... }
Kafka.new(partitioner: partitioner, ...)

Supported partitioning schemes

In order for semantic partitioning to work a partition_key must map to the same partition number every time. The general approach, and the one used by this library, is to hash the key and mod it by the number of partitions. There are many different algorithms that can be used to calculate a hash. By default crc32 is used. murmur2 is also supported for compatibility with Java based Kafka producers.

To use murmur2 hashing pass it as an argument to Partitioner. For example:

Kafka.new(partitioner: Kafka::Partitioner.new(hash_function: :murmur2))

Buffering and Error Handling

The producer is designed for resilience in the face of temporary network errors, Kafka broker failovers, and other issues that prevent the client from writing messages to the destination topics. It does this by employing local, in-memory buffers. Only when messages are acknowledged by a Kafka broker will they be removed from the buffer.

Typically, you'd configure the producer to retry failed attempts at sending messages, but sometimes all retries are exhausted. In that case, Kafka::DeliveryFailed is raised from Kafka::Producer#deliver_messages. If you wish to have your application be resilient to this happening (e.g. if you're logging to Kafka from a web application) you can rescue this exception. The failed messages are still retained in the buffer, so a subsequent call to #deliver_messages will still attempt to send them.

Note that there's a maximum buffer size; by default, it's set to 1,000 messages and 10MB. It's possible to configure both these numbers:

producer = kafka.producer(
  max_buffer_size: 5_000,           # Allow at most 5K messages to be buffered.
  max_buffer_bytesize: 100_000_000, # Allow at most 100MB to be buffered.
  ...
)

A final note on buffers: local buffers give resilience against broker and network failures, and allow higher throughput due to message batching, but they also trade off consistency guarantees for higher availability and resilience. If your local process dies while messages are buffered, those messages will be lost. If you require high levels of consistency, you should call #deliver_messages immediately after #produce.

Message Durability

Once the client has delivered a set of messages to a Kafka broker the broker will forward them to its replicas, thus ensuring that a single broker failure will not result in message loss. However, the client can choose when the leader acknowledges the write. At one extreme, the client can choose fire-and-forget delivery, not even bothering to check whether the messages have been acknowledged. At the other end, the client can ask the broker to wait until all its replicas have acknowledged the write before returning. This is the safest option, and the default. It's also possible to have the broker return as soon as it has written the messages to its own log but before the replicas have done so. This leaves a window of time where a failure of the leader will result in the messages being lost, although this should not be a common occurrence.

Write latency and throughput are negatively impacted by having more replicas acknowledge a write, so if you require low-latency, high throughput writes you may want to accept lower durability.

This behavior is controlled by the required_acks option to #producer and #async_producer:

# This is the default: all replicas must acknowledge.
producer = kafka.producer(required_acks: :all)

# This is fire-and-forget: messages can easily be lost.
producer = kafka.producer(required_acks: 0)

# This only waits for the leader to acknowledge.
producer = kafka.producer(required_acks: 1)

Unless you absolutely need lower latency it's highly recommended to use the default setting (:all).

Message Delivery Guarantees

There are basically two different and incompatible guarantees that can be made in a message delivery system such as Kafka:

  1. at-most-once delivery guarantees that a message is at most delivered to the recipient once. This is useful only if delivering the message twice carries some risk and should be avoided. Implicit is the fact that there's no guarantee that the message will be delivered at all.
  2. at-least-once delivery guarantees that a message is delivered, but it may be delivered more than once. If the final recipient de-duplicates messages, e.g. by checking a unique message id, then it's even possible to implement exactly-once delivery.

Of these two options, ruby-kafka implements the second one: when in doubt about whether a message has been delivered, a producer will try to deliver it again.

The guarantee is made only for the synchronous producer and boils down to this:

producer = kafka.producer

producer.produce("hello", topic: "greetings")

# If this line fails with Kafka::DeliveryFailed we *may* have succeeded in delivering
# the message to Kafka but won't know for sure.
producer.deliver_messages

# If we get to this line we can be sure that the message has been delivered to Kafka!

That is, once #deliver_messages returns we can be sure that Kafka has received the message. Note that there are some big caveats here:

  • Depending on how your cluster and topic is configured the message could still be lost by Kafka.
  • If you configure the producer to not require acknowledgements from the Kafka brokers by setting required_acks to zero there is no guarantee that the message will ever make it to a Kafka broker.
  • If you use the asynchronous producer there's no guarantee that messages will have been delivered after #deliver_messages returns. A way of blocking until a message has been delivered with the asynchronous producer may be implemented in the future.

It's possible to improve your chances of success when calling #deliver_messages, at the price of a longer max latency:

producer = kafka.producer(
  # The number of retries when attempting to deliver messages. The default is
  # 2, so 3 attempts in total, but you can configure a higher or lower number:
  max_retries: 5,

  # The number of seconds to wait between retries. In order to handle longer
  # periods of Kafka being unavailable, increase this number. The default is
  # 1 second.
  retry_backoff: 5,
)

Note that these values affect the max latency of the operation; see Understanding Timeouts for an explanation of the various timeouts and latencies.

If you use the asynchronous producer you typically don't have to worry too much about this, as retries will be done in the background.

Compression

Depending on what kind of data you produce, enabling compression may yield improved bandwidth and space usage. Compression in Kafka is done on entire messages sets rather than on individual messages. This improves the compression rate and generally means that compressions works better the larger your buffers get, since the message sets will be larger by the time they're compressed.

Since many workloads have variations in throughput and distribution across partitions, it's possible to configure a threshold for when to enable compression by setting compression_threshold. Only if the defined number of messages are buffered for a partition will the messages be compressed.

Compression is enabled by passing the compression_codec parameter to #producer with the name of one of the algorithms allowed by Kafka:

  • :snappy for Snappy compression.
  • :gzip for gzip compression.
  • :lz4 for LZ4 compression.
  • :zstd for zstd compression.

By default, all message sets will be compressed if you specify a compression codec. To increase the compression threshold, set compression_threshold to an integer value higher than one.

producer = kafka.producer(
  compression_codec: :snappy,
  compression_threshold: 10,
)

Producing Messages from a Rails Application

A typical use case for Kafka is tracking events that occur in web applications. Oftentimes it's advisable to avoid having a hard dependency on Kafka being available, allowing your application to survive a Kafka outage. By using an asynchronous producer, you can avoid doing IO within the individual request/response cycles, instead pushing that to the producer's internal background thread.

In this example, a producer is configured in a Rails initializer:

# config/initializers/kafka_producer.rb
require "kafka"

# Configure the Kafka client with the broker hosts and the Rails
# logger.
$kafka = Kafka.new(["kafka1:9092", "kafka2:9092"], logger: Rails.logger)

# Set up an asynchronous producer that delivers its buffered messages
# every ten seconds:
$kafka_producer = $kafka.async_producer(
  delivery_interval: 10,
)

# Make sure to shut down the producer when exiting.
at_exit { $kafka_producer.shutdown }

In your controllers, simply call the producer directly:

# app/controllers/orders_controller.rb
class OrdersController
  def create
    @order = Order.create!(params[:order])

    event = {
      order_id: @order.id,
      amount: @order.amount,
      timestamp: Time.now,
    }

    $kafka_producer.produce(event.to_json, topic: "order_events")
  end
end

Consuming Messages from Kafka

Note: If you're just looking to get started with Kafka consumers, you might be interested in visiting the Higher level libraries section that lists ruby-kafka based frameworks. Read on, if you're interested in either rolling your own executable consumers or if you want to learn more about how consumers work in Kafka.

Consuming messages from a Kafka topic with ruby-kafka is simple:

require "kafka"

kafka = Kafka.new(["kafka1:9092", "kafka2:9092"])

kafka.each_message(topic: "greetings") do |message|
  puts message.offset, message.key, message.value
end

While this is great for extremely simple use cases, there are a number of downsides:

  • You can only fetch from a single topic at a time.
  • If you want to have multiple processes consume from the same topic, there's no way of coordinating which processes should fetch from which partitions.
  • If the process dies, there's no way to have another process resume fetching from the point in the partition that the original process had reached.

Consumer Groups

The Consumer API solves all of the above issues, and more. It uses the Consumer Groups feature released in Kafka 0.9 to allow multiple consumer processes to coordinate access to a topic, assigning each partition to a single consumer. When a consumer fails, the partitions that were assigned to it are re-assigned to other members of the group.

Using the API is simple:

require "kafka"

kafka = Kafka.new(["kafka1:9092", "kafka2:9092"])

# Consumers with the same group id will form a Consumer Group together.
consumer = kafka.consumer(group_id: "my-consumer")

# It's possible to subscribe to multiple topics by calling `subscribe`
# repeatedly.
consumer.subscribe("greetings")

# Stop the consumer when the SIGTERM signal is sent to the process.
# It's better to shut down gracefully than to kill the process.
trap("TERM") { consumer.stop }

# This will loop indefinitely, yielding each message in turn.
consumer.each_message do |message|
  puts message.topic, message.partition
  puts message.offset, message.key, message.value
end

Each consumer process will be assigned one or more partitions from each topic that the group subscribes to. In order to handle more messages, simply start more processes.

Consumer Checkpointing

In order to be able to resume processing after a consumer crashes, each consumer will periodically checkpoint its position within each partition it reads from. Since each partition has a monotonically increasing sequence of message offsets, this works by committing the offset of the last message that was processed in a given partition. Kafka handles these commits and allows another consumer in a group to resume from the last commit when a member crashes or becomes unresponsive.

By default, offsets are committed every 10 seconds. You can increase the frequency, known as the offset commit interval, to limit the duration of double-processing scenarios, at the cost of a lower throughput due to the added coordination. If you want to improve throughput, and double-processing is of less concern to you, then you can decrease the frequency. Set the commit interval to zero in order to disable the timer-based commit trigger entirely.

In addition to the time based trigger it's possible to trigger checkpointing in response to n messages having been processed, known as the offset commit threshold. This puts a bound on the number of messages that can be double-processed before the problem is detected. Setting this to 1 will cause an offset commit to take place every time a message has been processed. By default this trigger is disabled (set to zero).

It is possible to trigger an immediate offset commit by calling Consumer#commit_offsets. This blocks the caller until the Kafka cluster has acknowledged the commit.

Stale offsets are periodically purged by the broker. The broker setting offsets.retention.minutes controls the retention window for committed offsets, and defaults to 1 day. The length of the retention window, known as offset retention time, can be changed for the consumer.

Previously committed offsets are re-committed, to reset the retention window, at the first commit and periodically at an interval of half the offset retention time.

consumer = kafka.consumer(
  group_id: "some-group",

  # Increase offset commit frequency to once every 5 seconds.
  offset_commit_interval: 5,

  # Commit offsets when 100 messages have been processed.
  offset_commit_threshold: 100,

  # Increase the length of time that committed offsets are kept.
  offset_retention_time: 7 * 60 * 60
)

For some use cases it may be necessary to control when messages are marked as processed. Note that since only the consumer position within each partition can be saved, marking a message as processed implies that all messages in the partition with a lower offset should also be considered as having been processed.

The method Consumer#mark_message_as_processed marks a message (and all those that precede it in a partition) as having been processed. This is an advanced API that you should only use if you know what you're doing.

# Manually controlling checkpointing:

# Typically you want to use this API in order to buffer messages until some
# special "commit" message is received, e.g. in order to group together
# transactions consisting of several items.
buffer = []

# Messages will not be marked as processed automatically. If you shut down the
# consumer without calling `#mark_message_as_processed` first, the consumer will
# not resume where you left off!
consumer.each_message(automatically_mark_as_processed: false) do |message|
  # Our messages are JSON with a `type` field and other stuff.
  event = JSON.parse(message.value)

  case event.fetch("type")
  when "add_to_cart"
    buffer << event
  when "complete_purchase"
    # We've received all the messages we need, time to save the transaction.
    save_transaction(buffer)

    # Now we can set the checkpoint by marking the last message as processed.
    consumer.mark_message_as_processed(message)

    # We can optionally trigger an immediate, blocking offset commit in order
    # to minimize the risk of crashing before the automatic triggers have
    # kicked in.
    consumer.commit_offsets

    # Make the buffer ready for the next transaction.
    buffer.clear
  end
end

Topic Subscriptions

For each topic subscription it's possible to decide whether to consume messages starting at the beginning of the topic or to just consume new messages that are produced to the topic. This policy is configured by setting the start_from_beginning argument when calling #subscribe:

# Consume messages from the very beginning of the topic. This is the default.
consumer.subscribe("users", start_from_beginning: true)

# Only consume new messages.
consumer.subscribe("notifications", start_from_beginning: false)

Once the consumer group has checkpointed its progress in the topic's partitions, the consumers will always start from the checkpointed offsets, regardless of start_from_beginning. As such, this setting only applies when the consumer initially starts consuming from a topic.

Shutting Down a Consumer

In order to shut down a running consumer process cleanly, call #stop on it. A common pattern is to trap a process signal and initiate the shutdown from there:

consumer = kafka.consumer(...)

# The consumer can be stopped from the command line by executing
# `kill -s TERM <process-id>`.
trap("TERM") { consumer.stop }

consumer.each_message do |message|
  ...
end

Consuming Messages in Batches

Sometimes it is easier to deal with messages in batches rather than individually. A batch is a sequence of one or more Kafka messages that all belong to the same topic and partition. One common reason to want to use batches is when some external system has a batch or transactional API.

# A mock search index that we'll be keeping up to date with new Kafka messages.
index = SearchIndex.new

consumer.subscribe("posts")

consumer.each_batch do |batch|
  puts "Received batch: #{batch.topic}/#{batch.partition}"

  transaction = index.transaction

  batch.messages.each do |message|
    # Let's assume that adding a document is idempotent.
    transaction.add(id: message.key, body: message.value)
  end

  # Once this method returns, the messages have been successfully written to the
  # search index. The consumer will only checkpoint a batch *after* the block
  # has completed without an exception.
  transaction.commit!
end

One important thing to note is that the client commits the offset of the batch's messages only after the entire batch has been processed.

Balancing Throughput and Latency

There are two performance properties that can at times be at odds: throughput and latency. Throughput is the number of messages that can be processed in a given timespan; latency is the time it takes from a message is written to a topic until it has been processed.

In order to optimize for throughput, you want to make sure to fetch as many messages as possible every time you do a round trip to the Kafka cluster. This minimizes network overhead and allows processing data in big chunks.

In order to optimize for low latency, you want to process a message as soon as possible, even if that means fetching a smaller batch of messages.

There are three values that can be tuned in order to balance these two concerns.

  • min_bytes is the minimum number of bytes to return from a single message fetch. By setting this to a high value you can increase the processing throughput. The default value is one byte.
  • max_wait_time is the maximum number of seconds to wait before returning data from a single message fetch. By setting this high you also increase the processing throughput – and by setting it low you set a bound on latency. This configuration overrides min_bytes, so you'll always get data back within the time specified. The default value is one second. If you want to have at most five seconds of latency, set max_wait_time to 5. You should make sure max_wait_time * num brokers + heartbeat_interval is less than session_timeout.
  • max_bytes_per_partition is the maximum amount of data a broker will return for a single partition when fetching new messages. The default is 1MB, but increasing this number may lead to better throughtput since you'll need to fetch less frequently. Setting it to a lower value is not recommended unless you have so many partitions that it's causing network and latency issues to transfer a fetch response from a broker to a client. Setting the number too high may result in instability, so be careful.

The first two settings can be passed to either #each_message or #each_batch, e.g.

# Waits for data for up to 5 seconds on each broker, preferring to fetch at least 5KB at a time.
# This can wait up to num brokers * 5 seconds.
consumer.each_message(min_bytes: 1024 * 5, max_wait_time: 5) do |message|
  # ...
end

The last setting is configured when subscribing to a topic, and can vary between topics:

# Fetches up to 5MB per partition at a time for better throughput.
consumer.subscribe("greetings", max_bytes_per_partition: 5 * 1024 * 1024)

consumer.each_message do |message|
  # ...
end

Customizing Partition Assignment Strategy

In some cases, you might want to assign more partitions to some consumers. For example, in applications inserting some records to a database, the consumers running on hosts nearby the database can process more messages than the consumers running on other hosts. You can use a custom assignment strategy by passing an object that implements #call as the argument assignment_strategy like below:

class CustomAssignmentStrategy
  def initialize(user_data)
    @user_data = user_data
  end

  # Assign the topic partitions to the group members.
  #
  # @param cluster [Kafka::Cluster]
  # @param members [Hash<String, Kafka::Protocol::JoinGroupResponse::Metadata>] a hash
  #   mapping member ids to metadata
  # @param partitions [Array<Kafka::ConsumerGroup::Assignor::Partition>] a list of
  #   partitions the consumer group processes
  # @return [Hash<String, Array<Kafka::ConsumerGroup::Assignor::Partition>] a hash
  #   mapping member ids to partitions.
  def call(cluster:, members:, partitions:)
    ...
  end
end

strategy = CustomAssignmentStrategy.new("some-host-information")
consumer = kafka.consumer(group_id: "some-group", assignment_strategy: strategy)

members is a hash mapping member IDs to metadata, and partitions is a list of partitions the consumer group processes. The method call must return a hash mapping member IDs to partitions. For example, the following strategy assigns partitions randomly:

class RandomAssignmentStrategy
  def call(cluster:, members:, partitions:)
    member_ids = members.keys
    partitions.each_with_object(Hash.new {|h, k| h[k] = [] }) do |partition, partitions_per_member|
      partitions_per_member[member_ids[rand(member_ids.count)]] << partition
    end
  end
end

If the strategy needs user data, you should define the method user_data that returns user data on each consumer. For example, the following strategy uses the consumers' IP addresses as user data:

class NetworkTopologyAssignmentStrategy
  def user_data
    Socket.ip_address_list.find(&:ipv4_private?).ip_address
  end

  def call(cluster:, members:, partitions:)
    # Display the pair of the member ID and IP address
    members.each do |id, metadata|
      puts "#{id}: #{metadata.user_data}"
    end

    # Assign partitions considering the network topology
    ...
  end
end

Note that the strategy uses the class name as the default protocol name. You can change it by defining the method protocol_name:

class NetworkTopologyAssignmentStrategy
  def protocol_name
    "networktopology"
  end

  def user_data
    Socket.ip_address_list.find(&:ipv4_private?).ip_address
  end

  def call(cluster:, members:, partitions:)
    ...
  end
end

As the method call might receive different user data from what it expects, you should avoid using the same protocol name as another strategy that uses different user data.

Thread Safety

You typically don't want to share a Kafka client object between threads, since the network communication is not synchronized. Furthermore, you should avoid using threads in a consumer unless you're very careful about waiting for all work to complete before returning from the #each_message or #each_batch block. This is because checkpointing assumes that returning from the block means that the messages that have been yielded have been successfully processed.

You should also avoid sharing a synchronous producer between threads, as the internal buffers are not thread safe. However, the asynchronous producer should be safe to use in a multi-threaded environment. This is because producers, when instantiated, get their own copy of any non-thread-safe data such as network sockets. Furthermore, the asynchronous producer has been designed in such a way to only a single background thread operates on this data while any foreground thread with a reference to the producer object can only send messages to that background thread over a safe queue. Therefore it is safe to share an async producer object between many threads.

Logging

It's a very good idea to configure the Kafka client with a logger. All important operations and errors are logged. When instantiating your client, simply pass in a valid logger:

logger = Logger.new("log/kafka.log")
kafka = Kafka.new(logger: logger, ...)

By default, nothing is logged.

Instrumentation

Most operations are instrumented using Active Support Notifications. In order to subscribe to notifications, make sure to require the notifications library:

require "active_support/notifications"
require "kafka"

The notifications are namespaced based on their origin, with separate namespaces for the producer and the consumer.

In order to receive notifications you can either subscribe to individual notification names or use regular expressions to subscribe to entire namespaces. This example will subscribe to all notifications sent by ruby-kafka:

ActiveSupport::Notifications.subscribe(/.*\.kafka$/) do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  puts "Received notification `#{event.name}` with payload: #{event.payload.inspect}"
end

All notification events have the client_id key in the payload, referring to the Kafka client id.

Producer Notifications

produce_message.producer.kafka is sent whenever a message is produced to a buffer. It includes the following payload:

  • value is the message value.
  • key is the message key.
  • topic is the topic that the message was produced to.
  • buffer_size is the size of the producer buffer after adding the message.
  • max_buffer_size is the maximum size of the producer buffer.

deliver_messages.producer.kafka is sent whenever a producer attempts to deliver its buffered messages to the Kafka brokers. It includes the following payload:

  • attempts is the number of times delivery was attempted.
  • message_count is the number of messages for which delivery was attempted.
  • delivered_message_count is the number of messages that were acknowledged by the brokers - if this number is smaller than message_count not all messages were successfully delivered.

Consumer Notifications

All notifications have group_id in the payload, referring to the Kafka consumer group id.

process_message.consumer.kafka is sent whenever a message is processed by a consumer. It includes the following payload:

  • value is the message value.
  • key is the message key.
  • topic is the topic that the message was consumed from.
  • partition is the topic partition that the message was consumed from.
  • offset is the message's offset within the topic partition.
  • offset_lag is the number of messages within the topic partition that have not yet been consumed.

start_process_message.consumer.kafka is sent before process_message.consumer.kafka, and contains the same payload. It is delivered before the message is processed, rather than after.

process_batch.consumer.kafka is sent whenever a message batch is processed by a consumer. It includes the following payload:

  • message_count is the number of messages in the batch.
  • topic is the topic that the message batch was consumed from.
  • partition is the topic partition that the message batch was consumed from.
  • highwater_mark_offset is the message batch's highest offset within the topic partition.
  • offset_lag is the number of messages within the topic partition that have not yet been consumed.

start_process_batch.consumer.kafka is sent before process_batch.consumer.kafka, and contains the same payload. It is delivered before the batch is processed, rather than after.

join_group.consumer.kafka is sent whenever a consumer joins a consumer group. It includes the following payload:

  • group_id is the consumer group id.

sync_group.consumer.kafka is sent whenever a consumer is assigned topic partitions within a consumer group. It includes the following payload:

  • group_id is the consumer group id.

leave_group.consumer.kafka is sent whenever a consumer leaves a consumer group. It includes the following payload:

  • group_id is the consumer group id.

seek.consumer.kafka is sent when a consumer first seeks to an offset. It includes the following payload:

  • group_id is the consumer group id.
  • topic is the topic we are seeking in.
  • partition is the partition we are seeking in.
  • offset is the offset we have seeked to.

heartbeat.consumer.kafka is sent when a consumer group completes a heartbeat. It includes the following payload:

  • group_id is the consumer group id.
  • topic_partitions is a hash of { topic_name => array of assigned partition IDs }

Connection Notifications

  • request.connection.kafka is sent whenever a network request is sent to a Kafka broker. It includes the following payload:
    • api is the name of the API that was called, e.g. produce or fetch.
    • request_size is the number of bytes in the request.
    • response_size is the number of bytes in the response.

Monitoring

It is highly recommended that you monitor your Kafka client applications in production. Typical problems you'll see are:

  • high network error rates, which may impact performance and time-to-delivery;
  • producer buffer growth, which may indicate that producers are unable to deliver messages at the rate they're being produced;
  • consumer processing errors, indicating exceptions are being raised in the processing code;
  • frequent consumer rebalances, which may indicate unstable network conditions or consumer configurations.

You can quite easily build monitoring on top of the provided instrumentation hooks. In order to further help with monitoring, a prebuilt Statsd and Datadog reporter is included with ruby-kafka.

What to Monitor

We recommend monitoring the following:

  • Low-level Kafka API calls:
    • The rate of API call errors to the total number of calls by both API and broker.
    • The API call throughput by both API and broker.
    • The API call latency by both API and broker.
  • Producer-level metrics:
    • Delivery throughput by topic.
    • The latency of deliveries.
    • The producer buffer fill ratios.
    • The async producer queue sizes.
    • Message delivery delays.
    • Failed delivery attempts.
  • Consumer-level metrics:
    • Message processing throughput by topic.
    • Processing latency by topic.
    • Processing errors by topic.
    • Consumer lag (how many messages are yet to be processed) by topic/partition.
    • Group join/sync/leave by client host.

Reporting Metrics to Statsd

The Statsd reporter is automatically enabled when the kafka/statsd library is required. You can optionally change the configuration.

require "kafka/statsd"

# Default is "ruby_kafka".
Kafka::Statsd.namespace = "custom-namespace"

# Default is "127.0.0.1".
Kafka::Statsd.host = "statsd.something.com"

# Default is 8125.
Kafka::Statsd.port = 1234

Reporting Metrics to Datadog

The Datadog reporter is automatically enabled when the kafka/datadog library is required. You can optionally change the configuration.

# This enables the reporter:
require "kafka/datadog"

# Default is "ruby_kafka".
Kafka::Datadog.namespace = "custom-namespace"

# Default is "127.0.0.1".
Kafka::Datadog.host = "statsd.something.com"

# Default is 8125.
Kafka::Datadog.port = 1234

Understanding Timeouts

It's important to understand how timeouts work if you have a latency sensitive application. This library allows configuring timeouts on different levels:

Network timeouts apply to network connections to individual Kafka brokers. There are two config keys here, each passed to Kafka.new:

  • connect_timeout sets the number of seconds to wait while connecting to a broker for the first time. When ruby-kafka initializes, it needs to connect to at least one host in seed_brokers in order to discover the Kafka cluster. Each host is tried until there's one that works. Usually that means the first one, but if your entire cluster is down, or there's a network partition, you could wait up to n * connect_timeout seconds, where n is the number of seed brokers.
  • socket_timeout sets the number of seconds to wait when reading from or writing to a socket connection to a broker. After this timeout expires the connection will be killed. Note that some Kafka operations are by definition long-running, such as waiting for new messages to arrive in a partition, so don't set this value too low. When configuring timeouts relating to specific Kafka operations, make sure to make them shorter than this one.

Producer timeouts can be configured when calling #producer on a client instance:

  • ack_timeout is a timeout executed by a broker when the client is sending messages to it. It defines the number of seconds the broker should wait for replicas to acknowledge the write before responding to the client with an error. As such, it relates to the required_acks setting. It should be set lower than socket_timeout.
  • retry_backoff configures the number of seconds to wait after a failed attempt to send messages to a Kafka broker before retrying. The max_retries setting defines the maximum number of retries to attempt, and so the total duration could be up to max_retries * retry_backoff seconds. The timeout can be arbitrarily long, and shouldn't be too short: if a broker goes down its partitions will be handed off to another broker, and that can take tens of seconds.

When sending many messages, it's likely that the client needs to send some messages to each broker in the cluster. Given n brokers in the cluster, the total wait time when calling Kafka::Producer#deliver_messages can be up to

n * (connect_timeout + socket_timeout + retry_backoff) * max_retries

Make sure your application can survive being blocked for so long.

Security

Encryption and Authentication using SSL

By default, communication between Kafka clients and brokers is unencrypted and unauthenticated. Kafka 0.9 added optional support for encryption and client authentication and authorization. There are two layers of security made possible by this:

Encryption of Communication

By enabling SSL encryption you can have some confidence that messages can be sent to Kafka over an untrusted network without being intercepted.

In this case you just need to pass a valid CA certificate as a string when configuring your Kafka client:

kafka = Kafka.new(["kafka1:9092"], ssl_ca_cert: File.read('my_ca_cert.pem'))

Without passing the CA certificate to the client it would be impossible to protect against man-in-the-middle attacks.

Using your system's CA cert store

If you want to use the CA certs from your system's default certificate store, you can use:

kafka = Kafka.new(["kafka1:9092"], ssl_ca_certs_from_system: true)

This configures the store to look up CA certificates from the system default certificate store on an as needed basis. The location of the store can usually be determined by: OpenSSL::X509::DEFAULT_CERT_FILE

Client Authentication

In order to authenticate the client to the cluster, you need to pass in a certificate and key created for the client and trusted by the brokers.

NOTE: You can disable hostname validation by passing ssl_verify_hostname: false.

kafka = Kafka.new(
  ["kafka1:9092"],
  ssl_ca_cert: File.read('my_ca_cert.pem'),
  ssl_client_cert: File.read('my_client_cert.pem'),
  ssl_client_cert_key: File.read('my_client_cert_key.pem'),
  ssl_client_cert_key_password: 'my_client_cert_key_password',
  ssl_verify_hostname: false,
  # ...
)

Once client authentication is set up, it is possible to configure the Kafka cluster to authorize client requests.

Using JKS Certificates

Typically, Kafka certificates come in the JKS format, which isn't supported by ruby-kafka. There's a wiki page that describes how to generate valid X509 certificates from JKS certificates.

Authentication using SASL

Kafka has support for using SASL to authenticate clients. Currently GSSAPI, SCRAM and PLAIN mechanisms are supported by ruby-kafka.

NOTE: With SASL for authentication, it is highly recommended to use SSL encryption. The default behavior of ruby-kafka enforces you to use SSL and you need to configure SSL encryption by passing ssl_ca_cert or enabling ssl_ca_certs_from_system. However, this strict SSL mode check can be disabled by setting sasl_over_ssl to false while initializing the client.

GSSAPI

In order to authenticate using GSSAPI, set your principal and optionally your keytab when initializing the Kafka client:

kafka = Kafka.new(
  ["kafka1:9092"],
  sasl_gssapi_principal: 'kafka/kafka.example.com@EXAMPLE.COM',
  sasl_gssapi_keytab: '/etc/keytabs/kafka.keytab',
  # ...
)

AWS MSK (IAM)

In order to authenticate using IAM w/ an AWS MSK cluster, set your access key, secret key, and region when initializing the Kafka client:

k = Kafka.new(
  ["kafka1:9092"],
  sasl_aws_msk_iam_access_key_id: 'iam_access_key',
  sasl_aws_msk_iam_secret_key_id: 'iam_secret_key',
  sasl_aws_msk_iam_aws_region: 'us-west-2',
  ssl_ca_certs_from_system: true,
  # ...
)

PLAIN

In order to authenticate using PLAIN, you must set your username and password when initializing the Kafka client:

kafka = Kafka.new(
  ["kafka1:9092"],
  ssl_ca_cert: File.read('/etc/openssl/cert.pem'),
  sasl_plain_username: 'username',
  sasl_plain_password: 'password'
  # ...
)

SCRAM

Since 0.11 kafka supports SCRAM.

kafka = Kafka.new(
  ["kafka1:9092"],
  sasl_scram_username: 'username',
  sasl_scram_password: 'password',
  sasl_scram_mechanism: 'sha256',
  # ...
)

OAUTHBEARER

This mechanism is supported in kafka >= 2.0.0 as of KIP-255

In order to authenticate using OAUTHBEARER, you must set the client with an instance of a class that implements a token method (the interface is described in Kafka::Sasl::OAuth) which returns an ID/Access token.

Optionally, the client may implement an extensions method that returns a map of key-value pairs. These can be sent with the SASL/OAUTHBEARER initial client response. This is only supported in kafka >= 2.1.0.

class TokenProvider
  def token
    "some_id_token"
  end
end
# ...
client = Kafka.new(
  ["kafka1:9092"],
  sasl_oauth_token_provider: TokenProvider.new
)

Topic management

In addition to producing and consuming messages, ruby-kafka supports managing Kafka topics and their configurations. See the Kafka documentation for a full list of topic configuration keys.

List all topics

Return an array of topic names.

kafka = Kafka.new(["kafka:9092"])
kafka.topics
# => ["topic1", "topic2", "topic3"]

Create a topic

kafka = Kafka.new(["kafka:9092"])
kafka.create_topic("topic")

By default, the new topic has 1 partition, replication factor 1 and default configs from the brokers. Those configurations are customizable:

kafka = Kafka.new(["kafka:9092"])
kafka.create_topic("topic",
  num_partitions: 3,
  replication_factor: 2,
  config: {
    "max.message.bytes" => 100000
  }
)

Create more partitions for a topic

After a topic is created, you can increase the number of partitions for the topic. The new number of partitions must be greater than the current one.

kafka = Kafka.new(["kafka:9092"])
kafka.create_partitions_for("topic", num_partitions: 10)

Fetch configuration for a topic (alpha feature)

kafka = Kafka.new(["kafka:9092"])
kafka.describe_topic("topic", ["max.message.bytes", "retention.ms"])
# => {"max.message.bytes"=>"100000", "retention.ms"=>"604800000"}

Alter a topic configuration (alpha feature)

Update the topic configurations.

NOTE: This feature is for advanced usage. Only use this if you know what you're doing.

kafka = Kafka.new(["kafka:9092"])
kafka.alter_topic("topic", "max.message.bytes" => 100000, "retention.ms" => 604800000)

Delete a topic

kafka = Kafka.new(["kafka:9092"])
kafka.delete_topic("topic")

After a topic is marked as deleted, Kafka only hides it from clients. It would take a while before a topic is completely deleted.

Design

The library has been designed as a layered system, with each layer having a clear responsibility:

  • The network layer handles low-level connection tasks, such as keeping open connections to each Kafka broker, reconnecting when there's an error, etc. See Kafka::Connection for more details.
  • The protocol layer is responsible for encoding and decoding the Kafka protocol's various structures. See Kafka::Protocol for more details.
  • The operational layer provides high-level operations, such as fetching messages from a topic, that may involve more than one API request to the Kafka cluster. Some complex operations are made available through Kafka::Cluster, which represents an entire cluster, while simpler ones are only available through Kafka::Broker, which represents a single Kafka broker. In general, Kafka::Cluster is the high-level API, with more polish.
  • The API layer provides APIs to users of the libraries. The Consumer API is implemented in Kafka::Consumer while the Producer API is implemented in Kafka::Producer and Kafka::AsyncProducer.
  • The configuration layer provides a way to set up and configure the client, as well as easy entrypoints to the various APIs. Kafka::Client implements the public APIs. For convenience, the method Kafka.new can instantiate the class for you.

Note that only the API and configuration layers have any backwards compatibility guarantees – the other layers are considered internal and may change without warning. Don't use them directly.

Producer Design

The producer is designed with resilience and operational ease of use in mind, sometimes at the cost of raw performance. For instance, the operation is heavily instrumented, allowing operators to monitor the producer at a very granular level.

The producer has two main internal data structures: a list of pending messages and a message buffer. When the user calls Kafka::Producer#produce, a message is appended to the pending message list, but no network communication takes place. This means that the call site does not have to handle the broad range of errors that can happen at the network or protocol level. Instead, those errors will only happen once Kafka::Producer#deliver_messages is called. This method will go through the pending messages one by one, making sure they're assigned a partition. This may fail for some messages, as it could require knowing the current configuration for the message's topic, necessitating API calls to Kafka. Messages that cannot be assigned a partition are kept in the list, while the others are written into the message buffer. The producer then figures out which topic partitions are led by which Kafka brokers so that messages can be sent to the right place – in Kafka, it is the responsibility of the client to do this routing. A separate produce API request will be sent to each broker; the response will be inspected; and messages that were acknowledged by the broker will be removed from the message buffer. Any messages that were not acknowledged will be kept in the buffer.

If there are any messages left in either the pending message list or the message buffer after this operation, Kafka::DeliveryFailed will be raised. This exception must be rescued and handled by the user, possibly by calling #deliver_messages at a later time.

Asynchronous Producer Design

The synchronous producer allows the user fine-grained control over when network activity and the possible errors arising from that will take place, but it requires the user to handle the errors nonetheless. The async producer provides a more hands-off approach that trades off control for ease of use and resilience.

Instead of writing directly into the pending message list, Kafka::AsyncProducer writes the message to an internal thread-safe queue, returning immediately. A background thread reads messages off the queue and passes them to a synchronous producer.

Rather than triggering message deliveries directly, users of the async producer will typically set up automatic triggers, such as a timer.

Consumer Design

The Consumer API is designed for flexibility and stability. The first is accomplished by not dictating any high-level object model, instead opting for a simple loop-based approach. The second is accomplished by handling group membership, heartbeats, and checkpointing automatically. Messages are marked as processed as soon as they've been successfully yielded to the user-supplied processing block, minimizing the cost of processing errors.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

Note: the specs require a working Docker instance, but should work out of the box if you have Docker installed. Please create an issue if that's not the case.

If you would like to contribute to ruby-kafka, please join our Slack team and ask how best to do it.

Circle CI

Support and Discussion

If you've discovered a bug, please file a Github issue, and make sure to include all the relevant information, including the version of ruby-kafka and Kafka that you're using.

If you have other questions, or would like to discuss best practises, how to contribute to the project, or any other ruby-kafka related topic, join our Slack team!

Roadmap

Version 0.4 will be the last minor release with support for the Kafka 0.9 protocol. It is recommended that you pin your dependency on ruby-kafka to ~> 0.4.0 in order to receive bugfixes and security updates. New features will only target version 0.5 and up, which will be incompatible with the Kafka 0.9 protocol.

v0.4

Last stable release with support for the Kafka 0.9 protocol. Bug and security fixes will be released in patch updates.

v0.5

Latest stable release, with native support for the Kafka 0.10 protocol and eventually newer protocol versions. Kafka 0.9 is no longer supported by this release series.

Higher level libraries

Currently, there are three actively developed frameworks based on ruby-kafka, that provide higher level API that can be used to work with Kafka messages and two libraries for publishing messages.

Message processing frameworks

Racecar - A simple framework that integrates with Ruby on Rails to provide a seamless way to write, test, configure, and run Kafka consumers. It comes with sensible defaults and conventions.

Karafka - Framework used to simplify Apache Kafka based Ruby and Rails applications development. Karafka provides higher abstraction layers, including Capistrano, Docker and Heroku support.

Phobos - Micro framework and library for applications dealing with Apache Kafka. It wraps common behaviors needed by consumers and producers in an easy and convenient API.

Message publishing libraries

DeliveryBoy – A library that integrates with Ruby on Rails, making it easy to publish Kafka messages from any Rails application.

WaterDrop – A library for Ruby and Ruby on Rails applications, to easy publish Kafka messages in both sync and async way.

Why Create A New Library?

There are a few existing Kafka clients in Ruby:

  • Poseidon seems to work for Kafka 0.8, but the project is unmaintained and has known issues.
  • Hermann wraps the C library librdkafka and seems to be very efficient, but its API and mode of operation is too intrusive for our needs.
  • jruby-kafka is a great option if you're running on JRuby.

We needed a robust client that could be used from our existing Ruby apps, allowed our Ops to monitor operation, and provided flexible error handling. There didn't exist such a client, hence this project.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/zendesk/ruby-kafka.

Author: Zendesk
Source Code: https://github.com/zendesk/ruby-kafka 
License: Apache-2.0 license

#ruby #kafka 

Ruby-kafka: A Ruby Client Library for Apache Kafka
Royce  Reinger

Royce Reinger

1659707040

Phobos: Simplifying Kafka for Ruby Apps

Phobos

Simplifying Kafka for Ruby apps!

Phobos is a micro framework and library for applications dealing with Apache Kafka.

  • It wraps common behaviors needed by consumers and producers in an easy and convenient API
  • It uses ruby-kafka as its Kafka client and core component
  • It provides a CLI for starting and stopping a standalone application ready to be used for production purposes

Why Phobos? Why not ruby-kafka directly? Well, ruby-kafka is just a client. You still need to write a lot of code to manage proper consuming and producing of messages. You need to do proper message routing, error handling, retrying, backing off and maybe logging/instrumenting the message management process. You also need to worry about setting up a platform independent test environment that works on CI as well as any local machine, and even on your deployment pipeline. Finally, you also need to consider how to deploy your app and how to start it.

With Phobos by your side, all this becomes smooth sailing.

Installation

Add this line to your application's Gemfile:

gem 'phobos'

And then execute:

$ bundle

Or install it yourself as:

$ gem install phobos

Usage

Phobos can be used in two ways: as a standalone application or to support Kafka features in your existing project - including Rails apps. It provides a CLI tool to run it.

Standalone apps

Standalone apps have benefits such as individual deploys and smaller code bases. If consuming from Kafka is your version of microservices, Phobos can be of great help.

Setup

To create an application with Phobos you need two things:

  • A configuration file (more details in the Configuration file section)
  • A phobos_boot.rb (or the name of your choice) to properly load your code into Phobos executor

Use the Phobos CLI command init to bootstrap your application. Example:

# call this command inside your app folder
$ phobos init
    create  config/phobos.yml
    create  phobos_boot.rb

phobos.yml is the configuration file and phobos_boot.rb is the place to load your code.

Consumers (listeners and handlers)

In Phobos apps listeners are configured against Kafka - they are our consumers. A listener requires a handler (a ruby class where you should process incoming messages), a Kafka topic, and a Kafka group_id. Consumer groups are used to coordinate the listeners across machines. We write the handlers and Phobos makes sure to run them for us. An example of a handler is:

class MyHandler
  include Phobos::Handler

  def consume(payload, metadata)
    # payload  - This is the content of your Kafka message, Phobos does not attempt to
    #            parse this content, it is delivered raw to you
    # metadata - A hash with useful information about this event, it contains: The event key,
    #            partition number, offset, retry_count, topic, group_id, and listener_id
  end
end

Writing a handler is all you need to allow Phobos to work - it will take care of execution, retries and concurrency.

To start Phobos the start command is used, example:

$ phobos start
[2016-08-13T17:29:59:218+0200Z] INFO  -- Phobos : <Hash> {:message=>"Phobos configured", :env=>"development"}
______ _           _
| ___ \ |         | |
| |_/ / |__   ___ | |__   ___  ___
|  __/| '_ \ / _ \| '_ \ / _ \/ __|
| |   | | | | (_) | |_) | (_) \__ \
\_|   |_| |_|\___/|_.__/ \___/|___/

phobos_boot.rb - find this file at ~/Projects/example/phobos_boot.rb

[2016-08-13T17:29:59:272+0200Z] INFO  -- Phobos : <Hash> {:message=>"Listener started", :listener_id=>"6d5d2c", :group_id=>"test-1", :topic=>"test"}

By default, the start command will look for the configuration file at config/phobos.yml and it will load the file phobos_boot.rb if it exists. In the example above all example files generated by the init command are used as is. It is possible to change both files, use -c for the configuration file and -b for the boot file. Example:

$ phobos start -c /var/configs/my.yml -b /opt/apps/boot.rb

You may also choose to configure phobos with a hash from within your boot file. In this case, disable loading the config file with the --skip-config option:

$ phobos start -b /opt/apps/boot.rb --skip-config

Consuming messages from Kafka

Messages from Kafka are consumed using handlers. You can use Phobos executors or include it in your own project as a library, but handlers will always be used. To create a handler class, simply include the module Phobos::Handler. This module allows Phobos to manage the life cycle of your handler.

A handler is required to implement the method #consume(payload, metadata).

Instances of your handler will be created for every message, so keep a constructor without arguments. If consume raises an exception, Phobos will retry the message indefinitely, applying the back off configuration presented in the configuration file. The metadata hash will contain a key called retry_count with the current number of retries for this message. To skip a message, simply return from #consume.

The metadata hash will also contain a key called headers with the headers of the consumed message.

When the listener starts, the class method .start will be called with the kafka_client used by the listener. Use this hook as a chance to setup necessary code for your handler. The class method .stop will be called during listener shutdown.

class MyHandler
  include Phobos::Handler

  def self.start(kafka_client)
    # setup handler
  end

  def self.stop
    # teardown
  end

  def consume(payload, metadata)
    # consume or skip message
  end
end

It is also possible to control the execution of #consume with the method #around_consume(payload, metadata). This method receives the payload and metadata, and then invokes #consume method by means of a block; example:

class MyHandler
  include Phobos::Handler

  def around_consume(payload, metadata)
    Phobos.logger.info "consuming..."
    output = yield payload, metadata
    Phobos.logger.info "done, output: #{output}"
  end

  def consume(payload, metadata)
    # consume or skip message
  end
end

Note: around_consume was previously defined as a class method. The current code supports both implementations, giving precendence to the class method, but future versions will no longer support .around_consume.

class MyHandler
  include Phobos::Handler

  def self.around_consume(payload, metadata)
    Phobos.logger.info "consuming..."
    output = yield payload, metadata
    Phobos.logger.info "done, output: #{output}"
  end

  def consume(payload, metadata)
    # consume or skip message
  end
end

Take a look at the examples folder for some ideas.

The hander life cycle can be illustrated as:

.start -> #consume -> .stop

or optionally,

.start -> #around_consume [ #consume ] -> .stop

Batch Consumption

In addition to the regular handler, Phobos provides a BatchHandler. The basic ideas are identical, except that instead of being passed a single message at a time, the BatchHandler is passed a batch of messages. All methods follow the same pattern as the regular handler except that they each end in _batch and are passed an array of Phobos::BatchMessages instead of a single payload.

To enable handling of batches on the consumer side, you must specify a delivery method of inline_batch in phobos.yml, and your handler must include BatchHandler. Using a delivery method of batch assumes that you are still processing the messages one at a time and should use Handler.

When using inline_batch, each instance of Phobos::BatchMessage will contain an instance method headers with the headers for that message.

class MyBatchHandler
  include Phobos::BatchHandler

  def around_consume_batch(payloads, metadata)
    payloads.each do |p|
      p.payload[:timestamp] = Time.zone.now
    end

    yield payloads, metadata
  end

  def consume_batch(payloads, metadata)
    payloads.each do |p|
      logger.info("Got payload #{p.payload}, #{p.partition}, #{p.offset}, #{p.key}, #{p.payload[:timestamp]}")
    end
  end

end

Note that retry logic will happen on the batch level in this case. If you are processing messages individually and an error happens in the middle, Phobos's retry logic will retry the entire batch. If this is not the behavior you want, consider using batch instead of inline_batch.

Producing messages to Kafka

ruby-kafka provides several options for publishing messages, Phobos offers them through the module Phobos::Producer. It is possible to turn any ruby class into a producer (including your handlers), just include the producer module, example:

class MyProducer
  include Phobos::Producer
end

Phobos is designed for multi threading, thus the producer is always bound to the current thread. It is possible to publish messages from objects and classes, pick the option that suits your code better. The producer module doesn't pollute your classes with a thousand methods, it includes a single method the class and in the instance level: producer.

my = MyProducer.new
my.producer.publish(topic: 'topic', payload: 'message-payload', key: 'partition and message key')

# The code above has the same effect of this code:
MyProducer.producer.publish(topic: 'topic', payload: 'message-payload', key: 'partition and message key')

The signature for the publish method is as follows:

def publish(topic: topic, payload: payload, key: nil, partition_key: nil, headers: nil)

When publishing a message with headers, the headers argument must be a hash:

my = MyProducer.new
my.producer.publish(topic: 'topic', payload: 'message-payload', key: 'partition and message key', headers: { header_1: 'value 1' })

It is also possible to publish several messages at once:

MyProducer
  .producer
  .publish_list([
    { topic: 'A', payload: 'message-1', key: '1' },
    { topic: 'B', payload: 'message-2', key: '2' },
    { topic: 'B', payload: 'message-3', key: '3', headers: { header_1: 'value 1', header_2: 'value 2' } }
  ])

There are two flavors of producers: regular producers and async producers.

Regular producers will deliver the messages synchronously and disconnect, it doesn't matter if you use publish or publish_list; by default, after the messages get delivered the producer will disconnect.

Async producers will accept your messages without blocking, use the methods async_publish and async_publish_list to use async producers.

An example of using handlers to publish messages:

class MyHandler
  include Phobos::Handler
  include Phobos::Producer

  PUBLISH_TO = 'topic2'

  def consume(payload, metadata)
    producer.async_publish(topic: PUBLISH_TO, payload: {key: 'value'}.to_json)
  end
end

Note about configuring producers

Since the handler life cycle is managed by the Listener, it will make sure the producer is properly closed before it stops. When calling the producer outside a handler remember, you need to shutdown them manually before you close the application. Use the class method async_producer_shutdown to safely shutdown the producer.

Without configuring the Kafka client, the producers will create a new one when needed (once per thread). To disconnect from kafka call kafka_client.close.

# This method will block until everything is safely closed
MyProducer
  .producer
  .async_producer_shutdown

MyProducer
  .producer
  .kafka_client
  .close

Note about producers with persistent connections

By default, regular producers will automatically disconnect after every publish call. You can change this behavior (which reduces connection overhead, TLS etc - which increases speed significantly) by setting the persistent_connections config in phobos.yml. When set, regular producers behave identically to async producers and will also need to be shutdown manually using the sync_producer_shutdown method.

Since regular producers with persistent connections have open connections, you need to manually disconnect from Kafka when ending your producers' life cycle:

MyProducer
  .producer
  .sync_producer_shutdown

Phobos as a library in an existing project

When running as a standalone service, Phobos sets up a Listener and Executor for you. When you use Phobos as a library in your own project, you need to set these components up yourself.

First, call the method configure with the path of your configuration file or with configuration settings hash.

Phobos.configure('config/phobos.yml')

or

Phobos.configure(kafka: { client_id: 'phobos' }, logger: { file: 'log/phobos.log' })

Listener connects to Kafka and acts as your consumer. To create a listener you need a handler class, a topic, and a group id.

listener = Phobos::Listener.new(
  handler: Phobos::EchoHandler,
  group_id: 'group1',
  topic: 'test'
)

# start method blocks
Thread.new { listener.start }

listener.id # 6d5d2c (all listeners have an id)
listener.stop # stop doesn't block

This is all you need to consume from Kafka with back off retries.

An executor is the supervisor of all listeners. It loads all listeners configured in phobos.yml. The executor keeps the listeners running and restarts them when needed.

executor = Phobos::Executor.new

# start doesn't block
executor.start

# stop will block until all listers are properly stopped
executor.stop

When using Phobos executors you don't care about how listeners are created, just provide the configuration under the listeners section in the configuration file and you are good to go.

Configuration file

The configuration file is organized in 6 sections. Take a look at the example file, config/phobos.yml.example.

The file will be parsed through ERB so ERB syntax/file extension is supported beside the YML format.

logger configures the logger for all Phobos components. It automatically outputs to STDOUT and it saves the log in the configured file.

kafka provides configurations for every Kafka::Client created over the application. All options supported by ruby-kafka can be provided.

producer provides configurations for all producers created over the application, the options are the same for regular and async producers. All options supported by ruby-kafka can be provided. If the kafka key is present under producer, it is merged into the top-level kafka, allowing different connection configuration for producers.

consumer provides configurations for all consumer groups created over the application. All options supported by ruby-kafka can be provided. If the kafka key is present under consumer, it is merged into the top-level kafka, allowing different connection configuration for consumers.

backoff Phobos provides automatic retries for your handlers. If an exception is raised, the listener will retry following the back off configured here. Backoff can also be configured per listener.

listeners is the list of listeners configured. Each listener represents a consumer group.

Additional listener configuration

In some cases it's useful to share most of the configuration between multiple phobos processes, but have each process run different listeners. In that case, a separate yaml file can be created and loaded with the -l flag. Example:

$ phobos start -c /var/configs/my.yml -l /var/configs/additional_listeners.yml

Note that the config file must still specify a listeners section, though it can be empty.

Custom configuration/logging

Phobos can be configured using a hash rather than the config file directly. This can be useful if you want to do some pre-processing before sending the file to Phobos. One particularly useful aspect is the ability to provide Phobos with a custom logger, e.g. by reusing the Rails logger:

Phobos.configure(
  custom_logger: Rails.logger,
  custom_kafka_logger: Rails.logger
)

If these keys are given, they will override the logger keys in the Phobos config file.

Instrumentation

Some operations are instrumented using Active Support Notifications.

In order to receive notifications you can use the module Phobos::Instrumentation, example:

Phobos::Instrumentation.subscribe('listener.start') do |event|
  puts(event.payload)
end

Phobos::Instrumentation is a convenience module around ActiveSupport::Notifications, feel free to use it or not. All Phobos events are in the phobos namespace. Phobos::Instrumentation will always look at phobos. events.

Executor notifications

  • executor.retry_listener_error is sent when the listener crashes and the executor wait for a restart. It includes the following payload:
    • listener_id
    • retry_count
    • waiting_time
    • exception_class
    • exception_message
    • backtrace
  • executor.stop is sent when executor stops

Listener notifications

  • listener.start_handler is sent when invoking handler.start(kafka_client). It includes the following payload:
    • listener_id
    • group_id
    • topic
    • handler
  • listener.start is sent when listener starts. It includes the following payload:
    • listener_id
    • group_id
    • topic
    • handler
  • listener.process_batch is sent after process a batch. It includes the following payload:
    • listener_id
    • group_id
    • topic
    • handler
    • batch_size
    • partition
    • offset_lag
    • highwater_mark_offset
  • listener.process_message is sent after processing a message. It includes the following payload:
    • listener_id
    • group_id
    • topic
    • handler
    • key
    • partition
    • offset
    • retry_count
  • listener.process_batch_inline is sent after processing a batch with batch_inline mode. It includes the following payload:
    • listener_id
    • group_id
    • topic
    • handler
    • batch_size
    • partition
    • offset_lag
    • retry_count
  • listener.retry_handler_error is sent after waiting for handler#consume retry. It includes the following payload:
    • listener_id
    • group_id
    • topic
    • handler
    • key
    • partition
    • offset
    • retry_count
    • waiting_time
    • exception_class
    • exception_message
    • backtrace
  • listener.retry_handler_error_batch is sent after waiting for handler#consume_batch retry. It includes the following payload:
    • listener_id
    • group_id
    • topic
    • handler
    • batch_size
    • partition
    • offset_lag
    • retry_count
    • waiting_time
    • exception_class
    • exception_message
    • backtrace
  • listener.retry_aborted is sent after waiting for a retry but the listener was stopped before the retry happened. It includes the following payload:
    • listener_id
    • group_id
    • topic
    • handler
  • listener.stopping is sent when the listener receives signal to stop.
    • listener_id
    • group_id
    • topic
    • handler
  • listener.stop_handler is sent after stopping the handler.
    • listener_id
    • group_id
    • topic
    • handler
  • listener.stop is send after stopping the listener.
    • listener_id
    • group_id
    • topic
    • handler

Plugins

List of gems that enhance Phobos:

Phobos DB Checkpoint is drop in replacement to Phobos::Handler, extending it with the following features:

  • Persists your Kafka events to an active record compatible database
  • Ensures that your handler will consume messages only once
  • Allows your system to quickly reprocess events in case of failures

Phobos Checkpoint UI gives your Phobos DB Checkpoint powered app a web gui with the features below. Maintaining a Kafka consumer app has never been smoother:

  • Search events and inspect payload
  • See failures and retry / delete them

Phobos Prometheus adds prometheus metrics to your phobos consumer.

  • Measures total messages and batches processed
  • Measures total duration needed to process each message (and batch)
  • Adds /metrics endpoit to scrape data

Development

After checking out the repo:

  • make sure docker is installed and running (for windows and mac this also includes docker-compose).
  • Linux: make sure docker-compose is installed and running.
  • run bin/setup to install dependencies
  • run docker-compose up -d --force-recreate kafka zookeeper to start the required kafka containers
  • run tests to confirm no environmental issues
    • wait a few seconds for kafka broker to get set up - sleep 30
    • run docker-compose run --rm test
    • make sure it reports X examples, 0 failures

You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Test

Phobos exports a spec helper that can help you test your consumer. The Phobos lifecycle will conveniently be activated for you with minimal setup required.

  • process_message(handler:, payload:, metadata: {}, encoding: nil) - Invokes your handler with payload and metadata, using a dummy listener (encoding and metadata are optional).
### spec_helper.rb
require 'phobos/test/helper'
RSpec.configure do |config|
  config.include Phobos::Test::Helper
  config.before(:each) do
    Phobos.configure(path_to_my_config_file)
  end
end 

### Spec file
describe MyConsumer do
  let(:payload) { 'foo' }
  let(:metadata) { Hash(foo: 'bar') }

  it 'consumes my message' do
    expect_any_instance_of(described_class).to receive(:around_consume).with(payload, metadata).once.and_call_original
    expect_any_instance_of(described_class).to receive(:consume).with(payload, metadata).once.and_call_original

    process_message(handler: described_class, payload: payload, metadata: metadata)
  end
end

Upgrade Notes

Version 2.0 removes deprecated ways of defining producers and consumers:

  • The before_consume method has been removed. You can have this behavior in the first part of an around_consume method.
  • around_consume is now only available as an instance method, and it must yield the values to pass to the consume method.
  • publish and async_publish now only accept keyword arguments, not positional arguments.

Example pre-2.0:

class MyHandler
  include Phobos::Handler

  def before_consume(payload, metadata)
    payload[:id] = 1
  end

  def self.around_consume(payload, metadata)
    metadata[:key] = 5
    yield
  end
end

In 2.0:

class MyHandler
  include Phobos::Handler

  def around_consume(payload, metadata)
    new_payload = payload.dup
    new_metadata = metadata.dup
    new_payload[:id] = 1
    new_metadata[:key] = 5
    yield new_payload, new_metadata
  end
end

Producer, 1.9:

  producer.publish('my-topic', { payload_value: 1}, 5, 3, {header_val: 5})

Producer 2.0:

  producer.publish(topic: 'my-topic', payload: { payload_value: 1}, key: 5, 
     partition_key: 3, headers: { header_val: 5})

Version 1.8.2 introduced a new persistent_connections setting for regular producers. This reduces the number of connections used to produce messages and you should consider setting it to true. This does require a manual shutdown call - please see Producers with persistent connections.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/klarna/phobos.

Linting

Phobos projects Rubocop to lint the code, and in addition all projects use Rubocop Rules to maintain a shared rubocop configuration. Updates to the shared configurations are done in phobos/shared repo, where you can also find instructions on how to apply the new settings to the Phobos projects.

Acknowledgements

Thanks to Sebastian Norde for the awesome logo!

Author: Phobos
Source Code: https://github.com/phobos/phobos 
License: Apache-2.0 license

#ruby #kafka 

Phobos: Simplifying Kafka for Ruby Apps

Spring Boot Debezium Kafka PostgreSQL Relationship

spring-boot-debezium-db-kafka

Spring-Boot-Debezium-Kafka-PostgreSQL Relationship

Installation

  • First,Configure docker-compose.yml
  • Second,Write Db-Kafka configuration to json file.
  • Third,Designed .sh file for DB-Kafka configuration file and Kafka-Connector

Using

  • First,docker-compose up -d
  • Second
  sh connectionCommand.sh

DebeziumCommand sh file

  • Third, Get Kafka-Drop and Looking heatbeat connection

KafkaDrop

  • Fourth,Open Spring Boot Project and Post a Request

image

  • Fifth,Get Kafka-Drop and Lookind our cdc connection

image

  • Sixth Click our cdc connection topics and looking View Messages

image

If we do not want to see null in the Update request, we enter this command

ALTER TABLE public.vehicles REPLICA IDENTITY FULL

Muharrem Koç

Download details:
Author: muharremkoc
Source code: https://github.com/muharremkoc/spring-boot-debezium-db-kafka
License:

#spring #java #springboot #kafka 

Spring Boot Debezium Kafka PostgreSQL Relationship

How to Use Kafka with Spring Boot in Microservices

Description

There are three microservices:
order-service - it sends Order events to the Kafka topic and orchestrates the process of a distributed transaction
payment-service - it performs local transaction on the customer account basing on the Order price
stock-service - it performs local transaction on the store basing on number of products in the Order

Here's the diagram with our architecture:

image

(1) order-service send a new Order -> status == NEW
(2) payment-service and stock-service receive Order and handle it by performing a local transaction on the data
(3) payment-service and stock-service send a reponse Order -> status == ACCEPT or status == REJECT
(4) order-service process incoming stream of orders from payment-service and stock-service, join them by Order id and sends Order with a new status -> status == CONFIRMATION or status == ROLLBACK or status == REJECTED
(5) payment-service and stock-service receive Order with a final status and "commit" or "rollback" a local transaction make before

Articles

This repository is used as the example for the following articles:

  1. Distributed Transactions in Microservices with Kafka Streams and Spring Boot - how to implement distributed transaction based on the SAGA pattern with Spring Boot and Kafka Streams
  2. Deep Dive into Saga Transactions with Kafka Streams and Spring Boot - how to implement distributed transaction based on the SAGA pattern with Spring Boot and fully Kafka Streams KStream and KTable. You need to switch to the streams-full branch.

Download details:
Author: piomin
Source code: https://github.com/piomin/sample-spring-kafka-microservices?
License:

#spring #java #springboot #kafka #microservices 

How to Use Kafka with Spring Boot in Microservices
高橋  陽子

高橋 陽子

1659107406

如何在 Quarkus 中使用 Kafka 流

在本文中,您將學習如何將 Kafka Streams 與 Quarkus 一起使用。我們將使用 Quarkus 而不是 Spring Cloud。如果您想了解什麼是流媒體平台以及它與傳統消息代理有何不同,本文適合您。此外,我們將研究 Quarkus 提供的與 Apache Kafka 相關的有用改進。

建築學

在我們的例子中,有兩個傳入的事件流。它們都代表傳入的訂單。這些訂單由 order-service 應用程序生成。它向主題發送買單,向 orders.buy 主題發送賣單 orders.sell 。然後, stock-service 應用程序接收並處理傳入事件。第一步,它需要將每條消息的密鑰從 更改 orderId 為 productId。這是因為它必須連接來自與同一產品相關的不同主題的訂單才能執行交易。最後,交易價格是買賣價格的平均值。

quarkus-kafka-streams-拱

我們正在構建一個簡化版的股票市場平台。每個購買訂單都包含客戶期望購買產品的最高價格。另一方面,每個銷售訂單都包含客戶準備出售其產品的最低價格。如果特定產品的賣單價格不高於買單價格,我們將執行交易。

每個訂單的有效期為 10 秒。在那之後, stock-service 應用程序將不會處理這樣的訂單,因為它被認為是過期的。每個訂單包含許多用於交易的產品。例如,我們可能以 10 的價格賣出 100 或以 11 的價格買入 200。因此,訂單可能會全部或部分實現。stock-service 應用程序嘗試將部分實現的訂單連接到其他新的或部分實現的訂單。 您可以在下圖中看到該過程的可視化。

quarkus-kafka-streams 應用程序

在本地運行 Apache Kafka

在我們跳轉到實現之前,我們需要運行 Apache Kafka 的本地實例。如果您不想在筆記本電腦上安裝它,最好的運行方式是使用 Redpanda。Redpanda 是一個兼容 Kafka API 的流媒體平台。與 Kafka 相比,在本地運行它相對容易。通常,您必須在筆記本電腦上安裝 Redpanda,然後使用他們的 CLI 創建集群。但是有了 Quarkus,您就不需要這樣做了!唯一的要求是安裝 Docker。由於 Quarkus Kafka 擴展和名為 Dev Services 的功能,它會在開發模式和運行測試時自動啟動 Kafka 代理。此外,應用程序是自動配置的。

為了啟用該功能,您唯一需要做的就是不在配置屬性中提供任何 Kafka 地址。開發服務使用Testcontainers運行 Kafka,因此如果您有 Docker 或任何其他支持 Testcontainers 運行的環境,您將獲得開箱即用的 Kafka 容器化實例。另一個重要的事情。首先,啟動order-service應用程序。它會自動在 Kafka 中創建所有必需的主題。然後運行stock-service應用程序。它使用 Quarkus Kafka Streams 擴展並驗證是否存在所需的主題。讓我們形象化它。

quarkus-kafka-streams-運行

使用 Quarkus 向 Kafka 發送事件

有幾種方法可以使用 Quarkus 向 Kafka 發送事件。因為我們需要發送鍵/值對,所以我們將使用該io.smallrye.reactive.messaging.kafka.Record對象。Quarkus 能夠連續生成和發送數據。Order在下面可見的代碼片段中,我們每 500 毫秒發送一個事件。每個都Order包含一個隨機productId,priceproductCount

@Outgoing("orders-buy")
public Multi<Record<Long, Order>> buyOrdersGenerator() {
   return Multi.createFrom().ticks().every(Duration.ofMillis(500))
      .map(order -> {
         Integer productId = random.nextInt(10) + 1;
         int price = prices.get(productId) + random.nextInt(200);
         Order o = new Order(
             incrementOrderId(),
             random.nextInt(1000) + 1,
             productId,
             100 * (random.nextInt(5) + 1),
             LocalDateTime.now(),
             OrderType.BUY,
             price);
         log.infof("Sent: %s", o);
         return Record.of(o.getId(), o);
   });
}

@Outgoing("orders-sell")
public Multi<Record<Long, Order>> sellOrdersGenerator() {
   return Multi.createFrom().ticks().every(Duration.ofMillis(500))
      .map(order -> {
         Integer productId = random.nextInt(10) + 1;
         int price = prices.get(productId) + random.nextInt(200);
         Order o = new Order(
             incrementOrderId(),
             random.nextInt(1000) + 1,
             productId,
             100 * (random.nextInt(5) + 1),
             LocalDateTime.now(),
             OrderType.SELL,
             price);
         log.infof("Sent: %s", o);
         return Record.of(o.getId(), o);
   });
}

我們還將定義一個@Incoming通道以接收由stock-service. 由於 Quarkus 將自動創建transactionsQuarkus Kafka Streams 使用的主題stock-service。老實說,我無法強制 Quarkus Kafka Streams 擴展自動創建主題。看來我們需要為此使用 SmallRye Reactive Messaging 擴展。

@Incoming("transactions")
public void transactions(Transaction transaction) {
   log.infof("New: %s", transaction);
}

當然,我們需要在 Maven 中包含 SmallRye Reactive Messaging 依賴項pom.xml

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
</dependency>

最後,讓我們提供配置設置。我們有兩個傳出主題和一個傳入主題。我們可以設置他們的名字。否則,Quarkus 使用與通道名稱相同的名稱。我們的主題名稱是orders.buy和。order.selltransactions

mp.messaging.outgoing.orders-buy.connector = smallrye-kafka
mp.messaging.outgoing.orders-buy.topic = orders.buy
mp.messaging.outgoing.orders-buy.key.serializer = org.apache.kafka.common.serialization.LongSerializer
mp.messaging.outgoing.orders-buy.value.serializer = io.quarkus.kafka.client.serialization.ObjectMapperSerializer

mp.messaging.outgoing.orders-sell.connector = smallrye-kafka
mp.messaging.outgoing.orders-sell.topic = orders.sell
mp.messaging.outgoing.orders-sell.key.serializer = org.apache.kafka.common.serialization.LongSerializer
mp.messaging.outgoing.orders-sell.value.serializer = io.quarkus.kafka.client.serialization.ObjectMapperSerializer

mp.messaging.incoming.transactions.connector = smallrye-kafka
mp.messaging.incoming.transactions.topic = transactions
mp.messaging.incoming.transactions.value.deserializer = pl.piomin.samples.streams.order.model.deserializer.TransactionDeserializer

就這樣。我們的訂單生成器已準備就緒。如果您的order-service應用程序 Quarkus 也將運行 Kafka (Redpanda) 實例。但首先,讓我們切換到第二個示例應用程序 - stock-service.

使用 Quarkus 使用 Kafka 流

在上一節中,我們向 Kafka 代理髮送消息。因此,我們使用標準 Quarkus 庫與基於 SmallRye Reactive Messaging 框架的 Kafka 集成。該stock-service應用程序將消息作為流使用,因此現在我們將使用一個模塊來集成 Kafka Streams。

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-kafka-streams</artifactId>
</dependency>

我們的應用程序還使用一個數據庫、一個 ORM 層並包含一些其他有用的模塊。

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

第一步,我們將合併兩個訂單流(買入和賣出),將其 Order 插入數據庫,並打印事件消息。你可能會問——既然我有 Kafka,為什麼我在這裡使用數據庫和 ORM 層 KTable?好吧,我需要有鎖支持的事務來協調訂單實現的狀態(參考介紹中的描述——完全和部分實現的訂單)。我將在下一節中為您提供有關它的更多詳細信息。

為了使用 Quarkus 處理流,我們需要聲明org.apache.kafka.streams.Topologybean。它包含所有的KStreamKTable定義。讓我們從負責從傳入訂單創建和發出交易的部分開始。創建了兩個KStream定義。其中第一個負責將兩個訂單流合併為一個訂單流,然後將新訂單流插入Order數據庫。productId他們中的第二個通過使用密鑰連接兩個流來創建和執行事務。但更多關於它在下一節。

@Produces
public Topology buildTopology() {
   ObjectMapperSerde<Order> orderSerde = 
      new ObjectMapperSerde<>(Order.class);
   ObjectMapperSerde<Transaction> transactionSerde = 
      new ObjectMapperSerde<>(Transaction.class);

   StreamsBuilder builder = new StreamsBuilder();

   KStream<Long, Order> orders = builder.stream(
      ORDERS_SELL_TOPIC,
      Consumed.with(Serdes.Long(), orderSerde));

   builder.stream(ORDERS_BUY_TOPIC, 
         Consumed.with(Serdes.Long(), orderSerde))
      .merge(orders)
      .peek((k, v) -> {
         log.infof("New: %s", v);
         logic.add(v);
      });

   builder.stream(ORDERS_BUY_TOPIC, 
         Consumed.with(Serdes.Long(), orderSerde))
      .selectKey((k, v) -> v.getProductId())
      .join(orders.selectKey((k, v) -> v.getProductId()),
         this::execute,
         JoinWindows.of(Duration.ofSeconds(10)),
         StreamJoined.with(Serdes.Integer(), orderSerde, orderSerde))
      .filterNot((k, v) -> v == null)
      .map((k, v) -> new KeyValue<>(v.getId(), v))
      .peek((k, v) -> log.infof("Done -> %s", v))
      .to(TRANSACTIONS_TOPIC, Produced.with(Serdes.Long(), transactionSerde));

}

要處理流,我們需要添加配置屬性。需要輸入主題列表。我們還可以覆蓋默認應用程序 ID 並啟用 Kafka 健康檢查。

quarkus.kafka-streams.application-id = stock
quarkus.kafka-streams.topics = orders.buy,orders.sell
quarkus.kafka.health.enabled = true

Kafka Streams 上的操作

現在,我們可能會在 Kafka Streams 上使用一些更高級的操作,而不僅僅是合併兩個不同的流。事實上,這是我們應用程序中的一個關鍵邏輯。我們需要將兩個不同的訂單流連接成一個,使用 productId 作為連接鍵。由於生產者設置 orderId 為消息鍵,我們首先需要 為 和  流調用selectKey 方法 。在我們的案例中,加入與同一產品相關的買賣訂單只是第一步。然後我們需要驗證買單的最高價是否不大於賣單的最低價。order.sellorders.buy

下一步是驗證這兩個之前是否沒有實現,因為它們也可能與流中的其他訂單配對。如果所有條件都滿足,我們可以創建一個新的交易。最後,我們可以將流鍵從 productId 更改為 the transactionId 並將其發送到專用 transactions 主題。

每次我們成功加入兩個訂單時,我們都會嘗試創建一個交易。該execute(...)方法在方法內被調用KStream join。首先,我們比較兩個訂單的價格。然後我們通過訪問H2數據庫來驗證兩個訂單的實現狀態。如果訂單仍未完全實現,我們可能會創建一個事務並更新數據庫中的訂單記錄。

private Transaction execute(Order orderBuy, Order orderSell) {
   if (orderBuy.getAmount() >= orderSell.getAmount()) {
      int count = Math.min(orderBuy.getProductCount(), 
                           orderSell.getProductCount());
      boolean allowed = logic
         .performUpdate(orderBuy.getId(), orderSell.getId(), count);
      if (!allowed)
         return null;
      else
         return new Transaction(
            ++transactionId,
            orderBuy.getId(),
            orderSell.getId(),
            count,
            (orderBuy.getAmount() + orderSell.getAmount()) / 2,
            LocalDateTime.now(),
            "NEW"
      );
   } else {
            return null;
   }
}

讓我們仔細看看 performUpdate() 方法內部調用的 execute() 方法。它啟動一個事務並鎖定兩個 Order 實體。然後它會驗證每個訂單的實現狀態,並在可能的情況下使用當前值更新它。只有當 performUpdate() 方法成功完成時,stock-service應用程序才會創建一個新事務。

@ApplicationScoped
public class OrderLogic {

    @Inject
    Logger log;
    @Inject
    OrderRepository repository;

    @Transactional
    public Order add(Order order) {
        repository.persist(order);
        return order;
    }

    @Transactional
    public boolean performUpdate(Long buyOrderId, Long sellOrderId, int amount) {
        Order buyOrder = repository.findById(buyOrderId, 
           LockModeType.PESSIMISTIC_WRITE);
        Order sellOrder = repository.findById(sellOrderId, 
           LockModeType.PESSIMISTIC_WRITE);
        if (buyOrder == null || sellOrder == null)
            return false;
        int buyAvailableCount = 
           buyOrder.getProductCount() - buyOrder.getRealizedCount();
        int sellAvailableCount = 
           sellOrder.getProductCount() - sellOrder.getRealizedCount();
        if (buyAvailableCount >= amount && sellAvailableCount >= amount) {
            buyOrder.setRealizedCount(buyOrder.getRealizedCount() + amount);
            sellOrder.setRealizedCount(sellOrder.getRealizedCount() + amount);
            repository.persist(buyOrder);
            repository.persist(sellOrder);
            return true;
        } else {
            return false;
        }
    }
}

很好🙂這就是我們在練習的第一部分需要做的所有事情。現在我們可以運行我們的兩個示例應用程序。

使用 Quarkus 運行和管理 Kafka Streams 應用程序

正如我之前提到的,我們首先需要啟動order-service. 它運行一個新的 Kafka 實例並創建所有必需的主題。啟動後立即準備發送新訂單。要在本地運行 Quarkus 應用程序,只需轉到order-service目錄並執行以下命令:

$ mvn quarkus:dev

docker ps只是為了驗證您可以使用該命令顯示運行 Docker 容器的列表。這是我的結果:

如您所見,Redpanda 實例正在運行,並且可以在隨機端口上使用49724。Quarkus 為我們做到了。但是,如果您的筆記本電腦上安裝了 Redpanda,您可以使用 CLI 查看創建的主題列表rpk

$ rpk topic list --brokers=127.0.0.1:49724

然後讓我們運行stock-service. 轉到stock-service目錄並mvn quarkus:dev再次運行。啟動後,它只是工作。得益於 Quarkus 開發服務,這兩個應用程序共享同一個實例。現在讓我們訪問位於 的 Quarkus Dev UI 控制台http://localhost:8080/q/dev/。找到標題為“Apache Kafka Streams”的磁貼。

您可以查看我們的 Kafka Streams 拓撲的可視化。我將圖像分成兩部分以獲得更好的可見性。

將 Kafka KTable 與 Quarkus 一起使用

我們已經完成了負責從傳入訂單創建交易的邏輯的實現。在下一步中,我們將對事務流執行分析操作。price * productsCount我們的主要目標是計算每個產品的交易總數、售出/購買的產品總數和交易總值 ( )。這是計算中使用的對像類。

@RegisterForReflection
public class TransactionTotal {
   private int count;
   private int amount;
   private int productCount;

   // GETTERS AND SETTERS
}

因為該對 Transaction 像不包含有關產品的信息,所以我們首先需要加入訂單才能訪問它。然後我們 KTable 按每個 productId 分組和聚合生成一個。之後,我們可能會調用一個聚合方法,該方法允許我們執行一些更複雜的計算。在這種特殊情況下,我們正在計算所有已執行交易的數量、它們的產品數量和總價值。結果 KTable 可以物化為狀態存儲。多虧了這一點,我們將能夠通過TRANSACTIONS_PER_PRODUCT_SUMMARY變量定義的名稱來查詢它。

KeyValueBytesStoreSupplier storePerProductSupplier = Stores.persistentKeyValueStore(
   TRANSACTIONS_PER_PRODUCT_SUMMARY);

builder.stream(TRANSACTIONS_TOPIC, Consumed.with(Serdes.Long(), transactionSerde))
   .selectKey((k, v) -> v.getSellOrderId())
   .join(orders.selectKey((k, v) -> v.getId()),
      (t, o) -> new TransactionWithProduct(t, o.getProductId()),
      JoinWindows.of(Duration.ofSeconds(10)),
      StreamJoined.with(Serdes.Long(), transactionSerde, orderSerde))
   .groupBy((k, v) -> v.getProductId(), Grouped.with(Serdes.Integer(), transactionWithProductSerde))
   .aggregate(
      TransactionTotal::new,
      (k, v, a) -> {
         a.setCount(a.getCount() + 1);
         a.setProductCount(a.getAmount() + v.getTransaction().getAmount());
         a.setAmount(a.getProductCount() +
            (v.getTransaction().getAmount() * v.getTransaction().getPrice()));
         return a;
      },
      Materialized.<Integer, TransactionTotal> as(storePerProductSupplier)
         .withKeySerde(Serdes.Integer())
         .withValueSerde(transactionTotalSerde))
   .toStream()
   .peek((k, v) -> log.infof("Total per product(%d): %s", k, v))
   .to(TRANSACTIONS_PER_PRODUCT_AGGREGATED_TOPIC, 
      Produced.with(Serdes.Integer(), transactionTotalSerde));

這是負責交互式查詢實現的類。它注入KafkaStreamsbean。然後它嘗試根據StockService.TRANSACTIONS_PER_PRODUCT_SUMMARY變量獲取持久存儲。結果,有一個ReadOnlyKeyValueStorewithInteger作為鍵和TransactionTotal一個值。我們可能會返回與特定產品相關的單個值productId( getTransactionsPerProductData) 或僅返回包含所有可用產品結果的列表 ( getAllTransactionsPerProductData)。

@ApplicationScoped
public class InteractiveQueries {

   @Inject
   KafkaStreams streams;

   public TransactionTotal getTransactionsPerProductData(Integer productId) {
      return getTransactionsPerProductStore().get(productId);
   }

   public Map<Integer, TransactionTotal> getAllTransactionsPerProductData() {
      Map<Integer, TransactionTotal> m = new HashMap<>();
      KeyValueIterator<Integer, TransactionTotal> it = getTransactionsPerProductStore().all();
      while (it.hasNext()) {
         KeyValue<Integer, TransactionTotal> kv = it.next();
         m.put(kv.key, kv.value);
      }
      return m;
   }

   private ReadOnlyKeyValueStore<Integer, TransactionTotal> getTransactionsPerProductStore() {
      return streams.store(
         StoreQueryParameters
            .fromNameAndType(StockService.TRANSACTIONS_PER_PRODUCT_SUMMARY, QueryableStoreTypes.keyValueStore()));
   }

}

最後,我們可以創建一個 REST 控制器,負責公開交互式查詢檢索到的數據。

@ApplicationScoped
@Path("/transactions")
public class TransactionResource {

    @Inject
    InteractiveQueries interactiveQueries;

    @GET
    @Path("/products/{id}")
    public TransactionTotal getByProductId(@PathParam("id") Integer productId) {
        return interactiveQueries.getTransactionsPerProductData(productId);
    }

    @GET
    @Path("/products")
    public Map<Integer, TransactionTotal> getAllPerProductId() {
        return interactiveQueries.getAllTransactionsPerProductData();
    }

}

現在您可以輕鬆查看與stock-service. 您只需要調用以下 REST 端點,例如:

$ curl http://localhost:8080/transactions/products
$ curl http://localhost:8080/transactions/products/3
$ curl http://localhost:8080/transactions/products/5

最後的想法

Quarkus 簡化了 Kafka Streams 和交互式查詢的工作。它為開發人員提供了有用的改進,例如在開發和測試模式下自動啟動 Kafka 或在開發 UI 控制台中可視化 Kafka 流。您可以輕鬆地將 Quarkus 方法與 Spring Cloud Stream Kafka 支持進行比較,因為我為這兩個框架實現了相同的邏輯。這是帶有 Spring Cloud Stream Kafka Streams 示例的 GitHub存儲庫。

鏈接:https ://piotrminkowski.com/2021/11/24/kafka-streams-with-quarkus/

#quarkus #java #kafka 

如何在 Quarkus 中使用 Kafka 流
Thierry  Perret

Thierry Perret

1659100165

Comment Utiliser Les Flux Kafka Avec Quarkus

Dans cet article, vous apprendrez à utiliser Kafka Streams avec Quarkus. Nous allons utiliser Quarkus au lieu de Spring Cloud. Si vous souhaitez comprendre ce qu'est une plateforme de streaming et en quoi elle diffère d'un courtier de messages traditionnel, cet article est pour vous. De plus, nous étudierons des améliorations utiles liées à Apache Kafka fournies par Quarkus.

Architecture

Dans notre cas, il y a deux flux entrants d'événements. Les deux représentent les commandes entrantes. Ces commandes sont générées par l'  order-service application. Il envoie des ordres d'achat au  orders.buy sujet et des ordres de vente au  orders.sell sujet. Ensuite, l'  stock-service application reçoit et gère les événements entrants. Dans un premier temps, il doit changer la clé de chaque message du  orderId au  productId. En effet, il doit joindre des commandes de différents sujets liés au même produit afin d'exécuter des transactions. Enfin, le prix de transaction est une moyenne des prix de vente et d'achat.

quarkus-kafka-streams-arch

Nous construisons une version simplifiée de la plateforme boursière. Chaque ordre d'achat contient un prix maximum auquel un client s'attend à acheter un produit. D'autre part, chaque ordre de vente contient un prix minimum auquel un client est prêt à vendre son produit. Si le prix de l'ordre de vente n'est pas supérieur au prix de l'ordre d'achat pour un produit particulier, nous effectuons une transaction.

Chaque commande est valable 10 secondes. Passé ce délai, l'  stock-service application ne traitera pas une telle commande puisqu'elle est considérée comme expirée. Chaque commande contient un certain nombre de produits pour une transaction. Par exemple, nous pouvons vendre 100 pour 10 ou acheter 200 pour 11. Par conséquent, un ordre peut être entièrement ou partiellement réalisé. L'  stock-service application essaie de joindre des commandes partiellement réalisées à d'autres commandes nouvelles ou partiellement réalisées. Vous pouvez voir la visualisation de ce processus dans l'image ci-dessous.

application-quarkus-kafka-streams

Exécutez Apache Kafka localement

Avant de passer à l'implémentation, nous devons exécuter une instance locale d'Apache Kafka. Si vous ne souhaitez pas l'installer sur votre ordinateur portable, la meilleure façon de l'exécuter est avec  Redpanda . Redpanda est une plateforme de streaming compatible avec l'API Kafka. Par rapport à Kafka, il est relativement facile de l'exécuter localement. Normalement, vous devez installer Redpanda sur votre ordinateur portable, puis créer un cluster à l'aide de leur CLI. Mais avec Quarkus, vous n'avez pas besoin de faire cela ! La seule exigence est d'avoir Docker installé. Grâce à l'extension Quarkus Kafka et à la fonctionnalité appelée Dev Services, il démarre automatiquement un courtier Kafka en mode dev et lors de l'exécution de tests. De plus, l'application est configurée automatiquement.

La seule chose que vous devez faire pour activer cette fonctionnalité est de NE PAS fournir d'adresse Kafka dans les propriétés de configuration. Dev Services utilise Testcontainers pour exécuter Kafka, donc si vous avez Docker ou tout autre environnement prenant en charge Testcontainers en cours d'exécution, vous obtenez une instance conteneurisée de Kafka prête à l'emploi. Une autre chose importante. Tout d'abord, lancez l' order-serviceapplication. Il crée automatiquement tous les sujets requis dans Kafka. Exécutez ensuite l' stock-serviceapplication. Il utilise l'extension Quarkus Kafka Streams et vérifie si les rubriques requises existent. Visualisons-le.

quarkus-kafka-streams-run

Envoyer des événements à Kafka avec Quarkus

Il existe plusieurs façons d'envoyer des événements à Kafka avec Quarkus. Parce que nous devons envoyer une paire clé/valeur, nous utiliserons l' io.smallrye.reactive.messaging.kafka.Recordobjet pour cela. Quarkus est capable de générer et d'envoyer des données en continu. Dans le fragment de code visible ci-dessous, nous envoyons un seul Orderévénement toutes les 500 ms. Chacun Ordercontient un aléatoire productId, priceet productCount.

@Outgoing("orders-buy")
public Multi<Record<Long, Order>> buyOrdersGenerator() {
   return Multi.createFrom().ticks().every(Duration.ofMillis(500))
      .map(order -> {
         Integer productId = random.nextInt(10) + 1;
         int price = prices.get(productId) + random.nextInt(200);
         Order o = new Order(
             incrementOrderId(),
             random.nextInt(1000) + 1,
             productId,
             100 * (random.nextInt(5) + 1),
             LocalDateTime.now(),
             OrderType.BUY,
             price);
         log.infof("Sent: %s", o);
         return Record.of(o.getId(), o);
   });
}

@Outgoing("orders-sell")
public Multi<Record<Long, Order>> sellOrdersGenerator() {
   return Multi.createFrom().ticks().every(Duration.ofMillis(500))
      .map(order -> {
         Integer productId = random.nextInt(10) + 1;
         int price = prices.get(productId) + random.nextInt(200);
         Order o = new Order(
             incrementOrderId(),
             random.nextInt(1000) + 1,
             productId,
             100 * (random.nextInt(5) + 1),
             LocalDateTime.now(),
             OrderType.SELL,
             price);
         log.infof("Sent: %s", o);
         return Record.of(o.getId(), o);
   });
}

Nous définirons également un @Incomingcanal unique afin de recevoir les transactions produites par le stock-service. Grâce à cela, Quarkus créera automatiquement le sujet transactionsutilisé par Quarkus Kafka Streams dans stock-service. Pour être honnête, je n'ai pas pu forcer l'extension Quarkus Kafka Streams à créer le sujet automatiquement. Il semble que nous devions utiliser l'extension SmallRye Reactive Messaging pour cela.

@Incoming("transactions")
public void transactions(Transaction transaction) {
   log.infof("New: %s", transaction);
}

Bien sûr, nous devons inclure la dépendance SmallRye Reactive Messaging au Maven pom.xml.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
</dependency>

Enfin, fournissons les paramètres de configuration. Nous avons deux sujets sortants et un seul sujet entrant. Nous pouvons définir leurs noms. Sinon, Quarkus utilise le même nom que le nom du canal. Les noms de nos sujets sont orders.buy, order.sellet transactions.

mp.messaging.outgoing.orders-buy.connector = smallrye-kafka
mp.messaging.outgoing.orders-buy.topic = orders.buy
mp.messaging.outgoing.orders-buy.key.serializer = org.apache.kafka.common.serialization.LongSerializer
mp.messaging.outgoing.orders-buy.value.serializer = io.quarkus.kafka.client.serialization.ObjectMapperSerializer

mp.messaging.outgoing.orders-sell.connector = smallrye-kafka
mp.messaging.outgoing.orders-sell.topic = orders.sell
mp.messaging.outgoing.orders-sell.key.serializer = org.apache.kafka.common.serialization.LongSerializer
mp.messaging.outgoing.orders-sell.value.serializer = io.quarkus.kafka.client.serialization.ObjectMapperSerializer

mp.messaging.incoming.transactions.connector = smallrye-kafka
mp.messaging.incoming.transactions.topic = transactions
mp.messaging.incoming.transactions.value.deserializer = pl.piomin.samples.streams.order.model.deserializer.TransactionDeserializer

C'est tout. Notre générateur de commandes est prêt. Si vous utilisez l' order-serviceapplication, Quarkus exécutera également l'instance Kafka (Redpanda). Mais d'abord, passons au deuxième exemple d'application - stock-service.

Consommer des flux Kafka avec Quarkus

Dans la section précédente, nous envoyions des messages au courtier Kafka. Par conséquent, nous avons utilisé une bibliothèque Quarkus standard pour l'intégration avec Kafka basée sur le framework SmallRye Reactive Messaging. L' stock-serviceapplication consomme des messages sous forme de flux, nous allons donc maintenant utiliser un module pour l'intégration de Kafka Streams.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-kafka-streams</artifactId>
</dependency>

Notre application utilise également une base de données, une couche ORM et inclut quelques autres modules utiles.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

Dans un premier temps, nous allons fusionner les deux flux d'ordres (achat et vente), insérer le  Order dans la base de données et imprimer le message d'événement. Vous pourriez demander - pourquoi j'utilise la base de données et la couche ORM ici puisque j'ai Kafka  KTable? Eh bien, j'ai besoin de transactions avec support de verrouillage afin de coordonner l'état de réalisation de la commande (reportez-vous à la description dans l'introduction - commandes entièrement et partiellement réalisées). Je vous donnerai plus de détails à ce sujet dans les sections suivantes.

Afin de traiter les flux avec Quarkus, nous devons déclarer le org.apache.kafka.streams.Topologybean. Il contient toutes les définitions KStreamet . KTableCommençons par la partie responsable de la création et de l'émission des transactions à partir des commandes entrantes. Deux KStreamdéfinitions sont créées. Le premier d'entre eux est chargé de fusionner deux flux de commandes en un seul, puis d'en insérer un nouveau Orderdans une base de données. Le second d'entre eux crée et exécute des transactions en joignant deux flux à l'aide de la productIdclé. Mais plus à ce sujet dans la section suivante.

@Produces
public Topology buildTopology() {
   ObjectMapperSerde<Order> orderSerde = 
      new ObjectMapperSerde<>(Order.class);
   ObjectMapperSerde<Transaction> transactionSerde = 
      new ObjectMapperSerde<>(Transaction.class);

   StreamsBuilder builder = new StreamsBuilder();

   KStream<Long, Order> orders = builder.stream(
      ORDERS_SELL_TOPIC,
      Consumed.with(Serdes.Long(), orderSerde));

   builder.stream(ORDERS_BUY_TOPIC, 
         Consumed.with(Serdes.Long(), orderSerde))
      .merge(orders)
      .peek((k, v) -> {
         log.infof("New: %s", v);
         logic.add(v);
      });

   builder.stream(ORDERS_BUY_TOPIC, 
         Consumed.with(Serdes.Long(), orderSerde))
      .selectKey((k, v) -> v.getProductId())
      .join(orders.selectKey((k, v) -> v.getProductId()),
         this::execute,
         JoinWindows.of(Duration.ofSeconds(10)),
         StreamJoined.with(Serdes.Integer(), orderSerde, orderSerde))
      .filterNot((k, v) -> v == null)
      .map((k, v) -> new KeyValue<>(v.getId(), v))
      .peek((k, v) -> log.infof("Done -> %s", v))
      .to(TRANSACTIONS_TOPIC, Produced.with(Serdes.Long(), transactionSerde));

}

Pour traiter les flux, nous devons ajouter des propriétés de configuration. Une liste de sujets d'entrée est requise. Nous pouvons également remplacer un identifiant d'application par défaut et activer la vérification de l'état de Kafka.

quarkus.kafka-streams.application-id = stock
quarkus.kafka-streams.topics = orders.buy,orders.sell
quarkus.kafka.health.enabled = true

Opérations sur les flux Kafka

Maintenant, nous pouvons utiliser des opérations plus avancées sur Kafka Streams que la simple fusion de deux flux différents. En fait, c'est une logique clé dans notre application. Nous devons joindre deux flux de commandes différents en un seul en utilisant le  productId comme clé de jonction. Étant donné que le producteur définit  orderId une clé de message, nous devons d'abord appeler la  selectKey méthode pour les   flux order.sell et  . orders.buyDans notre cas, joindre des ordres d'achat et de vente liés au même produit n'est qu'une première étape. Ensuite, nous devons vérifier si le prix maximum dans l'ordre d'achat n'est pas supérieur au prix minimum dans l'ordre de vente.

L'étape suivante consiste à vérifier si ces deux éléments n'ont pas été réalisés auparavant, car ils peuvent également être associés à d'autres commandes dans le flux. Si toutes les conditions sont remplies, nous pouvons créer une nouvelle transaction. Enfin, nous pouvons changer une clé de flux de productId en the  transactionId et l'envoyer au sujet dédié  transactions .

Chaque fois que nous réussissons à joindre deux commandes, nous essayons de créer une transaction. La execute(...)méthode est appelée dans la KStream joinméthode. Tout d'abord, nous comparons les prix des deux commandes. Ensuite, nous vérifions l'état de réalisation des deux commandes en accédant à la base de données H2. Si les commandes ne sont toujours pas entièrement réalisées, nous pouvons créer une transaction et mettre à jour les enregistrements de commandes dans la base de données.

private Transaction execute(Order orderBuy, Order orderSell) {
   if (orderBuy.getAmount() >= orderSell.getAmount()) {
      int count = Math.min(orderBuy.getProductCount(), 
                           orderSell.getProductCount());
      boolean allowed = logic
         .performUpdate(orderBuy.getId(), orderSell.getId(), count);
      if (!allowed)
         return null;
      else
         return new Transaction(
            ++transactionId,
            orderBuy.getId(),
            orderSell.getId(),
            count,
            (orderBuy.getAmount() + orderSell.getAmount()) / 2,
            LocalDateTime.now(),
            "NEW"
      );
   } else {
            return null;
   }
}

Examinons de plus près la  performUpdate() méthode appelée à l'intérieur de la  execute() méthode. Il initie une transaction et verrouille les deux  Order entités. Ensuite, il vérifie l'état de réalisation de chaque commande et le met à jour avec les valeurs actuelles si possible. Ce n'est que si la  performUpdate() méthode se termine avec succès que l' stock-serviceapplication crée une nouvelle transaction.

@ApplicationScoped
public class OrderLogic {

    @Inject
    Logger log;
    @Inject
    OrderRepository repository;

    @Transactional
    public Order add(Order order) {
        repository.persist(order);
        return order;
    }

    @Transactional
    public boolean performUpdate(Long buyOrderId, Long sellOrderId, int amount) {
        Order buyOrder = repository.findById(buyOrderId, 
           LockModeType.PESSIMISTIC_WRITE);
        Order sellOrder = repository.findById(sellOrderId, 
           LockModeType.PESSIMISTIC_WRITE);
        if (buyOrder == null || sellOrder == null)
            return false;
        int buyAvailableCount = 
           buyOrder.getProductCount() - buyOrder.getRealizedCount();
        int sellAvailableCount = 
           sellOrder.getProductCount() - sellOrder.getRealizedCount();
        if (buyAvailableCount >= amount && sellAvailableCount >= amount) {
            buyOrder.setRealizedCount(buyOrder.getRealizedCount() + amount);
            sellOrder.setRealizedCount(sellOrder.getRealizedCount() + amount);
            repository.persist(buyOrder);
            repository.persist(sellOrder);
            return true;
        } else {
            return false;
        }
    }
}

Nice🙂C'est tout ce que nous devons faire dans la première partie de notre exercice. Nous pouvons maintenant exécuter nos deux exemples d'applications.

Exécutez et gérez l'application Kafka Streams avec Quarkus

Comme je l'ai mentionné précédemment, nous devons d'abord démarrer le fichier order-service. Il exécute une nouvelle instance Kafka et crée tous les sujets requis. Immédiatement après le démarrage, il est prêt à envoyer de nouvelles commandes. Pour exécuter l'application Quarkus localement, accédez simplement au order-servicerépertoire et exécutez la commande suivante :

$ mvn quarkus:dev

Juste pour vérifier que vous pouvez afficher une liste exécutant des conteneurs Docker avec la docker pscommande. Voici mon résultat :

Comme vous le voyez, l'instance de Redpanda est en cours d'exécution et elle est disponible sur un port aléatoire 49724. Quarkus l'a fait pour nous. Cependant, si Redpanda est installé sur votre ordinateur portable, vous consultez la liste des sujets créés avec leur CLI rpk:

$ rpk topic list --brokers=127.0.0.1:49724

Alors lançons le stock-service. Allez dans le stock-servicerépertoire et exécutez mvn quarkus:devà nouveau. Après le démarrage, ça marche. Les deux applications partagent la même instance grâce aux Quarkus Dev Services. Accédons maintenant à la console Quarkus Dev UI disponible sur http://localhost:8080/q/dev/. Trouvez la tuile avec le titre "Apache Kafka Streams" .

Vous pouvez consulter une visualisation de notre topologie Kafka Streams. Je vais diviser l'image en deux parties pour une meilleure visibilité.

Utiliser Kafka KTable avec Quarkus

Nous avons déjà terminé l'implémentation de la logique responsable de la création des transactions à partir des commandes entrantes. Dans l'étape suivante, nous allons effectuer des opérations analytiques sur le flux de transactions. Notre objectif principal est de calculer le nombre total de transactions, le nombre total de produits vendus/achetés et la valeur totale des transactions ( price * productsCount) pour chaque produit. Voici la classe d'objet utilisée dans les calculs.

@RegisterForReflection
public class TransactionTotal {
   private int count;
   private int amount;
   private int productCount;

   // GETTERS AND SETTERS
}

Étant donné que l'  Transaction objet ne contient pas d'informations sur le produit, nous devons d'abord joindre la commande pour y accéder. Ensuite, nous produisons un   groupement et une agrégation KTable par par  . productIdAprès cela, nous pouvons invoquer une méthode d'agrégation qui nous permet d'effectuer des calculs plus complexes. Dans ce cas particulier, nous calculons le nombre de toutes les transactions exécutées, leur volume de produits et leur valeur totale. Le résultat  KTable peut être matérialisé en tant que magasin d'état. Grâce à cela nous pourrons l'interroger par le nom défini par la TRANSACTIONS_PER_PRODUCT_SUMMARYvariable.

KeyValueBytesStoreSupplier storePerProductSupplier = Stores.persistentKeyValueStore(
   TRANSACTIONS_PER_PRODUCT_SUMMARY);

builder.stream(TRANSACTIONS_TOPIC, Consumed.with(Serdes.Long(), transactionSerde))
   .selectKey((k, v) -> v.getSellOrderId())
   .join(orders.selectKey((k, v) -> v.getId()),
      (t, o) -> new TransactionWithProduct(t, o.getProductId()),
      JoinWindows.of(Duration.ofSeconds(10)),
      StreamJoined.with(Serdes.Long(), transactionSerde, orderSerde))
   .groupBy((k, v) -> v.getProductId(), Grouped.with(Serdes.Integer(), transactionWithProductSerde))
   .aggregate(
      TransactionTotal::new,
      (k, v, a) -> {
         a.setCount(a.getCount() + 1);
         a.setProductCount(a.getAmount() + v.getTransaction().getAmount());
         a.setAmount(a.getProductCount() +
            (v.getTransaction().getAmount() * v.getTransaction().getPrice()));
         return a;
      },
      Materialized.<Integer, TransactionTotal> as(storePerProductSupplier)
         .withKeySerde(Serdes.Integer())
         .withValueSerde(transactionTotalSerde))
   .toStream()
   .peek((k, v) -> log.infof("Total per product(%d): %s", k, v))
   .to(TRANSACTIONS_PER_PRODUCT_AGGREGATED_TOPIC, 
      Produced.with(Serdes.Integer(), transactionTotalSerde));

Voici la classe responsable de l'implémentation des requêtes interactives. Il injecte du KafkaStreamsharicot. Ensuite, il essaie d'obtenir un magasin persistant en se basant sur la StockService.TRANSACTIONS_PER_PRODUCT_SUMMARYvariable. Il en résulte un ReadOnlyKeyValueStoreavec Integercomme clé, et TransactionTotalcomme valeur. Nous pouvons renvoyer une seule valeur liée au particulier productId( getTransactionsPerProductData) ou simplement renvoyer une liste avec les résultats pour tous les produits disponibles ( getAllTransactionsPerProductData).

@ApplicationScoped
public class InteractiveQueries {

   @Inject
   KafkaStreams streams;

   public TransactionTotal getTransactionsPerProductData(Integer productId) {
      return getTransactionsPerProductStore().get(productId);
   }

   public Map<Integer, TransactionTotal> getAllTransactionsPerProductData() {
      Map<Integer, TransactionTotal> m = new HashMap<>();
      KeyValueIterator<Integer, TransactionTotal> it = getTransactionsPerProductStore().all();
      while (it.hasNext()) {
         KeyValue<Integer, TransactionTotal> kv = it.next();
         m.put(kv.key, kv.value);
      }
      return m;
   }

   private ReadOnlyKeyValueStore<Integer, TransactionTotal> getTransactionsPerProductStore() {
      return streams.store(
         StoreQueryParameters
            .fromNameAndType(StockService.TRANSACTIONS_PER_PRODUCT_SUMMARY, QueryableStoreTypes.keyValueStore()));
   }

}

Enfin, nous pouvons créer un contrôleur REST chargé d'exposer les données récupérées par les requêtes interactives.

@ApplicationScoped
@Path("/transactions")
public class TransactionResource {

    @Inject
    InteractiveQueries interactiveQueries;

    @GET
    @Path("/products/{id}")
    public TransactionTotal getByProductId(@PathParam("id") Integer productId) {
        return interactiveQueries.getTransactionsPerProductData(productId);
    }

    @GET
    @Path("/products")
    public Map<Integer, TransactionTotal> getAllPerProductId() {
        return interactiveQueries.getAllTransactionsPerProductData();
    }

}

Vous pouvez désormais consulter facilement les statistiques relatives aux transactions créées par le stock-service. Il vous suffit d'appeler les points de terminaison REST suivants, par exemple :

$ curl http://localhost:8080/transactions/products
$ curl http://localhost:8080/transactions/products/3
$ curl http://localhost:8080/transactions/products/5

Dernières pensées

Quarkus simplifie le travail avec Kafka Streams et les requêtes interactives. Il fournit des améliorations utiles pour les développeurs, telles que le démarrage automatique de Kafka dans les modes de développement et de test ou la visualisation des flux Kafka dans la console de développement de l'interface utilisateur. Vous pouvez facilement comparer l'approche Quarkus avec la prise en charge de Spring Cloud Stream Kafka puisque j'ai implémenté la même logique pour ces deux frameworks. Voici le référentiel GitHub avec l'exemple Spring Cloud Stream Kafka Streams.

Lien : https://piotrminkowski.com/2021/11/24/kafka-streams-with-quarkus/

#quarkus #java #kafka

Comment Utiliser Les Flux Kafka Avec Quarkus
Hans  Marvin

Hans Marvin

1659092940

How to Use Kafka Streams with Quarkus

In this article, you will learn how to use Kafka Streams with Quarkus. We are going to use Quarkus instead of Spring Cloud. If you would like to figure out what is a streaming platform and how it differs from a traditional message broker this article is for you. Moreover, we will study useful improvements related to Apache Kafka provided by Quarkus.

See more at: https://piotrminkowski.com/2021/11/24/kafka-streams-with-quarkus/

#quarkus #java #kafka 

How to Use Kafka Streams with Quarkus
Hoang  Kim

Hoang Kim

1659085680

Cách Sử Dụng Suối Kafka Với Quarkus

Trong bài viết này, bạn sẽ học cách sử dụng Kafka Streams với Quarkus. Chúng tôi sẽ sử dụng Quarkus thay vì Spring Cloud. Nếu bạn muốn tìm hiểu một nền tảng phát trực tuyến là gì và nó khác với một nhà môi giới tin nhắn truyền thống như thế nào thì bài viết này là dành cho bạn. Hơn nữa, chúng tôi sẽ nghiên cứu những cải tiến hữu ích liên quan đến Apache Kafka do Quarkus cung cấp.

Ngành kiến ​​​​trúc

Trong trường hợp của chúng ta, có hai luồng sự kiện đến. Cả hai đều đại diện cho các đơn đặt hàng đến. Các đơn đặt hàng này được tạo ra bởi  order-service ứng dụng. Nó gửi lệnh mua đến  orders.buy chủ đề và lệnh bán cho  orders.sell chủ đề. Sau đó,  stock-service ứng dụng nhận và xử lý các sự kiện đến. Trong bước đầu tiên, nó cần thay đổi khóa của mỗi thông báo từ  orderId đến  productId. Đó là bởi vì nó phải tham gia các đơn đặt hàng từ các chủ đề khác nhau liên quan đến cùng một sản phẩm để thực hiện giao dịch. Cuối cùng, giá giao dịch là giá trung bình của giá mua và bán.

quarkus-kafka-Stream-Arch

Chúng tôi đang xây dựng một phiên bản đơn giản hóa của nền tảng thị trường chứng khoán. Mỗi lệnh mua chứa một mức giá tối đa mà khách hàng đang mong đợi để mua một sản phẩm. Mặt khác, mỗi đơn đặt hàng có một mức giá tối thiểu mà khách hàng sẵn sàng bán sản phẩm của mình. Nếu giá đặt hàng bán không lớn hơn giá đặt hàng mua cho một sản phẩm cụ thể, chúng tôi đang thực hiện giao dịch.

Mỗi đơn hàng có giá trị trong 10 giây. Sau thời gian đó,  stock-service ứng dụng sẽ không xử lý đơn đặt hàng như vậy vì nó được coi là đã hết hạn. Mỗi đơn đặt hàng chứa một số sản phẩm cho một giao dịch. Ví dụ, chúng ta có thể bán 100 với giá 10 hoặc mua 200 với giá 11. Do đó, một đơn đặt hàng có thể được thực hiện toàn bộ hoặc một phần. Ứng  stock-service dụng cố gắng nối các đơn hàng đã thực hiện một phần với các đơn hàng mới hoặc đã thực hiện một phần khác. Bạn có thể xem hình dung của quá trình đó trong hình dưới đây.

quarkus-kafka-stream-app

Chạy Apache Kafka cục bộ

Trước khi bắt đầu triển khai, chúng ta cần chạy một phiên bản cục bộ của Apache Kafka. Nếu bạn không muốn cài đặt nó trên máy tính xách tay của mình, cách tốt nhất để chạy nó là với  Redpanda . Redpanda là một nền tảng phát trực tuyến tương thích với API Kafka. So với Kafka, nó tương đối dễ dàng để chạy nó cục bộ. Thông thường, bạn sẽ phải cài đặt Redpanda trên máy tính xách tay của mình và sau đó tạo một cụm bằng CLI của chúng. Nhưng với Quarkus bạn không cần phải làm vậy! Yêu cầu duy nhất là phải cài đặt Docker. Nhờ có tiện ích mở rộng Quarkus Kafka và tính năng được gọi là Dịch vụ Dev, nó sẽ tự động khởi động nhà môi giới Kafka ở chế độ nhà phát triển và khi chạy thử nghiệm. Hơn nữa, ứng dụng được cấu hình tự động.

Điều duy nhất bạn cần làm để kích hoạt tính năng đó là KHÔNG cung cấp bất kỳ địa chỉ Kafka nào trong thuộc tính cấu hình. Dev Services sử dụng Testcontainers để chạy Kafka, vì vậy nếu bạn có Docker hoặc bất kỳ môi trường nào khác hỗ trợ Testcontainers chạy, bạn sẽ nhận được một phiên bản Kafka có sẵn trong hộp. Một điều quan trọng khác. Đầu tiên, khởi động order-serviceứng dụng. Nó tự động tạo tất cả các chủ đề được yêu cầu trong Kafka. Sau đó chạy stock-serviceứng dụng. Nó sử dụng tiện ích mở rộng Quarkus Kafka Streams và xác minh xem các chủ đề được yêu cầu có tồn tại hay không. Hãy hình dung nó.

quarkus-kafka-stream-run

Gửi sự kiện cho Kafka với Quarkus

Có một số cách để gửi sự kiện đến Kafka bằng Quarkus. Bởi vì chúng tôi cần gửi cặp khóa / giá trị, chúng tôi sẽ sử dụng io.smallrye.reactive.messaging.kafka.Recordđối tượng cho việc đó. Quarkus có thể tạo và gửi dữ liệu liên tục. Trong đoạn mã hiển thị bên dưới, chúng tôi gửi một Ordersự kiện duy nhất trong mỗi 500 mili giây. Mỗi Orderchứa một ngẫu nhiên productId, priceproductCount.

@Outgoing("orders-buy")
public Multi<Record<Long, Order>> buyOrdersGenerator() {
   return Multi.createFrom().ticks().every(Duration.ofMillis(500))
      .map(order -> {
         Integer productId = random.nextInt(10) + 1;
         int price = prices.get(productId) + random.nextInt(200);
         Order o = new Order(
             incrementOrderId(),
             random.nextInt(1000) + 1,
             productId,
             100 * (random.nextInt(5) + 1),
             LocalDateTime.now(),
             OrderType.BUY,
             price);
         log.infof("Sent: %s", o);
         return Record.of(o.getId(), o);
   });
}

@Outgoing("orders-sell")
public Multi<Record<Long, Order>> sellOrdersGenerator() {
   return Multi.createFrom().ticks().every(Duration.ofMillis(500))
      .map(order -> {
         Integer productId = random.nextInt(10) + 1;
         int price = prices.get(productId) + random.nextInt(200);
         Order o = new Order(
             incrementOrderId(),
             random.nextInt(1000) + 1,
             productId,
             100 * (random.nextInt(5) + 1),
             LocalDateTime.now(),
             OrderType.SELL,
             price);
         log.infof("Sent: %s", o);
         return Record.of(o.getId(), o);
   });
}

Chúng tôi cũng sẽ xác định một @Incomingkênh duy nhất để nhận các giao dịch do stock-service. Nhờ đó Quarkus sẽ tự động tạo chủ đề transactionsđược Quarkus Kafka Streams sử dụng trong stock-service. Thành thật mà nói, tôi không thể buộc phần mở rộng Quarkus Kafka Streams tự động tạo chủ đề. Có vẻ như chúng ta cần sử dụng tiện ích Nhắn tin phản ứng SmallRye cho việc đó.

@Incoming("transactions")
public void transactions(Transaction transaction) {
   log.infof("New: %s", transaction);
}

Tất nhiên, chúng ta cần bao gồm sự phụ thuộc của SmallRye Reactive Messaging vào Maven pom.xml.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
</dependency>

Cuối cùng, hãy cung cấp cài đặt cấu hình. Chúng tôi có hai chủ đề gửi đi và một chủ đề đến. Chúng tôi có thể đặt tên của họ. Nếu không, Quarkus sử dụng cùng tên với tên của kênh. Tên của các chủ đề của chúng tôi là orders.buy, order.selltransactions.

mp.messaging.outgoing.orders-buy.connector = smallrye-kafka
mp.messaging.outgoing.orders-buy.topic = orders.buy
mp.messaging.outgoing.orders-buy.key.serializer = org.apache.kafka.common.serialization.LongSerializer
mp.messaging.outgoing.orders-buy.value.serializer = io.quarkus.kafka.client.serialization.ObjectMapperSerializer

mp.messaging.outgoing.orders-sell.connector = smallrye-kafka
mp.messaging.outgoing.orders-sell.topic = orders.sell
mp.messaging.outgoing.orders-sell.key.serializer = org.apache.kafka.common.serialization.LongSerializer
mp.messaging.outgoing.orders-sell.value.serializer = io.quarkus.kafka.client.serialization.ObjectMapperSerializer

mp.messaging.incoming.transactions.connector = smallrye-kafka
mp.messaging.incoming.transactions.topic = transactions
mp.messaging.incoming.transactions.value.deserializer = pl.piomin.samples.streams.order.model.deserializer.TransactionDeserializer

Đó là tất cả. Trình tạo đơn đặt hàng của chúng tôi đã sẵn sàng. Nếu bạn là order-serviceứng dụng Quarkus cũng sẽ chạy phiên bản Kafka (Redpanda). Nhưng trước tiên, hãy chuyển sang ứng dụng mẫu thứ hai - stock-service.

Sử dụng suối Kafka với Quarkus

Trong phần trước, chúng tôi đã gửi tin nhắn đến nhà môi giới Kafka. Do đó, chúng tôi đã sử dụng thư viện Quarkus tiêu chuẩn để tích hợp với Kafka dựa trên khung Nhắn tin phản ứng SmallRye. Ứng stock-servicedụng sử dụng thông báo dưới dạng luồng, vì vậy bây giờ chúng tôi sẽ sử dụng một mô-đun để tích hợp Kafka Streams.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-kafka-streams</artifactId>
</dependency>

Ứng dụng của chúng tôi cũng sử dụng cơ sở dữ liệu, lớp ORM và bao gồm một số mô-đun hữu ích khác.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

Trong bước đầu tiên, chúng ta sẽ hợp nhất cả hai luồng lệnh (mua và bán), chèn  Order vào cơ sở dữ liệu và in thông báo sự kiện. Bạn có thể hỏi - tại sao tôi sử dụng cơ sở dữ liệu và lớp ORM ở đây vì tôi có Kafka  KTable? Tôi cần các giao dịch có hỗ trợ khóa để điều phối trạng thái thực hiện đơn hàng (tham khảo mô tả trong phần giới thiệu - các đơn hàng được thực hiện đầy đủ và một phần). Tôi sẽ cung cấp cho bạn thêm chi tiết về nó trong các phần tiếp theo.

Để xử lý các luồng với Quarkus, chúng ta cần khai báo org.apache.kafka.streams.Topologybean. Nó chứa tất cả các định nghĩa KStreamKTable. Hãy bắt đầu chỉ với phần chịu trách nhiệm tạo và phát ra các giao dịch từ các đơn đặt hàng đến. Có hai KStreamđịnh nghĩa được tạo ra. Đầu tiên trong số họ chịu trách nhiệm hợp nhất hai luồng đơn hàng thành một luồng duy nhất và sau đó chèn luồng mới Ordervào cơ sở dữ liệu. Thứ hai trong số họ tạo và thực hiện các giao dịch bằng cách tham gia hai luồng bằng cách sử dụng productIdkhóa. Nhưng nhiều hơn về nó trong phần tiếp theo.

@Produces
public Topology buildTopology() {
   ObjectMapperSerde<Order> orderSerde = 
      new ObjectMapperSerde<>(Order.class);
   ObjectMapperSerde<Transaction> transactionSerde = 
      new ObjectMapperSerde<>(Transaction.class);

   StreamsBuilder builder = new StreamsBuilder();

   KStream<Long, Order> orders = builder.stream(
      ORDERS_SELL_TOPIC,
      Consumed.with(Serdes.Long(), orderSerde));

   builder.stream(ORDERS_BUY_TOPIC, 
         Consumed.with(Serdes.Long(), orderSerde))
      .merge(orders)
      .peek((k, v) -> {
         log.infof("New: %s", v);
         logic.add(v);
      });

   builder.stream(ORDERS_BUY_TOPIC, 
         Consumed.with(Serdes.Long(), orderSerde))
      .selectKey((k, v) -> v.getProductId())
      .join(orders.selectKey((k, v) -> v.getProductId()),
         this::execute,
         JoinWindows.of(Duration.ofSeconds(10)),
         StreamJoined.with(Serdes.Integer(), orderSerde, orderSerde))
      .filterNot((k, v) -> v == null)
      .map((k, v) -> new KeyValue<>(v.getId(), v))
      .peek((k, v) -> log.infof("Done -> %s", v))
      .to(TRANSACTIONS_TOPIC, Produced.with(Serdes.Long(), transactionSerde));

}

Để xử lý các luồng, chúng ta cần thêm các thuộc tính cấu hình. Một danh sách các chủ đề đầu vào là bắt buộc. Chúng tôi cũng có thể ghi đè một id ứng dụng mặc định và kích hoạt kiểm tra tình trạng Kafka.

quarkus.kafka-streams.application-id = stock
quarkus.kafka-streams.topics = orders.buy,orders.sell
quarkus.kafka.health.enabled = true

Hoạt động trên Kafka Streams

Giờ đây, chúng tôi có thể sử dụng một số thao tác nâng cao hơn trên Luồng Kafka thay vì chỉ hợp nhất hai luồng khác nhau. Trên thực tế, đó là một logic quan trọng trong ứng dụng của chúng tôi. Chúng ta cần kết hợp hai luồng thứ tự khác nhau thành một luồng duy nhất bằng cách sử dụng  productId làm khóa tham gia. Vì trình sản xuất đặt  orderId làm khóa thông báo, trước tiên chúng ta cần gọi  selectKey phương thức cho cả  luồng order.sell và  orders.buy luồng. Trong trường hợp của chúng tôi, tham gia các lệnh mua và bán liên quan đến cùng một sản phẩm chỉ là bước đầu tiên. Sau đó, chúng ta cần xác minh xem giá tối đa trong lệnh mua không lớn hơn giá tối thiểu trong lệnh bán hay không.

Bước tiếp theo là xác minh nếu cả hai điều này chưa được nhận ra trước đây, vì chúng cũng có thể được ghép nối với các đơn đặt hàng khác trong luồng. Nếu tất cả các điều kiện được đáp ứng, chúng tôi có thể tạo một giao dịch mới. Cuối cùng, chúng tôi có thể thay đổi khóa luồng từ productId thành  transactionId và gửi nó đến  transactions chủ đề dành riêng.

Mỗi khi tham gia thành công hai lệnh, chúng tôi đang cố gắng tạo một giao dịch. Phương execute(...)thức được gọi trong KStream joinphương thức. Thứ nhất, chúng tôi đang so sánh giá của cả hai đơn đặt hàng. Sau đó, chúng tôi xác minh trạng thái hiện thực của cả hai đơn đặt hàng bằng cách truy cập cơ sở dữ liệu H2. Nếu đơn đặt hàng vẫn chưa được thực hiện đầy đủ, chúng tôi có thể tạo giao dịch và cập nhật hồ sơ đơn đặt hàng trong cơ sở dữ liệu.

private Transaction execute(Order orderBuy, Order orderSell) {
   if (orderBuy.getAmount() >= orderSell.getAmount()) {
      int count = Math.min(orderBuy.getProductCount(), 
                           orderSell.getProductCount());
      boolean allowed = logic
         .performUpdate(orderBuy.getId(), orderSell.getId(), count);
      if (!allowed)
         return null;
      else
         return new Transaction(
            ++transactionId,
            orderBuy.getId(),
            orderSell.getId(),
            count,
            (orderBuy.getAmount() + orderSell.getAmount()) / 2,
            LocalDateTime.now(),
            "NEW"
      );
   } else {
            return null;
   }
}

Chúng ta hãy xem xét kỹ hơn  performUpdate() phương thức được gọi là bên trong  execute() phương thức. Nó bắt đầu một giao dịch và khóa cả hai  Order thực thể. Sau đó, nó xác minh từng trạng thái thực hiện đơn hàng và cập nhật nó với các giá trị hiện tại nếu có thể. Chỉ khi  performUpdate() phương thức kết thúc thành công, stock-serviceứng dụng mới tạo một giao dịch mới.

@ApplicationScoped
public class OrderLogic {

    @Inject
    Logger log;
    @Inject
    OrderRepository repository;

    @Transactional
    public Order add(Order order) {
        repository.persist(order);
        return order;
    }

    @Transactional
    public boolean performUpdate(Long buyOrderId, Long sellOrderId, int amount) {
        Order buyOrder = repository.findById(buyOrderId, 
           LockModeType.PESSIMISTIC_WRITE);
        Order sellOrder = repository.findById(sellOrderId, 
           LockModeType.PESSIMISTIC_WRITE);
        if (buyOrder == null || sellOrder == null)
            return false;
        int buyAvailableCount = 
           buyOrder.getProductCount() - buyOrder.getRealizedCount();
        int sellAvailableCount = 
           sellOrder.getProductCount() - sellOrder.getRealizedCount();
        if (buyAvailableCount >= amount && sellAvailableCount >= amount) {
            buyOrder.setRealizedCount(buyOrder.getRealizedCount() + amount);
            sellOrder.setRealizedCount(sellOrder.getRealizedCount() + amount);
            repository.persist(buyOrder);
            repository.persist(sellOrder);
            return true;
        } else {
            return false;
        }
    }
}

Tốt🙂Đó là tất cả những gì chúng ta cần làm trong phần đầu tiên của bài tập. Bây giờ chúng tôi có thể chạy cả hai ứng dụng mẫu của chúng tôi.

Chạy và quản lý ứng dụng Kafka Streams với Quarkus

Như tôi đã đề cập trước đây, trước tiên chúng ta cần bắt đầu order-service. Nó chạy một phiên bản Kafka mới và tạo tất cả các chủ đề bắt buộc. Ngay sau khi khởi động, nó đã sẵn sàng để gửi các đơn đặt hàng mới. Để chạy ứng dụng Quarkus cục bộ, chỉ cần vào thư mục order-servicevà thực hiện lệnh sau:

$ mvn quarkus:dev

Chỉ để xác minh, bạn có thể hiển thị danh sách đang chạy các vùng chứa Docker bằng docker pslệnh. Đây là kết quả của tôi:

Như bạn thấy phiên bản Redpanda đang chạy và nó có sẵn trên một cổng ngẫu nhiên 49724. Quarkus đã làm điều đó cho chúng tôi. Tuy nhiên, nếu bạn đã cài đặt Redpanda trên máy tính xách tay của mình, bạn hãy xem danh sách các chủ đề đã tạo với CLI của chúng rpk:

$ rpk topic list --brokers=127.0.0.1:49724

Sau đó, hãy chạy stock-service. Chuyển đến thư mục stock-servicevà chạy mvn quarkus:devlại một lần nữa. Sau khi khởi động, nó chỉ hoạt động. Cả hai ứng dụng đều chia sẻ cùng một phiên bản nhờ Dịch vụ Quarkus Dev. Bây giờ chúng ta hãy truy cập bảng điều khiển giao diện người dùng Quarkus Dev có sẵn tại http://localhost:8080/q/dev/. Tìm ô có tiêu đề “Apache Kafka Streams” .

Bạn có thể kiểm tra hình ảnh trực quan về cấu trúc liên kết Kafka Streams của chúng tôi. Tôi sẽ chia hình ảnh thành hai phần để hiển thị tốt hơn.

Sử dụng Kafka KTable với Quarkus

Chúng tôi đã hoàn thành việc triển khai logic chịu trách nhiệm tạo giao dịch từ các đơn đặt hàng đến. Trong bước tiếp theo, chúng tôi sẽ thực hiện các hoạt động phân tích trên luồng giao dịch. Mục tiêu chính của chúng tôi là tính toán tổng số giao dịch, tổng số sản phẩm đã bán / mua và tổng giá trị giao dịch ( price * productsCount) trên mỗi sản phẩm. Đây là lớp đối tượng được sử dụng trong tính toán.

@RegisterForReflection
public class TransactionTotal {
   private int count;
   private int amount;
   private int productCount;

   // GETTERS AND SETTERS
}

Vì  Transaction đối tượng không chứa thông tin về sản phẩm nên trước tiên chúng ta cần tham gia đơn hàng để truy cập. Sau đó, chúng tôi tạo ra một  KTable theo mỗi  productId nhóm và tổng hợp. Sau đó, chúng tôi có thể gọi một phương pháp tổng hợp cho phép chúng tôi thực hiện một số phép tính phức tạp hơn. Trong trường hợp cụ thể đó, chúng tôi đang tính toán số lượng tất cả các giao dịch đã thực hiện, khối lượng sản phẩm và tổng giá trị của chúng. Kết quả  KTable có thể được hiện thực hóa dưới dạng kho lưu trữ trạng thái. Nhờ đó, chúng tôi sẽ có thể truy vấn nó theo tên được xác định bởi TRANSACTIONS_PER_PRODUCT_SUMMARYbiến.

KeyValueBytesStoreSupplier storePerProductSupplier = Stores.persistentKeyValueStore(
   TRANSACTIONS_PER_PRODUCT_SUMMARY);

builder.stream(TRANSACTIONS_TOPIC, Consumed.with(Serdes.Long(), transactionSerde))
   .selectKey((k, v) -> v.getSellOrderId())
   .join(orders.selectKey((k, v) -> v.getId()),
      (t, o) -> new TransactionWithProduct(t, o.getProductId()),
      JoinWindows.of(Duration.ofSeconds(10)),
      StreamJoined.with(Serdes.Long(), transactionSerde, orderSerde))
   .groupBy((k, v) -> v.getProductId(), Grouped.with(Serdes.Integer(), transactionWithProductSerde))
   .aggregate(
      TransactionTotal::new,
      (k, v, a) -> {
         a.setCount(a.getCount() + 1);
         a.setProductCount(a.getAmount() + v.getTransaction().getAmount());
         a.setAmount(a.getProductCount() +
            (v.getTransaction().getAmount() * v.getTransaction().getPrice()));
         return a;
      },
      Materialized.<Integer, TransactionTotal> as(storePerProductSupplier)
         .withKeySerde(Serdes.Integer())
         .withValueSerde(transactionTotalSerde))
   .toStream()
   .peek((k, v) -> log.infof("Total per product(%d): %s", k, v))
   .to(TRANSACTIONS_PER_PRODUCT_AGGREGATED_TOPIC, 
      Produced.with(Serdes.Integer(), transactionTotalSerde));

Đây là lớp chịu trách nhiệm triển khai các truy vấn tương tác. Nó chích KafkaStreamsđậu. Sau đó, nó cố gắng lấy được lưu trữ liên tục dựa trên StockService.TRANSACTIONS_PER_PRODUCT_SUMMARYbiến. Kết quả là có một ReadOnlyKeyValueStorevới Integernhư một khóa và TransactionTotalmột giá trị. Chúng tôi có thể trả về một giá trị duy nhất liên quan đến productId( getTransactionsPerProductData) cụ thể hoặc chỉ trả về một danh sách với kết quả cho tất cả các sản phẩm có sẵn ( getAllTransactionsPerProductData).

@ApplicationScoped
public class InteractiveQueries {

   @Inject
   KafkaStreams streams;

   public TransactionTotal getTransactionsPerProductData(Integer productId) {
      return getTransactionsPerProductStore().get(productId);
   }

   public Map<Integer, TransactionTotal> getAllTransactionsPerProductData() {
      Map<Integer, TransactionTotal> m = new HashMap<>();
      KeyValueIterator<Integer, TransactionTotal> it = getTransactionsPerProductStore().all();
      while (it.hasNext()) {
         KeyValue<Integer, TransactionTotal> kv = it.next();
         m.put(kv.key, kv.value);
      }
      return m;
   }

   private ReadOnlyKeyValueStore<Integer, TransactionTotal> getTransactionsPerProductStore() {
      return streams.store(
         StoreQueryParameters
            .fromNameAndType(StockService.TRANSACTIONS_PER_PRODUCT_SUMMARY, QueryableStoreTypes.keyValueStore()));
   }

}

Cuối cùng, chúng ta có thể tạo một bộ điều khiển REST chịu trách nhiệm hiển thị dữ liệu được truy xuất bởi các truy vấn tương tác.

@ApplicationScoped
@Path("/transactions")
public class TransactionResource {

    @Inject
    InteractiveQueries interactiveQueries;

    @GET
    @Path("/products/{id}")
    public TransactionTotal getByProductId(@PathParam("id") Integer productId) {
        return interactiveQueries.getTransactionsPerProductData(productId);
    }

    @GET
    @Path("/products")
    public Map<Integer, TransactionTotal> getAllPerProductId() {
        return interactiveQueries.getAllTransactionsPerProductData();
    }

}

Bây giờ bạn có thể dễ dàng kiểm tra số liệu thống kê liên quan đến các giao dịch được tạo bởi stock-service. Bạn chỉ cần gọi các điểm cuối REST sau, ví dụ:

$ curl http://localhost:8080/transactions/products
$ curl http://localhost:8080/transactions/products/3
$ curl http://localhost:8080/transactions/products/5

Lời kết

Quarkus đơn giản hóa việc làm việc với Kafka Streams và các truy vấn tương tác. Nó cung cấp các cải tiến hữu ích cho các nhà phát triển như tự động khởi động Kafka trong chế độ phát triển và thử nghiệm hoặc trực quan hóa luồng Kafka trong bảng điều khiển giao diện người dùng dev. Bạn có thể dễ dàng so sánh phương pháp tiếp cận Quarkus với sự hỗ trợ của Spring Cloud Stream Kafka vì tôi đã triển khai cùng một logic cho cả hai khuôn khổ đó. Đây là kho lưu trữ GitHub với ví dụ về Spring Cloud Stream Kafka Streams.

Liên kết: https://piotrminkowski.com/2021/11/24/kafka-streams-with-quarkus/

#quarkus #java #kafka 

Cách Sử Dụng Suối Kafka Với Quarkus

Как использовать потоки Kafka с Quarkus

В этой статье вы узнаете, как использовать Kafka Streams с Quarkus. Мы собираемся использовать Quarkus вместо Spring Cloud. Если вы хотите разобраться, что такое потоковая платформа и чем она отличается от традиционного брокера сообщений, эта статья для вас. Кроме того, мы изучим полезные улучшения, связанные с Apache Kafka, предоставленные Quarkus.

Архитектура

В нашем случае есть два входящих потока событий. Оба они представляют входящие заказы. Эти заказы генерируются  order-service приложением. Он отправляет ордера на покупку в  orders.buy тему и ордера на продажу в  orders.sell тему. Затем  stock-service приложение получает и обрабатывает входящие события. На первом этапе необходимо изменить ключ каждого сообщения  orderId с  productId. Это потому, что он должен объединять заказы из разных тем, связанных с одним и тем же продуктом, для выполнения транзакций. Наконец, цена сделки представляет собой среднее значение цен продажи и покупки.

quarkus-kafka-потоки-арка

Мы создаем упрощенную версию платформы фондового рынка. Каждый заказ на покупку содержит максимальную цену, по которой клиент рассчитывает купить продукт. С другой стороны, каждый заказ на продажу содержит минимальную цену, по которой покупатель готов продать свой товар. Если цена ордера на продажу не превышает цену ордера на покупку определенного товара, мы совершаем транзакцию.

Каждый приказ действителен в течение 10 секунд. По истечении этого времени  stock-service приложение не будет обрабатывать такой заказ, так как он считается просроченным. Каждый заказ содержит ряд продуктов для транзакции. Например, мы можем продать 100 за 10 или купить 200 за 11. Следовательно, ордер может быть реализован полностью или частично. Приложение  stock-service пытается присоединить частично реализованные заказы к другим новым или частично реализованным заказам. Вы можете увидеть визуализацию этого процесса на картинке ниже.

quarkus-kafka-потоки-приложение

Запустите Apache Kafka локально

Прежде чем мы перейдем к реализации, нам нужно запустить локальный экземпляр Apache Kafka. Если вы не хотите устанавливать его на свой ноутбук, лучше всего запустить его с помощью  Redpanda . Redpanda — это потоковая платформа, совместимая с Kafka API. По сравнению с Kafka, его относительно легко запустить локально. Обычно вам нужно установить Redpanda на свой ноутбук, а затем создать кластер с помощью интерфейса командной строки. Но с Quarkus этого делать не нужно! Единственное требование — установить Docker. Благодаря расширению Quarkus Kafka и функции под названием Dev Services он автоматически запускает брокера Kafka в режиме разработки и при выполнении тестов. Более того, приложение настраивается автоматически.

Единственное, что вам нужно сделать, чтобы включить эту функцию, это НЕ указывать адрес Kafka в свойствах конфигурации. Dev Services использует Testcontainers для запуска Kafka, поэтому, если у вас есть Docker или любая другая среда, поддерживающая запуск Testcontainers, вы получаете контейнерный экземпляр Kafka из коробки. Еще одна важная вещь. Во-первых, запустите order-serviceприложение. Он автоматически создает все необходимые темы в Kafka. Затем запустите stock-serviceприложение. Он использует расширение Quarkus Kafka Streams и проверяет наличие необходимых тем. Давайте визуализируем это.

quarkus-kafka-streams-run

Отправляйте события в Kafka с помощью Quarkus

Существует несколько способов отправки событий в Kafka с помощью Quarkus. Поскольку нам нужно отправить пару ключ/значение, мы будем использовать io.smallrye.reactive.messaging.kafka.Recordдля этого объект. Quarkus может непрерывно генерировать и отправлять данные. Во фрагменте кода, показанном ниже, мы отправляем одно Orderсобытие каждые 500 мс. Каждый Orderсодержит случайный productId, priceи productCount.

@Outgoing("orders-buy")
public Multi<Record<Long, Order>> buyOrdersGenerator() {
   return Multi.createFrom().ticks().every(Duration.ofMillis(500))
      .map(order -> {
         Integer productId = random.nextInt(10) + 1;
         int price = prices.get(productId) + random.nextInt(200);
         Order o = new Order(
             incrementOrderId(),
             random.nextInt(1000) + 1,
             productId,
             100 * (random.nextInt(5) + 1),
             LocalDateTime.now(),
             OrderType.BUY,
             price);
         log.infof("Sent: %s", o);
         return Record.of(o.getId(), o);
   });
}

@Outgoing("orders-sell")
public Multi<Record<Long, Order>> sellOrdersGenerator() {
   return Multi.createFrom().ticks().every(Duration.ofMillis(500))
      .map(order -> {
         Integer productId = random.nextInt(10) + 1;
         int price = prices.get(productId) + random.nextInt(200);
         Order o = new Order(
             incrementOrderId(),
             random.nextInt(1000) + 1,
             productId,
             100 * (random.nextInt(5) + 1),
             LocalDateTime.now(),
             OrderType.SELL,
             price);
         log.infof("Sent: %s", o);
         return Record.of(o.getId(), o);
   });
}

Мы также определим один @Incomingканал для получения транзакций, созданных stock-service. Благодаря этому Quarkus автоматически создаст тему transactions, используемую Quarkus Kafka Streams, в файлах stock-service. Честно говоря, мне не удалось заставить расширение Quarkus Kafka Streams создавать тему автоматически. Кажется, нам нужно использовать для этого расширение SmallRye Reactive Messaging.

@Incoming("transactions")
public void transactions(Transaction transaction) {
   log.infof("New: %s", transaction);
}

Конечно, нам нужно включить зависимость SmallRye Reactive Messaging от Maven pom.xml.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
</dependency>

Наконец, давайте предоставим параметры конфигурации. У нас есть две исходящие темы и одна входящая тема. Мы можем установить их имена. В противном случае Quarkus использует то же имя, что и имя канала. Названия наших тем orders.buy, order.sellи transactions.

mp.messaging.outgoing.orders-buy.connector = smallrye-kafka
mp.messaging.outgoing.orders-buy.topic = orders.buy
mp.messaging.outgoing.orders-buy.key.serializer = org.apache.kafka.common.serialization.LongSerializer
mp.messaging.outgoing.orders-buy.value.serializer = io.quarkus.kafka.client.serialization.ObjectMapperSerializer

mp.messaging.outgoing.orders-sell.connector = smallrye-kafka
mp.messaging.outgoing.orders-sell.topic = orders.sell
mp.messaging.outgoing.orders-sell.key.serializer = org.apache.kafka.common.serialization.LongSerializer
mp.messaging.outgoing.orders-sell.value.serializer = io.quarkus.kafka.client.serialization.ObjectMapperSerializer

mp.messaging.incoming.transactions.connector = smallrye-kafka
mp.messaging.incoming.transactions.topic = transactions
mp.messaging.incoming.transactions.value.deserializer = pl.piomin.samples.streams.order.model.deserializer.TransactionDeserializer

Это все. Наш генератор заказов готов. Если вы используете order-serviceприложение Quarkus, оно также будет запускать экземпляр Kafka (Redpanda). Но сначала переключимся на второй пример приложения — stock-service.

Использование потоков Kafka с помощью Quarkus

В предыдущем разделе мы отправляли сообщения брокеру Kafka. Поэтому мы использовали стандартную библиотеку Quarkus для интеграции с Kafka на основе фреймворка SmallRye Reactive Messaging. Приложение stock-serviceпотребляет сообщения в виде потоков, поэтому сейчас мы воспользуемся модулем для интеграции с Kafka Streams.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-kafka-streams</artifactId>
</dependency>

Наше приложение также использует базу данных, слой ORM и включает в себя некоторые другие полезные модули.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

На первом этапе мы собираемся объединить оба потока ордеров (покупка и продажа), вставить их  Order в базу данных и распечатать сообщение о событии. Вы можете спросить — почему я использую здесь базу данных и слой ORM, если у меня есть Kafka  KTable? Ну а транзакции с поддержкой блокировки мне нужны для того, чтобы согласовывать статус реализации заказа (см. описание во введении — полностью и частично реализованные заказы). Я дам вам более подробную информацию об этом в следующих разделах.

Чтобы обрабатывать потоки с помощью Quarkus, нам нужно объявить org.apache.kafka.streams.Topologybean-компонент. Он содержит все определения KStreamи KTable. Начнем как раз с части, отвечающей за создание и эмитирование транзакций из входящих ордеров. Созданы два KStreamопределения. Первый из них отвечает за объединение двух потоков заказов в один и последующую вставку нового Orderв базу данных. Второй из них создает и выполняет транзакции, объединяя два потока по productIdключу. Но об этом в следующем разделе.

@Produces
public Topology buildTopology() {
   ObjectMapperSerde<Order> orderSerde = 
      new ObjectMapperSerde<>(Order.class);
   ObjectMapperSerde<Transaction> transactionSerde = 
      new ObjectMapperSerde<>(Transaction.class);

   StreamsBuilder builder = new StreamsBuilder();

   KStream<Long, Order> orders = builder.stream(
      ORDERS_SELL_TOPIC,
      Consumed.with(Serdes.Long(), orderSerde));

   builder.stream(ORDERS_BUY_TOPIC, 
         Consumed.with(Serdes.Long(), orderSerde))
      .merge(orders)
      .peek((k, v) -> {
         log.infof("New: %s", v);
         logic.add(v);
      });

   builder.stream(ORDERS_BUY_TOPIC, 
         Consumed.with(Serdes.Long(), orderSerde))
      .selectKey((k, v) -> v.getProductId())
      .join(orders.selectKey((k, v) -> v.getProductId()),
         this::execute,
         JoinWindows.of(Duration.ofSeconds(10)),
         StreamJoined.with(Serdes.Integer(), orderSerde, orderSerde))
      .filterNot((k, v) -> v == null)
      .map((k, v) -> new KeyValue<>(v.getId(), v))
      .peek((k, v) -> log.infof("Done -> %s", v))
      .to(TRANSACTIONS_TOPIC, Produced.with(Serdes.Long(), transactionSerde));

}

Для обработки потоков нам нужно добавить свойства конфигурации. Требуется список вводных тем. Мы также можем переопределить идентификатор приложения по умолчанию и включить проверку работоспособности Kafka.

quarkus.kafka-streams.application-id = stock
quarkus.kafka-streams.topics = orders.buy,orders.sell
quarkus.kafka.health.enabled = true

Операции над потоками Kafka

Теперь мы можем использовать более сложные операции с потоками Kafka, чем просто объединение двух разных потоков. На самом деле, это ключевая логика в нашем приложении. Нам нужно объединить два разных потока заказов в один, используя в  productId качестве ключа соединения. Поскольку производитель устанавливает  orderId в качестве ключа сообщения, нам сначала нужно вызвать  selectKey метод для обоих  order.sell и  orders.buy потоков. В нашем случае объединение заказов на покупку и продажу, связанных с одним и тем же продуктом, — это только первый шаг. Затем нам нужно проверить, не превышает ли максимальная цена в ордере на покупку минимальную цену в ордере на продажу.

Следующим шагом является проверка того, не были ли реализованы обе эти функции ранее, поскольку они также могут быть связаны с другими ордерами в потоке. Если все условия соблюдены, мы можем создать новую транзакцию. Наконец, мы можем изменить ключ потока с productId на  transactionId и отправить его в специальную  transactions тему.

Каждый раз, когда мы успешно объединяем два заказа, мы пытаемся создать транзакцию. Метод execute(...)вызывается внутри KStream joinметода. Во-первых, мы сравниваем цены обоих заказов. Затем мы проверяем статус реализации обоих заказов, обращаясь к базе данных H2. Если заказы еще не полностью реализованы, мы можем создать транзакцию и обновить записи заказов в базе данных.

private Transaction execute(Order orderBuy, Order orderSell) {
   if (orderBuy.getAmount() >= orderSell.getAmount()) {
      int count = Math.min(orderBuy.getProductCount(), 
                           orderSell.getProductCount());
      boolean allowed = logic
         .performUpdate(orderBuy.getId(), orderSell.getId(), count);
      if (!allowed)
         return null;
      else
         return new Transaction(
            ++transactionId,
            orderBuy.getId(),
            orderSell.getId(),
            count,
            (orderBuy.getAmount() + orderSell.getAmount()) / 2,
            LocalDateTime.now(),
            "NEW"
      );
   } else {
            return null;
   }
}

Давайте подробнее рассмотрим  performUpdate() метод, вызываемый внутри  execute() метода. Он инициирует транзакцию и блокирует обе  Order сущности. Затем он проверяет статус реализации каждого заказа и обновляет его текущими значениями, если это возможно. Только в случае  performUpdate() успешного завершения метода stock-serviceприложение создает новую транзакцию.

@ApplicationScoped
public class OrderLogic {

    @Inject
    Logger log;
    @Inject
    OrderRepository repository;

    @Transactional
    public Order add(Order order) {
        repository.persist(order);
        return order;
    }

    @Transactional
    public boolean performUpdate(Long buyOrderId, Long sellOrderId, int amount) {
        Order buyOrder = repository.findById(buyOrderId, 
           LockModeType.PESSIMISTIC_WRITE);
        Order sellOrder = repository.findById(sellOrderId, 
           LockModeType.PESSIMISTIC_WRITE);
        if (buyOrder == null || sellOrder == null)
            return false;
        int buyAvailableCount = 
           buyOrder.getProductCount() - buyOrder.getRealizedCount();
        int sellAvailableCount = 
           sellOrder.getProductCount() - sellOrder.getRealizedCount();
        if (buyAvailableCount >= amount && sellAvailableCount >= amount) {
            buyOrder.setRealizedCount(buyOrder.getRealizedCount() + amount);
            sellOrder.setRealizedCount(sellOrder.getRealizedCount() + amount);
            repository.persist(buyOrder);
            repository.persist(sellOrder);
            return true;
        } else {
            return false;
        }
    }
}

Nice🙂Это все, что нам нужно сделать в первой части нашего упражнения. Теперь мы можем запустить оба наших примера приложений.

Запускайте приложение Kafka Streams и управляйте им с помощью Quarkus

Как я уже упоминал ранее, нам сначала нужно запустить файл order-service. Он запускает новый экземпляр Kafka и создает все необходимые темы. Сразу после запуска он готов к отправке новых заказов. Чтобы запустить приложение Quarkus локально, просто перейдите в order-serviceкаталог и выполните следующую команду:

$ mvn quarkus:dev

Просто для проверки вы можете отобразить список запущенных контейнеров Docker с помощью docker psкоманды. Вот мой результат:

Как вы видите, экземпляр Redpanda запущен и доступен на произвольном порту 49724. Quarkus сделал это за нас. Однако, если на вашем ноутбуке установлена ​​Redpanda, вы можете проверить список созданных тем с их CLI rpk:

$ rpk topic list --brokers=127.0.0.1:49724

Тогда запустим stock-service. Перейдите в stock-serviceкаталог и запустите mvn quarkus:devеще раз. После запуска он просто работает. Оба приложения используют один и тот же экземпляр благодаря Quarkus Dev Services. Теперь давайте получим доступ к консоли пользовательского интерфейса Quarkus Dev, доступной по адресу http://localhost:8080/q/dev/. Найдите плитку с заголовком «Apache Kafka Streams» .

Вы можете проверить визуализацию нашей топологии Kafka Streams. Я разделю изображение на две части для лучшей наглядности.

Используйте Kafka KTable с Quarkus

Мы уже закончили реализацию логики, отвечающей за создание транзакций из поступающих заказов. На следующем шаге мы собираемся выполнить аналитические операции над потоком транзакций. Наша основная цель — рассчитать общее количество транзакций, общее количество проданных/купленных продуктов и общую стоимость транзакций ( price * productsCount) по каждому продукту. Вот класс объекта, используемый в вычислениях.

@RegisterForReflection
public class TransactionTotal {
   private int count;
   private int amount;
   private int productCount;

   // GETTERS AND SETTERS
}

Поскольку  Transaction объект не содержит информации о продукте, нам сначала нужно присоединиться к заказу, чтобы получить к нему доступ. Затем мы производим  KTable по  productId группировке и агрегации. После этого мы можем вызвать агрегатный метод, который позволит нам выполнить более сложные вычисления. В этом конкретном случае мы рассчитываем количество всех выполненных транзакций, их объем продуктов и общую стоимость. Результат  KTable может быть материализован как хранилище состояний. Благодаря этому мы сможем запрашивать его по имени, определенному TRANSACTIONS_PER_PRODUCT_SUMMARYпеременной.

KeyValueBytesStoreSupplier storePerProductSupplier = Stores.persistentKeyValueStore(
   TRANSACTIONS_PER_PRODUCT_SUMMARY);

builder.stream(TRANSACTIONS_TOPIC, Consumed.with(Serdes.Long(), transactionSerde))
   .selectKey((k, v) -> v.getSellOrderId())
   .join(orders.selectKey((k, v) -> v.getId()),
      (t, o) -> new TransactionWithProduct(t, o.getProductId()),
      JoinWindows.of(Duration.ofSeconds(10)),
      StreamJoined.with(Serdes.Long(), transactionSerde, orderSerde))
   .groupBy((k, v) -> v.getProductId(), Grouped.with(Serdes.Integer(), transactionWithProductSerde))
   .aggregate(
      TransactionTotal::new,
      (k, v, a) -> {
         a.setCount(a.getCount() + 1);
         a.setProductCount(a.getAmount() + v.getTransaction().getAmount());
         a.setAmount(a.getProductCount() +
            (v.getTransaction().getAmount() * v.getTransaction().getPrice()));
         return a;
      },
      Materialized.<Integer, TransactionTotal> as(storePerProductSupplier)
         .withKeySerde(Serdes.Integer())
         .withValueSerde(transactionTotalSerde))
   .toStream()
   .peek((k, v) -> log.infof("Total per product(%d): %s", k, v))
   .to(TRANSACTIONS_PER_PRODUCT_AGGREGATED_TOPIC, 
      Produced.with(Serdes.Integer(), transactionTotalSerde));

Вот класс, отвечающий за реализацию интерактивных запросов. Он вводит KafkaStreamsбоб. Затем он пытается получить постоянное хранилище на основе StockService.TRANSACTIONS_PER_PRODUCT_SUMMARYпеременной. В результате есть ReadOnlyKeyValueStorewith Integerи в качестве ключа, и TransactionTotalв качестве значения. Мы можем вернуть одно значение, связанное с конкретным productId( getTransactionsPerProductData), или просто вернуть список с результатами для всех доступных продуктов ( getAllTransactionsPerProductData).

@ApplicationScoped
public class InteractiveQueries {

   @Inject
   KafkaStreams streams;

   public TransactionTotal getTransactionsPerProductData(Integer productId) {
      return getTransactionsPerProductStore().get(productId);
   }

   public Map<Integer, TransactionTotal> getAllTransactionsPerProductData() {
      Map<Integer, TransactionTotal> m = new HashMap<>();
      KeyValueIterator<Integer, TransactionTotal> it = getTransactionsPerProductStore().all();
      while (it.hasNext()) {
         KeyValue<Integer, TransactionTotal> kv = it.next();
         m.put(kv.key, kv.value);
      }
      return m;
   }

   private ReadOnlyKeyValueStore<Integer, TransactionTotal> getTransactionsPerProductStore() {
      return streams.store(
         StoreQueryParameters
            .fromNameAndType(StockService.TRANSACTIONS_PER_PRODUCT_SUMMARY, QueryableStoreTypes.keyValueStore()));
   }

}

Наконец, мы можем создать контроллер REST, отвечающий за предоставление данных, полученных интерактивными запросами.

@ApplicationScoped
@Path("/transactions")
public class TransactionResource {

    @Inject
    InteractiveQueries interactiveQueries;

    @GET
    @Path("/products/{id}")
    public TransactionTotal getByProductId(@PathParam("id") Integer productId) {
        return interactiveQueries.getTransactionsPerProductData(productId);
    }

    @GET
    @Path("/products")
    public Map<Integer, TransactionTotal> getAllPerProductId() {
        return interactiveQueries.getAllTransactionsPerProductData();
    }

}

Теперь вы можете легко проверить статистику, связанную с транзакциями, созданными stock-service. Вам просто нужно вызвать следующие конечные точки REST, например:

$ curl http://localhost:8080/transactions/products
$ curl http://localhost:8080/transactions/products/3
$ curl http://localhost:8080/transactions/products/5

Последние мысли

Quarkus упрощает работу с Kafka Streams и интерактивными запросами. Он предоставляет полезные улучшения для разработчиков, такие как автоматический запуск Kafka в режимах разработки и тестирования или визуализацию потоков Kafka в консоли пользовательского интерфейса разработчика. Вы можете легко сравнить подход Quarkus с поддержкой Spring Cloud Stream Kafka, поскольку я реализовал одинаковую логику для обеих этих платформ. Вот репозиторий GitHub с примером Spring Cloud Stream Kafka Streams.

Ссылка: https://piotrminkowski.com/2021/11/24/kafka-streams-with-quarkus/

#quarkus #java #kafka 

Как использовать потоки Kafka с Quarkus