Comprendre les courtiers de messages. Apprentissage des mécanismes de messagerie avec ActiveMQ et Kafka. Chapitre 3. Kafka

Suite de la traduction d'un petit livre :
Comprendre les courtiers de messages
auteur : Jakub Korab, éditeur : O'Reilly Media, Inc., date de publication : juin 2017, ISBN : 9781492049296.

Partie traduite précédente : Comprendre les courtiers de messages. Apprentissage des mécanismes de messagerie avec ActiveMQ et Kafka. Chapitre 1 Introduction

CHAPITRE 3

Kafka

Kafka a été développé par LinkedIn pour contourner certaines des limitations des courtiers de messages traditionnels et éviter d'avoir à configurer plusieurs courtiers de messages pour différentes interactions point à point, ce qui est décrit dans ce livre sous « Scaling up and out » à la page 28 Cas d'utilisation LinkedIn s'est largement appuyé sur l'ingestion unidirectionnelle de très grandes quantités de données, telles que les clics sur les pages et les journaux d'accès, tout en permettant à ces données d'être utilisées par plusieurs systèmes sans affecter la productivité des producteurs ou des autres consommateurs. En fait, la raison d'être de Kafka est d'obtenir le type d'architecture de messagerie décrit par Universal Data Pipeline.

Etant donné cet objectif ultime, d'autres exigences se sont naturellement imposées. Kafka devrait :

  • Soyez extrêmement rapide
  • Fournir plus de bande passante lorsque vous travaillez avec des messages
  • Prise en charge des modèles éditeur-abonné et point à point
  • Ne ralentissez pas avec l'ajout de consommateurs. Par exemple, les performances de la file d'attente et de la rubrique dans ActiveMQ se dégradent à mesure que le nombre de consommateurs sur la destination augmente.
  • Être évolutif horizontalement ; si un courtier qui conserve les messages ne peut le faire qu'à la vitesse maximale du disque, il est logique d'aller au-delà d'une seule instance de courtier pour augmenter les performances
  • Limiter l'accès au stockage et à la récupération des messages

Pour y parvenir, Kafka a adopté une architecture qui a redéfini les rôles et les responsabilités des clients et des courtiers de messagerie. Le modèle JMS est très orienté courtier, où le courtier est responsable de la distribution des messages et les clients n'ont qu'à se soucier de l'envoi et de la réception des messages. Kafka, d'autre part, est centré sur le client, le client prenant en charge de nombreuses caractéristiques d'un courtier traditionnel, telles que la distribution équitable de messages pertinents aux consommateurs, en échange d'un courtier extrêmement rapide et évolutif. Pour les personnes qui ont travaillé avec des systèmes de messagerie traditionnels, travailler avec Kafka nécessite un changement d'avis fondamental.
Cette orientation technique a conduit à la création d'une infrastructure de messagerie capable d'augmenter le débit de plusieurs ordres de grandeur par rapport à un courtier conventionnel. Comme nous le verrons, cette approche s'accompagne de compromis, ce qui signifie que Kafka n'est pas adapté à certains types de charges de travail et de logiciels installés.

Modèle de destination unifiée

Pour répondre aux exigences décrites ci-dessus, Kafka a combiné la messagerie de publication-abonnement et point à point sous un seul type de destination - sujet. Ceci est déroutant pour les personnes qui ont travaillé avec des systèmes de messagerie, où le mot "sujet" fait référence à un mécanisme de diffusion à partir duquel (à partir du sujet) la lecture n'est pas durable. Les sujets Kafka doivent être considérés comme un type de destination hybride, tel que défini dans l'introduction de ce livre.

Pour le reste de ce chapitre, sauf indication contraire explicite, le terme "topic" fera référence à un topic Kafka.

Pour bien comprendre le comportement des sujets et les garanties qu'ils offrent, nous devons d'abord examiner comment ils sont implémentés dans Kafka.
Chaque sujet dans Kafka a son propre journal.
Les producteurs qui envoient des messages à Kafka écrivent dans ce journal, et les consommateurs lisent à partir du journal à l'aide de pointeurs qui avancent constamment. Périodiquement, Kafka supprime les parties les plus anciennes du journal, que les messages de ces parties aient été lus ou non. Un élément central de la conception de Kafka est que le courtier ne se soucie pas de savoir si les messages sont lus ou non - c'est la responsabilité du client.

Les termes "journal" et "pointeur" n'apparaissent pas dans Documentation de Kafka. Ces termes bien connus sont utilisés ici pour faciliter la compréhension.

