Table des matières

Qu’est-ce que le streaming de données ?

Dans un monde où les systèmes temps réel sont légion, le traitement traditionnel des données n’est plus viable. Les entreprises travaillent de plus en plus avec des flux massifs de données (financières, logs d’application, données IoT…) et il est nécessaire d’avoir des systèmes adaptés et capables de gérer ces types de flux.

Une plateforme de streaming de données est un système hautement scalable, capable d’ingérer en continu des millions d’événements par seconde à partir de diverses sources.

Les données collectées sont alors disponibles en quelques millisecondes pour des applications consommatrices d’événements, qui peuvent réagir aux événements au fur et à mesure qu’ils se produisent.

L’objectif ultime d’une plateforme de streaming est de capturer et distribuer les événements importants (fonctionnellement) au fur et à mesure qu’ils se produisent, et de permettre aux entreprises de concevoir des processus métiers qui réagissent en temps réel à ces événements, pour offrir des expériences client réactives, personnalisées et riches.

On peut citer plusieurs cas d’utilisation qui profitent directement des avantages de ce genre de plateforme :

  • La détection de fraude en temps réel
  • L’ingestion de données de capteurs IoT
  • La construction de tableaux de bord en temps réel
  • Le monitoring et les métriques (log, trafic, réseau…)
  • Le suivi et le tracking (de véhicules par exemple)

Attention à ne pas confondre le type de données et l’usage qui en est fait. On peut collecter des données en continu sans qu’il soit nécessaire de les traiter en continu.

Si ce côté temps réel n’est pas nécessaire, on peut envisager du traitement par lot (aka. batch) ou séquentiel. Cela peut permettre d’effectuer des analyses plus pointues et des calculs complexes, au prix d’une latence plus importante.

Si l’on a besoin d’avoir un résultat instantané, alors on passe du côté du stream computing, au prix d’une architecture et d’une puissance de calcul plus importante.

Comment ça fonctionne ?

Pour commencer, il est important de définir certains concepts intervenant dans une plateforme de streaming de données.

Tout d’abord, il est nécessaire de faire la différence entre un message, une commande et un événement. Ces 3 éléments gravitent autour des mêmes concepts, mais ont des applications et aspirations bien différentes.

Message

Conceptuellement, un message est une requête émise d’un système à un autre contenant une charge utile (aka. payload) de toutes les données nécessaires à son traitement ainsi que des attributs (aka. métadonnées).

Il peut utiliser différents formats, et est basé sur un contrat d’interface qui existe entre deux systèmes (l’émetteur et le récepteur).

Un message n’a pas d’intention particulière, ce qui le rend générique, mais aussi moins significatif. C’est d’ailleurs pour ça que l’on utilise deux autres concepts, en plus de celui de message.

Dans le cadre d’une application de messagerie instantanée, on envoie un message à un autre utilisateur contenant : “Hello ! Comment vas-tu ?”.

Il s’agit d’un message. Le payload est le texte “Hello ! Comment vas-tu ?”. Les métadonnées pourraient être un identifiant de message, des données d’horodatage, un statut (eg. envoyé, lu, reçu…), un indicateur de priorité ou encore la langue du message.

Commande

Une commande est un type de message spécifique, émis par un système en direction d’un autre, lui demandant de réaliser une action précise (aka. impératif).

Une commande représente un futur possible (changement d’état) et le système cible à la possibilité de la valider, l’approuver, la rejeter, la traiter ou y répondre.

Lorsqu’une commande est lancée, l’action n’est pas encore réalisée et l’état du système n’est pas encore affecté.

On est dans l’orchestration.

Dans le cadre de l’utilisation d’un logiciel de dessin sur ordinateur, on sélectionne l’outil Crayon puis on dessine une ligne.

Le choix de l’outil Crayon est une commande envoyée au logiciel pour qu’il change l’outil actif. Le dessin d’une ligne est une autre commande pour indiquer au logiciel de dessiner une ligne à l’endroit spécifié.

Evènement

Un événement est un message qui représente un fait. Il sert à informer que quelque chose s’est déjà produit dans le passé ou qu’un changement vient d’avoir lieu. C’est un objet léger. L’émetteur du message n’a aucune attente vis-à-vis de la façon dont est géré l’évènement.

Contrairement au message, un évènement contient des informations sur ce qui est arrivé, mais ne contient pas les données qui ont déclenché l’évènement.

Et puisqu’un événement représente le passé, une fois produit, il ne change jamais, on dit qu’il est immuable (immutable en anglais). De ce fait, contrairement à une commande, un événement ne peut pas être approuvé ou rejeté par ceux qui le reçoivent (personne ne peut réécrire le passé !).

Lorsque l’évènement est instancié, l’action est déjà réalisée et l’état du système est déjà affecté.

On est dans la chorégraphie.

Dans le cadre d’une conférence en ligne, il existe une plateforme permettant aux participants de poser des questions en ligne.

Lorsque l’on soumet une question, la plateforme génère un évènement “Question soumise”. Les organisateurs peuvent alors être notifiés de cet évènement et choisir de répondre à la question.

L’évènement vient ici signaler qu’une action (la soumission d’une question) a eu lieu.

Orchestration vs Chorégraphie

