Conception d'architecture réactive avec Scala et Akka

Cet article explore la construction de systèmes réactifs en utilisant Scala et Akka, en se basant sur les principes de l'architecture réactive et les schémas de messagerie Actor.

Principes de conception pour les systèmes d'entreprise

L'architecture réactive repose sur plusieurs principes fondamentaux pour gérer la concurrence, la parallélisation et la résilience :

  1. Éviter le partage : Si le partage de ressources (données, files d'attente, processus) entraîne des problèmes, il faut cesser de les partager.
  2. Simuler les conditions réelles : L'environnement de développement doit se rapprocher autant que possible des conditions difficiles rencontrées en production.
  3. Allocation de threads spécialisés : Si la conception de threads universels s'avère complexe, allouer des threads dédiés à des types de tâches spécifiques lorsqu'ils sont nécessaires.
  4. Consommation de travail réactive : Les composants doivent accepter le travail uniquement lorsqu'ils sont prêts à le traiter, plutôt que de sonder activement.
  5. Externaliser la complexité : Confier les tâches complexes de planification ou de gestion des tâches à des systèmes spécialisés.
  6. Conception axée sur la tolérance aux pannes : Les pannes sont inévitables ; concevoir le système pour qu'il y résiste grâce à des mécanismes de récupération prédéfinis.

Modèles de messagerie et Scala

Les messages sont au cœur de la communication dans les systèmes Actor. L'utilisation de classes de traits (case classes) en Scala est particulièrement efficace pour définir des modèles de messages normalisés :

  • Support du pattern matching : Les classes de traits facilitent l'utilisation des capacités de pattern matching de Scala, permettant aux Acteurs de différencier et de traiter les messages reçus.
  • Immuabilité : Elles sont un moyen simple de créer des types de données immuables, ce qui est crucial pour la prévisibilité dans les systèmes concurrents.
  • Génération automatique de méthodes : Les classes de traits génèrent automatiquement des méthodes utiles comme equals(), hashCode(), toString() et copy().

Conventions pour la définition des messages

Il est recommandé de suivre ces conventions lors de la déclaration des types de messages dans le code Scala :

  1. Association des messages aux Acteurs : Placer la définition des messages destinés à un Acteur spécifique à proximité de la définition de cet Acteur. Cela améliore la clarté sur les messages qu'un Acteur est censé recevoir et traiter.
  2. Centralisation des messages communs : Définir les types de messages génériques dans un seul fichier source Scala. Ceci est particulièrement utile lors de l'intégration de plusieurs systèmes Actor ou lors de l'utilisation d'un bus de messages, facilitant la localisation et le déploiement des messages partagés.

Gestion de la livraison des messages

Bien que les messages puissent être envoyés via ActorRef ou ActorSelection, il est possible que l'Acteur destinataire soit arrêté ou en cours d'arrêt. Pour garantir la livraison, des stratégies de rechargement et de nouvelle tentative sont nécessaires. Akka Persistence offre des fonctionnalités pour gérer ces scénarios.

L'ajout de la dépendance akka-persistence permet de gérer la persistance des messages :


</div>Comportements dynamiques des Acteurs
------------------------------------

Contrairement aux objets traditionnels, les Acteurs peuvent modifier leur comportement de manière dynamique. En utilisant `ActorContext` et le **State design pattern**, un Acteur peut changer de comportement en acceptant de nouveaux blocs de code pour définir son état et ses réactions.

Akka Cluster
------------

Akka Cluster permet de construire des systèmes distribués, tolérants aux pannes et à haute disponibilité en formant un cluster de nœuds. Chaque nœud est identifié de manière unique par son `ActorSystem`, son nom d'hôte et son port.

- **Protocole Gossip :** Les nœuds communiquent leur état via un protocole de type gossip pour maintenir le bon fonctionnement du cluster.
- **Identification unique :** L'identifiant d'un nœud est une combinaison de son nom d'hôte, de son port et de l'instance `ActorSystem`.

### Concepts clés d'Akka Cluster

- **Singleton d'acteur :** Un Acteur unique qui s'exécute sur un seul nœud du cluster, souvent utilisé comme point d'entrée centralisé ou comme répartiteur de tâches (work router).
- **Sharding d'acteur :** Permet de répartir uniformément les Acteurs à travers les nœuds du cluster. Il est configuré via des plugins de journal Akka Persistence. Cependant, le sharding ne résout pas les problèmes de surcharge causés par le partage de données au niveau applicatif.
- **Garantie de livraison :** Par défaut, les messages sont envoyés "au plus une fois". La fonctionnalité `AtLeastOnceDelivery` d'Akka Persistence garantit que les messages sont livrés au moins une fois, permettant la reprise après des pannes de nœuds.
- **Cluster Receptionist :** Gère la communication entre les Acteurs clients externes et les Acteurs internes du cluster. Il doit être présent sur chaque nœud du cluster pour enregistrer les services exposés aux clients.
- **Routeurs de cluster :**
    - **Group :** Le routeur communique avec un ensemble d'Acteurs existants répartis sur plusieurs nœuds.
    - **Pool :** Le routeur gère le cycle de vie des Acteurs qu'il route, pouvant en créer de nouveaux sur différents nœuds.

Le routeur de type **Pool** est particulièrement utile lorsqu'un seul routeur est responsable de l'acheminement vers un Acteur, souvent configuré comme un singleton d'acteur. Il est idéal pour les tâches gourmandes en ressources où le travail est distribué uniformément.

Surveillance et équilibrage de charge
-------------------------------------

Akka Cluster propose divers mécanismes pour surveiller et équilibrer la charge des nœuds :

- **Sélecteurs de métriques :** Les routeurs peuvent être configurés pour équilibrer la charge en fonction de métriques telles que `heap` (mémoire JVM), `load` (charge système UNIX), `cpu` ou une combinaison `mix`.
- **Hyperic Sigar :** Une bibliothèque intégrée pour collecter des informations système précises, souvent plus fiable que JMX ou les outils en ligne de commande UNIX pour la surveillance de la charge.

L'ajout de la dépendance Sigar permet d'utiliser ces fonctionnalités :

<div>```
<!-- https://mvnrepository.com/artifact/org.fusesource/sigar -->
<dependency>
    <groupId>org.fusesource</groupId>
    <artifactId>sigar</artifactId>
    <version>1.6.4</version>
</dependency>

Tests avec ScalaTest

ScalaTest offre un cadre robuste pour les tests, notamment pour les systèmes Actor. La méthode test comportemental, basée sur le Behavior-Driven Development (BDD) et la Spécification par l'Exemple (SBE), permet de définir les exigences logicielles à partir de scénarios réels.

La dépendance ScalaTest est la suivante :


</div>Gestion de la concurrence et des threads
----------------------------------------

La programmation multithread présente des défis tels que les **interblocages (deadlocks)**, les **verrous actifs (livelocks)**, la **famine de threads (thread starvation)**, le code inefficace et le **faux partage (false sharing)**. Le faux partage survient lorsque des threads différents modifient des données situées sur la même ligne de cache, entraînant des invalidations coûteuses du cache.

- **Couplage temporel :** Les dépendances entre threads peuvent être réduites en utilisant des files d'attente pour gérer le travail. Un thread ajoute des tâches à la file, et les threads "travailleurs" les retirent pour exécution.
- **Files d'attente sans verrou :** Des structures comme `ConcurrentLinkedQueue` peuvent aider à minimiser la contention. Cependant, une conception anti-faux partage est également essentielle pour une performance optimale. La difficulté réside souvent dans l'identification des objets potentiellement partagés dans la même ligne de cache.

Modèles d'architecture : Pipeline et Filtres
--------------------------------------------

Le modèle **Pipeline et Filtres** consiste à enchaîner plusieurs étapes de traitement. Chaque étape est découplée des autres, permettant une réorganisation ou un remplacement facile en cas de nouvelles exigences.

Types de messages et canaux
---------------------------

La complexité des messages peut être classée comme suit : Commandes &lt; Événements &lt; Documents.

- **Messages de cmomande :** Généralement suivis par des messages d'événement.
- **Messages d'événement :** Leur nom indique souvent ce qui s'est passé (verbe au passé). Ils peuvent être utilisés comme identifiants et données scalaires, déclenchant des commandes dans le système récepteur.
- **Messages document :** Adaptés pour transporter de grandes charges utiles structurées et complexes. Ils sont utilisés lorsque plus d'informations que celles contenues dans une commande ou un événement sont nécessaires.

### Canaux de messagerie

Akka propose plusieurs types de canaux pour la communication :

- **Point à point :** Indispensable au modèle Actor, remplaçant les appels de méthode. L'expéditeur doit connaître l'adresse du destinataire, et l'ordre des messages est préservé.
- **Publication-souscription :** Supporté localement et à distance.
- **Canal de type de données :** Reçoit des messages d'un type de données spécifié sans inspection du contenu.
- **Canal de type de données invalide :** Pour les messages que l'Acteur ne peut pas reconnaître ou traiter dans son état actuel.
- **Canal de messages morts (Dead Letter Channel) :** Utilisé lorsque le système Actor ne parvient pas à livrer un message (par exemple, à un Acteur arrêté).
- **Mécanismes de garantie de livraison :** Akka Framework fournit des outils pour assurer la livraison des messages.

Étiquettes: Scala akka akka-cluster reactive actor-model

Publié le 27 juillet à 20h47