Comment choisir son broker de messages ?

Simon Emmanuel RIVAS
Catégorie : Azure / Event Grid / Event Hubs Kafka
28/09/2022

Quiconque a eu à construire un système distribué s’est nécessairement intéressé à la question de la communication entre les composants de ce système. Assez classiquement, un broker de messages peut être mis en œuvre pour introduire certaines fonctionnalités/propriétés aux communications telles que du découplage temporel ou de la fiabilité. C’est particulièrement vrai dans le cadre d’échanges de données inter applicatifs, où ledit système distribué est composé d’une collection de systèmes qui s’échangent des messages.
Vient alors l’épineuse question du choix du broker : idéalement, ce choix devrait se faire sur la base des propriétés attendues pour les communications. Les différences de vue et de compréhension d’une problématique peuvent déjà engendrer un grand nombre de débats sur ce choix. En complément, les organisations humaines étant ce qu’elles sont, il arrive que des considérations peu factuelles entrent en ligne de compte :

  • Le choix corporate de tel ou tel broker en tant que solution “catch all” (parfois pour la raison suivante);
  • Des idées pré-conçues sur tel ou tel mécanisme, telle ou telle capacité d’un broker.

NOTE 1 : un choix inadapté ne signifie pas que “ça ne va pas marcher”, mais plutôt qu’on va introduire des biais et/ou nécessiter des mécanismes de compensation plus ou moins inefficaces et disgracieux d’une part et coûteux à implémenter d’autre part.

NOTE 2 : on peut très bien arriver à la conclusion que plusieurs types de broker sont nécessaires sur l’ensemble du périmètre du système distribué. Il est même possible que pour une communication (un flux de messages) donné, plusieurs types de broker soient également nécessaires.
Les 2 biais décrits à l’instant sont – dans mon expérience – particulièrement applicables lorsqu’il s’agit de choisir entre un broker à base de logs d’une part et un broker à base de file d’attente ou à base de souscription (au sens de la classification Gartner, cf le rappel ci-dessous) d’autre part : ce sera le sujet principal de cet article.

 

Rappel : types de brokers

 

Commençons par clarifier rapidement le vocabulaire relatif aux types de broker :

  • Orienté log/journal : il s’agit essentiellement d’un journal en écriture seule. Les consommateurs ne “suppriment” pas un message après l’avoir lu mais parcourent simplement le journal en se basant sur un offset, qu’ils font progresser au fil de la lecture du journal. Cet offset peut être maintenu côté consommateur ou côté broker. Dans le monde Azure, cela correspond à Event Hubs (aussi bien avec ou sans surface Kafka);
  • Orienté queue/file d’attente : le broker crée des files d’attente pour chaque consommateur et s’appuie sur un système de routage pour acheminer les messages vers les files d’attentes des consommateurs. Une fois un message traité par un consommateur, celui-ci est supprimé de la file d’attente. Dans le monde Azure, cela correspond à Service Bus;
  • Orienté souscription : un consommateur (souscripteur) enregistre à la fois un filtre de sélection des messages/événements qui l’intéressent et un endpoint sur lequel il s’attend à recevoir les messages, puis à l’exécution le broker détermine quel(s) endpoint(s) invoquer pour chaque message sur la base des règles qui ont été enregistrées. La grosse différence avec un broker à base de file d’attente est que le broker est ici actif et pousse le message vers le endpoint du souscripteur. Dans le monde Azure, cela correspond à Event Grid.

 

Rappel : types de message

 

Complétons ces rappels par un petit mot sur les différentes catégories de messages :

      • Document : photo instantanée de l’état d’un objet, par exemple une fiche client ou une image de stock;
      • Commande : comme son nom le suggère, c’est un ordre qui est émit et qui traduit une attente particulière de l’émetteur vis-à-vis du ou des récepteurs;
      • Evénement : constatation d’un fait, quelque chose qui s’est produit. Autrement dit : constatation d’un changement d’état.

Ce distinguo n’est de prime abord pas très exploitable énoncé ainsi : ces raccourcis sont des moyens mnémotechniques qui enveloppent plusieurs considérations, décrites plus bas. Au final, on peut dire que c’est un peu comme l’île de Tortuga où on ne sait aller que si on y a déjà été : ces raccourcis ne sont efficaces que si l’on sait ce qui se cache derrière!

Nous allons progressivement décortiquer cela au fil des chapitres suivants.

 

L’intention / l’attente

 