L’orchestration est une approche centralisée. On va retrouver un service (aka. l’orchestrateur) qui est responsable de diriger le processus et de déterminer la logique d’interaction entre les services. Il va définir le flux d’exécution, c’est-à-dire l’ordonnancement et la gestion des appels aux autres services. On se retrouve donc avec des dépendances plus explicites et un couplage fort, car l’orchestrateur connait et interagit directement avec chacun des services (il doit savoir où se trouvent les services, comment les appeler, quels formats ils utilisent…).

La chorégraphie est une approche décentralisée. Chaque service sait quand et comment interagir avec les autres services. Il n’y a pas de contrôleur central. On est souvent dans une logique évènementielle, ou les services écoutent et réagissent à ces évènements selon leur propre logique interne. On se retrouve donc avec des dépendances plus implicites et un couplage faible, car les services ont seulement besoin de s’accorder sur le format et la signification des évènements. La responsabilité est distribuée (comme celle de la gestion des erreurs). Cela évite les SPOF (aka. Single Point Of Failure) mais augmente la complexité du système.

N’hésitez pas à lire l’excellent ouvrage de Sam Newman : Building Microservices.

Messaging vs Streaming

Une plateforme de Messaging (message broker en anglais) est un système assez simple, composé généralement de files d’attente de messages (message queue en anglais) qui peuvent toutes avoir un ou plusieurs consommateurs et/ou producteurs de messages.

Dans une file d’attente avec plusieurs consommateurs, la file d’attente tentera de répartir les messages uniformément entre chaque consommateur, et garantira que chaque message ne sera livré qu’une seule fois. Après consommation, les messages sont perdus.

Elles possèdent souvent 2 options qui très utiles :

  • Durabilité : lorsque tous les consommateurs sont occupés, le message broker" stocke les messages en mémoire, dans l’attente que les consommateurs puissent les consommer, plutôt que de forcer le transit vers les consommateurs et potentiellement perdre des messages.
  • Persistance : par défaut, les messages sont souvent stockés en mémoire. Pour s’assurer de ne pas perdre les messages reçus (lors d’un redémarrage du système par exemple), le message broker peut persister ses messages sur un espace de stockage pérenne (disque dur, bucket S3…) et ainsi s’assurer de ne pas perdre la liste des messages en attente.

La durabilité et la persistance sont deux options importantes recherchées dans les files d’attente de messages, mais toutes les files d’attente n’en ont pas nécessairement besoin, c’est pourquoi elles sont optionnelles.

De l’autre côté, une plateforme de Streaming diffère par son organisation de la donnée. En effet, les messages sont organisés dans un fichier de journal ou un topic. Chaque consommateur s’abonne alors aux fichiers de journaux ou topics qu’il souhaite, et il recevra tous les messages qui transitent par la plateforme de Streaming.

Avec la configuration appropriée, la plateforme enverra le même message à chaque abonné, dans un ordre spécifique (celui du fichier de log).

Le point intéressant avec une plateforme de Streaming c’est que les consommateurs ont la possibilité de recevoir les nouveaux messages, mais ils ont aussi la possibilité d’accéder au fichier de log et de lire les messages depuis l’instant précis qu’ils souhaitent (depuis le début de vie du topic, depuis hier soir à 18h04, …).

Ainsi, le consommateur devient responsable de l’emplacement de lecture auquel il se trouve, contrairement à une plateforme de Messaging où c’est la file d’attente qui s’assure de ne délivrer le message qu’à un seul consommateur.

De plus, après avoir livré un message, la file d’attente le supprimera. Pour retraiter un message, il faut alors prévoir des mécanismes de sauvegarde des messages reçus. A contrario, puisqu’une plateforme de Streaming se base sur un fichier de log, les consommateurs peuvent alors à loisir avancer ou reculer dans le fichier de log et retraiter les messages qu’ils ont déjà reçus.

Le modèle publish/subscribe (aka. pub/sub)

Contrairement à un système point à point, où les applications s’envoient directement des messages (et donc en connaissant le(s) destinataire(s)), le modèle pub/sub s’organise autour d’un concept de publication de messages vers des sujets. Ce modèle est revenu sur le devant de la scène avec l’avènement des applications micro-services et cloud-native.

Le modèle pub/sub fait intervenir plusieurs acteurs :

  • Editeur/producteur (aka. pub) : créé les messages et les envois au broker
  • Courtier/broker : reçoit les messages entrants et les distribue vers les abonnés. Le broker a la charge du queueing (aka. la gestion des files d’attente), du routage et de la traduction (en cas de protocoles différents)
  • Abonné/consommateur (aka. sub) : s’abonne aux sujets qui l’intéressent pour consulter les messages du broker

L’élément fondamental dans le modèle pub/sub est la dissociation (aka. le découplage) des clients producteurs et consommateurs, et ce, sous plusieurs dimensions :

  • Dissociation spatiale : les clients n’ont pas besoin de se connaitre (adresse IP et port par exemple)
  • Dissociation temporelle : les clients n’ont pas besoin de s’exécuter en même temps
  • Dissociation de synchronisation : les opérations ne sont pas interrompues lors de l’envoi ou la lecture

Cette dissociation permet de grandement fiabiliser l’ensemble de la chaine, notamment la gestion des indisponibilités (temporaires). Elle contribue aussi à la scalabilité et la réactivité du système, en déplaçant la responsabilité de délivrance des messages au courtier et non aux expéditeurs des messages.

