Meilleures pratiques pour la réutilisation de conteneurs AWS Lambda

Optimisation des démarrages à chaud lors de la connexion d'AWS Lambda à d'autres services

AWS Lambda offre une grande évolutivité en raison de son statut sans serveur et sans état, permettant à de nombreuses copies de la fonction lambda d'être créées instantanément (comme décrit ici). Cependant, lors de l'écriture du code de l'application, vous souhaiterez probablement avoir accès à certaines données avec état. Cela signifie se connecter à un magasin de données tel qu'une instance RDS ou S3. Cependant, la connexion à d'autres services à partir d'AWS Lambda ajoute du temps à votre code de fonction. Une évolutivité élevée peut également avoir des effets secondaires, tels que l’atteinte du nombre maximal de connexions autorisées à une instance RDS. Une option pour y remédier consiste à utiliser la réutilisation de conteneur dans AWS Lambda pour conserver la connexion et réduire le temps d'exécution de lambda.

Il existe quelques diagrammes utiles pour expliquer le cycle de vie d’une demande lambda.

Les événements suivants se produisent pendant un démarrage à froid, lorsque votre fonction est appelée pour la première fois ou après une période d'inactivité:

  • Le code et les dépendances sont téléchargés.
  • Un nouveau conteneur est démarré.
  • Le runtime est démarré.

La dernière action consiste à démarrer votre code, ce qui se produit chaque fois que la fonction lambda est appelée. Si le conteneur est réutilisé pour un appel ultérieur de la fonction lambda, nous pouvons sauter au début du code. C'est ce qu'on appelle un démarrage à chaud. C'est l'étape que nous pouvons optimiser lors de la connexion à d'autres services en définissant la connexion en dehors du champ d'application de la méthode du gestionnaire.

Connexion à d'autres services AWS à partir de Lambda

Exemple: Connexion à une instance RDS, icônes AWS provenant d'ici

Nous avons un exemple de base commun à suivre: nous souhaitons nous connecter à une ressource conteneur pour extraire des données d'enrichissement. Dans cet exemple, une charge JSON arrive avec un ID et la fonction Lambda se connecte à une instance RDS exécutant PostgreSQL pour rechercher le nom correspondant de l'ID afin que nous puissions renvoyer la charge enrichie. Étant donné que la fonction lambda se connecte à RDS, qui réside dans un VPC, la fonction lambda doit désormais également vivre dans un sous-réseau privé. Cela ajoute quelques étapes au démarrage à froid - une interface réseau élastique VPC (ENI) doit être connectée (comme mentionné dans le blog de Jeremy Daly, cela ajoute du temps à vos démarrages à froid).

Remarque: nous pourrions éviter d'utiliser un VPC si nous utilisions un stockage clé / valeur avec DynamoDB au lieu de RDS.

J'examinerai deux solutions à cette tâche, la première est ma solution «naïve», tandis que la seconde optimise les temps de démarrage à chaud en réutilisant la connexion pour les invocations suivantes. Ensuite, nous comparerons les performances de chaque solution.

Option 1 - Connexion à RDS dans le gestionnaire

Cet exemple de code montre comment aborder cette tâche avec naïveté - la connexion à la base de données se trouve dans la méthode du gestionnaire. Il existe une requête de sélection simple pour extraire le nom de l'ID avant de renvoyer le contenu, qui inclut désormais le nom.

Voyons comment cette option fonctionne lors d’un petit test avec une rafale de 2000 invocations avec une simultanéité de 20. La durée minimale est de 18 ms avec une moyenne de 51 ms et un peu plus d’une seconde (durée de démarrage à froid).

Durée Lambda

Le graphique ci-dessous montre qu'il existe un nombre maximal de huit connexions à la base de données.

Nombre de connexions à la base de données RDS dans une fenêtre de 5 minutes.

Option 2 - Utiliser une connexion globale

La deuxième option consiste à définir la connexion comme globale en dehors de la méthode du gestionnaire. Ensuite, dans le gestionnaire, nous ajoutons une vérification pour voir si la connexion existe, et ne vous connectez que si ce n’est pas le cas. Cela signifie que la connexion n’est établie qu’une fois par conteneur. Définir la connexion de cette manière avec le conditionnel en place signifie qu'il n'est pas nécessaire d'établir une connexion si cela n'est pas requis par la logique de code.

Nous ne fermons plus la connexion à la base de données. La connexion reste donc pour un appel ultérieur de la fonction. La réutilisation de la connexion réduit considérablement les durées de démarrage à chaud - la durée moyenne est environ 3 fois plus rapide et le minimum est de 1 ms au lieu de 18 ms.

Durées Lambda

La connexion à une instance RDS est une tâche fastidieuse, et le fait de ne pas avoir à se connecter à chaque appel est bénéfique pour les performances. Lorsque vous vous connectez à la base de données pour une requête de base de données simple, nous obtenons un nombre maximal de connexions de base de données de 20, ce qui correspond au niveau de simultanéité (nous avons effectué 20 invocations simultanées x 100 fois). Lorsque la rafale d'invocations s'arrête, les connexions se ferment progressivement.

Maintenant qu'AWS a augmenté la limite de durée lambda à 15 minutes, cela signifie que les connexions à la base de données peuvent durer plus longtemps et que vous risquez d'atteindre le nombre maximal de connexions RDS. Le nombre maximal de connexions par défaut peut être remplacé dans les paramètres du groupe de paramètres RDS, bien que l’augmentation du nombre maximal de connexions puisse entraîner des problèmes d’allocation de mémoire. Les instances plus petites peuvent avoir une valeur par défaut max_connections inférieure à 100. Tenez compte de ces limites et ajoutez uniquement une logique d'application pour vous connecter à la base de données en cas de besoin.

