Reactor + Reactive Streams

44 views
Skip to first unread message

Laurent Caillette

unread,
Apr 28, 2015, 3:03:24 AM4/28/15
to tec...@googlegroups.com
L'initiative "Reactive Streams"
http://www.reactive-streams.org
définit un modèle de programmation dit "réactif" afin de traiter de
gros volumes de données avec une faible latence, en tirant le meilleur
parti de la machine physique. Les auteurs parlent d'un //standard pour
un traitement asynchrone avec un retour de pression (backpressure)
non-bloquant//. Chaque mot à son importance et nous allons voir toutes
les implications.

C'est une spécification minimale (4 interfaces en Java, 7 méthodes en
tout) avec des règles très précises sur les exceptions lancées, la
concurrence d'accès et tout ça. Parmi les auteurs on trouve des mecs
de Netflix, Pivotal, RedHat, Twitter, Typesafe et Kaazing, donc
probablement pas que des nains de jardin.

Sur cette spécification se sont bâtis plusieurs projets qui
fournissent des bibliothèques spécialisées pour consommer ou produire
des événéments (connecteurs HTTP, générateurs de HTML...) avec comme
point commun de remplacer le conteneur d'application qui manipule
plein de threads et des flux plus ou moins bloquants.

Après une présentation du potentiel des "Reactive Streams", ce billet
détaillera le projet "Reactor"
https://github.com/reactor
qui développe l'idée de base sour la forme d'une plomberie de bas
niveau tout à fait passionnante, même si je n'ai pas encore eu
l'occasion de l'expérimenter.


== Reactive Streams : le contrat fondamental

L'expérience montre que le matériel est exploité au mieux quand on
fait tourner autant de fils d'exécution (threads) qu'il y a de cœurs,
pour minimiser le coût de la préemption. Autrement dit les serveurs
qui font tourner beaucoup de fils d'exécution donnent l'impression
d'attendre les entrées-sorties, mais ce sont les changements de
contexte (passage en mode noyau, invalidation des caches du
processeur) qui coûtent le plus cher. D'où l'idée d'utiliser un seul
fil d'exécution par cœur, en s'interdisant de faire attendre les fils
d'exécution. Le blog "Mechanical Sympathy"
http://mechanical-sympathy.blogspot.com
fournit une abondance de détails là-dessus.

L'initiative "Reactive Streams" consiste à isoler le modèle de
programmation commun aux applications qui fonctionnent de cette façon.
Bien sûr les entrées-sorties sont non-bloquantes et asynchrones mais
se passer du ``BufferedInputStream`` n'est pas suffisant. À la place
on va s'échanger des blocs de données encapsulés dans des événements.
De quelle façon ? Est-ce le fil d'exécution qui extrait des données
issues du réseau qui va appeler des consommateurs d'événements ? Ou
les consommateurs qui vont appeler le producteur ? Mais dans ce cas,
s'il n'y a pas de données il ne reste qu'à bloquer le fil d'exécution
appelant, ce qu'on veut éviter. Par ailleurs, quel contrat définir
pour autoriser le traitement de lots d'événements ? Et comment
reconfigurer à chaud la topologie des producteurs-consommateurs ?

L'"API Java"
http://www.reactive-streams.org/reactive-streams-1.0.0.RC5-javadoc
répond à toutes ces questions avec seulement quatre interfaces :
``Publisher``, ``Subscriber``, ``Subscription`` et ``Consumer``. Un
``Subscriber`` se branche sur un ``Publisher``, et quand l'abonnement
est effectif il reçoit un objet ``Subscription`` qui lui permet de
demander au ``Publisher`` un certain nombre d'événements à consommer
(les événements sont d'un type générique), ou alors de mettre fin à la
relation. Le ``Publisher`` peut alors appeler le ``Subscriber`` pour
lui dire de traiter l'événement suivant ou de s'arrêter. Voilà pour le
principe de base, d'une rare élégance, et qui permet à différents
projets d'intéropérer.

Après, la "spécification"
https://github.com/reactive-streams/reactive-streams-jvm
énonce en tout 43 règles sur ce qui se passe au niveau des fils
d'exécution, la façon de gérer les erreurs, le contrôle du flux
d'événements, les changements d'état, etc. Un TCK (Test Compatibility
Kit) permet de valider les concrétisations de l'API. C'est du sérieux.


== Pourquoi ce truc est important

J'ai les boules parce que j'ai codé ma propre version du Reactor avant
de découvrir que des gens beaucoup plus compétents s'étaient penchés
sur la question. C'est en gouglant sur le retour de pression que je
suis finalement tombé sur le Reactor, et donc sur les Reactive
Streams.

Le nom du Reactor correspond à celui d'un modèle de conception qui
sépare les unités de traitement du choix des modalités d'exécution. Ce
dernier comprend le transfert d'événements, l'affectations des fils
d'exécution, etc. Le modèle du Reactor pousse à écrire des briques
fonctionnelles synchrones et isolées de toute concurrence d'accès,
donc plus faciles à fiabiliser. La logique applicative est plus
lisible car le code applique littéralement des spécifications du genre
"Si j'ai ça en entrée je dois avoir ça en sortie." Par contre si on
enchaîne les traitements asynchrones, il est plus difficile d'analyser
des conséquences d'un événément en entrée.

Un aspect qui peut favoriser l'adoption, c'est le besoin croissant de
pousser l'information du serveur vers le client (plus rien à voir avec
le "client-serveur" des années 90). Pour ce besoin là le serveur HTTP
qui attend qu'on l'appelle pour aller piocher dans un SGBDR, c'est
archi-mort. L'approche réactive fournit une vraie solution
architecturale de la cave au grenier. Bon ça nécessite quelques
changements d'habitudes, comme le passage à une persistance
journalisée, mais c'est un autre sujet.