Ce modèle est très utilisé, mais vient néanmoins avec son lot de problématiques et/ou difficultés :

  • La sécurité (aka. authentification) : il est important de s’assurer que l’on ne laisse pas n’importe qui lire et écrire dans les files, et pour cela, le moyen le plus fiable est de demander une authentification
  • La gestion des messages incohérents, qui peuvent causer des soucis au moment de leur consommation. Il est important d’être capable de les détecter/identifier et de les corriger et/ou supprimer.
  • La détection des doublons, qui au même titre que les messages incohérents doivent être traités pour éviter les effets de bord
  • L’ordonnancement des messages, en particulier dans les systèmes distribués où plusieurs émetteurs envoient des messages à un même topic
  • La garantie de livraison, dans le cas où il y a plusieurs récepteurs et que des pannes réseaux sont susceptibles d’arriver, cela peut nécessiter des mécanismes de confirmation et de retransmission

Les protocoles

Les messages entrants peuvent utiliser plusieurs protocoles. Nous allons faire le tour des principaux protagonistes.

AMQP (Advanced Message Queuing Protocol)

AMQP est un protocole open source et normalisé permettant de transférer de manière asynchrone, sécurisée et fiable des messages entre 2 parties. Il a été créé en 2003 par John O’Hara (de la banque JPMorgan Chase) et est désormais géré par l’organisme OASIS (ISO/IEC 19464:2014). Il s’agit du protocole le plus populaire et il est considéré par beaucoup comme le standard.

Techniquement parlant, c’est un protocole binaire (au niveau de la couche 7 – application - du modèle OSI, au même titre que HTTP ou TCP par exemple), c’est-à-dire qu’il décrit le format des données envoyées sous forme de flux d’octets. Cela veut donc dire que toute application qui peut créer et interpréter des messages conformes à ce format peut interagir avec d’autres outils conformes, et ce, quel que soit le langage (aka. interopérabilité).

Comme son nom l’indique, on va naturellement retrouver dans AMQP la notion de queue (aka. file d’attente en français) dans laquelle seront déposés les messages. Attention tout de même, avec AMQP, un message n’est jamais envoyé directement dans une file, mais passe par une zone d’échange (aka. exchange, sorte de boîte aux lettres). Ensuite, les règles de livraison sont gérées via des liaisons (aka. bindings) et des attributs de message (aka. routing keys).

AMQP propose plusieurs modes de livraison des messages :

  • fire-and-forget : le message est envoyé sans attendre d’accusé de réception. Il n’y a aucune garantie que le message sera livré ou traité. Il s’agit du mode le plus rapide, mais le moins fiable.
  • at-most-once : un seul envoi avec la possibilité qu’il soit manqué (jamais livré) mais sans risque de doublon.
  • at-least-once : livraison garantie avec la possibilité de messages dupliqués. Nécessite généralement l’utilisation d’accusé de réception.
  • exactly-once : livraison unique garantie. Il s’agit du mode le plus fiable, mais il y a un impact sur les performances. Il est assez complexe à mettre en oeuvre et n’est pas recommandé pour toutes les applications.

Pour être complet, on peut ajouter le support de l’authentification et du chiffrement des messages, la possibilité d’accuser la réception des messages ainsi qu’une très large disponibilité de bibliothèques et d’outils.

MQTT (Message Queuing Telemetry Transport)

MQTT est un protocole open source permettant d’assurer la communication non permanente entre des appareils. Il a été créé en 1999 par Andy Stanford-Clark (IBM) et Arlen Nipper (EuroTech) pour surveiller un oléoduc dans le désert ! Il a donc été conçu pour être léger, avec notamment un soin particulier sur la consommation de bande passante et de CPU.

C’est devenu une norme ISO (ISO/IEC 20922:2016) en 2016. Il est essentiellement utilisé dans le cas de communication M2M (machine-to-machine) et dans le domaine de l’IoT. Le protocole repose sur TCP/IP et au même titre que AMQP, il s’agit d’un protocole binaire.

Attention, la charge utile des messages est limitée à 256 Mo

MQTT propose 3 niveaux de QoS (très proche de ceux d’AMQP) :

  • QoS 0 : chaque message est envoyé à l’abonné une seule fois, sans accusé de réception (aka. at most once) et sans conservation des messages
  • QoS 1 : le broker tente de remettre le message et attend un accusé de réception de la part de l’abonné. En cas de dépassement du délai, le message est renvoyé (aka. at least once)
  • QoS 2 : le client et le broker utilisent 2 paires de paquets pour s’assurer que le message a été remis (aka. exactly once)

Les clients peuvent spécifier le niveau de QoS maximal qu’ils souhaitent recevoir, mais plus le niveau est élevé et plus c’est gourmand en termes de latence et de bande passante.

Une session MQTT est composée de 4 étapes :

  • La connexion : création d’une connexion TCP/IP vers le broker
  • L’authentification : soit via un certificat SSL, soit via un couple login/password (moins gourmand) ou sans rien (certains brokers peuvent être ouverts en anonyme)
  • La communication : envoi des messages sur le broker (en respectant l’un des niveaux de QoS) ou abonnement/désabonnement à un topic
  • La terminaison : fermeture de la connexion

