Grains worker sans état

Par défaut, le runtime Orleans ne crée pas plus d’une activation d’un même grain au sein du cluster. Il s’agit de l’expression la plus intuitive du modèle Virtual Actor où chaque grain correspond à une entité à type/identité unique. Cependant, il existe aussi des cas où une application doit effectuer des opérations sans état fonctionnelles qui ne sont pas liées à une entité particulière du système. Par exemple, si le client envoie des demandes avec des charges utiles compressées qui doivent être décompressées avant de pouvoir être routées vers le grain cible pour traitement, cette logique de décompression/routage n’est pas liée à une entité spécifique dans l’application et peut facilement effectuer un scale-out.

Quand l’attribut StatelessWorkerAttribute est appliqué à une classe de grain, il indique au runtime Orleans que les grains de cette classe doivent être traités comme des grains worker sans état. Les grains worker sans état ont les propriétés suivantes qui rendent leur exécution très différente de celle des classes de grain normales.

  1. Le runtime Orleans crée plusieurs activations d’un même grain worker sans état sur différents silos du cluster.
  2. Les demandes adressées aux grains worker sans état sont exécutées localement tant que le silo est compatible, c’est pourquoi elles n’engendrent aucun coût de réseau ou de sérialisation. Si le silo local n’est pas compatible, les demandes sont transférées à un silo compatible.
  3. Le runtime Orleans crée automatiquement des activations supplémentaires d’un grain worker sans état si ceux qui existent déjà sont occupés. Le nombre maximal d’activations que le runtime peut créer par silo pour un grain worker sans état est limité par défaut par le nombre de cœurs de processeur dont est équipée la machine, à moins que ce nombre soit spécifié explicitement à l’aide de l’argument facultatif maxLocalWorkers.
  4. Du fait des points 2 et 3, les activations de grain worker sans état ne peuvent pas être gérées individuellement. Deux demandes ultérieures adressées à un grain worker sans état peuvent être traitées par différentes activations de celui-ci.

Les grains worker sans état offrent un moyen simple de créer un pool d’activations de grain géré automatiquement qui se charge d’effectuer un scale-up et un scale-down en fonction de la charge réelle. Le runtime recherche toujours les activations de grain worker sans état disponibles dans le même ordre. De ce fait, il dispatche toujours les demandes à la première activation locale inactive qu’il trouve et s’en remet à la dernière activation si toutes les précédentes sont occupées. Si toutes les activations sont occupées et que la limite d’activation n’a pas été atteinte, elle crée une activation supplémentaire à la fin de la liste et lui dispatche la demande. Cela signifie que lorsque le rythme des demandes adressées à un grain worker sans état s’accélère et que les activations existantes sont toutes occupées, le runtime étend le pool de ses activations jusqu’à la limite. À l’inverse, quand la charge retombe et qu’un petit nombre d’activations du grain worker sans état suffit à la gérer, les activations en fin de liste ne reçoivent pas de demandes. Elles deviennent inactives et sont finalement désactivés par le processus de collecte d’activations standard. Ainsi, le pool d’activations finit par se réduire pour s’adapter à la charge.

L’exemple suivant définit une classe de grain worker sans état MyStatelessWorkerGrain avec la limite du nombre maximal d’activations par défaut.

[StatelessWorker]
public class MyStatelessWorkerGrain : Grain, IMyStatelessWorkerGrain
{
    // ...
}

L’appel à un grain worker sans état s’effectue de la même façon que pour n’importe quel autre grain. La seule différence est que dans la plupart des cas, un seul ID de grain est utilisé, par exemple 0 ou Guid.Empty. Il est possible d’utiliser plusieurs ID de grain quand il existe plusieurs pools de grains worker (un par ID est souhaitable).

var worker = GrainFactory.GetGrain<IMyStatelessWorkerGrain>(0);
await worker.Process(args);

Cet exemple définit une classe de grain worker sans état limitée à une activation à un grain par silo.

[StatelessWorker(1)] // max 1 activation per silo
public class MyLonelyWorkerGrain : ILonelyWorkerGrain
{
    //...
}

Notez que StatelessWorkerAttribute ne modifie pas la réentrance de la classe de grain cible. Comme les autres grains, les grains worker sans état ne sont pas réentrants par défaut. Ils peuvent le devenir explicitement en ajoutant un ReentrantAttribute à la classe de grain.

État

La partie « sans état » de « worker sans état » ne signifie pas qu’un worker sans état ne peut pas avoir d’état et qu’il se limite à l’exécution d’opérations fonctionnelles. Comme n’importe quel autre grain, un grain worker sans état peut charger et garder en mémoire l’état dont il a besoin. Étant donné qu’il est possible de créer plusieurs activations d’un même grain worker sans état sur un ou plusieurs silos du cluster, il n’existe aucun mécanisme facile permettant de coordonner l’état des différentes activations.

Plusieurs modèles utiles impliquent l’état du worker sans état.

Éléments de cache chaud avec scale-out

Pour les éléments de cache chaud qui présentent un débit élevé, la conservation de chaque élément de ce type dans un grain worker sans état produit les effets suivants :

  1. Ces éléments font automatiquement l’objet d’un scale-out au sein d’un silo et dans tous les silos du cluster, et ;
  2. Les données sont toujours disponibles localement sur le silo qui a reçu la demande du client via sa passerelle, si bien que les demandes peuvent être traitées sans saut de réseau supplémentaire vers un autre silo.

Réduire l’agrégation de style

Dans certains scénarios, les applications doivent calculer certaines métriques sur tous les grains d’un type particulier du cluster et établir des rapports réguliers sur les agrégats. Il peut s’agir par exemple d’établir des rapports sur plusieurs joueurs par carte de jeu, sur la durée moyenne d’un appel VoIP, etc. Si, parmi les milliers voire les millions de grains, chacun devait rendre compte de ses métriques à un seul agrégateur global, l’agrégateur serait immédiatement surchargé et dans l’impossibilité de faire face au déferlement de rapports. L’approche alternative consiste à transformer cette tâche en deux étapes (ou plus) pour réduire l’agrégation de style. La première couche d’agrégation consiste à faire état de l’envoi des métriques d’un grain à un grain de pré-agrégation worker sans état. Le runtime Orleans crée automatiquement plusieurs activations du grain worker sans état pour chaque silo. Sachant que tous ces appels seront traités localement sans appels distants ni sérialisation des messages, ce type d’agrégation sera nettement moins coûteux que dans le cas d’un traitement à distance. Désormais, chacune des activations de grain worker sans état de pré-agrégation, indépendamment ou en coordination avec d’autres activations locales, peut envoyer ses rapports agrégés à l’agrégateur final global (ou à une autre couche de réduction si nécessaire) sans la surcharger.