Sans insister sur le gain en performances, notons que si on veut
pousser vers le client il faut des sessions, et s'il y a plusieurs
serveurs on se tape des histoires de réplication. Si tout tient sur un
seul serveur ça fait beaucoup de boulot en moins.

Une clarification sur le retour de pression : à l'origine je cherchais
des infos sur le retour de pression au niveau TCP, qui consiste à ne
pas consommer des données qu'on ne saurait pas traiter. TCP bloque
alors l'émetteur, ce qui évite de péter la mémoire ou de perdre des
données. Les Reactive Streams parlent de retour de pression
non-bloquant au niveau des fils d'exécution, ce qui autre chose. Mais
ce modèle de programmation, qui laisse le ``Subscriber`` dire combien
il veut consommer d'événements, débouche naturellement sur le blocage
voulu au niveau de TCP, puisqu'il suffit de ne pas dire qu'on veut
consommer de nouvelles données.


== Reactor

Reactor est une collection de bibliothèques, principalement en Java,
pour intéropérer avec d'autres produits, et les connecter comme
spécifié par les Reactive Streams.

C'est le "Disruptor"
https://lmax-exchange.github.io/disruptor
de LMAX qui fournit l'épine dorsale du Reactor. Le Disruptor est une
structure de file pour échanger des données entre fils d'exécution,
avec des débits colossaux par rapport à une ``LinkedBlockingQueue``.
Techniquement il s'agit d'une structure en anneau et de verrous
tournants (spinlocks) qui consistent à lire en boucle une variable
``volatile``. Évidemment ça nécessite un serveur dédié avec plusieurs
cœurs, et un seul fil d'exécution pour les verrous tournants. (Si on
n'aime pas, à cause de la consommation électrique par exemple, l'API
du Disruptor permet de passer à des stratégies moins agressives.)
Mais, aussi impressionnant que paraisse le Disruptor, on voit mal
comment faire du code applicatif par-dessus.

C'est là qu'intervient le Reactor en fournissant les outils pour créer
des topologies complexes de producteurs et de consommateurs
d'événements : éclatement-regroupement, filtres, bus... Le Disruptor
se retrouve enfoui dans un objet ``Dispatcher`` qui gère plein de
choses qu'on préfère ignorer. Par exemple, si on dépasse la capacité
de l'anneau du Disruptor c'est la fin des haricots. Mais comme l'API
des Reactive Streams définit explicitement le nombre d'événements
souhaités, on donne au Reactor le moyen de poser les bonnes limites.
Il y a comme ça des mariages d'amour qui ne doivent au hasard.

Le Reactor peut intégrer des composants qui font des entrées-sorties
bloquantes, en leur fournissant un parc de fils d'exécution
réutilisables (thread pool).

En standard, le Reactor sait s'interfacer avec des bibliothèques
orientées performance : Kryo, Netty, Protobuf, ZeroMQ. Il parle aussi
à d'autres bibliothèques basées sur les Reactive Streams, notamment
"RxJava"
http://reactivex.io
et "Ratpack"
http://www.ratpack.io
. Les choix artistiques du site de Ratpack ont tendance à m'exaspérer,
mais sinon pour faire des sites Web Ratpack a l'air de soutenir la
comparaison avec `Play!`, troquant les capacités de rechargement à
chaud contre une maîtrise plus poussée des flux d'événements
complexes.

Au rayon des animaux exotiques, on trouve une déclinaison du Reactor
en JavaScript (Rhino), et une pour Android. En parlant de JavaScript,
on serait tenté de dire que oui mais `Node.js` il fait déjà tout ça.
Faux, `Node.js` utilise bien des appels en retour (callbacks) pour des
entrées-sorties asynchrones, mais tous les traitements applicatifs se
font dans un seul fil d'exécution, ce dont les auteurs ne se vantent
pas trop.

L'API du Reactor fait peur au début. La bonne nouvelle c'est qu'avec
les bases posées par les Reactive Streams, on a un principe
fédérateur, au lieu d'API hétérogènes jamais d'accord sur ce qui est
synchrone ou non et les événements qu'on tire ou ceux qu'on pousse.
Une utilisation envisageable du Reactor, c'est en complément d'un
produit comme Ratpack pour certaines chaînes de traitement. L'intérêt
-- ou l'inconvénient -- de ce genre d'outils c'est qu'il nécessitent
des idées claires au niveau de l'architecture.

Quelqu'un a déjà expérimenté quelque chose qui s'apparente plus ou
moins aux Reactive Streams ?
Reply all
Reply to author
Forward
0 new messages