Ce modèle est complètement différent d'ActiveMQ, où les messages de toutes les files d'attente sont stockés dans le même journal et le courtier marque les messages comme supprimés après leur lecture.
Creusons maintenant un peu plus et examinons le journal du sujet plus en détail.
Le log Kafka se compose de plusieurs partitions (Figure 3-1). Kafka garantit un ordre strict dans chaque partition. Cela signifie que les messages écrits sur la partition dans un certain ordre seront lus dans le même ordre. Chaque partition est implémentée sous la forme d'un fichier journal continu qui contient sous-ensemble (sous-ensemble) de tous les messages envoyés au sujet par ses producteurs. Le sujet créé contient, par défaut, une partition. L'idée de partitions est l'idée centrale de Kafka pour la mise à l'échelle horizontale.

Comprendre les courtiers de messages. Apprentissage des mécanismes de messagerie avec ActiveMQ et Kafka. Chapitre 3. Kafka
Illustration 3-1. Cloisons Kafka

Lorsqu'un producteur envoie un message à un sujet Kafka, il décide à quelle partition envoyer le message. Nous verrons cela plus en détail plus tard.

Lecture des messages

Le client qui veut lire les messages gère un pointeur nommé appelé groupe de consommateurs, qui pointe vers compenser messages dans la partition. Un décalage est une position incrémentielle qui commence à 0 au début d'une partition. Ce groupe de consommateurs, référencé dans l'API via le group_id défini par l'utilisateur, correspond à un consommateur ou système logique.

La plupart des systèmes de messagerie lisent les données de la destination en utilisant plusieurs instances et threads pour traiter les messages en parallèle. Ainsi, il y aura généralement de nombreuses instances de consommateurs partageant le même groupe de consommateurs.

Le problème de la lecture peut être représenté comme suit :

  • Le sujet a plusieurs partitions
  • Plusieurs groupes de consommateurs peuvent utiliser un sujet en même temps
  • Un groupe de consommateurs peut avoir plusieurs instances distinctes

Il s'agit d'un problème plusieurs-à-plusieurs non trivial. Pour comprendre comment Kafka gère les relations entre les groupes de consommateurs, les instances de consommateurs et les partitions, examinons une série de scénarios de lecture de plus en plus complexes.

Consommateurs et groupes de consommateurs

Prenons comme point de départ un sujet avec une partition (Figure 3-2).

Comprendre les courtiers de messages. Apprentissage des mécanismes de messagerie avec ActiveMQ et Kafka. Chapitre 3. Kafka
Illustration 3-2. Le consommateur lit à partir de la partition

Lorsqu'une instance de consommateur se connecte avec son propre group_id à ce sujet, une partition de lecture et un décalage dans cette partition lui sont attribués. La position de ce décalage est configurée dans le client comme un pointeur vers la position la plus récente (message le plus récent) ou la position la plus ancienne (message le plus ancien). Le consommateur demande (interroge) les messages de la rubrique, ce qui entraîne leur lecture séquentielle à partir du journal.
La position de décalage est régulièrement renvoyée à Kafka et stockée sous forme de messages dans un sujet interne _consumer_offsets. Les messages lus ne sont toujours pas supprimés, contrairement à un courtier classique, et le client peut rembobiner le décalage pour retraiter les messages déjà consultés.

Lorsqu'un deuxième consommateur logique se connecte en utilisant un group_id différent, il gère un deuxième pointeur indépendant du premier (Figure 3-3). Ainsi, un sujet Kafka agit comme une file d'attente où il y a un consommateur et comme un sujet normal de publication-abonnement (pub-sub) auquel plusieurs consommateurs s'abonnent, avec l'avantage supplémentaire que tous les messages sont stockés et peuvent être traités plusieurs fois.

Comprendre les courtiers de messages. Apprentissage des mécanismes de messagerie avec ActiveMQ et Kafka. Chapitre 3. Kafka
Illustration 3-3. Deux consommateurs appartenant à des groupes de consommateurs différents lisent depuis la même partition

Consommateurs dans un groupe de consommateurs

Lorsqu'une instance de consommateur lit les données d'une partition, elle a le contrôle total du pointeur et traite les messages comme décrit dans la section précédente.
Si plusieurs instances de consommateurs étaient connectées avec le même group_id à un sujet avec une partition, alors l'instance qui s'est connectée en dernier aura le contrôle sur le pointeur et à partir de ce moment elle recevra tous les messages (Figure 3-4).

