Kafka et microservices : un aperçu

Kafka et microservices : un aperçu

Salut tout le monde. Dans cet article, je vais vous expliquer pourquoi chez Avito nous avons choisi Kafka il y a neuf mois et de quoi il s'agit. Je vais partager l'un des cas d'utilisation : un courtier de messages. Et enfin, parlons des avantages que nous avons retirés de l’utilisation de l’approche Kafka as a Service.

problème

Kafka et microservices : un aperçu

Tout d’abord, un peu de contexte. Il y a quelque temps, nous avons commencé à nous éloigner de l'architecture monolithique, et Avito propose déjà plusieurs centaines de services différents. Ils disposent de leurs propres référentiels, de leur propre pile technologique et sont responsables de leur part de la logique métier.

L’un des problèmes d’un grand nombre de services est la communication. Le service A souhaite souvent connaître les informations dont dispose le service B. Dans ce cas, le service A accède au service B via une API synchrone. Le service B veut savoir ce qui se passe avec les services D et D, et ils s'intéressent à leur tour aux services A et B. Lorsqu'il existe de nombreux services « curieux », les connexions entre eux se transforment en un enchevêtrement enchevêtré.

Parallèlement, le service A peut devenir indisponible à tout moment. Et que doivent faire le service B et tous les autres services qui y sont connectés ? Et si une chaîne d'appels synchrones séquentiels est nécessaire pour mener à bien une opération commerciale, la probabilité d'échec de l'ensemble de l'opération devient encore plus élevée (et plus la chaîne est longue, plus elle est élevée).

Choix technologique

Kafka et microservices : un aperçu

D'accord, les problèmes sont clairs. Ils peuvent être éliminés en créant un système de messagerie centralisé entre les services. Désormais, chacun des services n'a besoin que de connaître ce système de messagerie. De plus, le système lui-même doit être tolérant aux pannes et évolutif horizontalement, et également, en cas d'accident, accumuler un tampon d'accès pour un traitement ultérieur.