Utilisation d'une connexion globale pour d'autres tâches

Lambda Connexion à S3

Une tâche courante que nous pourrions avoir besoin d’effectuer avec Lambda consiste à accéder aux données avec état à partir de S3. L'extrait de code ci-dessous est un plan directeur de fonction Python Lambda fourni par AWS - vous pouvez y accéder en vous connectant à la console AWS et en cliquant ici. Vous pouvez voir dans le code que le client S3 est entièrement défini en dehors du gestionnaire lors de l'initialisation du conteneur, alors que pour l'exemple RDS, la connexion globale était définie dans le gestionnaire. Les deux approches vont définir les variables globales leur permettant d'être disponibles pour les invocations suivantes.

Extrait de code de plan directeur lambda s3-get-object https://console.aws.amazon.com/lambda/home?region=us-east-1#/create/new?bp=s3-get-object-python

Décryptage des variables d'environnement

La console lambda vous offre la possibilité de chiffrer vos variables d’environnement pour une sécurité accrue. L'extrait de code suivant est un exemple Java fourni par AWS d'un script d'assistance pour décrypter les variables d'environnement à partir d'une fonction Lambda. Vous pouvez accéder à l'extrait de code en suivant ce didacticiel (plus précisément à l'étape 6). DECRYPTED_KEY étant défini comme une classe globale, la fonction et la logique decryptKey () ne sont appelées qu'une seule fois par conteneur lambda. Par conséquent, nous verrons une amélioration significative des durées de démarrage à chaud.

https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions et https://docs.aws.amazon.com/lambda/latest/dg/tutorial-env_console.html

Utilisation de variables globales dans d'autres solutions FaaS

Cette approche n’est pas isolée pour AWS Lambda. La méthode d’utilisation d’une connexion globale peut également être appliquée aux fonctions sans serveur d’autres fournisseurs de cloud. La page trucs et astuces de Google Cloud Functions fournit une bonne explication pour les variables non paresseuses (lorsque la variable est toujours initialisée en dehors de la méthode du gestionnaire) par rapport aux variables paresseuses (la variable globale est uniquement définie en cas de besoin).

Autres meilleures pratiques

Voici d'autres bonnes pratiques à garder à l'esprit.

Essai

L'utilisation de FaaS facilite la création d'une architecture de microservices. Et disposer de petites fonctionnalités discrètes va de pair avec des tests unitaires efficaces. Pour aider vos tests unitaires:

  • N'oubliez pas d'exclure les dépendances de test du paquet lambda.
  • Séparez la logique de la méthode du gestionnaire, comme vous le feriez avec la méthode principale d'un programme.

Dépendances et taille du paquet

Réduire la taille du package de déploiement signifie que le téléchargement du code sera plus rapide à l'initialisation et donc améliorera vos temps de démarrage à froid. Supprimez les bibliothèques inutilisées et le code mort pour réduire la taille du fichier ZIP de déploiement. AWS SDK est fourni pour les environnements d'exécution Python et JavaScript, il n'est donc pas nécessaire de les inclure dans votre package de déploiement.

Si Node.js est votre environnement d'exécution Lambda préféré, vous pouvez appliquer une minification et une modification afin de réduire la taille du code de votre fonction et de celle de votre package de déploiement. Certains aspects, mais pas tous, de la minification et de l'ulgification peuvent être appliqués à d'autres exécutions, par exemple. vous ne pouvez pas supprimer les espaces du code python, mais vous pouvez supprimer les commentaires et raccourcir les noms de variables.

Réglage de la mémoire

Expérimentez pour trouver la quantité optimale de mémoire pour la fonction Lambda. Vous payez pour l'allocation de mémoire, donc doubler la mémoire signifie que vous devez payer le double par milliseconde; mais la capacité de calcul augmente avec la mémoire allouée, ce qui pourrait potentiellement réduire le temps d'exécution à moins de la moitié de ce qu'il était. Il existe déjà des outils utiles pour sélectionner le paramètre de mémoire optimal pour vous, comme celui-ci.

De conclure…

Une chose à considérer est de savoir si l’application de la méthode de réutilisation de connexion est nécessaire. Si votre fonction lambda n'est appelée que rarement, par exemple une fois par jour, vous ne tirerez aucun avantage de l'optimisation pour les démarrages à chaud. Il y a souvent un compromis à faire entre l'optimisation des performances et la lisibilité de votre code - le terme «oubli» parle pour lui-même! En outre, l'ajout de variables globales à votre code pour réutiliser des connexions à d'autres services peut rendre votre code plus difficile à tracer. Deux questions me viennent à l’esprit:

  • Un nouveau membre de l'équipe comprendra-t-il votre code?
  • Votre équipe et vous-même pourrez-vous déboguer le code à l'avenir?

Mais vous avez probablement choisi Lambda pour son ampleur et souhaitez des performances élevées et des coûts bas. Trouvez donc l'équilibre qui correspond aux besoins de votre équipe.

Ces opinions sont celles de l'auteur. Sauf indication contraire dans cet article, Capital One n’est affilié à aucune des entreprises mentionnées, ni approuvé par celle-ci. Toutes les marques commerciales et autres propriétés intellectuelles utilisées ou affichées appartiennent à leurs propriétaires respectifs. Cet article est © 2019 Capital One.