Comprendre les courtiers de messages. Apprentissage des mécanismes de messagerie avec ActiveMQ et Kafka. Chapitre 3. Kafka
Illustration 3-4. Deux consommateurs du même groupe de consommateurs lisent depuis la même partition

Ce mode de traitement, dans lequel le nombre d'instances de consommateurs dépasse le nombre de partitions, peut être considéré comme une sorte de consommateur exclusif. Cela peut être utile si vous avez besoin d'un clustering "actif-passif" (ou "chaud-chaud") de vos instances de consommateur, bien que l'exécution de plusieurs consommateurs en parallèle ("actif-actif" ou "chaud-chaud") soit beaucoup plus typique que consommateurs En veille.

Le comportement de distribution des messages décrit ci-dessus peut être surprenant par rapport au comportement d'une file d'attente JMS normale. Dans ce modèle, les messages envoyés à la file d'attente seront répartis de manière égale entre les deux consommateurs.

Le plus souvent, lorsque nous créons plusieurs instances de consommateurs, nous le faisons soit pour traiter les messages en parallèle, soit pour augmenter la vitesse de lecture, soit pour augmenter la stabilité du processus de lecture. Étant donné qu'une seule instance de consommateur peut lire les données d'une partition à la fois, comment cela est-il réalisé dans Kafka ?

Une façon de procéder consiste à utiliser une seule instance de consommateur pour lire tous les messages et les transmettre au pool de threads. Bien que cette approche augmente le débit de traitement, elle augmente la complexité de la logique du consommateur et ne fait rien pour augmenter la robustesse du système de lecture. Si une copie du consommateur tombe en panne en raison d'une panne de courant ou d'un événement similaire, la soustraction s'arrête.

La manière canonique de résoudre ce problème dans Kafka est d'utiliser bОplus de partitions.

Partitionnement

Les partitions sont le principal mécanisme de parallélisation de la lecture et de la mise à l'échelle d'un sujet au-delà de la bande passante d'une seule instance de courtier. Pour mieux comprendre cela, considérons une situation où il y a un sujet avec deux partitions et un consommateur s'abonne à ce sujet (Figure 3-5).

Comprendre les courtiers de messages. Apprentissage des mécanismes de messagerie avec ActiveMQ et Kafka. Chapitre 3. Kafka
Illustration 3-5. Un consommateur lit à partir de plusieurs partitions

Dans ce scénario, le consommateur reçoit le contrôle des pointeurs correspondant à son group_id dans les deux partitions et commence à lire les messages des deux partitions.
Lorsqu'un consommateur supplémentaire pour le même group_id est ajouté à ce sujet, Kafka réaffecte l'une des partitions du premier au deuxième consommateur. Après cela, chaque instance du consommateur lira à partir d'une partition du sujet (Figure 3-6).

Pour vous assurer que les messages sont traités en parallèle dans 20 threads, vous avez besoin d'au moins 20 partitions. S'il y a moins de partitions, vous vous retrouverez avec des consommateurs qui n'ont rien sur quoi travailler, comme décrit précédemment dans la discussion sur les consommateurs exclusifs.

Comprendre les courtiers de messages. Apprentissage des mécanismes de messagerie avec ActiveMQ et Kafka. Chapitre 3. Kafka
Illustration 3-6. Deux consommateurs du même groupe de consommateurs lisent à partir de partitions différentes

Ce schéma réduit considérablement la complexité du courtier Kafka par rapport à la distribution des messages requise pour maintenir la file d'attente JMS. Ici, vous n'avez pas à vous soucier des points suivants :

  • Quel consommateur doit recevoir le message suivant, en fonction de l'allocation circulaire, de la capacité actuelle des tampons de prélecture ou des messages précédents (comme pour les groupes de messages JMS).
  • Quels messages sont envoyés à quels consommateurs et s'ils doivent être renvoyés en cas d'échec.

Le courtier Kafka n'a qu'à transmettre séquentiellement les messages au consommateur lorsque celui-ci en fait la demande.

Cependant, les exigences de parallélisation de la relecture et du renvoi des messages échoués ne disparaissent pas - la responsabilité de celles-ci passe simplement du courtier au client. Cela signifie qu'ils doivent être pris en compte dans votre code.

Envoi de messages

Il est de la responsabilité du producteur de ce message de décider à quelle partition envoyer un message. Pour comprendre le mécanisme par lequel cela se fait, nous devons d'abord considérer ce que nous envoyons exactement.