Sélectionnons maintenant la technologie sur laquelle la livraison des messages sera implémentée. Pour ce faire, comprenons d’abord ce que nous en attendons :

  • les messages entre services ne doivent pas être perdus ;
  • les messages peuvent être dupliqués ;
  • les messages peuvent être stockés et lus sur une profondeur de plusieurs jours (tampon persistant) ;
  • les services peuvent s'abonner aux données qui les intéressent ;
  • plusieurs services peuvent lire les mêmes données ;
  • les messages peuvent contenir une charge utile détaillée et volumineuse (transfert d'état transporté par événement) ;
  • Parfois, vous devez garantir l’ordre des messages.

Il était également extrêmement important pour nous de choisir le système le plus évolutif et le plus fiable avec un débit élevé (au moins 100 XNUMX messages de plusieurs kilo-octets par seconde).

À ce stade, nous avons dit au revoir à RabbitMQ (difficile à maintenir stable à des rps élevés), PGQ de SkyTools (pas assez rapide et n'évolue pas bien) et NSQ (pas persistant). Nous utilisons toutes ces technologies dans notre entreprise, mais elles n'étaient pas adaptées au problème à résoudre.

Ensuite, nous avons commencé à examiner des technologies qui étaient nouvelles pour nous : Apache Kafka, Apache Pulsar et NATS Streaming.

Pulsar a été le premier à être abandonné. Nous avons décidé que Kafka et Pulsar sont des solutions assez similaires. Et malgré le fait que Pulsar a été testé par de grandes entreprises, qu'il est plus récent et offre une latence plus faible (en théorie), nous avons décidé de laisser Kafka parmi ces deux-là comme standard de facto pour de telles tâches. Nous reviendrons probablement à Apache Pulsar dans le futur.

Et maintenant, il reste deux candidats : NATS Streaming et Apache Kafka. Nous avons étudié les deux solutions en détail et toutes deux étaient adaptées à la tâche. Mais au final, nous avions peur de la relative jeunesse de NATS Streaming (et du fait que l'un des principaux développeurs, Tyler Treat, ait décidé de quitter le projet et de créer le sien - Liftbridge). Dans le même temps, le mode Clustering de NATS Streaming n'offrait pas la possibilité d'une forte mise à l'échelle horizontale (ce n'est probablement plus un problème après l'ajout du mode de partitionnement en 2017).

Cependant, NATS Streaming est une technologie intéressante écrite en Go et prise en charge par la Cloud Native Computing Foundation. Contrairement à Apache Kafka, il n'a pas besoin de Zookeeper pour fonctionner (peut-être on pourra bientôt en dire autant de Kafka), puisqu'il implémente RAFT en interne. Dans le même temps, NATS Streaming est plus facile à administrer. Nous n’excluons pas que nous revenions à cette technologie à l’avenir.

Et pourtant, aujourd’hui notre gagnant est Apache Kafka. Lors de nos tests, il s'est avéré assez rapide (plus d'un million de messages par seconde en lecture et en écriture avec un volume de messages de 1 kilo-octet), assez fiable, hautement évolutif et éprouvé par l'expérience de la production de grandes entreprises. De plus, Kafka prend en charge au moins plusieurs grandes entreprises commerciales (nous utilisons par exemple la version Confluent), et Kafka dispose également d'un écosystème développé.

Kafka en résumé

Avant de commencer, je voudrais immédiatement recommander un excellent livre - "Kafka : le guide définitif" (il existe aussi une traduction russe, mais les termes sont un peu ahurissants). Il contient les informations dont vous avez besoin pour acquérir une compréhension de base de Kafka et même un peu plus. La documentation d'Apache et le blog de Confluent sont également bien rédigés et faciles à lire.

Voyons donc à vol d’oiseau le fonctionnement de Kafka. La topologie de base de Kafka comprend le producteur, le consommateur, le courtier et le gardien de zoo.

Broker

Kafka et microservices : un aperçu

Le courtier est responsable du stockage de vos données. Toutes les données sont stockées sous forme binaire et le courtier sait peu de choses sur ce qu'elles sont et quelle est leur structure.

Chaque type d'événement logique se trouve généralement dans sa propre rubrique distincte. Par exemple, l'événement de création d'une annonce peut tomber dans le sujet item.created, et l'événement de sa modification peut tomber dans item.changed. Les sujets peuvent être considérés comme des classificateurs d'événements. Au niveau du sujet, vous pouvez définir des paramètres de configuration tels que :

  • la quantité de données stockées et/ou leur âge (retention.bytes, retention.ms) ;
  • facteur de redondance des données (facteur de réplication) ;
  • taille maximale d'un message (max.message.bytes) ;
  • le nombre minimum de répliques cohérentes auxquelles les données peuvent être écrites dans une rubrique (min.insync.replicas) ;
  • la possibilité d'effectuer un basculement sur une réplique en retard non synchrone avec une perte potentielle de données (unclean.leader.election.enable) ;
  • et beaucoup plus (https://kafka.apache.org/documentation/#topicconfigs).

À son tour, chaque sujet est divisé en une ou plusieurs partitions. C'est dans les partis que les événements finissent par tomber. S'il y a plus d'un courtier dans le cluster, les partitions seront réparties uniformément entre tous les courtiers (dans la mesure du possible), ce qui permettra à la charge d'écriture et de lecture dans une rubrique d'être répartie sur plusieurs courtiers à la fois.

Sur le disque, les données de chaque partition sont stockées sous forme de fichiers de segments, par défaut égaux à un gigaoctet (contrôlés via log.segment.bytes). Une caractéristique importante est que les données sont supprimées des partitions (lorsque la rétention est déclenchée) par segments (vous ne pouvez pas supprimer un événement d'une partition, vous ne pouvez supprimer qu'un segment entier et uniquement celui inactif).

Zookeeper

Zookeeper agit en tant que magasin de métadonnées et coordinateur. C'est lui qui est capable de dire si les courtiers sont en vie (vous pouvez regarder cela à travers les yeux du gardien de zoo en utilisant zookeeper-shell avec la commande ls /brokers/ids), quel courtier est le contrôleur (get /controller), si les partitions sont synchronisées avec leurs répliques (get /brokers/topics/topic_name/partitions/partition_number/state). Aussi, c'est chez zookeeper que le producteur et le consommateur s'adresseront en premier pour savoir sur quel courtier quels sujets et quelles partitions sont stockés. Dans les cas où un facteur de réplication supérieur à 1 est spécifié pour un sujet, zookeeper indiquera quelles partitions sont les leaders (elles seront écrites et lues). En cas de panne du courtier, les informations sur les nouvelles partitions leader seront enregistrées dans zookeeper (à partir de la version 1.1.0 de manière asynchrone, et c'est important).

Dans les anciennes versions de Kafka, zookeeper était également responsable du stockage des compensations, mais elles sont désormais stockées dans une rubrique spéciale. __consumer_offsets sur le courtier (bien que vous puissiez toujours utiliser zookeeper à ces fins).

Le moyen le plus simple de transformer vos données en citrouille est de perdre les informations du gardien de zoo. Dans un tel scénario, il sera très difficile de comprendre quoi lire et d'où.

Producteur

Producer est le plus souvent un service qui écrit directement des données sur Apache Kafka. Le producteur sélectionne un sujet dans lequel stocker ses messages de sujet et commence à y écrire des informations. Par exemple, le producteur pourrait être un service publicitaire. Dans ce cas, il enverra des événements tels que « annonce créée », « annonce mise à jour », « annonce supprimée », etc. vers des sujets thématiques. Chaque événement est une paire clé-valeur.

Par défaut, tous les événements sont distribués entre les partitions de sujet à l'aide d'un tourniquet si la clé n'est pas spécifiée (ordre perdu) et via MurmurHash (clé) si la clé est présente (ordre dans une partition).

Il convient de noter d'emblée que Kafka garantit l'ordre des événements uniquement au sein d'un seul lot. Mais en réalité, ce n’est souvent pas un problème. Par exemple, vous pouvez être sûr d'ajouter toutes les modifications apportées à la même déclaration dans une seule partition (préservant ainsi l'ordre de ces modifications dans la déclaration). Vous pouvez également envoyer un numéro de séquence dans l'un des champs d'événement.

Consommateur

Kafka et microservices : un aperçu

Le consommateur est responsable de la récupération des données depuis Apache Kafka. Si l’on revient à l’exemple ci-dessus, le consommateur pourrait être un service de modération. Ce service sera abonné au sujet du service publicitaire et lorsqu'une nouvelle annonce apparaîtra, il la recevra et l'analysera pour vérifier sa conformité à certaines politiques spécifiées.

Apache Kafka se souvient des événements récents reçus par le consommateur (une rubrique de service est utilisée à cet effet). __consumer__offsets), garantissant ainsi que si la lecture réussit, le consommateur ne recevra pas deux fois le même message. Cependant, si vous utilisez l'option activate.auto.commit = true et déléguez entièrement le travail de suivi de la position du consommateur dans le sujet à Kafka, vous pouvez perdre des données. Dans le code de production, le plus souvent la position du consommateur est contrôlée manuellement (le développeur contrôle le moment où la validation de l'événement de lecture doit avoir lieu).

Dans les cas où un seul consommateur ne suffit pas (par exemple, le flux de nouveaux événements est très important), vous pouvez ajouter plusieurs consommateurs supplémentaires en les reliant entre eux dans un groupe de consommateurs. Un groupe de consommateurs est logiquement exactement identique à un consommateur, mais avec des données réparties entre les membres du groupe. Cela permet à chaque participant de prendre sa part de messages, augmentant ainsi la vitesse de lecture.

Résultats de test

Kafka et microservices : un aperçu

Je n’écrirai pas ici beaucoup de texte explicatif, je partagerai juste les résultats obtenus. Les tests ont été effectués sur 3 machines physiques (12 CPU, 384 Go de RAM, 15 10 disques SAS, XNUMX Go/s Net), les courtiers et le zookeeper ont été déployés dans lxc.

Test de performance

Lors des tests, les résultats suivants ont été obtenus.

  • La vitesse d'enregistrement simultané de messages de 1 Ko par 9 producteurs est de 1300000 XNUMX XNUMX événements par seconde.
  • La vitesse de lecture simultanée de messages de 1 Ko par 9 consommateurs est de 1500000 XNUMX XNUMX événements par seconde.

Tests de tolérance aux pannes

Lors des tests, les résultats suivants ont été obtenus (3 courtiers, 3 gardiens de zoo).

  • L'arrêt anormal de l'un des courtiers n'entraîne pas l'arrêt ou l'indisponibilité du cluster. Le travail se poursuit normalement, mais les courtiers restants ont une lourde charge de travail.
  • La terminaison anormale de deux courtiers dans le cas d'un cluster de trois courtiers et min.isr = 2 conduit à ce que le cluster soit indisponible en écriture, mais accessible en lecture. Si min.isr = 1, le cluster continue d'être disponible en lecture et en écriture. Cependant, ce mode contredit l’exigence d’une sécurité élevée des données.
  • Un arrêt anormal de l'un des serveurs Zookeeper n'entraîne pas l'arrêt ou l'indisponibilité du cluster. Les travaux se poursuivent normalement.
  • Un arrêt anormal de deux serveurs Zookeeper entraîne l'indisponibilité du cluster jusqu'à ce qu'au moins un des serveurs Zookeeper soit restauré. Cette affirmation est vraie pour un cluster Zookeeper de 3 serveurs. De ce fait, après recherches, il a été décidé d'augmenter le cluster Zookeeper à 5 serveurs pour augmenter la tolérance aux pannes.

Kafka en tant que service

Kafka et microservices : un aperçu

Nous sommes convaincus que Kafka est une excellente technologie qui nous permet de résoudre la tâche qui nous est assignée (implémenter un courtier de messages). Cependant, nous avons décidé d'interdire aux services d'accéder directement à Kafka et de l'ajouter à un service de bus de données. Pourquoi avons-nous fait cela ? En fait, il y a plusieurs raisons.

  • Data-bus a pris en charge toutes les tâches liées à l'intégration avec Kafka (implémentation et configuration des consommateurs et des producteurs, surveillance, alertes, journalisation, mise à l'échelle, etc.). Ainsi, l'intégration avec le courtier de messages est aussi simple que possible.

  • Le bus de données nous a permis de nous éloigner d'un langage ou d'une bibliothèque spécifique pour travailler avec Kafka.

  • Le bus de données a permis à d'autres services d'abstraire la couche de stockage. Peut-être qu'à un moment donné, nous remplacerons Kafka par Pulsar, et personne ne remarquera rien (tous les services ne connaissent que l'API du bus de données).

  • Data-bus a pris en charge la validation des schémas d'événements.

  • L'authentification est mise en œuvre à l'aide d'un bus de données.

  • Sous le couvert du bus de données, nous pouvons mettre à jour tranquillement les versions de Kafka sans temps d'arrêt, gérer de manière centralisée les configurations des producteurs, des consommateurs, des courtiers, etc.

  • Data-bus nous a permis d'ajouter les fonctionnalités dont nous avions besoin et qui ne sont pas dans Kafka (comme l'audit de sujets, la surveillance des anomalies dans le cluster, la création de DLQ, etc.).

  • Data-bus vous permet de mettre en œuvre le basculement de manière centralisée pour tous les services.

