Nous aborderons dans cet article les différents patterns que les développeurs, avec leur responsabilité dans l’intégration de données, peuvent être amenés à mettre en œuvre dans le domaine du streaming. Mais tout d’abord présentons la plateforme qui a pour ambition de devenir la plateforme centralisée de stockage et d’échange de toutes les toutes les données émises par une entreprise en temps réel : Apache Kafka.

L’étude du fonctionnement de Kafka exigerait au moins un livré dédié, le but de cette section est d’en expliquer brièvement quelques concepts fondamentaux.
Apache Kafka est une plateforme de streaming, c’est-à-dire une plateforme conçue pour supporter de façon optimale le stockage et la consommation de flux d’évènements.

La structure dans laquelle Kafka stocke et lit les événements correspond à un log, pas dans le sens log applicatif, mais plutôt log de commit qui garde l’historique de toutes les modifications effectuées dans le temps, comme dans les bases de données. Dans Kafka, les objets qui implémentent les logs sont les partitions (figure 2.4).

Les événements sont indexés dans les partions par des offsets dont la valeur est croissante pour chaque nouvel évènement ajouté (figure 2.4). C’est grâce aux offsets indiqués dans les requêtes de lecture que Kafka retrouve les enregistrements à retourner aux consommateurs. La lecture normale d’une partition se faisant en partant des enregistrements les plus anciens au plus récents, elle est optimisée par le fait que Kafka connait à chaque instant l’offset des prochains enregistrements à retourner au consommateur.

Pattern 2

Figure 2-4 : Messages indexés par ordre croissant suivant leur arrivée dans un commit de log

Les partitions appartiennent à des groupes appelés topics (figure 2.5). Une des particularités de Kafka par rapport aux autres middlewares de messagerie est son aspect distribué, cela rend la scalabilité horizontale possible et fiabilise le mécanisme de réplication qui permet la tolérance aux pannes. Kafka supporte en effet la notion de cluster et favorise en conséquence la répartition des partitions d’un topic sur les différentes machines d’un cluster. De même, les partitions d’un topic sont automatiquement répliquées par des partitions réparties sur les machines du cluster en fonction d’un facteur de réplication configurable.
Parmi les répliques d’une partition, une a le rôle de leader (leader replica), c’est la partition où Kafka va écrire ou lire lorsqu’une requête arrive. Les autres répliques (follower replica) doivent juste se synchroniser en permanence avec leur leader. Ainsi lorsque la partition leader rencontre un problème, elle est remplacée sur le champ à la suite d’une élection par une des partitions répliques qui est à jour.

Pattern 2

Figure 2-5 : Les partitions font partie d’un topic

Le fait d’avoir plusieurs partitions par topic est un facteur de scalabilité. A l’écriture, différentes instances de l’application pourront écrire dans différentes partitions, grâce à la présence d’une clef utilisée dans les enregistrements comme entrée d’une fonction de hachage pour les ventiler entre les partitions. Et durant la consommation, Kafka offre la possibilité de créer un groupe de consommateurs sur un topic. Chaque consommateur étant chargé de la lecture d’un sous-ensemble de partitions (figure 2.6).

Pattern 3

Figure 2-6 : Parallélisation de la lecture des partitions d’un topic

Maintenant que l’on a présenté la plateforme Kafka revenons à la présentation des différents patterns évoqués au début de l’article.

Pattern 1 : Stream of events

Ce pattern émerge des situations où les applications d’un système distribué doivent réagir instantanément aux évènements publiés par d’autres applications du système.
Prenons pour exemple celui du système d’information d’une chaine d’hôtels. Dans un tel système, un événement comme la consultation de la page web d’un hôtel sera consommé par plusieurs applications du système pour différents besoins (figure 2.1) : amélioration des résultats des recherches, proposition d’offres promotionnelles, A/B testing pour recueillir les informations sur la manière dont différentes versions du site sont consultées, etc.

Pattern 4

Figure 2-1 : Consommation d’un événement par plusieurs services

 