Alors que dans JMS, nous utilisons une structure de message avec des métadonnées (en-têtes et propriétés) et un corps contenant la charge utile (charge utile), dans Kafka, le message est paire "clé-valeur". La charge utile du message est envoyée sous forme de valeur. La clé, quant à elle, est principalement utilisée pour le partitionnement et doit contenir clé spécifique à la logique métierpour mettre les messages liés dans la même partition.

Dans le chapitre 2, nous avons discuté du scénario de pari en ligne où les événements liés doivent être traités dans l'ordre par un seul consommateur :

  1. Le compte utilisateur est configuré.
  2. L'argent est crédité sur le compte.
  3. Un pari est fait qui retire de l'argent du compte.

Si chaque événement est un message publié dans un sujet, la clé naturelle serait l'ID de compte.
Lorsqu'un message est envoyé à l'aide de l'API Kafka Producer, il est transmis à une fonction de partition qui, compte tenu du message et de l'état actuel du cluster Kafka, renvoie l'ID de la partition à laquelle le message doit être envoyé. Cette fonctionnalité est implémentée en Java via l'interface Partitioner.

Cette interface ressemble à ceci :

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

L'implémentation de Partitioner utilise l'algorithme de hachage à usage général par défaut sur la clé pour déterminer la partition, ou round-robin si aucune clé n'est spécifiée. Cette valeur par défaut fonctionne bien dans la plupart des cas. Cependant, à l'avenir, vous voudrez écrire le vôtre.

Rédaction de votre propre stratégie de partitionnement

Examinons un exemple dans lequel vous souhaitez envoyer des métadonnées avec la charge utile du message. La charge utile dans notre exemple est une instruction pour effectuer un dépôt sur le compte de jeu. Une instruction est quelque chose dont nous aimerions avoir la garantie qu'elle ne sera pas modifiée lors de la transmission et nous voulons être sûrs que seul un système en amont de confiance peut initier cette instruction. Dans ce cas, les systèmes émetteur et récepteur s'accordent sur l'utilisation d'une signature pour authentifier le message.
Dans JMS normal, nous définissons simplement une propriété "signature de message" et l'ajoutons au message. Cependant, Kafka ne nous fournit pas de mécanisme pour transmettre les métadonnées, seulement une clé et une valeur.

La valeur étant une charge utile de virement bancaire dont nous voulons préserver l'intégrité, nous n'avons d'autre choix que de définir la structure de données à utiliser dans la clé. En supposant que nous ayons besoin d'un ID de compte pour le partitionnement, puisque tous les messages liés à un compte doivent être traités dans l'ordre, nous proposerons la structure JSON suivante :

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

Étant donné que la valeur de la signature varie en fonction de la charge utile, la stratégie de hachage par défaut de l'interface Partitioner ne regroupera pas de manière fiable les messages associés. Par conséquent, nous devrons écrire notre propre stratégie qui analysera cette clé et partitionnera la valeur accountId.

Kafka inclut des sommes de contrôle pour détecter la corruption des messages dans le magasin et dispose d'un ensemble complet de fonctionnalités de sécurité. Même ainsi, des exigences spécifiques à l'industrie, telles que celle ci-dessus, apparaissent parfois.

La stratégie de partitionnement de l'utilisateur doit garantir que tous les messages associés se retrouvent dans la même partition. Bien que cela semble simple, l'exigence peut être compliquée par l'importance de l'ordre des messages associés et la fixation du nombre de partitions dans un sujet.

Le nombre de partitions dans un sujet peut changer au fil du temps, car elles peuvent être ajoutées si le trafic dépasse les attentes initiales. Ainsi, les clés de message peuvent être associées à la partition à laquelle elles ont été envoyées à l'origine, ce qui implique un élément d'état à partager entre les instances du producteur.

Un autre facteur à prendre en compte est la répartition uniforme des messages entre les partitions. En règle générale, les clés ne sont pas réparties uniformément entre les messages et les fonctions de hachage ne garantissent pas une distribution équitable des messages pour un petit ensemble de clés.
Il est important de noter que, quelle que soit la façon dont vous choisissez de diviser les messages, le séparateur lui-même devra peut-être être réutilisé.

Considérez la nécessité de répliquer les données entre les clusters Kafka dans différents emplacements géographiques. À cette fin, Kafka est livré avec un outil de ligne de commande appelé MirrorMaker, qui est utilisé pour lire les messages d'un cluster et les transférer vers un autre.

