Comprendre les événements et les architectures axées sur les événements - AWS Lambda

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Comprendre les événements et les architectures axées sur les événements

Certains AWS services peuvent appeler directement vos fonctions Lambda. Ces services transmettent des événements à votre fonction Lambda. Ces événements qui déclenchent une fonction Lambda peuvent être presque n'importe quoi, qu'il s'agisse d'une requête HTTP via API Gateway, d'un calendrier géré par une EventBridge règle, d'un AWS IoT événement ou d'un événement Amazon S3. Lorsqu'ils sont transmis à votre fonction, les événements sont des données structurées au format JSON. La structure JSON varie en fonction du service qui la génère et du type d'événement.

Lorsqu'une fonction est déclenchée par un événement, cela s'appelle une invocation. Alors que les appels de fonctions Lambda peuvent durer jusqu'à 15 minutes, Lambda est particulièrement adapté aux appels courts d'une seconde ou moins. Cela est particulièrement vrai pour les architectures pilotées par les événements. Dans une architecture axée sur les événements, chaque fonction Lambda est traitée comme un microservice, chargé d'exécuter un ensemble restreint d'instructions spécifiques.

Avantages des architectures axées sur les événements

Remplacement des interrogations et des webhooks par des événements

De nombreuses architectures traditionnelles utilisent des mécanismes de sondage et de webhook pour communiquer l'état entre les différents composants. L’interrogation peut s’avérer très inefficace pour récupérer les mises à jour, car il existe un décalage entre la mise à disposition des nouvelles données et la synchronisation avec les services en aval. Les webhooks ne sont pas toujours pris en charge par les autres microservices auxquels vous souhaitez vous intégrer. Ils peuvent également nécessiter des configurations d’autorisation et d’authentification personnalisées. Dans les deux cas, il est difficile de mettre à l’échelle ces méthodes d’intégration à la demande sans travail supplémentaire de la part des équipes de développement.

architectures guidées par les événements (illustration 7)

Ces deux mécanismes peuvent être remplacés par des événements, qui peuvent être filtrés, acheminés et redirigés en aval vers les microservices consommateurs. Cette approche peut entraîner une réduction de la consommation de bande passante, de l’utilisation du processeur et potentiellement une baisse des coûts. Ces architectures peuvent également réduire la complexité, étant donné que chaque unité fonctionnelle est plus petite et qu’il y a souvent moins de code.

architectures guidées par les événements (illustration 8)

Les architectures axées sur les événements peuvent également faciliter la conception de near-real-time systèmes, aidant ainsi les entreprises à s'éloigner du traitement par lots. Les événements sont générés au moment où l’état de l’application change. Le code personnalisé d’un microservice doit donc être conçu pour gérer le traitement d’un seul événement. La mise à l’échelle étant gérée par le service Lambda, cette architecture peut gérer des augmentations significatives du trafic sans modifier le code personnalisé. Au fur et à mesure que les événements augmentent verticalement, la couche de calcul qui les traite augmente également.

Réduction de la complexité

Les microservices permettent aux développeurs et aux architectes de décomposer des flux de travail complexes. Par exemple, un monolithe de commerce électronique peut être divisé en processus d’acceptation des commandes et de paiement avec des services d’inventaire, de livraison et de comptabilité distincts. Ce qui peut s'avérer complexe à gérer et à orchestrer dans un monolithe devient une série de services découplés qui communiquent de manière asynchrone avec les événements.

architectures guidées par les événements (illustration 9)

Cette approche permet également d’assembler des services qui traitent les données à des rythmes différents. Dans ce cas, un microservice d’acceptation de commandes peut stocker de gros volumes de commandes entrantes en mettant les messages en mémoire tampon dans une file d’attente SQS.

Un service de traitement des paiements, qui est généralement plus lent en raison de la complexité du traitement des paiements, peut recevoir un flux constant de messages provenant de la file d’attente SQS. Il peut orchestrer une logique complexe de gestion des nouvelles tentatives et des erreurs en utilisant AWS Step Functions et en coordonnant les flux de paiement actifs pour des centaines de milliers de commandes.

Amélioration de la capacité de mise à l’échelle et de l’extensibilité

Les microservices génèrent des événements qui sont généralement publiés sur des services de messagerie tels qu’Amazon SNS et Amazon SQS. Ils se comportent comme une mémoire tampon élastique entre les microservices et aident à gérer la mise à l’échelle lorsque le trafic augmente. Des services tels qu'Amazon EventBridge peuvent ensuite filtrer et acheminer les messages en fonction du contenu de l'événement, tel que défini dans les règles. Par conséquent, les applications basées sur les événements peuvent être plus évolutives et offrir une meilleure redondance que les applications monolithiques.

