- Asynchrone
- Contre-Pression
- Traitement par lots
- Composant
- Délégation
- Élasticité (au contraire de scalabilité)
- Échec (au contraire d'erreur)
- Isolation (et Confinement)
- Transparence de l'emplacement
- Piloté par message (au contraire de piloté par evenement)
- Non-Blocking
- Protocole
- Réplication
- Ressource
- Scalabilité
- Système
- Utilisateur
Le Dictionnaire Oxford définit le terme asynchrone comme “n'existe ou ne se produit pas en même temps”. Dans le contexte de ce manifeste cela signifie que le traitement d'une demande peut se produire à n'importe quel moment, parfois après sa transmission du client au service. Le client ne peut directement observer, ou se synchroniser avec, l'execution qui se produit dans le service. C'est donc l'opposé du traitement synchrone qui implique que le client ne reprend sa propre exécution qu'une fois que le service a traité la demande.
Quand un composant peine à suivre le rythme, le système dans son ensemble doit répondre de manière sensée. Il est inacceptable qu'un composant soumis à une contrainte échoue de manière catastrophique ou oublie de traiter des messages de manière incontrôlée. Puisqu'il ne peut pas faire face et qu'il ne peut pas échouer, il doit communiquer le fait qu'il est soumis à des contraintes aux composants en amont et ainsi les amener à réduire la charge. Cette contre-pression est un mécanisme important de rétroaction qui permet aux systèmes de répondre à la charge de manière adaptée, plutôt que de s'effondrer sous elle. La contre-pression peut remonter en cascade jusqu'à l'utilisateur, auquel cas les fonctionnalités peuvent se dégrader, mais ce mécanisme garantira que le système est résilient à la charge, et fournira des informations qui pourront permettre au système lui-même d'utiliser des ressources supplémentaires pour aider en répartissant la charge, voir Élasticité.
Les ordinateurs actuels sont optimisés pour l'exécution répétée de la même tâche: les caches d'instructions et la prédiction de branche augmentent le nombre d'instructions qui peuvent être traitées par seconde tout en gardant la fréquence d'horloge inchangée. Cela signifie que donner en rafale des tâches différentes au même cœur d'un processeur ne bénéficiera pas des performances optimales qui pourraient être obtenues autrement: si possible, nous devrions structurer le programme de sorte que son exécution alterne moins fréquemment entre les différentes tâches. Cela peut signifier le traitement d'un ensemble données par lots, ou cela peut signifier l'exécution de différentes étapes de traitement sur des threads matériels dédiés.
Le même raisonnement s'applique à l'utilisation de ressources externes qui nécessitent synchronisation et coordination. La bande passante d'entrée/sortie offerte par les périphériques de stockage persistants peut s'améliorer considérablement si les commandes sont émises à partir d'un seul thread (et donc d'un seul cœur du processeur) au lieu de lutter pour la bande passante de tous les cœurs. L'utilisation d'un point d'entrée unique présente l'avantage supplémentaire que les opérations peuvent être réorganisées pour mieux s'adapter aux modèles d'accès optimaux du périphérique (les périphériques de stockage actuels fonctionnent mieux pour un accès linéaire que aléatoire).
De plus, le traitement par lots offre la possibilité de partager le coût d'opérations coûteuses telles que les entrée/sortie ou les calculs coûteux. Par exemple, regrouper plusieurs données dans le même paquet réseau ou bloc de disques pour augmenter l'efficacité et réduire le taux d'utilisation.
Ce que nous décrivons est une architecture logicielle modulaire. C'est une idée très ancienne, voir par exemple Parnas (1972) [ACM]. Nous utilisons le terme «composant» en raison de sa proximité avec le compartiment, ce qui implique que chaque composant est autonome, encapsulé et isolé des autres composants. Cette notion s'applique avant tout aux caractéristiques d'exécution du système, mais elle se reflétera généralement également dans la structure du code source du module. Alors que différents composants peuvent utiliser les mêmes modules logiciels pour effectuer des tâches courantes, le code source qui définit le comportement du niveau supérieur de chaque composant est alors un module qui lui est propre. Les entrée/sorties des composants sont souvent étroitement lié à un contexte délimité dans le domaine du problème. Cela signifie que la conception du système a tendance à refléter le domaine du problème et est donc facile à faire évoluer, tout en conservant l'isolement. Les protocoles de message fournissent une couche de correspondance et de communication naturelle entre les contextes délimités (composants).
La délégation d'une tâche asynchrone à un autre composant signifie que l'exécution de la tâche aura lieu dans le contexte de cet autre composant. Ce contexte délégué implique de s'exécuter dans un contexte de gestion des erreurs différent, sur un thread différent, dans un processus différent ou sur un nœud de réseau différent, pour ne nommer que quelques possibilités. Le but de la délégation est de transférer la responsabilité de traitement d'une tâche à un autre composant afin que le composant délégant puisse effectuer un autre traitement ou éventuellement observer la progression de la tâche déléguée au cas où une action supplémentaire serait requise, telle que la gestion d'échec ou rapporter la progression.
L'élasticité signifie que pour répondre à une demande variable dans le temps, la capacité d'un système augmente ou diminue automatiquement et proportionnellement à mesure que des ressources sont ajoutées ou supprimées. Le système doit être évolutif (voir Scalabilité) pour lui permettre de bénéficier de l'ajout ou de la suppression dynamique de ressources lors de l'exécution. L'élasticité s'appuie donc sur la scalabilité et s'enrichie en ajoutant la notion de gestion automatique des ressources.
Un échec est un événement inattendu dans un service qui l'empêche de continuer à fonctionner normalement. Un échec empêchera généralement de répondre aux demandes actuelles, et éventuellement toutes les suivantes, des clients. Cela contraste avec une erreur, qui est une condition attendue et codée, par exemple une erreur découverte lors de la validation des entrées, qui sera communiquée au client dans le cadre du traitement normal du message. Les pannes sont inattendues et nécessiteront une intervention avant que le système puisse reprendre au même niveau de fonctionnement. Cela ne signifie pas que les pannes sont toujours fatales, mais plutôt qu'une certaine capacité du système sera réduite suite à une panne. Les erreurs font partie des opérations normales, sont traitées immédiatement et le système continuera de fonctionner à la même capacité après une erreur.
Des pannes sont, par exemple, un dysfonctionnement matériel, des processus se terminant en raison de l'épuisement fatal des ressources, des défauts de programme qui entraînent un état interne corrompu.
L'isolement peut être défini comme étant le découplage, à la fois dans le temps et l'espace. Le découplage dans le temps signifie que l'expéditeur et le récepteur peuvent avoir des cycles de vie indépendants — ils n'ont pas besoin d'être présents en même temps pour que la communication soit possible. Il est activé en ajoutant des entrées/sorties asynchrones entre les composants, communiquant via l'envoie de message. Le découplage dans l'espace (défini comme Transparence de l'emplacement) signifie que l'expéditeur et le destinataire n'ont pas à s'exécuter dans le même processus, mais là où le personnel d’exploitation ou l'exécutable lui-même décide qu’ils sont les plus efficaces, ce qui peut changer au cours du cycle de vie d’une application.
Le véritable isolement va au-delà de la notion d'encapsulation que l'on trouve dans la plupart des langages orientés objet et nous donne un compartimentage et un confinement:
- Des États et comportements: il permet des conceptions sans données partagées et minimise les coûts de contention et de cohérence (tels que définis dans la loi universelle sur la scalabilité;
- Des Échecs: il permet aux échecs d'être capturés, signalés et gérés à un niveau précis au lieu de les laisser remonter en cascade vers d'autres composants.
Une forte isolation entre les composants repose sur la communication via des protocoles bien définis et permet un couplage léger, conduisant à des systèmes plus faciles à comprendre, à étendre, à tester et à faire évoluer.
Les systèmes élastiques doivent être adaptatifs et réagir en permanence aux changements de la demande, ils doivent efficacement augmenter et diminuer leur échelle. Un élément clé qui simplifie énormément ce problème est de réaliser que nous faisons tous de l'informatique distribuée. Cela est vrai, que nous exécutions nos systèmes sur un seul nœud (avec plusieurs CPU indépendants communiquant sur la liaison QPI) ou sur un cluster de nœuds (avec des machines indépendantes communiquant sur le réseau). Accepter cet état de fait signifie qu'il n'y a pas de différence conceptuelle entre la mise à l'échelle verticale sur un CPU multicœur ou horizontalement sur un cluster de machines.
Si tous nos composants sont mobiles, et que la communication locale n’est qu’une optimisation, nous n’avons pas besoin de définir une topologie de système statique et un modèle de déploiement dès le départ. Nous pouvons laisser cette décision au personnel d’exploitation ou à l'éxécutable, qui peuvent adapter et optimiser le système selon la façon dont il est utilisé.
Ce découplage dans l'espace (voir la définition de l'isolation), possible grace aux envoie de messages asynchrone, et le découplage des instances d'exécutable de leurs références est ce que nous appelons la transparence de l'emplacement. La transparence de l'emplacement est souvent confondue avec «l'informatique distribuée transparente», alors que c'est en fait le contraire: nous acceptons le réseau et toutes ses contraintes - comme la défaillance partielle, les divisions du réseau, les messages perdus et sa nature asynchrone et basée sur les messages - en les mettant en avant dans le modèle de programmation, au lieu d'essayer d'émuler l'appel de méthode au travers d'un réseau (à la RPC, XA etc.). Notre point de vue sur la transparence des emplacements est en parfait accord avec [une note sur l'informatique distribuée] (http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.41.7628) de Waldo et al.
Un message est une donnée envoyé à une destination spécifique. Un événement est un signal émis par un composant lorsqu'il atteint un état donné. Dans un système basé sur les messages, les destinataires adressables attendent l'arrivée des messages et y réagissent, sinon ils restent en sommeil. Dans un système basé sur des événements, les écouteurs de notification sont liés aux sources d'événements de telle sorte qu'ils sont invoqués lorsque l'événement est émis. Cela signifie qu'un système piloté par les événements se concentre sur les sources d'événements adressables tandis qu'un système piloté par les messages se concentre sur les destinataires adressables. Un message peut contenir un événement codé comme charge utile.
La résilience est plus difficile à atteindre dans un système événementiel en raison de la nature éphémère des chaînes de consommation d'événements: lorsque le traitement est activé et que les écouteurs sont liés afin de réagir et de transformer le résultat, ces écouteurs gèrent généralement le succès ou l'échec directement et le rapport au client. D'autre part, répondre à la défaillance d'un composant, pour restaurer son bon fonctionnement, nécessite le traitement de ces défaillances, qui ne sont pas lié aux demandes éphémères des clients, mais qui permettent de garder l'intégrité global du composant.
Dans la programmation concurrante, un algorithme est considéré comme non bloquant si les fils d'éxécution (thread) en compétition pour une ressource ne peuvent voir leur exécution indéfiniment retardée par une exclusion mutuelle protégeant cette ressource. En pratique, cela se manifeste généralement sous la forme d'une API qui permet d'accéder à la [ressource] (# Resource) si elle est disponible, sinon elle fait un retour immédiat en informant l'appelant que la ressource n'est pas actuellement disponible ou qu'une opération a été lancée et n'est pas encore terminée. Une API non bloquante pour une ressource permet à l'appelant de faire d'autres travaux plutôt que d'être bloqué en attendant que la ressource soit disponible. Cela peut être complété en permettant au client de la ressource de s'inscrire pour être averti lorsque la ressource est disponible ou que l'opération est terminée.
Un protocole définit le traitement et l'étiquette pour l'échange ou la transmission de messages entre composants. Les protocoles sont formulés comme des relations entre les participants à l'échange, l'état accumulé du protocole et l'ensemble de messages autorisé à être envoyé. Cela signifie qu'un protocole décrit les messages qu'un participant peut envoyer à un autre participant à un moment donné. Les protocoles peuvent être classés selon la forme de l'échange, les plus courantes sont requête-réponse, requête-réponse répétée (comme dans HTTP), publication-abonnement et flux (à la fois push et pull).
Par rapport aux interfaces de programmation locales, un protocole est plus générique puisqu’il peut inclure plus de deux participants et qu’il prévoit une progression de l’état de l’échange de messages; une interface ne spécifie qu’une interaction à la fois entre l’appelant et le récepteur.
Il convient de noter qu’un protocole tel que défini ici spécifie simplement quels messages peuvent être envoyés, mais pas comment ils sont envoyés : encodage, décodage (i.e. codecs), et les mécanismes de transport sont des détails de mise en œuvre qui sont transparents quant à l’utilisation du protocole par les composants.
L’exécution simultanée d’un composant à différents endroits est appelée réplication. Cela peut signifier exécuter sur différents fils d'éxécution ou groupes de fils d'éxécution, processus, nœuds réseau ou centres informatiques. La réplication offre scalabilité, où la charge de travail entrante est répartie sur plusieurs instances d’un composant, ou la résilience, lorsque la charge de travail entrante est répétée à de multiples instances qui traitent les mêmes demandes en parallèle. Ces approches peuvent être mixtes, par exemple en s’assurant que toutes les transactions relatives à un certain utilisateur du composant seront exécutées par deux instances alors que le nombre total d’instances varie avec la charge entrante, (voir Élasticité).
Lorsqu’un composant à état est reproduit, il faut prendre soin de synchroniser les données d’état entre les répliques, sinon les clients de ce composant doivent être au courant du schéma de réplication et l’encapsulation est violée. Le choix du système de synchronisation en général présente un compromis entre la cohérence et la disponibilité, où la disponibilité parfaite ne peut être obtenue que si les répliques sont autorisées à diverger pendant des périodes limitées (cohérence éventuelle) et la cohérence parfaite exige que toutes les répliques avance leur état dans une section critique. Entre ces deux extrêmes se trouvent un éventail de solutions possibles, à partir de laquelle chaque composant peut choisir celle qui est la plus appropriée à ses besoins.
Tout ce sur quoi un composant s'appuie pour exécuter sa fonction est une ressource qui doit être provisionnée en fonction des besoins du composant. Cela comprend l'allocation de CPU, la mémoire principale et le stockage persistant ainsi que la bande passante réseau, la bande passante de la mémoire principale, les caches CPU, les liaisons CPU inter-socket, les minuteries et la planification des tâches, d'autres périphériques d'entrée et de sortie, des services externes comme les bases de données ou les systèmes de fichiers réseau etc. L'élasticité et la résilience de toutes ces ressources doivent être prises en compte, car le manque d'une ressource requise empêchera le composant de fonctionner en cas de besoin.
La capacité d'un système à utiliser plus de ressources informatiques afin d'augmenter ses performances est mesurée par le rapport entre le gain de débit et l'augmentation des ressources. Un système parfaitement évolutif se caractérise par le fait que les deux nombres sont proportionnels: une double allocation de ressources doublera le débit. La scalabilité est généralement limitée par l'introduction de goulots d'étranglement ou de points de synchronisation dans le système, conduisant à une scalabilité contrainte, voir Loi d'Amdahl et Modèle de scalabilité universel de Gunther.
Un système fournit des services à ses utilisateurs ou clients. Les systèmes peuvent être grands ou petits, auquel cas ils comprennent plusieurs ou seulement quelques composants. Tous les composants d'un système collaborent pour fournir ces services. Dans de nombreux cas, les composants sont dans une relation client-serveur au sein du même système (par exemple les composants frontaux reposant sur des composants back-end). Un système partage un modèle de résilience commun, ce qui veut dire que l'échec d'un composant est géré au sein du système et délégué d'un composant à un autre. Il est utile de visualiser les groupes de composants d'un système en tant que sous-systèmes s'ils sont isolés du reste du système dans leur fonction, ressources ou modes de défaillance.
Nous utilisons ce terme de manière informelle pour désigner tout consommateur d'un service, qu'il soit humain ou autre.