MirrorMaker doit comprendre les clés du sujet répliqué afin de maintenir un ordre relatif entre les messages lors de la réplication entre clusters, car le nombre de partitions pour ce sujet peut ne pas être le même dans deux clusters.

Les stratégies de partitionnement personnalisées sont relativement rares, car le hachage par défaut ou le round robin fonctionnent bien dans la plupart des scénarios. Cependant, si vous avez besoin de garanties de commande solides ou si vous avez besoin d'extraire des métadonnées à partir de charges utiles, le partitionnement est quelque chose que vous devriez examiner de plus près.

Les avantages d'évolutivité et de performances de Kafka proviennent du transfert de certaines des responsabilités du courtier traditionnel vers le client. Dans ce cas, on décide de distribuer des messages potentiellement liés entre plusieurs consommateurs travaillant en parallèle.

Les courtiers JMS doivent également faire face à ces exigences. Fait intéressant, le mécanisme d'envoi de messages liés au même consommateur, mis en œuvre via les groupes de messages JMS (une variante de la stratégie d'équilibrage de charge persistant (SLB)), nécessite également que l'expéditeur marque les messages comme liés. Dans le cas de JMS, le courtier est chargé d'envoyer ce groupe de messages connexes à un consommateur parmi d'autres et de transférer la propriété du groupe si le consommateur tombe en panne.

Accords de producteurs

Le partitionnement n'est pas la seule chose à prendre en compte lors de l'envoi de messages. Examinons les méthodes send() de la classe Producer dans l'API Java :

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

Il convient de noter immédiatement que les deux méthodes renvoient Future, ce qui indique que l'opération d'envoi n'est pas effectuée immédiatement. Le résultat est qu'un message (ProducerRecord) est écrit dans le tampon d'envoi pour chaque partition active et envoyé au courtier en tant que thread d'arrière-plan dans la bibliothèque client Kafka. Bien que cela rende les choses incroyablement rapides, cela signifie qu'une application inexpérimentée peut perdre des messages si son processus est arrêté.

Comme toujours, il existe un moyen de rendre l'opération d'envoi plus fiable au détriment des performances. La taille de ce buffer peut être fixée à 0, et le thread applicatif émetteur sera forcé d'attendre que le transfert du message vers le broker soit terminé, comme suit :

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

En savoir plus sur la lecture des messages

La lecture des messages présente des complexités supplémentaires sur lesquelles il faut spéculer. Contrairement à l'API JMS, qui peut exécuter un écouteur de message en réponse à un message, le Consommateur Kafka ne fait que des sondages. Regardons de plus près la méthode sondage()utilisé à cet effet :

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

La valeur de retour de la méthode est une structure de conteneur contenant plusieurs objets dossier de consommation à partir potentiellement de plusieurs partitions. dossier de consommation est lui-même un objet détenteur pour une paire clé-valeur avec des métadonnées associées, telles que la partition dont il est dérivé.

Comme indiqué au chapitre 2, nous devons garder à l'esprit ce qui arrive aux messages après qu'ils ont été traités avec succès ou sans succès, par exemple, si le client est incapable de traiter le message ou s'il abandonne. Dans JMS, cela était géré via un mode d'accusé de réception. Le courtier supprimera le message traité avec succès ou redistribuera le message brut ou faux (en supposant que des transactions ont été utilisées).
Kafka fonctionne très différemment. Les messages ne sont pas supprimés dans le courtier après la relecture, et ce qui se passe en cas d'échec relève de la responsabilité du code de relecture lui-même.

Comme nous l'avons dit, le groupe de consommateurs est associé au décalage dans le journal. La position du journal associée à ce décalage correspond au prochain message à émettre en réponse à sondage(). Le moment où ce décalage augmente est déterminant pour la lecture.

Revenant au modèle de lecture discuté précédemment, le traitement des messages se compose de trois étapes :

  1. Récupérer un message pour le lire.
  2. Traiter le message.
  3. Confirmer le message.

Le consommateur Kafka est livré avec une option de configuration activer.auto.commit. Il s'agit d'un paramètre par défaut fréquemment utilisé, comme c'est souvent le cas avec les paramètres contenant le mot "auto".