Une première question intéressante à se poser est : le système émetteur a-t-il une intention particulière lorsqu’il émet un message en particulier? S’attend-il à ce que quelque chose se produise à l’autre bout de la ligne? Ou encore : a-t-il besoin que quelque chose se produise pour continuer à faire son travail?
Si non, ce système peut être considéré comme un fournisseur de données, qui informe le reste du monde de quelque chose dont lui seul a connaissance à un instant T. Il peut s’agir :

  • De mises à jour de données (des photographies instantanées d’objets métier dont il a la responsabilité, aka : des documents). Dans ce cas, les mises à jour de documents ont probablement une durée de validité limitée, mais au-delà de cette considération il est difficile de faire un choix de broker tranché sur la base de ce seul critère.
  • Ou de faits qui se sont produits (aka : des événements). Si la seule intention du système émetteur est au final de consigner ces événements, alors l’usage d’un broker à base de log est généralement une bonne option pour ce besoin mais comme pour le point précédent il est difficile de faire un choix de broker sans plus de critères.

Dans le cas contraire – autrement dit, l’émetteur a une attente par rapport à l’avenir – alors écrire dans un journal n’est probablement pas le plus efficace. Le “producteur” va plutôt émettre une commande et va alors en réalité se comporter comme un client qui consomme un service. Cette commande peut nécessiter :

  • De s’assurer qu’elle n’est honorée qu’une fois et/ou qu’elle n’est “valable” que pendant un certain laps de temps : un broker à base de fil d’attente est alors un bon candidat pour apporter ce niveau de fonctionnalité.
  • Ou d’être routée de manière fine – sur la base de filtres – que vers certains souscripteurs (le ou les fournisseurs de service, en fait). De plus, le résultat de cette commande peut avoir besoin d’être corrélé à la commande/requête d’origine (pattern request-reply typique) : un broker à base de files d’attente est alors également particulièrement indiqué.

Je me suis ici focalisé sur les attentes du producteur, mais le même genre de questions peut (et doit) se poser du côté des consommateurs. On va voir au paragraphe suivant que les attentes peuvent être divergentes.

 

La relation entre les systèmes

 

Au paragraphe précédent, j’ai introduit discrètement la problématique des relations entre les systèmes au travers de la notion d’attente ou intention (sous-entendu : l’intention du producteur par rapport au(x) consommateur(s)).
Mais cette problématique va largement plus loin que simplement de savoir ce qu’attend le producteur : le consommateur peut lui aussi avoir certaines attentes, qui ne collent pas nécessairement à la perfection avec celles du producteur. Rien d’étonnant à cela : tout ceci a déjà été largement documenté et discuté dans le cadre du Domain Driven Design (ou DDD – au passage incroyablement riche, instructif et efficace à de nombreux niveaux). Je vais faire ici un raccourci grossier mais efficace : dans le cadre de nos discussions, un broker de messages va servir à matérialiser (ou plutôt motoriser) une frontière entre 2 contextes (plus concrètement, et en première approximation, entre 2 systèmes).

Sauf qu’en réalité, c’est un peu comme à la frontière entre deux pays : il y a deux côtés à une frontière, et les formalités pour passer d’un pays (contexte) à l’autre dépendent des accords (relations) entre ces pays.

Prenons un exemple : un système de gestion d’entrepôt met à disposition un flux quasi temps réel de mises à jour de stock. Il n’attend rien de particulier d’un quelconque consommateur de ces mises à jour de stock : il informe quiconque est intéressé que tel article est sorti de tel entrepôt dans telle quantité (idem pour les entrées en stock). Mais qu’un autre système écoute ou non ces mises à jour ne change rien du tout à la réalité de son monde : quoi qu’il arrive le mouvement de stock s’est produit, que cela intéresse quelqu’un ou non.

De l’autre côté de la frontière, des systèmes/partenaires (par exemple des points de vente ou des sites marchands) ont besoin des mises à jour mais uniquement pour un sous-ensemble bien précis du stock, peut-être avec une fréquence plus faible pour alimenter leur propre cache interne (par exemple une fois par nuit). En complément, on peut également imaginer qu’un autre consommateur soit intéressé par l’intégralité des mises à jours pour effectuer des calculs prédictifs en vue d’adapter l’activité d’une chaîne de production.

Dans ce cas de figure, le producteur ne peut clairement accommoder les exigences divergentes des différents consommateurs. On peut même pousser jusqu’à affirmer que ces exigences ne sont pas son problème.

Comment cela se traduit-il du point de vue terre à terre de la motorisation des frontières? Selon toute vraisemblance, se reposer exclusivement sur un broker à base de logs – ce qui pourrait être une solution adaptée du point de vue du système de gestion d’entrepôt, cf chapitre suivant – est une posture trop limitative ne permettant pas de répondre de manière efficace aux exigences de tous les consommateurs. Dès lors, on se retrouve face au choix de faire porter aux consommateurs certaines fonctionnalités (comme filtrer le flux pour ne prendre que ce qui les intéresse) ou associer 2 types de brokers pour apporter un service plus complet à un plus grand nombre de consommateurs.