Imaginons à présent les situations où l’on devrait avoir à réagir non pas à un, mais plutôt à des dizaines, voire des centaines de types d’évènements produits et consommés par les applications du système d’information. Dans un système d’information construit de façon classique sans plateforme de streaming, cela donnerait naissance au type d’architecture illustré par la figure 2.2. La complexité d’une telle architecture et le coût de maintenance qu’elle induit paraissent évidentes. D’où l’idée de connecter toutes ces applications à travers une plateforme de streaming spécialisée dans la gestion de flux d’événements (figure 2.3).

Pattern 6

Figure 2-2 : Architecture évènementielle sans plateforme de streaming

 

Pattern 7

Figure 2-3 : Utilisation d’une plateforme de streaming dans une architecture événementielle

Pattern 2 : Keep Events Compatible

Ce pattern vise à garantir la compatibilité des structures des événements envoyés et consommés dans Kafka par rapport à un schéma. Etant donné que Kafka favorise le découplage entre les applications qu’elle connecte, les différentes équipes s’occupant de ces applications peuvent avoir tendance à modifier les structures des enregistrements échangés durant les évolutions, en oubliant l’impact que cela pourrait avoir sur d’autres applications consommatrices de ces enregistrements.
Ces propos sont illustrés par les figures 2.7 et 2.8 où le service de booking modifie le format des timestamps représentés en milliseconde vers un format human readable. Les applications consommatrices non averties au préalable du changement ne vont évidemment pas pouvoir consommer les événements dans ce nouveau format.

Pattern 8

Figure 2-7 : Le service de booking produit des événements consommés par plusieurs applications

 

Pattern 9

Figure 2-8 : Les consommateurs ne peuvent pas consommer les événements sir leur structure change sans avertissement au préalable

Tout comme les signatures des opérations définissent les contrats d’une API, les schémas des événements définissent les contrats dans un processus de streaming. Confluent, la société fondée par les auteurs de Kafka, a développé Schema Registry, un registre de schéma qui peut être référencé durant les processus de sérialisation et désérialisation pour s’assurer que les consommateurs et les producteurs utilisent le même schéma pour la lecture et l’écriture des mêmes événements (figure 2.9).
Il existe des plugin maven et gradle qui permettent des interactions avec le registre de schéma durant les phases de développement, comme télécharger en local des schémas présents sur le registre ou au contraire uploader des schémas vers le registre, mais surtout la possibilité de valider la compatibilité des schémas en local par rapport aux schémas présents sur le registre.

pattern 11

Figure 2-9 : Registre de schéma utilisé pour la sérialisation et la désérialisation des enregistrements

Pattern 3 : Ridiculously Parallel Transformations

Ce pattern de streaming consiste à tirer parti de la parallélisation fournie naturellement par Kafka lors de la consommation des événements d’un topic, comme nous le disions dans la section d’introduction de Kafka. Ce pattern peut s’appliquer dans des situations où l’on doit appliquer des transformations triviales sur les événements produits dans Kafka, comme par exemple la suppression de champs sensibles avant d’autres utilisations ultérieures des événements (figure 2.10, champ cc).

 

Pattern 12

Figure 2-10 : Masquage d’une information sensible

 

Avec Kafka, cette transformation peut être effectuée de façon scalable et résiliente à très faible coût sans ajouter de complexité supplémentaire à l’application qui produit les événements (figure 2.11). Celle-ci va juste se contenter de les écrire dans un topic, pendant que les transformations se feront par une autre application qui va ainsi se contenter de consommer les événements à partir du bon topic Kafka.
Le fait que les événements soient écrits dans plusieurs partitions permet de paralléliser les transformations par la création de groupes de consommateurs Kafka. Chaque consommateur lisant et transformant les événements des partitions qui lui sont attribuées (figure 2.11). Et comme nous le disions, le fait que les partitions soient répliquées permet à Kafka de mettre en œuvre le mécanisme de failover lorsque l’une d’elles rencontre un problème.

 

Pattern 13

Figure 2-11 : Transformation en parallèle des événements indépendamment de leur production

 