Avant Kafka 0.10, un client utilisant cette option enverrait le décalage du dernier message lu lors du prochain appel sondage() après traitement. Cela signifiait que tous les messages qui avaient déjà été récupérés pouvaient être retraités si le client les avait déjà traités mais était détruit de manière inattendue avant d'appeler sondage(). Étant donné que le courtier ne conserve aucun état du nombre de fois qu'un message a été lu, le prochain consommateur qui récupère ce message ne saura pas que quelque chose de grave s'est produit. Ce comportement était pseudo-transactionnel. Le décalage n'était validé que si le message était traité avec succès, mais si le client abandonnait, le courtier envoyait à nouveau le même message à un autre client. Ce comportement était conforme à la garantie de remise des messages "au moins une fois«.

Dans Kafka 0.10, le code client a été modifié afin que la validation soit déclenchée périodiquement par la bibliothèque cliente, comme configuré auto.commit.interval.ms. Ce comportement se situe quelque part entre les modes JMS AUTO_ACKNOWLEDGE et DUPS_OK_ACKNOWLEDGE. Lors de l'utilisation de la validation automatique, les messages peuvent être validés, qu'ils aient ou non été réellement traités - cela peut se produire dans le cas d'un consommateur lent. Si un consommateur abandonnait, les messages seraient récupérés par le consommateur suivant, en commençant à la position validée, ce qui pourrait entraîner un message manqué. Dans ce cas, Kafka n'a pas perdu les messages, le code de lecture ne les a tout simplement pas traités.

Ce mode a la même promesse que dans la version 0.9 : les messages peuvent être traités, mais s'il échoue, le décalage peut ne pas être validé, ce qui peut entraîner le doublement de la livraison. Plus vous récupérez de messages lors de l'exécution sondage(), plus ce problème.

Comme indiqué dans «Lecture des messages d'une file d'attente», à la page 21, il n'existe pas de livraison unique d'un message dans un système de messagerie lorsque les modes de défaillance sont pris en compte.

Dans Kafka, il existe deux manières de valider (commit) un décalage (offset) : automatiquement et manuellement. Dans les deux cas, les messages peuvent être traités plusieurs fois si le message a été traité mais a échoué avant la validation. Vous pouvez également choisir de ne pas traiter du tout le message si la validation s'est produite en arrière-plan et que votre code a été terminé avant de pouvoir être traité (peut-être dans Kafka 0.9 et versions antérieures).

Vous pouvez contrôler le processus manuel de validation de décalage dans l'API client Kafka en définissant le paramètre activer.auto.commit à false et en appelant explicitement l'une des méthodes suivantes :

void commitSync();
void commitAsync();

Si vous souhaitez traiter le message "au moins une fois", vous devez valider l'offset manuellement avec commitSync()en exécutant cette commande immédiatement après le traitement des messages.

Ces méthodes ne permettent pas d'accuser réception des messages avant leur traitement, mais elles ne font rien pour éliminer les retards de traitement potentiels tout en donnant l'apparence d'être transactionnelles. Il n'y a pas de transactions à Kafka. Le client n'a pas la possibilité d'effectuer les opérations suivantes :

  • Annuler automatiquement un faux message. Les consommateurs eux-mêmes doivent gérer les exceptions résultant de charges utiles problématiques et de pannes de backend, car ils ne peuvent pas compter sur le courtier pour redistribuer les messages.
  • Envoyez des messages à plusieurs sujets en une seule opération atomique. Comme nous le verrons bientôt, le contrôle de différents sujets et partitions peut résider sur différentes machines du cluster Kafka qui ne coordonnent pas les transactions lors de leur envoi. Au moment d'écrire ces lignes, du travail a été fait pour rendre cela possible avec le KIP-98.
  • Associez la lecture d'un message d'un sujet à l'envoi d'un autre message à un autre sujet. Encore une fois, l'architecture de Kafka dépend de nombreuses machines indépendantes fonctionnant comme un seul bus et aucune tentative n'est faite pour le cacher. Par exemple, il n'y a pas de composants API qui vous permettraient de lier consommateur и Producteur dans une opération. Dans JMS, ceci est fourni par l'objet Sessionà partir de laquelle sont créés Producteurs de messages и MessageConsommateurs.

Si nous ne pouvons pas compter sur les transactions, comment pouvons-nous fournir une sémantique plus proche de celles fournies par les systèmes de messagerie traditionnels ?

S'il est possible que le décalage du consommateur augmente avant que le message n'ait été traité, comme lors d'une panne du consommateur, le consommateur n'a aucun moyen de savoir si son groupe de consommateurs a raté le message lorsqu'il se voit attribuer une partition. Une stratégie consiste donc à rembobiner le décalage à la position précédente. L'API client Kafka fournit les méthodes suivantes pour cela :

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

