web-dev-qa-db-fra.com

Quel type de "EventBus" utiliser au printemps? Intégré, Reactor, Akka?

Nous allons démarrer une nouvelle application Spring 4 dans quelques semaines. Et nous aimerions utiliser une architecture événementielle. Cette année, j'ai lu ça et là sur "Reactor" et en le cherchant sur le web, je suis tombé sur "Akka".

Donc pour l'instant nous avons 3 choix:

Je n'ai pas pu trouver une vraie comparaison de ceux-ci.


Pour l'instant, nous avons juste besoin de quelque chose comme:

  • X s'inscrit pour écouter Event E
  • Y s'inscrit pour écouter Event E
  • Z envoie un Event E

Et puis X et Y recevront et géreront l'événement.

Nous l'utiliserons très probablement de manière asynchrone, mais il y aura certainement des scénarios synchrones. Et nous envoyons très probablement toujours une classe comme événement. (Les échantillons Reactor utilisent principalement des chaînes et des modèles de chaînes, mais il prend également en charge les objets).


Pour autant que je sache, ApplicationEvent fonctionne de manière synchrone par défaut et Reactor fonctionne de manière asynchrone. Et Reactor permet également d'utiliser la méthode await() pour la rendre un peu synchrone. Akka fournit plus ou moins la même chose que Reactor, mais prend également en charge l'accès à distance.

Concernant la méthode await() de Reactor: peut-elle attendre la fin de plusieurs threads? Ou peut-être même un ensemble partiel de ces fils? Si nous prenons l'exemple ci-dessus:

  • X s'inscrit pour écouter Event E
  • Y s'inscrit pour écouter Event E
  • Z envoie un Event E

Est-il possible de le rendre synchrone, en disant: Attendez que X et Y pour terminer. Et est-il possible de le faire attendre juste pour X, mais pas pour Y?


Peut-être qu'il existe également des alternatives? Qu'en est-il par exemple de JMS?

Beaucoup de questions, mais j'espère que vous pourrez fournir des réponses!

Je vous remercie!


EDIT: exemples de cas d'utilisation

  1. Lorsqu'un événement spécifique est renvoyé, j'aimerais créer 10000 e-mails. Chaque e-mail doit être généré avec un contenu spécifique à l'utilisateur. Je créerais donc beaucoup de threads (max = cœurs de processeur système) qui créent les mails et ne bloquent pas le thread de l'appelant, car cela peut prendre quelques minutes.

  2. Lorsqu'un événement spécifique est renvoyé, j'aimerais collecter des informations auprès d'un nombre inconnu de services. Chaque extraction prend environ 100 ms. Ici, je pourrais imaginer utiliser await de Reactor, car j'ai besoin de ces informations pour continuer mon travail dans le thread principal.

  3. Lorsqu'un événement spécifique est déclenché, j'aimerais effectuer certaines opérations en fonction de la configuration de l'application. L'application doit donc pouvoir (dé) enregistrer dynamiquement les utilisateurs/gestionnaires d'événements. Ils feront leur propre truc avec l'événement et je m'en fiche. Je créerais donc un thread pour chacun de ces gestionnaires et continuerais simplement à faire mon travail dans le thread principal.

  4. Découplage simple: je connais essentiellement tous les récepteurs, mais je ne veux tout simplement pas appeler tous les récepteurs dans mon code. Cela devrait principalement se faire de manière synchrone.

On dirait que j'ai besoin d'un ThreadPool ou d'un RingBuffer. Ces frameworks ont-ils des RingBuffers dynamiques, dont la taille augmente si nécessaire?

38
Benjamin M

Je ne suis pas sûr de pouvoir répondre adéquatement à votre question dans ce petit espace. Mais je vais essayer! :)

