Cet article est issu du blog de Kévin Llopis, Technical Officer chez CARBON. Vous pouvez retrouver tous ses articles sur son blog personnel.
Un premier article “AWS Lambda : Optimiser les lambdas en Java — Partie 1” avait été publié précédemment pour présenter les solutions possibles, en vue d’optimiser les lambdas implémentées en Java. La solution recommandée était basée sur le couple GraalVM / Quarkus qui n’ajoute aucun coût supplémentaire au compte AWS et permet de résoudre l’initialisation lente de la lambda. Ceci était possible en se reposant sur un exécutable natif.
Cela dit, une autre solution consiste à se baser sur SnapStart. Mais avant de la mettre en place, il faut être conscient de ses limitations et dans quels cas il est recommandé de l’utiliser. C’est justement l’objet de ce nouvel article.
Dans un premier temps, prenons de la hauteur sur le fonctionnement de SnapStart. Nous présenterons ensuite un cas pratique en détail.
Quel est le principe de fonctionnement de SnapStart ?
SnapStart a un objectif précis qui consiste à optimiser l’initialisation des lambdas implémentées spécifiquement en Java. En effet, il s’agit de ne plus avoir à exécuter la phase de “Cold Start” comme présenté dans le schéma ci-dessous :
Pour cela, le processus suivant est réalisé :
- La création de l’environnement d’exécution ainsi que l’initialisation du code de la lambda sont réalisées à la publication de la lambda. Puis, une sauvegarde de l’environnement d’exécution (au niveau de la mémoire et du disque) est réalisée. Elle porte le nom de “snapshot”.
- Au premier appel de la lambda, la “snapshot” est restaurée en vue d’exécuter le code de la lambda. C’est ce qui nous permet d’éviter la phase de “Cold Start”.
En ce qui concerne les étapes “Pre Snapshot Hook” et “Post Snapshot Hook”, elles permettent respectivement d’exécuter du code avant l’enregistrement de la snapshot et après restauration.
De quelle manière SnapStart impacte le cycle de vie d’une lambda ?
Dans le cas d’une lambda sans SnapStart, la phase de “Cold Start” se déroulera suite au premier appel de la lambda. Puis, en cas de nouveaux appels à la lambda, la phase de “Cold Start” ne sera plus exécutée. Cela est dû au fait que ces étapes ont déjà eu lieu suite au premier appel de la lambda. D’autant plus que l’environnement d’exécution est toujours le même que durant le premier appel. Cela dit, puisque chaque environnement d’exécution a également une durée de vie, il sera supprimé après une certaine période de temps (quelques minutes). Ce qui signifie que suite à la suppression de l’environnement d’exécution, une nouvelle phase de “Cold Start” aura lieu lors du prochain appel à la lambda.
Dans le cas d’une lambda supportant SnapStart, les étapes de “Cold Start” ont lieu durant la publication de la lambda comme décrit dans le schéma précédent. Donc, au premier appel de la lambda, au lieu d’exécuter une phase de “Cold Start”, il s’agit de rétablir l’état de l’environnement d’exécution comprenant le code déjà initialisé. Puis, de la même manière que pour le cas “sans SnapStart”, les prochains appels exécutent directement le code de la lambda jusqu’à suppression de l’environnement d’exécution.
SnapStart en action !
Pour mettre en pratique SnapStart, nous allons nous baser sur le même cas d’utilisation que dans la première partie, il s’agit d’obtenir une liste de Talk
en appelant l’endpoint GET /carbon/talks
. Cet endpoint sera accessible à la seule condition de s’être authentifié auprès du service Amazon Cognito et d’avoir transmis un access token en tant que Bearer Token.
L’endpoint GET /carbon/talks
sera exposé à travers l’API Gateway et son appel aura pour effet d’invoquer la lambda GetTalkList
. En effet, cette lambda sera chargée d’interroger la base de données (hébergée par le service RDS) en vue de renvoyer la liste de Talk
attendus.
La lambda GetTalkList
que nous allons optimiser par le biais de SnapStart est implémentée “naïvement” en Java :
Elle fait l’objet d’une latence importante pour deux raisons :
- La présence de la phase de “Cold Start”.
- Une lenteur importante à l’invocation qui s’explique principalement par le fait que pour réduire l’utilisation de la mémoire, la JVM retarde l’initialisation de libraries jusqu’à ce que la librairie soit appelée pour la première fois dans une application.
En activant SnapStart pour cette lambda, la phase de “Cold Start” est retirée du cycle de vie, mais elle est en revanche remplacée par la restauration de la “snapshot”, ce qui signifie que pour le premier appel l’optimisation n’est pas significative comme présenté ci-dessous.
Nous pouvons également constater que lors du second appel, le cas supportant SnapStart a une durée d’invocation similaire au cas sans solution d’optimisation.
Cependant, concernant l’optimisation du premier appel, cela nous permet de limiter le risque de “timeout”. En effet, c’est le risque de toutes les lambdas Java possédant une durée d’initialisation et d’invocation importantes.
Quelles sont les principales limitations de SnapStart ?
Bien que l’activation de SnapStart nous permet d’optimiser la lambda en retirant la phase de “Cold Start”, elle souffre également de limitations qui réduisent son champ d’utilisation :
- Le premier appel reste peu optimisé
L’optimisation constatée précédemment reste peu significative à cause de la phase de restauration. - Les conteneurs d’images ne sont pas supportés
En effet, une lambda peut être déployée soit par le biais d’une archive zip (ou un jar si elle est implémentée en Java), soit en s’appuyant sur une image de conteneur. Or, les conteneurs d’images sont de plus en plus utilisés dans les projets de nos jours en vue de déployer les lambdas. Ce qui a pour effet de priver ces lambdas Java de l’optimisation offerte par SnapStart. - Les “Custom runtime” ne sont pas supportés
Les lambdas s’exécutent dans un environnement d’exécution qui prend en charge spécifiquement le langage de programmation de la lambda (Java, Python3, Node.js, …). Mais il arrive que nous devions exécuter la lambda depuis une image native (produite par GraalVM). Dans ces conditions, l’environnement d’exécution à choisir correspond au “Custom runtime”. Or, ce type d’environnement d’exécution n’est pas supporté par SnapStart, ce qui signifie que nous ne pouvons pas combiner la solution GraalVM / Quarkus (présentée dans la première partie) et SnapStart.
Conclusion
SnapStart apporte une solution pour optimiser la latence des lambdas Java, sans ajouter des coûts supplémentaires à la facture du compte AWS. Il s’agit de la solution recommandée par AWS. Cela dit, lorsque le déploiement des lambdas par conteneurs d’images a lieu, SnapStart n’est pas supportée. Dans cette situation la solution basée sur GraalVM / Quarkus convient d’être utilisée pour optimiser les lambdas Java.
D’autre part, la solution GraalVM / Quarkus permet de bénéficier dès le premier appel d’une optimisation significative, ce qui en fait une solution plus efficace et couvrant plus de cas d’utilisation en comparaison de SnapStart.
Mais quelque soit la solution à mettre en place pour optimiser les lambdas Java, il ne fait aucun doute que dans une équipe de développeurs, il sera nécessaire d’investir du temps dans la prise en main de ces solutions. Or, en implémentant les lambdas en Node.js ou bien en Python 3, cet effort d’optimisation ne sera pas indispensable. Il convient à chaque équipe d’estimer s’il est plus judicieux de se tourner vers Java, Python ou Node.js au regard de leurs besoins.