Ce système est également très extensible, ce qui permet aux autres équipes d’étendre les fonctionnalités et d’en ajouter sans affecter les microservices de traitement des commandes et des paiements. En publiant des événements à l'aide de cette application EventBridge, elle s'intègre aux systèmes existants, tels que le microservice d'inventaire, mais permet également à toute future application de s'intégrer en tant que consommateur d'événements. Les producteurs d’événements ne connaissent pas les consommateurs d’événements, ce qui peut contribuer à simplifier la logique des microservices.

Compromis des architectures guidées par les événements

Latence variable

Contrairement aux applications monolithiques, qui peuvent tout traiter dans le même espace mémoire sur un seul appareil, les applications guidées par les événements communiquent entre les réseaux. Cette conception introduit une latence variable. Bien qu’il soit possible de concevoir des applications pour réduire la latence, les applications monolithiques peuvent presque toujours être optimisées pour réduire la latence au détriment de la capacité de mise à l’échelle et de la disponibilité.

Les charges de travail qui nécessitent des performances constantes à faible latence, telles que les applications de trading à haute fréquence dans les banques ou l'automatisation robotique en moins d'une milliseconde dans les entrepôts, ne sont pas de bons candidats pour une architecture axée sur les événements.

Cohérence à terme

Un événement représente un changement d’état, et comme de nombreux événements transitent par différents services d’une architecture à un moment donné, ces charges de travail sont souvent cohérentes à terme. Cela complique le traitement des transactions, la gestion des doublons ou la détermination de l’état global exact d’un système.