Pour le moment, pour commencer à envoyer des événements au courtier de messages, il vous suffit de connecter une petite bibliothèque à votre code de service. C'est tout. Vous avez la possibilité d'écrire, de lire et de mettre à l'échelle avec une seule ligne de code. L’intégralité de l’implémentation vous est cachée, seules quelques poignées de taille de lot ressortent. Sous le capot, le service de bus de données augmente le nombre requis d'instances productrices et consommatrices dans Kubernetes et leur fournit la configuration nécessaire, mais tout cela est transparent pour votre service.

Bien entendu, il n’existe pas de solution miracle et cette approche a ses limites.

  • Le bus de données doit être pris en charge en interne, par opposition aux bibliothèques tierces.
  • Le bus de données augmente le nombre d'interactions entre les services et le courtier de messages, ce qui entraîne des performances inférieures à celles de Kafka seul.
  • Tout ne peut pas être caché aux services aussi facilement ; nous ne voulons pas dupliquer les fonctionnalités de KSQL ou de Kafka Streams dans le bus de données, nous devons donc parfois autoriser les services à y accéder directement.

Dans notre cas, les avantages l'emportaient sur les inconvénients et la décision de couvrir le courtier de messages avec un service distinct était justifiée. Au cours de l’année d’exploitation, nous n’avons eu aucun accident ni problème grave.

PS Merci à ma petite amie, Ekaterina Obalyaeva, pour les superbes photos de cet article. Si vous les avez aimés, ici il y a d'autres illustrations à venir.

Source: habr.com

Ajouter un commentaire