Figure 5 - Diagramme des échanges en Reactive Streams
Cette spécification permet ainsi de relier différentes technologies qui ont choisi de la respecter, telles qu’un producteur utilisant Akka Streams et un consommateur dans Spark Streaming. L’information de pression peut remonter la chaîne de traitement pour propager la gestion de la pression au delà des frontières de la brique technologique à l’origine de la back-pressure.
Reactive Streams propose une version 1.0 de son API en Java. Des versions Javascript et protocole réseau sont également disponibles. Nombre d’éditeurs Open Source ont déjà adopté cette spécification, et certains en ont déjà releasé une implémentation. Parmi eux, nous citons : Akka Streams, Vert.x, Reactor, Spark Streaming, RxJava. Cette spécification suggère par ailleurs l’utilisation d’un push-pull dynamique pour répondre aux problèmes posés par un système en pur pull ou en pur push. Ce mode sera abordé plus en détail plus bas dans l’article.
Comme indiqué précédemment, pour éviter une surcharge du consommateur dans une communication en push, il est nécessaire d’utiliser des stratégie de back-pressure. Ce modèle étant éprouvé depuis longtemps, notamment dans les chaînes de traitement et dans certains protocoles, on y trouve plusieurs stratégies, dont les plus utilisées sont :
Figure 6 - Le Xon/Xoff
Cette stratégie repose sur l’exploitation de deux messages spéciaux, envoyés par le consommateur vers le producteur. Par exemple, lors d’une communication physique via un câble série (RS232 par exemple), il existe un tampon qui accumule les caractères reçu avant qu’ils ne soient consommé par les applications. Ce tampon ayant une taille fixe, il y a un risque de le remplir et de perdre les caractères suivant. Pour éviter cela, un protocole consiste en envoyer un caractère XOFF (Ctrl-S, 0x13) dès que 80% du tampon est plein. Cela laisse le temps à l’information à arriver vers le producteur, avant la saturation du tampon. Ce dernier cesse alors d’envoyer des caractères. Lorsque la taille du tampon tombe à 20%, un caractère XON (Ctrl-Q, 0x11) est envoyé pour informer qu’il est possible d’envoyer les prochains caractères. Cela doit être traité avant l'assèchement du tampon. Cette approche de gestion logicielle du flux de traitement est une approche de back-pressure, gérée par le consommateur. L’idée pour appliquer cette stratégie dans le modèle push est de définir deux niveaux pour le tampon : haut et bas, fixés empiriquement selon la vitesse remplissage de ce dernier. Le tampon ne doit pas saturer avant que le producteur n’ait reçu le message d’arrêt. De la même façon que pour le protocole RS232 défini ci-avant, lorsque la taille du tampon dépasse la valeur haute, le consommateur envoie un message à son producteur pour demander l'arrêt de l’émission de nouveaux éléments. Une fois le tampon redescendu sous la valeur basse, il envoie un message de reprise au producteur. Parmi les impacts à noter pour ce mode, on relève qu’une perte de messages est possible dans le transit si la communication est rompue entre producteur et consommateur (le Xoff n’est pas reçu par le producteur et ce dernier continue à envoyer des messages qui ne seront donc potentiellement pas traités). Par ailleurs, de la latence peut être constatée lorsque les messages de Xon ou Xoff ne sont pas traités immédiatement une fois réceptionnés par le producteur en raison de l’encombrement de son propre tampon.
Figure 7 - Le ACK/NACK
Le modèle ACK, pour ACKnowledgement (acquittement), consiste à envoyer au producteur un message spécifique pour valider la réception d’autres messages (un par un, les x derniers messages, etc). C’est une approche où la gestion de la pression est effectuée conjointement par le producteur et le consommateur et où l’acquittement sert de vecteur de gestion de la pression. Cette stratégie est notamment utilisée dans le protocole TCP (ce qui fait partie des différences avec le protocole IP). Dans les piles TCP/IP, il y a des tampons ayant une taille limitée (backlog de connections, comment le backlog TCP fonctionne). Le producteur de la donnée utilise un tampon local des paquets en cours d’émission. Chacun doit être acquitté par le consommateur. Cela permet la réémission du paquet après un délai arbitraire sans attendre l’acquittement (méthode ARQ). Cela permet également de gérer la pression dans le TCP au niveau du consommateur. Lorsque tous ses tampons sont pleins, il ignore les nouveaux paquets et envoie un NACK (Negative-ACKnowledgement) où aucun acquittement n’est alors émis. Le paquet sera réémis plus tard par la pile IP du producteur. Différentes stratégies permettent au producteur d’identifier le débit maximum toléré par le consommateur et les différentes erreurs potentielles. Suivant les stacks IP, les stratégies sont composées.
Il est à noter que les approches utilisées par Windows et Linux sont différentes. En ajustant dynamiquement les fenêtres de réception et/ou d'émission (cwin), une application peut propager à sa pile IP une situation de pression applicative. Ainsi, les clients réduiront naturellement le débit réseau grâce au protocole sous-jacent. Certains algorithmes ne sont pas adaptés pour des réseaux rapides et disposant d’une importante bande passante (plusieurs gigabits par seconde). D’autres algorithmes permettent alors de garder le débit le plus proche possible de la bande passante disponible pour des réseaux rapides. Dans le framework d’acteurs Akka, Lightbend (ex. Typesafe) a rajouté un module TCP pour gérer les connexions TCP : Akka TCP. Il est basé sur un modèle de communication par acquittement décliné en trois variantes :
De manière plus générale, le mode ACK/NACK a l’avantage de ne pas risquer la perte de messages en raison de rupture éventuelle de communication. Inversement, il occasionne l’échange et le traitement d’un plus grand nombre de messages, d’où un overhead réseau et CPU.
Figure 8 - Le push-back
Le principe du push-back repose sur le rejeu des éléments. Une fois que le tampon du consommateur est plein, les messages “poussés” suivants sont renvoyés au producteur. Cela a plusieurs impacts :
Les éléments renvoyés peuvent dépendre du cas d’usage ou du métier. On peut par exemple choisir de renvoyer les plus vieux, les plus récents, les moins prioritaires ou encore le tampon en entier. Cette approche présente l’avantage d’injecter plus de spécificité métier dans la stratégie du consommateur, ce qu’on ne retrouve pas dans les autres stratégies de push. Le consommateur peut également déléguer le message vers un autre consommateur, si cette stratégie n’est pas trop gourmande en ressources. Mais cela relève plutôt de la mauvaise pratique car la gestion des consommateurs devrait être centralisée au niveau du producteur pour une gestion plus efficace de la pression.
De même que dans le push, il existe plusieurs stratégies de back-pressure dans le mode pull, dont les principales sont :
Figure 9 - Le Pull simple : un pull avec une requête de 1
Le modèle le plus simple du pull, consiste à demander les éléments un par un. C’est l’équivalent de l’approche ACK mais en sens inverse : la transmission du message est ici initiée par le consommateur. Le principal défaut de ce modèle est le nombre de messages émis et la latence. Pour chaque donnée produite, deux messages ont été émis : la demande et le message. Une quantité supplémentaire de communication et de bande passante est nécessaire relativement à un push simple. Le second problème réside dans le fait qu’entre le moment où une nouvelle requête est émise et le moment où la donnée associée arrive, le consommateur n’a rien à faire. Ce temps d’inactivité est perdu et n’est pas mis à profit pour traiter d’autres messages. Les autres approches de pull se proposent de demander le plus de messages possible dans la même requête, en mode “micro-batch”. Cela permet d’une part de remplir rapidement le tampon du consommateur pour minimiser les chances qu’il soit oisif et, d’autre part, de réduire le round-trip ainsi que le volume réseau.
Figure 10 - Le Watermark
La stratégie Watermark est typiquement adaptée à des consommateurs effectuant eux-mêmes le traitement des messages (sans délégation à d’autres entités). A la façon d’un Xon/Xoff, on définit deux valeurs limites. Ces limites peuvent être ajustées dynamiquement par le consommateur selon son niveau de saturation. La High-Watermark correspond à la taille maximale à requéter, alors que la Low-Watermark est une limite basse servant à tenter de garantir un apport constant d’éléments pour le consommateur. La figure 10 présente le fonctionnement typique de la stratégie Watermark, où sont définies les quantités suivantes :
La demande de messages au producteur prend alors en compte le nombre de messages en transit (demandés mais non encore reçus, b). Le nombre d’éléments requétés à tout moment au producteur ne dépasse jamais la High-Watermark. Aucune nouvelle demande n’est lancée tant que le nombre de requêtes en transit dépasse la Low-Watermark. C’est le cas notamment dans le framework Akka Streams, où le tampon se trouve être la mailbox des acteurs et est dissocié du fonctionnement de cette stratégie. A chaque élément reçu, on attend de finir son traitement avant d’évaluer la prochaine requête, qui peut être nul. Ce fonctionnement tente de garantir au consommateur d’avoir constamment des éléments à traiter.
Figure 11 - Le Max in Flight
Le fonctionnement du “Max in Flight” est plus évolué que celui de la stratégie précédente, où on considère de plus la quantité :
Cette stratégie est typiquement adaptée à un consommateur n’effectuant pas lui-même tout ou partie du traitement des messages reçus. Par exemple, les éléments “In Flight” peuvent correspondre aux éléments en cours de traitement en aval du consommateur (par des acteurs de traitement), voire des éléments à traiter et stockés dans le tampon interne. Dans les couches Akka Streams, l’implémentation du calcul de cette valeur est d’ailleurs librement adaptable. Si le nombre d’éléments à demander est n’, la somme ( b + p + k + n’ ) ne doit à aucun moment dépasser la valeur “maxInFlight”. Les éléments en cours de traitement sont alors également pris en compte dans le calcul des éléments à demander. Cette stratégie permet d’intégrer la capacité instantanée de traitement des éléments et d’adapter à celle-ci le nombre d’éléments demandés. Elle permet également d’optimiser l’utilisation du consommateur en veillant à provisionner des éléments à traiter et éviter les temps d’attente pour effectuer les traitements.
N’utiliser qu’une approche push n’est pas efficace lorsque le consommateur est lent. L’approche pull n’est pas efficace lorsque que le consommateur est rapide. Il existe des stratégies qui combinent les deux approches push et pull. La difficulté consiste à choisir les conditions de basculement d’une approche à une autre et d’éviter de basculer trop régulièrement.
Le concept de base du push-pull dynamique consiste à passer dynamiquement d’un mode pull à un mode push selon le rapport de vitesse entre consommateur(s) et producteur. L’idée est de pouvoir adapter des stratégies différentes dans chacun des deux modes. Par exemple un push avec un ACK et un pull en Watermark. Dans le cas où le producteur est plus lent que le consommateur, on privilégie le mode push. A l’inverse dans le cas d’un producteur plus rapide, on choisi le pull qui permet de gérer naturellement la pression, en laissant les messages en attente dans le producteur. Dans la pratique, le (rare) push-pull dynamique que l’on peut trouver (Akka Streams), est une implémentation en pur pull avec une demande sans limite pour passer à en mode “équivalent à un push”. Si le consommateur est plus rapide que le producteur, le nombre d’éléments requétés sera toujours plus important que le nombre d’éléments que peut fournir le producteur. Le producteur n’aura donc pas à attendre pour émettre la donnée et l’émet immédiatement, ce qui se rapproche du mode push mais reste conceptuellement du pull. Ce fonctionnement peut donc être qualifié de push-pull dynamique. Reactive Streams propose également la solution “request(Long.MaxValue)” (requête du nombre maximal d’éléments autorisés par le langage), ce qui représente conceptuellement une requête d’un nombre infini d’éléments et provoque implicitement un fonctionnement en push. Dans ce cas, le consommateur semble condamné à subir ce mode indéfiniment. Il est alors conseillé de prévoir une implémentation du producteur permettant de débrayer ce mode. Cela pourrait être un retour au mode pull à la réception d’une requête d’un nombre inférieur d’éléments.
Certaines stacks techniques, telles que CORBA (Event Service) et les technologies P2P, des stratégies mixtes de type hybride sont utilisées :
Il n’y a pas de bascule dynamique d’un mode à l’autre, le choix d’un mode est définitivement adopté à la conception des services.
Il existe des solutions qui ne sont pas basées sur les principes de push ou de pull.
Une approche proposée par RxJava comme alternative à la back-pressure consiste à bloquer le thread du producteur, avec un verrou classique, et de le relâcher une fois que la pression devient acceptable. Elle a cependant le désavantage d’aller a l’encontre du Manifeste Réactif et de mobiliser des ressources.
Nous avons identifié dans cette partie les différentes formes de back-pressure possibles ainsi que les différentes stratégies qu'il est possible d'utiliser. Concrètement, une chaîne de processus dont des maillons reçoivent des informations de pression par back-pressure doit prendre des mesures pour faire baisser la pression. Oui, mais comment ? Nous donnerons les réponses dans la Partie II de cet article_._