méthode chercher() peut être utilisé avec la méthode
offsetsForTimes(Carte timestampsToSearch) pour revenir à un état à un moment précis du passé.

Implicitement, l'utilisation de cette approche signifie qu'il est très probable que certains messages précédemment traités seront lus et traités à nouveau. Pour éviter cela, nous pouvons utiliser la lecture idempotente, comme décrit au chapitre 4, pour garder une trace des messages précédemment consultés et éliminer les doublons.

Alternativement, votre code consommateur peut rester simple, tant que la perte ou la duplication de message est acceptable. Lorsque nous considérons les cas d'utilisation pour lesquels Kafka est couramment utilisé, tels que la gestion des événements de journal, les mesures, le suivi des clics, etc., nous comprenons que la perte de messages individuels est peu susceptible d'avoir un impact significatif sur les applications environnantes. Dans de tels cas, les valeurs par défaut sont parfaitement acceptables. D'autre part, si votre application doit envoyer des paiements, vous devez prendre soin de chaque message individuel. Tout dépend du contexte.

Des observations personnelles montrent qu'à mesure que l'intensité des messages augmente, la valeur de chaque message individuel diminue. Les messages volumineux ont tendance à être utiles lorsqu'ils sont affichés sous une forme agrégée.

La haute disponibilité

L'approche de Kafka en matière de haute disponibilité est très différente de celle d'ActiveMQ. Kafka est conçu autour de clusters scale-out où toutes les instances de courtier reçoivent et distribuent des messages en même temps.

Un cluster Kafka se compose de plusieurs instances de courtier s'exécutant sur différents serveurs. Kafka a été conçu pour fonctionner sur du matériel autonome ordinaire, où chaque nœud dispose de son propre stockage dédié. L'utilisation du stockage en réseau (SAN) n'est pas recommandée car plusieurs nœuds de calcul peuvent rivaliser de temps.Ыe intervalles de stockage et créer des conflits.

Kafka est toujours allumé système. De nombreux grands utilisateurs de Kafka n'arrêtent jamais leurs clusters et le logiciel se met toujours à jour avec un redémarrage séquentiel. Ceci est réalisé en garantissant la compatibilité avec la version précédente pour les messages et les interactions entre courtiers.

Brokers connectés à un cluster de serveurs Gardien de zoo, qui agit comme un registre de données de configuration et est utilisé pour coordonner les rôles de chaque courtier. ZooKeeper lui-même est un système distribué qui offre une haute disponibilité grâce à la réplication des informations en établissant quorum.

Dans le cas de base, un sujet est créé dans un cluster Kafka avec les propriétés suivantes :

  • Le nombre de partitions. Comme indiqué précédemment, la valeur exacte utilisée ici dépend du niveau de lecture parallèle souhaité.
  • Le facteur de réplication (facteur) détermine le nombre d'instances de courtier du cluster qui doivent contenir des journaux pour cette partition.

En utilisant ZooKeepers pour la coordination, Kafka tente de répartir équitablement les nouvelles partitions entre les courtiers du cluster. Ceci est fait par une seule instance qui agit en tant que contrôleur.

Lors de l'exécution pour chaque partition thématique Contrôleur attribuer des rôles à un courtier le meneur (leader, maître, présentateur) et suiveurs (suiveurs, esclaves, subordonnés). Le courtier, agissant en tant que leader de cette partition, est chargé de recevoir tous les messages qui lui sont envoyés par les producteurs et de distribuer les messages aux consommateurs. Lorsque des messages sont envoyés à une partition de rubrique, ils sont répliqués sur tous les nœuds de courtier agissant en tant que suiveurs pour cette partition. Chaque nœud contenant les journaux d'une partition est appelé réplique. Un courtier peut agir en tant que leader pour certaines partitions et en tant que suiveur pour d'autres.

Un suiveur contenant tous les messages détenus par le leader est appelé réplique synchronisée (un réplica qui est dans un état synchronisé, réplica in-sync). Si un courtier agissant en tant que leader pour une partition tombe en panne, tout courtier qui est à jour ou synchronisé pour cette partition peut prendre le relais du rôle de leader. C'est une conception incroyablement durable.