Pour pimenter ces réflexions, reste la dimension de la responsabilité : qui fait quoi là dedans? On parle bien ici de gens : quelle équipe a la responsabilité d’implémenter telle ou telle fonctionnalité. En effet chaque développement est une construction socio-technique, c’est-à-dire qu’au-delà du pur “code”, il y a des périmètres de responsabilité en jeu. Ce sujet est loin d’être trivial et il est crucial pour la bonne marche de l’organisation : c’est notamment – sans s’y limiter – le distinguo entre intégration tactique et intégration stratégique, qui mérite un article à part entière.

 

Confusion entre besoins internes et attentes externes

 

Pour aller plus loin, un contexte au sens DDD peut avoir besoin d’un broker pour motoriser ses propres besoins/mécanismes internes. Dans l’exemple précédent, le système de gestion d’entrepôt peut être composé de multiples micro-services qui produisent (et consomment) chacun des événements. Ce contexte peut nécessiter de publier ces événements dans un broker à base de journal.
La tentation est alors forte de faire d’une pierre deux coups et donc d’exposer simplement ce broker à des consommateurs extérieurs. Pourquoi pas, mais attention à ne pas confondre les besoins internes avec les besoins des consommateurs externes : il est possible (et même probable) que ce faisant on déporte côté consommateur la charge de certaines fonctionnalités qui pourraient être offertes côté producteur et ainsi être mutualisées.

Par ex : si dans notre exemple des consommateurs n’ont besoin que de sous-ensembles des événements publiés, alors chacun devra lire l’intégralité du log et faire son propre tri. C’est assez inefficace pour un consommateur et ça devient du gaspillage de ressources pour plusieurs consommateurs. Dans ce cas, il peut être plus intéressant d’utiliser les fonctionnalités d’un broker à base de files d’attentes ou de souscriptions pour motoriser les frontières du contexte.

NOTE : il est techniquement possible de faire du “routage” sur la base de la partition dans un broker à base de logs. Attention: c’est une forme de couplage! Cette solution peut être entendable / acceptable au sein d’un même contexte, beaucoup moins pour un contexte tiers.

 

La valeur du flux

 

Jusqu’ici, on a parlé d’événements mais laissé de côté la nuance entre événements discrets et flux d’événements. Prenons un exemple tiré de la physique pour illustrer la nuance : position vs vitesse.
Chaque relevé de position a une valeur en soit et permet de répondre à certains cas d’usage (ex : contrôle de présence dans une zone), mais peut-être que la séquence des positions va apporter une information nécessaire pour répondre à un autre cas d’usage. On peut obtenir la vitesse en extrapolant à partir de la séquence des positions : cela permet effectivement de contrôler la vitesse à partir du relevé des positions.

Autrement dit, le flux des positions a une signification propre, que l’on peut exploiter en plus de chacune des positions distinctes. Une autre manière de le voir est qu’on monte ici d’un niveau d’abstraction par rapport à la donnée de base (comme la dérivée, en mathématiques).
Dans ce genre de scenario, un broker à base de logs est fortement conseillé.

NOTE : Bien que cela sorte largement du cadre de cette discussion, attention néanmoins à garder en tête les limitations de l’analyse de flux en temps réel (si besoin de temps réel) : les seules dimensions d’analyse (indexes) efficaces sont le temps et la clé de partition. Attention également à la profondeur d’analyse qui devrait être restreinte.

 

La valeur de la séquence

 

De manière connexe, un autre point à prendre en considération est l’importance de la séquence. Pour y voir plus clair, on peut par exemple se poser la question de la gestion d’un cas non nominal dans un flux de messages : que faire quand on “rate” (quelle qu’en soit la raison) un message/événement? Ou qu’on en change l’ordre?
Et en premier lieu : quel impact concret pour le consommateur? Quelle signification fonctionnelle?
Reprenons l’exemple de la gestion d’entrepôt : une erreur d’intégration sur un des événements de stock (ou une interversion d’événements) peut donner lieu à des erreurs de calcul de stock, qui peuvent entrainer des opérations de réassort sur tel ou tel produit. A la clé, une perte d’efficacité de toute la chaîne de traitement et du gaspillage de ressources de l’entreprise.
Dans un tel cas, le choix d’un broker uniquement orienté souscription peut s’avéré risqué et un broker orienté log semble largement plus indiqué. Cependant, la notion d’ordre étant importante, il faudra attacher un soin particulier au partitionnement.
De plus, quel traitement apporter au cas non-nominal? Autrement dit : que faire du message qu’on n’a pas réussi à traiter? Quatre options sont possibles :

  • Arrêt du processus de consommation : c’est le mode le plus “safe” et le plus conservateur par rapport à la valeur de la séquence. Il nécessite a priori une action manuelle pour d’abord traiter le cas d’erreur (quel que soit le traitement) puis réamorcer le traitement automatique. Cette option est peu vraisemblable dès que le volume de messages devient conséquent ou que les impératifs de délai de traitement sont contraignants. Elle peut néanmoins être préférable au risque de corrompre le consommateur.
  • Deadlettering en vue d’une potentielle re-soumission : techniquement faisable mais contraignant. En effet on ne peut pas se contenter de resoumettre le message dans le flux puisque cela reviendrait à corrompre la séquence. La mécanique de réintégration du message côté consommateur doit donc être solide.
  • Deadlettering simple (“skip”) : c’est le cas des messages toxiques, qui peuvent se produire lorsque l’émetteur évolue et commence à publier des messages qui ne correspondent pas (encore) à un cas d’usage prévu par le consommateur.
  • Rejeu depuis un point de la séquence : je décris cette option plus en détail ci-dessous.

 