Pour être complet, MQTT supporte lui aussi le chiffrement (SSL/TLS) ainsi que l’authentification (simple ou via OAuth), un système de rubriques hiérarchisées, et dans sa dernière version (MQTT 5.0) un mécanisme d’expiration des messages et de gestion des codes d’erreur dans les accusés de réception.

STOMP (Simple/Streaming Text Oriented Messaging Protocol)

Contrairement à AMQP et MQTT, STOMP est un protocole textuel, conçu dans l’esprit d’HTTP. Il est très simple à implémenter côté client, et on peut même se connecter à un serveur STOMP via un vulgaire telnet !

Évidemment, il en existe beaucoup d’autres (XMPP, OpenWire, HTTP…) mais les 3 que nous avons présentés sont les plus utilisés dans le domaine de l’ingestion de données.

Le cycle de vie : Ingestion / Stockage / Diffusion

Le cycle de vie de la majorité des plateformes de streaming tourne généralement autour de 4 étapes :

  • La collecte et l’ingestion : cette couche est responsable de la réception des évènements entrants, avec le support de plusieurs protocoles et à partir de diverses sources (capteurs, journaux, applications…)
  • Le traitement et l’analyse : les données sont traitées, avec des opérations comme le filtrage, l’agrégation ou l’enrichissement, puis analysées pour obtenir des informations ou détecter des modèles
  • Le stockage : les données ingérées sont stockées et organisées dans cette couche. Les données sont réparties dans des topics (en fonction de leur typologie), qui sont eux-mêmes découpés en partitions (il s’agit de la plus petite unité de stockage). C’est ce découpage qui permet à une plateforme de streaming d’offre une scalabilité et un débit important aux consommateurs (les partitions sont réparties sur plusieurs brokers).
  • La diffusion/consommation : cette couche est responsable de la mise à disposition de la donnée auprès des applications/services concernés

Avantages et inconvénients

Comme toujours dans le monde de l’informatique, il est bon de rappeler qu’il n’existe pas de Silver Bullet. L’approche Event-Driven à travers une plateforme de streaming offre de nombreux avantages, mais arrive aussi avec son lot d’inconvénients.

Avantages

Nous allons commencer par faire le tour des avantages.

Découplage applicatif

Avec une approche traditionnelle, chaque service est chargé d’appeler et coordonner ses interactions entre tous les sous-services qui lui sont nécessaires à l’élaboration d’un cas d’usage. Cette approche peut rapidement entrainer des goulots d’étranglement et amener à des deadlocks applicatifs distribués (service A appelle service B, et service B doit rappeler service A). De plus, elle rend les services fortement interdépendants entre eux, que ce soit en termes d’interface d’échange que de disponibilité en ligne.

Avec une approche event-first, on supprime ce découplage en imposant à chaque service de lever un événement signifiant la fin d’un traitement de son côté. Dès lors, les autres services dont dépend le cas d’usage, doivent s’abonner aux typologies d’événements, effectuer par eux-mêmes les actions qui les concernent, et terminer en levant un événement indiquant la fin du traitement.

L’approche event-first a pour avantage de ne pas exiger de contraintes en termes de disponibilités aux services (si je suis absent 5 minutes, le temps d’une mise à jour, je traiterai les messages reçus à mon retour) et limite le couplage au format de données échangé dans les événements (format qui peut être versionné au besoin).

Auditabilité du système

Un système conçu autour de l’approche event-first, est un système que l’on peut auditer en temps réel à travers de l’analyse de flux (aka. stream analytics). Ces analyses peuvent conduire à de la détection de fraude ou d’anomalies dans les processus ou dans les données entrantes.

Augmente la composabilité du système

Avec une documentation correcte des événements, il devient alors plus simple pour le métier et les développeurs de déterminer quels sont les événements sur lesquels s’appuyer pour répondre aux cas d’usages et besoin métier. Et ainsi faire évoluer correctement le système, voire d’y inclure de nouvelles briques au besoin.

Force à rendre visible son travail

Puisque le principal moteur de communication est l’événement, il devient alors vital pour les équipes en charge de développer les services et événements, de se montrer ouvertes et de documenter correctement leurs événements, en expliquant les tenants et aboutissants qui conduisent au design de leurs événements. Ainsi, en rendant visible son travail, chaque intervenant sera autonome pour déterminer quoi consommer, ainsi que pour comprendre quand et pourquoi un événement est déprécié/mis à jour.

Fourni une source de vérité unique pour la donnée business

Les événements représentants des faits passés, chaque événement en transit par la plateforme de streaming peut alors faire office de source de vérité. On peut alors se replonger dans l’historique des événements pour déterminer la chaine causale de l’évolution d’un élément.

Inconvénients

Parmi les inconvénients fréquemment croisés avec cette approche, on retrouve :

Le coût de mise en place initiale

Ce coût est réparti selon 3 axes :

  • Infrastructure : La mise en place d’une plateforme de streaming, hautement disponible et géorépliquée, nécessite de bonnes connaissances réseau, système, et de ladite plateforme pour qu’elle soit configurée correctement.
  • Code : L’approche event-first étant différente de l’approche traditionnelle, elle nécessite d’écrire du code plus concis pour chaque cas d’usage, mais sa nature hautement distribuée oblige à écrire du code plus robuste, et mieux préparé :
    1. A l’échec : Il est impératif de savoir “Comment réagir en cas d’échec de mon service ?” (faut-il envoyer des événements spécifiques ? Rejouer le traitement ultérieurement ? …) et “Comment réagir aux échecs des autres services ?” suite à un événement indiquant l’impossibilité de traiter un cas d’usage
    2. A l’évolution : Il faut savoir s’adapter aux mises à jour des schémas des événements auxquels on est abonné, et avoir des alertes en cas de mise à jour/remplacement d’un événement, pour pouvoir correctement mettre à jour nos services
  • Modèle mental : Passer d’une méthode traditionnelle à une approche event-first n’est pas inné. Il est donc important de préparer les développeurs à cette approche, au travers de la formation, mais aussi de l’accompagnement au design des services et des chorégraphies entre les événements.