Le système ApplicationEvent et le Reactor de Spring sont vraiment très distincts en ce qui concerne les fonctionnalités. Le routage ApplicationEvent est basé sur le type géré par le ApplicationListener. Quoi de plus compliqué que cela et vous devrez implémenter la logique vous-même (ce n'est pas nécessairement une mauvaise chose, cependant). Cependant, Reactor fournit une couche de routage complète qui est également très légère et complètement extensible. Toute similitude de fonction entre les deux extrémités quant à leur capacité à s'abonner et à publier des événements, ce qui est vraiment une caractéristique de tout système événementiel. N'oubliez pas non plus le nouveau module spring-messaging Avec Spring 4. C'est un sous-ensemble des outils disponibles dans Spring Integration et fournit également des abstractions pour construire autour d'une architecture événementielle.

Reactor vous aidera à résoudre quelques problèmes clés que vous auriez autrement à gérer vous-même:

Correspondance du sélecteur : Reactor fait une correspondance entre Selector, qui englobe une plage de correspondances - à partir d'un simple appel à .equals(Object other) , à une correspondance de modèle d'URI plus complexe qui permet l'extraction d'un espace réservé. Vous pouvez également étendre les sélecteurs intégrés avec votre propre logique personnalisée afin d'utiliser des objets riches comme clés de notification (comme les objets de domaine, par exemple).

API Stream et Promise : Vous avez déjà mentionné l'API Promise en référence à la méthode .await(), qui est vraiment destiné au code existant qui attend un comportement de blocage. Lors de l'écriture de nouveau code à l'aide de Reactor, il ne peut pas être suffisamment souligné pour utiliser des compositions et des rappels pour utiliser efficacement les ressources système en ne bloquant pas les threads. Le blocage de l'appelant n'est presque jamais une bonne idée dans une architecture qui dépend d'un petit nombre de threads pour exécuter un grand volume de tâches. Les futurs ne sont tout simplement pas évolutifs dans le cloud, c'est pourquoi les applications modernes exploitent des solutions alternatives.

Votre application pourrait être architecturée avec Streams ou Promises, mais honnêtement, je pense que vous trouverez le Stream plus flexible. Le principal avantage est la composabilité de l'API, qui vous permet de câbler des actions dans une chaîne de dépendances sans blocage. En tant qu'exemple complètement informel basé sur votre cas d'utilisation de messagerie, vous décrivez:

@Autowired
Environment env;
@Autowired
SmtpClient client;

// Using a ThreadPoolDispatcher
Deferred<DomainObject, Stream<DomainObject>> input = Streams.defer(env, THREAD_POOL);

input.compose()
  .map(new Function<DomainObject, EmailTemplate>() {
    public EmailTemplate apply(DomainObject in) {
      // generate the email
      return new EmailTemplate(in);
    }
  })
  .consume(new Consumer<EmailTemplate>() {
    public void accept(EmailTemplate email) {
      // send the email
      client.send(email);
    }
  });

// Publish input into Deferred
DomainObject obj = reader.readNext();
if(null != obj) {
  input.accept(obj);
}

Reactor fournit également le Boundary qui est essentiellement un CountDownLatch pour bloquer les consommateurs arbitraires (vous n'avez donc pas à construire un Promise si tout ce que vous voulez faire est pour une complétion Consumer). Vous pouvez utiliser un Reactor brut dans ce cas et utiliser les méthodes on() et notify() pour déclencher la vérification de l'état du service.

Pour certaines choses, cependant, il semble que ce que vous voulez soit un Future renvoyé d'un ExecutorService, non? Pourquoi ne pas simplement garder les choses simples? Le réacteur ne sera réellement utile que dans les situations où vos performances de débit et votre efficacité en tête sont importantes. Si vous bloquez le thread appelant, alors vous allez probablement effacer les gains d'efficacité que Reactor vous donnera de toute façon, donc vous pourriez être mieux dans ce cas en utilisant un ensemble d'outils plus traditionnel.

La bonne chose à propos de l'ouverture de Reactor est qu'il n'y a rien qui empêche les deux d'interagir. Vous pouvez mélanger librement Futures avec Consumers sans statique. Dans ce cas, gardez à l'esprit que vous ne serez jamais aussi rapide que votre composant le plus lent.

30
Jon Brisbin

Ignorons le ApplicationEvent du Spring car il n'est vraiment pas conçu pour ce que vous demandez (c'est plus sur la gestion du cycle de vie du bean).

Ce que vous devez comprendre, c'est si vous voulez le faire

  1. la voie orientée objet (ie acteurs, consommateurs dynamiques, inscrits à la volée) [~ # ~] ou [~ # ~]
  2. la voie du service (consommateurs statiques, inscrits au démarrage).

Voici votre exemple de X et Y:

  1. instances éphémères (1) ou sont-elles
  2. singletons/objets de service à longue durée de vie (2)?

Si vous devez enregistrer des consommateurs à la volée, Akka est un bon choix (je ne suis pas sûr du réacteur car je ne l'ai jamais utilisé). Si vous ne voulez pas faire votre consommation d'objets éphémères, vous pouvez utiliser JMS ou AMQP.

Vous devez également comprendre que ce type de bibliothèques essaie de résoudre deux problèmes:

  1. Concurrence (c'est-à-dire faire des choses en parallèle sur la même machine)
  2. Distribution (c'est-à-dire faire des choses en parallèle sur plusieurs machines)

Reactor et Akka se concentrent principalement sur # 1. Akka a récemment ajouté la prise en charge du cluster et l'abstraction de l'acteur facilite la tâche # 2. Les files d'attente de messages (JMS, AMQP) se concentrent sur # 2.

Pour mon propre travail, je fais l'itinéraire de service et j'utilise un Guava EventBus et RabbitMQ fortement modifiés. J'utilise des annotations similaires à Guava Eventbus mais j'ai également des annotations pour les objets envoyés sur le bus, mais vous pouvez simplement utiliser EventBus de Guava en mode Async en tant que POC, puis créer les vôtres comme je l'ai fait.

Vous pourriez penser que vous avez besoin d'avoir des consommateurs dynamiques (1) mais la plupart des problèmes peuvent être résolus avec un simple pub/sub. La gestion des consommateurs dynamiques peut également être délicate (d'où Akka est un bon choix car le modèle d'acteur a toutes sortes de gestion pour cela)

6
Adam Gent

Définissez soigneusement ce que vous voulez du cadre. Si un framework a plus de fonctionnalités que vous n'en avez besoin, ce n'est pas toujours bon. Plus de fonctionnalités signifie plus de bugs, plus de code à apprendre et moins de performances.

Certaines caractéristiques à prendre en compte sont:

  • la nature des acteurs (fils ou objets légers)
  • capacité à travailler sur un cluster de machines (Akka)
  • files d'attente de messages persistants (JMS)
  • des fonctionnalités spécifiques comme les signaux (événements sans information), les transitions (objets pour combiner des messages de différents ports en un événement complexe, voir réseaux de Petri), etc.

Soyez prudent avec les fonctionnalités synchrones comme wait - cela bloque tout le thread et est dangereux lorsque les acteurs sont exécutés sur un pool de threads (famine de threads).

Plus de cadres à examiner:

Fork-Join Pool - dans certains cas, permet await sans famine de thread

Systèmes de workflow scientifiques

Dataflow framework for Java - signaux, transitions

ADD-ON : Deux types d'acteurs.

Généralement, le système de travail parallèle peut être représenté sous forme de graphique, où les nœuds actifs s'envoient des messages. En Java, comme dans la plupart des autres langages traditionnels, les nœuds actifs (acteurs) peuvent être implémentés sous forme de threads ou de tâches (Runnable ou Callable) exécutées par un pool de threads. Normalement, une partie des acteurs sont des fils et une partie des tâches. Les deux approches ont leurs avantages et leurs inconvénients, il est donc essentiel de choisir la mise en œuvre la plus appropriée pour chaque acteur du système. En bref, les threads peuvent bloquer (et attendre les événements) mais consomment beaucoup de mémoire pour leurs piles. Les tâches ne peuvent pas bloquer mais utiliser des piles partagées (de threads dans un pool).

Si une tâche appelle une opération de blocage, elle exclut un thread groupé du service. Si de nombreuses tâches se bloquent, elles peuvent exclure tous les threads, provoquant un blocage - les tâches qui peuvent débloquer les tâches bloquées ne peuvent pas s'exécuter. Ce type de blocage est appelé famine de fil. Si, pour éviter la famine de threads, configurez le pool de threads comme illimité, nous convertissons simplement les tâches en threads, perdant ainsi les avantages des tâches.

Pour éliminer les appels aux opérations de blocage dans les tâches, la tâche doit être divisée en deux (ou plus) - le premier appel de tâche bloque l'opération et se termine, et le reste est formaté comme une tâche asynchrone démarrée à la fin de l'opération de blocage. Bien sûr, l'opération de blocage doit avoir une interface asynchrone alternative. Ainsi, par exemple, au lieu de lire le socket de manière synchrone, les bibliothèques NIO ou NIO2 doivent être utilisées.

Malheureusement, la bibliothèque standard Java n'a pas d'homologues asynchrones pour les installations de synchronisation populaires telles que les files d'attente et les sémaphores. Heureusement, les sont faciles à mettre en œuvre à partir de zéro (voir Dataflow framework for Java pour des exemples ).

Ainsi, faire des calculs uniquement avec des tâches non bloquantes est possible mais augmente la taille du code. Le conseil évident est d'utiliser des threads dans la mesure du possible et des tâches uniquement pour de simples calculs massifs.

3
Alexei Kaigorodov