Une partie de la configuration du producteur est le paramètre acques, qui détermine le nombre de répliques qui doivent accuser réception (accuser réception) d'un message avant que le thread d'application continue d'envoyer : 0, 1 ou tous. Si réglé sur TOUTE, puis lorsqu'un message est reçu, le meneur renverra une confirmation au producteur dès qu'il aura reçu des confirmations (accusés de réception) de l'enregistrement de plusieurs cues (dont lui-même) définis par le paramètre topic min.insync.replicas (par défaut 1). Si le message ne peut pas être répliqué avec succès, le producteur lèvera une exception d'application (Pas assez de répliques ou NotEnoughReplicasAfterAppend).

Une configuration typique crée un topic avec un facteur de réplication de 3 (1 leader, 2 followers par partition) et le paramètre min.insync.replicas est défini sur 2. Dans ce cas, le cluster permettra à l'un des courtiers gérant la partition de rubrique de descendre sans affecter les applications clientes.

Cela nous ramène au compromis déjà familier entre performance et fiabilité. La réplication se produit au prix d'un temps d'attente supplémentaire pour les confirmations (accusés de réception) des abonnés. Cependant, comme elle s'exécute en parallèle, la réplication sur au moins trois nœuds a les mêmes performances que deux (sans tenir compte de l'augmentation de l'utilisation de la bande passante du réseau).

En utilisant ce schéma de réplication, Kafka évite intelligemment la nécessité d'écrire physiquement chaque message sur le disque avec l'opération synchroniser(). Chaque message envoyé par le producteur sera écrit dans le journal de partition, mais comme expliqué au chapitre 2, l'écriture dans un fichier se fait initialement dans la mémoire tampon du système d'exploitation. Si ce message est répliqué sur une autre instance de Kafka et se trouve dans sa mémoire, la perte du leader ne signifie pas que le message lui-même a été perdu - il peut être repris par une réplique synchronisée.
Refus d'effectuer l'opération synchroniser() signifie que Kafka peut recevoir des messages aussi vite qu'il peut les écrire en mémoire. Inversement, plus longtemps vous pouvez éviter de vider la mémoire sur le disque, mieux c'est. Pour cette raison, il n'est pas rare que les courtiers Kafka se voient allouer 64 Go ou plus de mémoire. Cette utilisation de la mémoire signifie qu'une seule instance de Kafka peut facilement s'exécuter à des vitesses plusieurs milliers de fois plus rapides qu'un courtier de messages traditionnel.

Kafka peut également être configuré pour appliquer l'opération synchroniser() aux paquets de messages. Étant donné que tout dans Kafka est orienté package, cela fonctionne plutôt bien pour de nombreux cas d'utilisation et constitue un outil utile pour les utilisateurs qui exigent des garanties très solides. Une grande partie des performances pures de Kafka provient des messages qui sont envoyés au courtier sous forme de paquets et que ces messages sont lus par le courtier dans des blocs séquentiels à l'aide zéro-copie opérations (opérations pendant lesquelles la tâche de copier des données d'une zone mémoire à une autre n'est pas effectuée). Ce dernier est un gros gain de performances et de ressources et n'est possible que grâce à l'utilisation d'une structure de données de journal sous-jacente qui définit le schéma de partition.

De bien meilleures performances sont possibles dans un cluster Kafka qu'avec un seul courtier Kafka, car les partitions de rubrique peuvent évoluer sur de nombreuses machines distinctes.

Les résultats de

Dans ce chapitre, nous avons examiné comment l'architecture Kafka réinvente la relation entre les clients et les courtiers pour fournir un pipeline de messagerie incroyablement robuste, avec un débit plusieurs fois supérieur à celui d'un courtier de messages conventionnel. Nous avons discuté de la fonctionnalité qu'il utilise pour y parvenir et brièvement examiné l'architecture des applications qui fournissent cette fonctionnalité. Dans le chapitre suivant, nous examinerons les problèmes courants que les applications basées sur la messagerie doivent résoudre et discuterons des stratégies pour les résoudre. Nous terminerons le chapitre en expliquant comment parler des technologies de messagerie en général afin que vous puissiez évaluer leur adéquation à vos cas d'utilisation.

Partie traduite précédente : Comprendre les courtiers de messages. Apprentissage des mécanismes de messagerie avec ActiveMQ et Kafka. Chapitre 1

Traduction faite : tele.gg/middle_java

A suivre ...

Seuls les utilisateurs enregistrés peuvent participer à l'enquête. se connecters'il te plait.

Kafka est-il utilisé dans votre organisation ?

  • Oui

  • Aucun

  • Utilisé auparavant, maintenant non

  • Nous prévoyons d'utiliser

38 utilisateurs ont voté. 8 utilisateurs se sont abstenus.

Source: habr.com

Ajouter un commentaire