Mythe du “replay” par un système depuis un offset :

 

Ces problématiques de gestion des cas non nominaux amènent un argument entendu fréquemment, et qui a souvent pour but de faire pencher la balance en direction d’un broker à base de logs, est qu’on peut rejouer des messages depuis un certain point dans le temps. Cet argument est évidemment alléchant, mais il faut faire attention à ne pas confondre un gadget et un requirement.
La question à se poser est donc : pour quel cas d’usage avéré, en réalité?

  • Attention à ne pas confondre avec un mécanisme de sauvegarde / restauration : un log ne DOIT PAS contenir une profondeur infinie d’événements. On le sait dans le monde des bases de données, le journal des transactions ne doit pas enfler indéfiniment. Il faut régulièrement effectuer des backups full pour vider les journaux et avoir une photographie de l’état de la base de donnée. Corollaire : un log ne remplace pas un système de backup.
  • Attention à ne pas confondre avec un mécanisme de reprise de données : si le seul vrai besoin est d’introduire un simple décalage de consommation dans le temps (ex : démarrage échelonné), il y a d’autres solutions, comme jouer sur le TTL sur un broker à base de file d’attente. D’autre part, il est généralement peu efficace de tailler un fonctionnement nominal sur la base des contraintes d’un cas d’usage exceptionnel tel que l’initialisation d’un système / la reprise de données.
  • Dans le cas d’une reprise sur erreur, on peut effectivement imaginer un mécanisme de “rewind” / replay. Il est alors vital de bien le prévoir dans la conception et les opérations. De plus :
    • Le recours à du deadlettering + resoumission sur un système à base de file d’attente ou de souscription ne serait-il pas suffisant? Autrement dit : est-il important de rejouer le flux complet depuis l’erreur (ie : c’est le flux qui a une valeur, avec sa séquence d’événements)?
    • Les processi de consommation doivent être idempotents, sinon le remède risque d’être pire que le mal;
    • Attention à l’impact en exploitation : quelles sont les contraintes induites sur les consommateurs concernés? Par ex : restauration préalable de l’état du consommateur? Interruption de service le temps du replay? Etc
    • Attention à la mise en œuvre : un “retour en arrière” dans la consommation du flux de messages (ie : retour à un offset antérieur) est plus ou moins facile en fonction du broker et du consommateur. Prenons l’exemple d’une Azure Function : avec Azure Event Hubs, on a accès aux offsets dans un storage. Avec Kafka en surface d’Event Hubs, on n’y a pas accès directement, il faut passer par les APIs Kafka. Dans tous les cas, le process doit être prévu en plus de l’outillage.

     

    Conclusion

     

    Ce survol des principales questions à se poser illustre l’importance de l’analyse en intégration dans l’architecture des systèmes distribués. Notons également que j’ai à peine égratigné la surface de sujets fondamentaux tels que l’impact organisationnel des sujets d’intégration.
    En guise de conclusion, je propose ci-dessous un raccourci à la hache :

    • Si l’ensemble des consommateurs est maîtrisé (ie : intérieur d’un contexte, ou tout simplement en entrée dudit contexte, pour recevoir des messages) alors le terrain est propice – si les besoins le justifient – à la mise en œuvre d’un broker orienté log. Concrètement, on va se retrouver dans ce cas de figure à l’intérieur d’un même périmètre de responsabilité, c’est-à-dire lorsque les systèmes participant à l’échange sont sous la responsabilité d’une même équipe, ou alors sur une frontière entrante c’est-à-dire pour recevoir des messages d’autres contextes (pattern de fan-in).
    • En revanche, un tel choix sera plus limitant pour exposer des frontières vers l’extérieur (c’est-à-dire diffuser des messages vers des consommateurs d’autres contextes).

    Néanmoins ce n’est qu’un raccourci très personnel et limité, qui n’a de réel intérêt que pour celui ou celle qui a déjà suivi le chemin complet (cf Tortuga 🙂 ).