Pas de transactions distribuées

Une chose que la méthode traditionnelle autorise, c’est d’appliquer des transactions globales à tout un cas d’usage. En effet, il est simple pour un monolithe de poser un verrou sur une base de données, le temps de faire tous ses enregistrements, puis de faire sauter le verrou.

Dans un environnement micro-service, chaque service dispose de sa base de données, dès lors, poser un verrou commun à plusieurs services devient alors impossible. Qui plus est, la dure réalité que nous imposent les 8 illusions de l’informatique distribuées rend clairement irrationnelle l’approche transactionnelle dans un environnement micro-service.

Au final, il faut adopter le principe d’Eventual Consistency, et préparer ses applicatifs aux différents cas d’échec des cas d’usages.

Les pré-requis

Enfin, dans la liste des prérequis à mettre en place dans une approche event-first, on retrouve :

  • La documentation des événements Constituer une documentation des événements émis et consommés de chaque service est indispensable et le format de spécification Async API (https://www.asyncapi.com/) y répond parfaitement. De plus, des outils tels que EventCatalog (https://www.eventcatalog.dev/) permettent d’éditer rapidement des sites web dédiés à la documentation et le parcours de ses événements. Ce dernier permet de générer une documentation des évènements sous la forme d’une UI web, et génère même des diagrammes représentants les relations entre les différentes briques.

  • Utiliser des formats standard pour le transit des événements Depuis quelques années, le format standard pour l’échange d’événements tant à devenir CloudEvents (https://cloudevents.io/). Il a été adopté par un grand nombre d’acteurs Cloud (Salesforce, Azure, IBM, …) et tant à devenir la référence du modèle d’enveloppe de données/métadonnées des événements.

Les solutions existantes

Il existe sur le marché de nombreux outils pour répondre aux besoins décrit ci-dessus.

Attention, cette liste n’est évidemment pas exhaustive ! J’ai fait le choix de mettre le focus sur quelques outils, mais il en existe d’autres qui peuvent rendre des services similaires.

Dans le choix d’une solution, il me semble important de faire attention aux points suivants :

  • Les options de déploiement
  • La facilité de mise en œuvre (SDKs, documentation, APIs…)
  • La scalabilité et la fiabilité
  • Les performances (débit et latence)
  • Les protocoles supportés
  • La simplicité de migration (attention au vendor lock-in)
  • Le support

Concernant les options de déploiement, il existe essentiellement 2 modes :

  • self-hosted : il s’agit de solutions que l’on doit déployer et installer par soi-même sur une infrastructure (on-premise ou cloud). Il est donc nécessaire d’avoir des compétences avancées dans différents domaines. En revanche, cette complexité permet d’obtenir un meilleur contrôle ainsi qu’une personnalisation plus poussée.
  • managé : il s’agit de solution “clé en main” pour lesquelles vous payez pour un service. Les problématiques d’hébergement, de gestion et de disponibilité ne vous concernent pas, et vous n’avez qu’à vous préoccuper de la configuration.

Les solutions managées sont pertinentes, notamment grâce à la rapidité et la simplicité de mise en œuvre. Néanmoins, les composantes coût et extensibilité sont moins intéressantes.

Apache Kafka

Développé par LinkedIn, le projet Kafka a été cédé à la fondation Apache en 2012. Depuis 2014, la société Confluent propose une version managée de Kafka, mais l’outil reste bien évidemment disponible pour être installé par ses propres moyens. Il est écrit en Scala/Java et est basé sur le modèle pub/sub (il peut aussi faire du queuing).

Kafka est composé de 5 APIs :

  • Producer API : permet aux applications l’envoie de données vers les topics
  • Consumer API : permet aux applications de lire les données sur les topics
  • Streams API : permet de transformer des flux entrants en topic
  • Connect API : permet d’implémenter des connecteurs pour récupérer des données à partir d’un système extérieur
  • AdminClient API : permet de gérer et d’inspecter les topics, les brokers et les différents éléments de Kafka

Kafka stocke les messages dans des topics (aka. catégories). Les producteurs envoient des messages dans les topics et les consommateurs vont chercher (aka. lire) les messages dans ces mêmes topics. Ces topics sont partitionnés et chaque partition respecte quelques règles :

  • L’ordonnancement des messages
  • L’identification des messages
  • L’immutabilité des messages (une fois écrit, on ne peut plus les modifier)

La réplication est mise en œuvre au niveau de la partition (via des replicas sur un autre broker) et c’est ce qui permet d’assurer l’équilibrage de charge. Profitons-en pour définir la notion de broker, qui est tout simplement un serveur Kafka (aka. nœud). Un ensemble de broker va donc représenter un cluster Kafka.

Kafka ne maintient pas de liste des consommateurs avec leur avancement. Ce sont donc les consommateurs qui sont responsables de la lecture sur un topic via un mécanisme d’offset (positionnable à loisir dans le flux, on peut donc revenir dans le passé).

Kafka offre la possibilité de manipuler les données des flux en temps réel via une syntaxe proche du SQL. Il s’agit de KSQL (aka. Kafka SQL) qui s’appuie sur l’API Streams. Il est aussi possible d’intégrer dans ces requêtes des données en provenance d’autres sources (externes) via Kafka Connect.

Pour conclure, Kafka offre de très hautes performances, permettant d’ingérer une grande quantité de données tout en conservant une faible latence. Nativement, il est résilient et extensible, notamment via l’ajout simple de nouveaux nœuds. Enfin, l’outil dispose d’une communauté importante et mature.

RabbitMQ

RabbitMQ est une plateforme de message open source, développée en Erlang depuis 2007. C’est l’une des solutions les plus anciennes du comparatif, et de ce fait, elle dispose d’une communauté importante et mature. Depuis 2013, son propriétaire est Pivotal (après avoir été vendu par la société qui l’avait initialement créé).

Bien que la pièce centrale de RabbitMQ, son serveur d’échange, soit codée en Erlang, il propose des passerelles pour différents protocoles (AMQP, HTTP, STOMP et MQTT) ce qui lui permet d’être utilisé depuis un grand nombre de stack techniques.

RabbitMQ est particulièrement adapté dans les cas où les règles de routage des messages sont complexes. Ces règles de routage sont gérées par un composant appelé exchange. Celui-ci est chargé de recevoir et d’acheminer les messages vers les bonnes files d’attente.

A contrario, RabbitMQ est moins adapté aux scénarios nécessitant un fort débit d’ingestion de messages.

NATS

NATS est une solution open-source développée par Synadia et dont la 1ère version est sortie en 2011. Elle est écrite en GoLang (à l’origine, c’était du Ruby 😉). Elle est désormais maintenue par la CNCF (Cloud Native Compute Foundation).

Son approche du traitement des messages est assez différente de celle de Kafka dans le sens où ceux-ci ne sont pas persistés. Si un abonné s’arrête, lors de son retour, les messages publiés entre-temps sur le topic seront perdus.

NATS propose plusieurs modèles de communication :

  • Le fameux pub/sub
  • Les groupes de file d’attente
  • Request/reply (même principe que pub/sub mais chaque requête fait l’objet d’une réponse de la part de l’abonné)

Toutefois, NATS propose une surcouche, NATS Streaming, permettant de configurer et gérer une persistance des messages (en mémoire, dans des fichiers ou dans une base de données). Il existe même une API permettant d’écrire ses propres connecteurs. Il propose aussi de nouvelles fonctionnalités :

  • At-least-once-delivery : permet de redistribuer les messages si nécessaire
  • Possibilité de relire les messages d’un topic à partir d’un timestamp (dans le passé donc)
  • Possibilité de configurer des limitations au niveau du nombre de messages envoyés ou lus

NATS Streaming nécessite un serveur spécifique à cause de l’utilisation du format de message ProtoBuf. Mais il permet d’apporter la résilience qu’il manquait à NATS à l’origine. Il est dorénavant remplacé par JetStream, qui permet de supprimer certaines limitations (support des wildcards, gestion plus fine des accès, ajout du pull pour les clients…).

NATS est particulièrement adapté dans le cas où vous avez besoin d’un haut niveau de performance et d’une très faible latence, mais où la perte de données est acceptable.

NATS peut être déployée selon plusieurs modes :

  • Clustering (= réplication, mais sans scalabilité…)
  • Fault tolerance
  • Partitionning

Attention, le clustering n’est pas compatible avec le partitionnement/fault tolerance !

NATS est un excellent outil, mais comparativement aux autres, sa communauté et son écosystème sont assez légers.

AWS MSK

AWS MSK (aka. Managed Service for Apache Kafka) est une offre commerciale, 100% managée par AWS, permettant d’avoir un cluster Kafka hautement disponible à disposition dans une infrastructure hébergée et maintenue par AWS.

MSK existe sous 2 formes :

  1. Serverless : Dans ce format, le service va automatiquement gérer la scalabilité sur les besoins en calcul et en stockage, pour vous permettre d’encaisser la charge, et d’utiliser Kafka à la demande
  2. Provisonned : Dans ce format, les ressources du cluster sont définies par le client, et bien que l’infogérance du service soit orchestrée par AWS (mise à jour serveur, zookeeper, …), c’est au client de gérer la scalabilité qu’il souhaite avoir sur le service.

AWS propose une API pour MSK permettant la création et la gestion du cluster. Celle-ci est accessible via une CLI, un SDK, des outils d’IaC ou encore la console AWS.

Il est possible de gérer l’authentification directement via les rôles IAM d’AWS, et donc de se passer des ACL Kafka.

Enfin, son intégration avec les autres services AWS est excellente (Cloudwatch, S3, Lambda…) et constitue un service de qualité pour la mise en place d’une plateforme de streaming.

GCP Pub/Sub

GCP Pub/Sub est un service de messaging entièrement managé par Google.

Il existe 2 types de service :

  • Pub/Sub : c’est le choix par défaut pour la plupart des applications. Il s’agit de l’offre la plus complète en termes de fiabilité, d’intégration et d’automatisation.
  • Pub/Sub Lite : similaire à l’offre par défaut, il permet d’obtenir une solution à moindre coût, mais au détriment de la fiabilité. Il est aussi nécessaire de préprovisionner la capacité de stockage et de débit. Cette offre est à réserver aux applications ayant des besoins légers.

Contrairement à AWS MSK, il ne s’agit pas d’un journal d’évènements, mais d’un système de files d’attente de messages (semblable à RabbitMQ par exemple).

Si l’on cherche un équivalent chez AWS, il faudrait plutôt regarder du côté AWS SNS (Simple Notification Service) avec SQS (Simple Queue Service).

Comme chez AWS, Pub/Sub est très bien intégré dans la galaxie de services de GCP, comme Cloud Functions, Cloud DataFlow et BigQuery par exemple.

Azure Event Hub

Azure EventHub est un service d’ingestion de données en temps réel entièrement managé par Microsoft Azure.

Il dispose qu’un écosystème basé sur le protocole AMQP et disponible dans de nombreux langages (.NET, Java, Python, Javascript…), ainsi qu’une intégration fluide aux différents services Azure (Azure Stream Analytics, Azure Functions, Azure Databricks, Azure Storage…).

En plus des niveaux Basic et Standard, il existe des niveaux Premium et Dédié offrant des performances supérieures, et notamment un nombre de groupes de consommateurs plus important et une durée de rétention plus élevée.

Il existe une fonctionnalité permettant de le rendre compatible avec les producteurs et consommateurs Apache Kafka à partir de la version 1.0 (ce qui permet de migrer vers EventHub sans avoir besoin de réécrire ses applications).

Il est intéressant de noter les différences de nommage entre les 2 solutions :

Kafka Event Hub
Cluster Espace de noms
Rubrique Event Hub
Partition Partition
Groupe de consommateurs Groupe de consommateurs
Offset Offset

Attention à ne pas confondre avec Azure Event Grid ! Event Grid traite lui aussi des évènements, mais est axé sur leur gestion et le routage (automatisation des workflows, intégration d’applications…).

Demo time !

J’ai beaucoup écrit, mais on a pas encore vu une seule ligne de code… On va donc écrire un démonstrateur en C#/.NET, avec un producteur et deux consommateurs, autour d’un bus RabbitMQ. Let’s go !

docker-compose

Pour commencer, on va avoir besoin d’un RabbitMQ. Par chance, il existe une image Docker et on va donc pouvoir très rapidement monter un docker-compose :

version: '3'

services:
  rabbitmq:
    image: rabbitmq:management
    container_name: rabbitmq
    environment:
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
    ports:
      - "5672:5672"
      - "15672:15672"

networks:
  default:
    driver: bridge

Une fois démarré, vous allez pouvoir accéder à l’interface d’administration web de RabbitMQ, sur le port 15672.

Celle-ci est très complète et permet d’obtenir de nombreuses informations sur votre instance RabbitMQ ainsi que de réaliser certaines opérations :

  • Le nombre de messages, le débit et le nombre de connexions courantes
  • L’ensemble des connexions actives (ip, port, nom d’utilisateur, débit…)
  • La liste des exchanges et la possibilité d’en créer
  • La liste des queues et la possibilité d’en créer
  • Pour chaque queue, des statistiques d’usages ainsi que des outils permettant de manipuler les messages (publier, récupérer, déplacer ou supprimer)
  • Des fonctionnalités d’administration, comme la gestion des utilisateurs, des virtual hosts ou des policies

Le producteur

Pour interagir avec RabbitMQ, je vais utiliser EasyNetQ. EasyNetQ est une bibliothèque cliente pour RabbitMQ spécifiquement conçue pour .NET. Elle offre une abstraction de plus haut niveau par rapport à la bibliothèque officielle de RabbitMQ et est plus simple et rapide à utiliser.

Commençons par définir deux classes, qui représentent deux types de message. Via une annotation, on peut spécifier le nom de la queue et de l’exchange qui seront générés.

Par défaut, EasyNetQ concatène le namespace, le type du message et l’id de souscription (par exemple, dans mon cas, MAN.Sample.RabbitMQ.ControlMessage, MAN.Sample.RabbitMQ_clientOne).

[Queue("SampleMessageQueue", ExchangeName = "SampleMessageExchange")]
public class SampleMessage
{
    public Guid Id { get; set; }

    public string Payload { get; set; }
}

[Queue("ControlMessageQueue", ExchangeName = "ControlMessageExchange")]
public class ControlMessage
{
    public Guid Id { get; set; }

    public DateTime Date { get; set; }

    public int Priority { get; set; }
}

Ensuite, on écrit le code chargé de se connecter à l’instance RabbitMQ et de publier des messages.

using EasyNetQ;
using MAN.Sample.RabbitMQ;

// 1. Je me connecte à l'instance locale de RabbitMQ et j'active les logs vers la console
using (var bus = RabbitHutch.CreateBus("host=localhost", register => register.EnableConsoleLogger()))
{
    // 2. Préparation d'un message planifié qui sera automatiquement envoyé au bout d'une heure
    // Une queue dédiée est créée pour cela : SampleMessageQueue_00_01_00_00
    var takeABreakMessage = new SampleMessage { Id = Guid.NewGuid(), Payload = "It's time to take a break, isn't it?" };
    bus.Scheduler.FuturePublish(takeABreakMessage, TimeSpan.FromHours(1));

    // 3. On demande à l'utilisateur de saisir un texte
    var input = string.Empty;
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Please enter a message. 'Q' to exit.");

    while((input = Console.ReadLine()) != "Q")
    {
        // 4. On publie 2 messages en utilisant le mécanisme de Topic Based Routing
        // On va jouer sur la routing key, ce qui va permettre aux consommateurs de filtrer les messages
        // https://github.com/EasyNetQ/EasyNetQ/wiki/Topic-Based-Routing
        var id = Guid.NewGuid();
        bus.PubSub.Publish(new ControlMessage { Id = id, Date = DateTime.Now, Priority = Random.Shared.Next(1, 10) }, "message.control");
        bus.PubSub.Publish(new SampleMessage { Id = id, Payload = input }, "message.public");

        Console.WriteLine("Message successfully published !");
    }
}

Les consommateurs

Passons maintenant aux consommateurs. Ils sont au nombre de deux :

  • ClientOne : consomme les 2 types de message (ControlMessage et SampleMessage) via la routing key message.** (à noter, l’utilisation du wildcard au niveau de la routing key)
  • ClientTwo : consomme uniquement le message SampleMessage via la routing key message.public

Attention, pour la lisibilité, j’ai mélangé le code des deux consommateurs dans le même fichier.

using EasyNetQ;
using MAN.Sample.RabbitMQ;

using (var bus = RabbitHutch.CreateBus("host=localhost", register => register.EnableConsoleLogger()))
{
    Console.ForegroundColor = ConsoleColor.Green;

    // 1. On s'abonne à un type de message, en filtrant sur une routing key

    // ClientOne
    bus.PubSub.Subscribe<SampleMessage>("clientOne", HandleSampleMessage, x => x.WithTopic("message.*"));
    bus.PubSub.Subscribe<ControlMessage>("clientOne", HandleControlMessage, x => x.WithTopic("message.*"));
    Console.WriteLine("I'm ClientOne. Listen now... 'Enter' to exit.");

    // ClientTwo
    bus.PubSub.Subscribe<SampleMessage>("clientTwo", HandleSampleMessage, x => x.WithTopic("message.public"));
    Console.WriteLine("I'm ClientTwo. Listen now... 'Enter' to exit.");    
    
    Console.ReadLine(); 
}

// 2a. Gestion des messages de type 'SampleMessage'
static void HandleSampleMessage(SampleMessage message)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine($"Message received : [{message.Id}] '{message.Payload}'");
    Console.ResetColor();
}

// 2b. Gestion des messages de type 'ControlMessage'
static void HandleControlMessage(ControlMessage message)
{
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine($"Control message received : {message.Id} | {message.Date} | {message.Priority}");
    Console.ResetColor();
}

Si j’exécute mes trois applications, je vais avoir le comportant suivant :

  • Mes deux clients sont en écoute et mon serveur attend la saisie d’un message dans la console
  • Lors de l’envoi d’un message, le comportement des clients est différent :
    • ClientOne va recevoir le message contenant le texte saisi (SampleMessage), ainsi que le message de contrôle avec les métadonnées (ControlMessage)
    • ClientTwo va recevoir uniquement le message contenant le texte saisi (SampleMessage)

Conclusion

Les plateformes de streaming de données ont révolutionné la manière dont les informations sont échangées, traitées et analysées en temps réel. Elles permettent aux entreprises de répondre rapidement aux besoins changeants du marché, d’optimiser leurs opérations et de fournir des services innovants. Le streaming de données, contrairement aux méthodes traditionnelles de traitement des données, permet une interaction continue avec les données, offrant ainsi une réactivité sans précédent.

Le choix entre le messaging et le streaming, l’orchestration et la chorégraphie, ainsi que la compréhension du modèle pub/sub, sont essentiels pour déterminer la meilleure approche pour chaque application spécifique.

Il existe de nombreuses solutions, chacune ayant ses propres avantages et domaines d’application. Le choix dépendra des besoins spécifiques de l’entreprise, de la nature des données à traiter et des objectifs à atteindre. Vous aurez peut-être noté que j’ai omis de mentionner Apache Pulsar ? C’est voulu, un article dédié à son sujet est en cours de rédaction ;)

À mesure que le volume de données continue de croître et que la demande pour des analyses en temps réel augmente, l’importance des plateformes de streaming de données ne fera que s’accroître. Les entreprises qui adoptent et maîtrisent ces technologies seront mieux positionnées pour prospérer à l’ère numérique.

Pour terminer, il est important de souligner les synergies croissantes entre le streaming de données et l’intelligence artificielle. En combinant ces deux domaines, on peut non seulement traiter des volumes massifs de données en temps réel, mais aussi en tirer des insights instantanés grâce à des modèles d’IA. Cela permet d’automatiser des décisions, de détecter des anomalies en temps réel, d’optimiser des processus en continu et de personnaliser les expériences utilisateur à une échelle jamais atteinte auparavant. À l’avenir, l’intégration du streaming de données avec l’IA promet de redéfinir les frontières de l’innovation, ouvrant la voie à des applications et des services qui étaient autrefois considérés comme de la pure science-fiction !

Je remercie Emilien Guilmineau pour son aide à la rédaction de cet article ;)