Certaines charges de travail contiennent une combinaison d'exigences qui sont finalement cohérentes (par exemple, le nombre total de commandes dans l'heure en cours) ou fortement cohérentes (par exemple, l'inventaire actuel). Pour les charges de travail nécessitant une forte cohérence des données, il existe des modèles d'architecture qui le soutiennent. Par exemple :

  • DynamoDB peut fournir des lectures très cohérentes, parfois avec une latence plus élevée, consommant un débit supérieur à celui du mode par défaut. DynamoDB peut également prendre en charge les transactions afin de garantir la cohérence des données.

  • Vous pouvez utiliser Amazon RDS pour les fonctionnalités nécessitant des propriétés ACID, bien que les bases de données relationnelles soient généralement moins évolutives que les bases de données NoSQL telles que DynamoDB. Le Proxy Amazon RDS peut aider à gérer le regroupement et la mise à l’échelle des connexions à partir de clients éphémères tels que les fonctions Lambda.

Les architectures basées sur les événements sont généralement conçues en fonction d’événements individuels plutôt que de grands lots de données. En général, les flux de travail sont conçus pour gérer les étapes d’un événement ou d’un flux d’exécution individuel au lieu de traiter plusieurs événements simultanément. En mode sans serveur, le traitement des événements en temps réel est préférable au traitement par lots : les lots doivent être remplacés par de nombreuses mises à jour incrémentielles de moindre envergure. Cela peut rendre les charges de travail plus disponibles et évolutives, mais il est également plus difficile pour les événements de prendre conscience des autres événements.

Renvoi des valeurs aux appelants

Dans de nombreux cas, les applications basées sur des événements sont asynchrones. Cela signifie que les services des appelants n’attendent pas les requêtes des autres services pour poursuivre d’autres tâches. Il s’agit d’une caractéristique fondamentale des architectures guidées par les événements qui permet la capacité de mise à l’échelle et la flexibilité. Cela signifie que la transmission des valeurs renvoyées ou du résultat d'un flux de travail est plus complexe que dans les flux d'exécution synchrones.

La plupart des appels Lambda dans les systèmes de production sont asynchrones et répondent à des événements provenant de services tels qu'Amazon S3 ou Amazon SQS. Dans ces cas, le succès ou l’échec du traitement d’un événement est souvent plus important que le renvoi d’une valeur. Des fonctionnalités telles que les files d'attente de lettres mortes (DLQs) dans Lambda sont fournies pour vous permettre d'identifier et de réessayer les événements ayant échoué, sans avoir à en informer l'appelant.

Débogage entre les services et les fonctions

Le débogage de systèmes pilotés par des événements est également différent de celui d'une application monolithique. Étant donné que différents systèmes et services transmettent des événements, il n'est pas possible d'enregistrer et de reproduire l'état exact de plusieurs services en cas d'erreur. Étant donné que chaque invocation de service et de fonction comporte des fichiers journaux distincts, il peut être plus compliqué de déterminer ce qu’il est advenu d’un événement spécifique à l’origine d’une erreur.

Trois conditions importantes sont requises pour élaborer une approche de débogage réussie dans les systèmes guidés par les événements. Tout d'abord, un système de journalisation robuste est essentiel, fourni par l'ensemble AWS des services et intégré aux fonctions Lambda par Amazon. CloudWatch Deuxièmement, dans ces systèmes, il est important de s’assurer que chaque événement possède un identifiant de transaction journalisé à chaque étape de la transaction, afin de faciliter la recherche de journaux.

Enfin, il est fortement recommandé d'automatiser l'analyse et l'analyse des journaux en utilisant un service de débogage et de surveillance tel que. AWS X-Ray Cela peut consommer les journaux de plusieurs invocations et services Lambda, ce qui permet d’identifier plus facilement la cause racine des problèmes. Consultez la procédure de résolution des problèmes pour une présentation détaillée de l'utilisation de X-Ray à des fins de résolution des problèmes.

Anti-patterns dans les applications basées sur les événements basées sur Lambda

Lorsque vous créez des architectures pilotées par des événements avec Lambda, faites attention aux anti-modèles techniquement fonctionnels, mais qui peuvent ne pas être optimaux du point de vue de l'architecture et des coûts. Cette section fournit des conseils généraux sur ces anti-modèles, mais n'est pas prescriptive.

Le monolithe Lambda

Dans de nombreuses applications migrées depuis des serveurs traditionnels, telles que les EC2 instances Amazon ou les applications Elastic Beanstalk, les développeurs « modifient et modifient » le code existant. Cela se traduit souvent par une seule fonction Lambda qui contient toute la logique d’application déclenchée pour tous les événements. Pour une application Web de base, une fonction Lambda monolithique gérerait toutes les routes API Gateway et s’intégrerait à toutes les ressources en aval nécessaires.

architectures guidées par les événements (illustration 13)

Cette approche présente plusieurs inconvénients :

  • Taille du package — La fonction Lambda peut être beaucoup plus importante car elle contient tout le code possible pour tous les chemins, ce qui ralentit l'exécution du service Lambda.

  • Difficile d'appliquer le moindre privilège — Le rôle d'exécution de la fonction doit autoriser l'accès à toutes les ressources nécessaires pour tous les chemins, ce qui rend les autorisations très larges. Il s'agit d'un problème de sécurité. De nombreux chemins du monolithe fonctionnel n’ont pas besoin de toutes les autorisations accordées.

  • Plus difficile à mettre à niveau — Dans un système de production, toute mise à niveau d'une fonction unique est plus risquée et peut endommager l'ensemble de l'application. La mise à niveau d’un seul chemin dans la fonction Lambda est une mise à niveau de l’ensemble de la fonction.

  • Plus difficile à maintenir — Il est plus difficile d'avoir plusieurs développeurs travaillant sur le service puisqu'il s'agit d'un référentiel de code monolithique. Cela alourdit également la charge cognitive des développeurs et complique la création d’une couverture de test appropriée pour le code.

  • Plus difficile de réutiliser le code — Il est plus difficile de séparer les bibliothèques réutilisables des monolithes, ce qui complique la réutilisation du code. Au fur et à mesure que vous développez et soutenez de plus en plus de projets, il peut être plus difficile de soutenir le code et de mettre à l’échelle la vitesse de votre équipe.

  • Plus difficile à tester — À mesure que le nombre de lignes de code augmente, il devient plus difficile de tester toutes les combinaisons possibles d'entrées et de points d'entrée dans la base de code. Il est généralement plus facile de mettre en œuvre des tests unitaires pour les petits services avec moins de code.

L’alternative préférable consiste à décomposer la fonction Lambda monolithique en microservices individuels, en mappant une seule fonction Lambda à une seule tâche bien définie. Dans cette application Web simple dotée de quelques points de terminaison d’API, l’architecture basée sur les microservices qui en résulte peut être basée sur les routes API Gateway.

architectures guidées par les événements (illustration 14)

Modèles récursifs qui provoquent des fonctions Lambda incontrôlables

AWS les services génèrent des événements qui invoquent des fonctions Lambda, et les fonctions Lambda peuvent envoyer des messages aux services. AWS En général, le service ou la ressource qui invoque une fonction Lambda doit être différent du service ou de la ressource vers lequel la fonction émet des sorties. Si vous ne parvenez pas à gérer cela, vous risquez de créer des boucles infinies.

Par exemple, une fonction Lambda écrit un objet dans un objet Amazon S3, qui à son tour invoque la même fonction Lambda via un événement put. L’invocation entraîne l’écriture d’un deuxième objet dans le compartiment, qui invoque la même fonction Lambda :

architectures guidées par les événements (illustration 15)

Bien que le potentiel de boucles infinies existe dans la plupart des langages de programmation, cet anti-modèle est susceptible de consommer davantage de ressources dans les applications sans serveur. Lambda et Amazon S3 évoluent automatiquement en fonction du trafic. La boucle peut donc entraîner le dimensionnement de Lambda afin de consommer toute la simultanéité disponible et Amazon S3 continuera à écrire des objets et à générer davantage d'événements pour Lambda.

Cet exemple utilise S3, mais le risque de boucles récursives existe également dans Amazon SNS, Amazon SQS, DynamoDB et d'autres services. Vous pouvez utiliser la détection de boucle récursive pour détecter et éviter cet anti-modèle.

Fonctions Lambda appelant des fonctions Lambda

Les fonctions permettent l’encapsulation et la réutilisation du code. La plupart des langages de programmation prennent en charge le concept d’appel synchrone de fonctions au sein d’une base de code. Dans ce cas, l’appelant attend la réponse de la fonction.

Lorsque cela se produit sur un serveur traditionnel ou une instance virtuelle, le planificateur du système d’exploitation passe à d’autres tâches disponibles. Le fait que le processeur fonctionne à 0 % ou à 100 % n’a aucune incidence sur le coût global de l’application, puisque vous payez le coût fixe lié à la possession et à l’exploitation d’un serveur.

Ce modèle ne s’adapte souvent pas bien au développement sans serveur. Prenons l’exemple d’une simple application de commerce électronique composée de trois fonctions Lambda qui traitent une commande :

architectures guidées par les événements (illustration 16)

Dans ce cas, la fonction Créer une commande appelle la fonction Traiter le paiement, qui à son tour appelle la fonction Créer une facture. Bien que ce flux synchrone puisse fonctionner au sein d’une seule application sur un serveur, il introduit plusieurs problèmes évitables dans une architecture sans serveur distribuée :

  • Coût — Avec Lambda, vous payez pour la durée d'une invocation. Dans cet exemple, pendant que les fonctions de création de facture s'exécutent, deux autres fonctions sont également exécutées en attente, comme indiqué en rouge sur le diagramme.

  • Gestion des erreurs — Dans les appels imbriqués, la gestion des erreurs peut devenir beaucoup plus complexe. Par exemple, une erreur dans Créer une facture peut nécessiter que la fonction de traitement du paiement annule le débit, ou qu'elle réessaie le processus de création de facture.

  • Couplage étroit — Le traitement d'un paiement prend généralement plus de temps que la création d'une facture. Dans ce modèle, la disponibilité de l’ensemble du flux de travail est limitée par la fonction la plus lente.

  • Mise à l'échelle — La simultanéité des trois fonctions doit être égale. Dans un système occupé, cela utilise plus de simultanéité que ce qui serait nécessaire autrement.

Dans les applications sans serveur, il existe deux approches courantes pour éviter ce schéma. Tout d'abord, utilisez une file d'attente Amazon SQS entre les fonctions Lambda. Si un processus en aval est plus lent qu’un processus en amont, la file d’attente conserve les messages de manière durable et dissocie les deux fonctions. Dans cet exemple, la fonction Create order publierait un message dans une file d'attente SQS, tandis que la fonction Process payment consomme les messages de la file d'attente.

La deuxième approche consiste à utiliser AWS Step Functions. Pour les processus complexes présentant plusieurs types d’échec et une logique de nouvelle tentative, Step Functions peut aider à réduire la quantité de code personnalisé nécessaire pour orchestrer le flux de travail. Par conséquent, Step Functions orchestre les tâches et gère efficacement les erreurs et les nouvelles tentatives, et les fonctions Lambda ne contiennent que de la logique métier.

Attente synchrone au sein d’une seule fonction Lambda

Au sein d’une unique fonction Lambda, assurez-vous que les activités potentiellement simultanées ne sont pas planifiées de manière synchrone. Par exemple, une fonction Lambda peut écrire dans un compartiment S3, puis dans une table DynamoDB :

architectures guidées par les événements (illustration 17)

Dans cette conception, les temps d'attente sont aggravés car les activités sont séquentielles. Dans les cas où la deuxième tâche dépend de l'achèvement de la première, vous pouvez réduire le temps d'attente total et le coût d'exécution en utilisant deux fonctions Lambda distinctes :

architectures guidées par les événements (illustration 19)

Dans cette conception, la première fonction Lambda répond immédiatement après avoir placé l'objet dans le compartiment Amazon S3. Le service S3 invoque la deuxième fonction Lambda, qui écrit ensuite des données dans la table DynamoDB. Cette approche réduit le temps d’attente total lors des exécutions de fonctions Lambda.