Ce pattern de streaming est désigné, à cause de sa simplicité, par l’expression « Hipster Stream Processing », la simplicité étant une des valeurs (sinon la valeur) essentielles des hipsters (figure 2.12).

Pattern 14

Figure 2-12 : Hipster et son « fixie » (vélo à pignon fixe) 🙂

 

Ce pattern est couramment utilisé dans les pipelines où les données sont extraites d’un datastore vers Kafka pour être transformées et/ou filtrées avant d’être stockées dans un autre datastore. Mais l’implémentation correcte de ce pattern nécessite tout de même une bonne connaissance du fonctionnement de Kafka pour s’assurer que le pipeline gère correctement la parallélisation et les situations d’erreurs, et il faudra encore énormément d’effort pour pouvoir monitorer le processus.
C’est dans le but de simplifier la création de pipelines scalables et fiables que Kafka Connect a été développé (figure 2.13). Ce framework qui est inclus dans Kafka implémente la parallélisation et la gestion correcte des cas d’erreurs, et fournit une interface de monitoring. Il faut juste lui fournir des plugins, appelés connecteurs, pour chaque datasource où il doit lire ou écrire des données.
Signe d’une bonne adoption du framework, on peut trouver des connecteurs pour de nombreuses data sources.

pattern 15

Figure 2-13 : Kafka Connect implémente la lecture et l’écriture dans Kafka de façon scalable et fiable

Un dernier point concernant ce pattern est qu’il faut éviter de l’utiliser quand les transformations effectuées au cours du streaming impliquent des gestions d’état. Les transformations impliquant des gestions d’état ont fait l’objet d’un autre framework appelé Kafka Streams.

Pattern 4 : Streaming Data Enrichment

Le dernier pattern de la présentation concerne le besoin de gestion d’état intermédiaire au cours du streaming pour pouvoir enrichir les informations du flux à partir d’autres sources de données. L’enrichissement des évènements d’un flux à partir d’autres sources de données n’est cependant pas le seul traitement qui implique une gestion d’état, les opérations d’agrégation ou les opérations portant sur des fenêtres temporelles (pour le calcul de moyennes mobiles par exemple) impliquent aussi une gestion d’état. Toutes ces opérations sont en fait des primitives d’un framework de streaming et leur implémentation dépasse le cadre du développement ordinaire d’applications métier. Raison pour laquelle il existe nombre de frameworks de streaming qui implémentent et mettent à disposition de telles opérations : Apache Storm, Apache Samza, Apache Spark, Apache Spark Streaming, etc. Depuis sa version 0.10.0, Kafka fournit aussi désormais son framework de streaming appelé Kafka Streams.
Dans la suite, nous allons mettre en évidence la limite d’un consommateur Kafka élémentaire pour la mise en œuvre de l’enrichissement d’un flux et montrer en deux instructions comment un framework de streaming tel que Kafka Streams permet de répondre facilement au besoin.
Nous allons reprendre l’exemple de la chaine d’hôtels où des événements sont envoyés dans Kafka lorsque les clients consultent les pages des sites de la chaine. La chaine d’hôtel voudrait réagir à ces événements et envoyer des mails de promotion aux clients fidèles si les propriétés qu’ils consultent font l’objet d’offres de promotion. Mais l’événement PageViewEvent ne contient que les identifiants du client et de la propriété, qui ne permettent pas de connaître le statut et le niveau de fidélité du client et de savoir si la propriété consultée faite l’objet d’offres de promotion. Nous devons donc enrichir les événements PageViewEvent avec des informations supplémentaires sur le client et la propriété, afin que la décision d’envoi d’offres de promotion au client puisse être prise (figures 2.14 et 2.15).
Vu que les informations complètes sur les clients et les propriétés sont stockées dans la base de données, la première solution qu’on entrevoit est de requêter ces informations pour chaque événement de consultation pour produire un nouvel événement enrichi (figure 2.16). Cette solution pose un problème lorsque que les événements arrivent à un rythme élevé que la base de données ne peut pas supporter. Sa latence étant significativement supérieure à celle du système de streaming.

Pattern 16

Figure 2-14 : Enrichissement de l’évènement de consultation de page

 

Pattern 17

Figure 2 15 Enrichissement des PageViewEvent pour décider d’envoyer ou non des promotions sous forme de PromotionEvent

 

Pattern 18

Figure 2-16 : Enrichissement des événements à partir de la base de données : problème de latence et forte sollicitation de la base

Lorsque nous avons des problèmes de latence et/ou de performance avec la base de données, on essaye souvent de s’en sortir en chargeant les données dans un cache en local (figure 2.17). Cependant, mettre à jour un cache est juste l’une des deux choses compliquées de l’informatique : une mise à jour trop fréquente retire l’avantage du cache tandis que des mises à jour espacées risquent de laisser le cache avec des données périmées. L’idée de disposer d’un cache n’est pas tout à fait mauvaise, à condition de résoudre de façon optimale ce problème de mise à jour.

Pattern 19

Figure 2-17 : Enrichissement des événements à partir du cache : à quelle fréquence mettre à jour le cache ?

Il se trouve que la majorité des bases de données permettent de capturer les changements qui ont lieu sur les données autrement que par des émissions régulières de requêtes SQL, grâce principalement aux techniques de triggers ou par la consultation du journal des transactions. Ces mécanismes sont désignés par le terme générique de CDC (Change Data Capture). La solution proposée dans Kafka pour tenir à jour un cache sur une base de données est d’alimenter une partition dédiée au cache à partir des changements capturés par un connecteur CDC (figure 2.18).

Pattern 20

Figure 2-18 : Connecteur CDC pour mettre à jour les données de la base importées dans une partition de Kafka

La tenue à jour du cache de la base de données n’est pas pour autant suffisante pour fiabiliser le reste du processus de streaming. Nous avons vu que l’enrichissement des événements se fait par une jointure avec le contenu du cache sur la base de données. L’application a besoin pour cela de faire une autre mise en cache dans sa mémoire. Le fameux état qu’on évoquait au début de cette section. La gestion de cet état soulève des questions :

  • Comment faire en sorte qu’il tienne en mémoire ?
  • Que faire de cet état lorsque l’application tombe en panne et qu’on doive supporter la tolérance aux pannes ?
  • Dans un contexte de calcul distribué, que faire lorsque le traitement d’une partition est assigné dynamiquement à d’autres nœuds ?

Ces questions doivent être étudiées lorsqu’on veut avoir un processus de streaming fiable et scalable. Et, Kafka Streams, à l’instar des autres frameworks de streaming, y répond à sa façon. Le listing 2.1 montre la solution pour procéder à l’enrichissement qui était notre problème de départ. La première instruction permet de créer un flux à partir du topic contenant les informations provenant de la base de données sur le statut de fidélité des clients. La seconde instruction demande à KafkaStreams de faire la jointure de ce flux avec le flux de consultation des pages, laissant au développeur le soin de produire le résultat de chaque jointure.

Pattern 18

Listing 2-1 : Jointure entre un stream et une table transformée en stream avec KafkaStreams

Conclusion

L’évolution de l’intégration de données vers le streaming répond au besoin croissant de pourvoir réagir instantanément aux changements continus de notre monde. Les développeurs d’applications ont un rôle plus important dans ce mode d’intégration, étant directement responsables de la production et de la consommation des données. Apache Kafka est une plateforme de streaming développée par les ingénieurs de LinkedIn pour pallier aux limitations des middlewares de messagerie existants, principalement en matière de performance, scalabilité et tolérance aux pannes.
Par la suite nous avons vu quatre principaux patterns de mise en œuvre du streaming avec Apache Kafka : la mise en communication de toutes les applications à travers une unique plateforme Kafka, l’utilisation d’un registre de schéma pour s’assurer que les événements écrits dans Kafka respectent un schéma convenu avec leurs consommateurs, la parallélisation de traitements élémentaires avec tolérance aux pannes et enfin l’enrichissement des événements d’un flux à partir d’une base de données.

Ressources