Cet article a été rédigé par AWS Sr. Developer Advocate, Mohammed Fazalullah Qudrath, et publié avec autorisation.
Dans cet article, vous comprendrez les bases du fonctionnement des environnements d’exécution Lambda et les différentes manières d’améliorer le temps de démarrage et les performances des applications Java sur Lambda.
Pourquoi optimiser les applications Java sur AWS Lambda ?
La tarification d’AWS Lambda est conçue pour vous facturer en fonction de la durée d’exécution, arrondie à la milliseconde la plus proche. Le coût d’exécution d’une fonction Lambda sera proportionnel à la quantité de mémoire allouée à la fonction. Par conséquent, l’optimisation des performances peut également entraîner des optimisations de coûts à long terme.
Pour les environnements d’exécution gérés par Java, une nouvelle JVM est démarrée et le code d’application Java est chargé. Cela entraîne une surcharge supplémentaire par rapport aux langages interprétés et contribue aux performances de démarrage initiales.
Pour mieux comprendre cela, la section suivante donne un aperçu de haut niveau du fonctionnement des environnements d’exécution AWS Lambda.
Un aperçu sous le capot d’AWS Lambda
Les environnements d’exécution AWS Lambda sont l’infrastructure sur laquelle votre code est exécuté. Lors de la création d’une fonction Lambda, vous pouvez fournir un environnement d’exécution (par exemple, Java 11). Le runtime inclut toutes les dépendances nécessaires pour exécuter votre code (par exemple, la JVM).
L’environnement d’exécution d’une fonction est isolé des autres fonctions et de l’infrastructure sous-jacente à l’aide de la technologie Firecracker micro VM. Cela garantit la sécurité et l’intégrité du système.
Lorsque votre fonction est invoquée pour la première fois, un nouvel environnement d’exécution est créé. Il téléchargera votre code, lancera la JVM, puis initialisera et exécutera votre application. C’est ce qu’on appelle un démarrage à froid. L’environnement d’exécution initialisé peut alors traiter une seule requête à la fois et reste tiède pour les requêtes suivantes pendant un temps donné. Si un environnement d’exécution à chaud n’est pas disponible pour une demande ultérieure (par exemple, lors de la réception d’appels simultanés), un nouvel environnement d’exécution doit être créé, ce qui entraîne un autre démarrage à froid.
Par conséquent, pour optimiser les applications Java sur Lambda, il faut examiner l’environnement d’exécution, le temps nécessaire pour charger les dépendances et la vitesse à laquelle il peut être lancé pour gérer les demandes.
Améliorations sans changement de code
Choisir les bons paramètres de mémoire avec Power Tuning
Le choix de la mémoire allouée aux fonctions Lambda est un exercice d’équilibre entre vitesse (durée) et coût. Bien que vous puissiez exécuter manuellement des tests sur les fonctions en configurant des allocations de mémoire alternatives et en évaluant le temps d’exécution, l’application AWS Lambda Power Tuning automatise le processus.
Cet outil utilise AWS Step Functions pour exécuter de nombreuses versions simultanées d’une fonction Lambda avec différentes allocations de mémoire et mesurer les performances. La fonction d’entrée est exécutée dans votre compte AWS, avec des requêtes HTTP en direct et une interaction SDK, afin d’évaluer les performances dans un scénario de production en direct.
Vous pouvez représenter graphiquement les résultats pour visualiser les compromis en termes de performances et de coûts. Dans cet exemple, vous pouvez voir qu’une fonction a le coût le plus bas à 2048 Mo de mémoire, mais l’exécution la plus rapide à 3072 Mo :
Activation de la compilation hiérarchisée
La compilation à plusieurs niveaux est une fonctionnalité de la machine virtuelle Java HotSpot (JVM) qui permet à la JVM d’appliquer plusieurs niveaux d’optimisation lors de la traduction du bytecode Java en code natif. Il est conçu pour améliorer le temps de démarrage et les performances des applications Java.
L’atelier d’optimisation Java montre les étapes pour activer la compilation à plusieurs niveaux sur une fonction Lambda.
Activation de SnapStart sur une fonction Lambda
SnapStart est une nouvelle fonctionnalité annoncée pour les fonctions AWS Lambda exécutées sur Java Coretto 11. Lorsque vous publiez une version de fonction avec SnapStart activé, Lambda initialise votre fonction et prend un instantané de l’état de la mémoire et du disque de l’environnement d’exécution initialisé. Il chiffre l’instantané et le met en cache pour un accès à faible latence. Lorsque la fonction est appelée pour la première fois et à mesure que les appels augmentent, Lambda reprend de nouveaux environnements d’exécution à partir de l’instantané mis en cache au lieu de les initialiser à partir de zéro, ce qui améliore la latence de démarrage. Cette fonctionnalité peut améliorer jusqu’à 10 fois les performances de démarrage des applications sensibles à la latence sans frais supplémentaires, généralement sans modification de votre code de fonction. Suivez ces étapes pour activer SnapStart sur une fonction Lambda.
Configuration de la simultanéité provisionnée
La simultanéité provisionnée sur AWS Lambda maintient les fonctions initialisées et hyper prêtes à répondre en quelques millisecondes. Lorsque cette fonctionnalité est activée pour une fonction Lambda, le service Lambda prépare le nombre spécifié d’environnements d’exécution pour répondre aux appels. Vous payez pour la mémoire réservée, qu’elle soit utilisée ou non. Cela se traduit également par des coûts inférieurs au prix à la demande dans les cas où il y a une charge constante sur votre fonction.
Améliorations avec la refactorisation du code
Utilisation de dépendances légères
Choisissez entre des dépendances alternatives en fonction de la richesse des fonctionnalités par rapport aux besoins de performances. Par exemple, lors du choix d’une bibliothèque de journalisation, utilisez SLF4J SimpleLogger si le besoin est simplement de consigner les entrées, au lieu d’utiliser Log4J2 qui apporte beaucoup plus de fonctionnalités à la table qui ne peuvent pas être utilisées. Cela améliorera le temps de démarrage de près de 25 %.
Les frameworks d’API d’accès aux données Spring Data JPA et Spring Data JDBC peuvent être comparés de la même manière. L’utilisation du framework Spring Data JDBC réduira le temps de démarrage d’une fonction Lambda d’env. 25 % en faisant des compromis sur l’ensemble des fonctionnalités d’un ORM avancé.
Optimisation pour le kit SDK AWS
Assurez-vous que toutes les actions coûteuses, telles que l’établissement d’une connexion client S3 ou d’une connexion à la base de données, sont effectuées en dehors du code du gestionnaire. La même instance de votre fonction peut réutiliser ces ressources pour de futures invocations. Cela permet de réduire les coûts en réduisant la durée de la fonction.
Dans l’exemple suivant, l’instance S3Client est initialisée dans le constructeur à l’aide d’une méthode de fabrique statique. Si le conteneur géré par l’environnement Lambda est réutilisé, l’instance S3Client initialisée est réutilisée.
public class App implements RequestHandler<Object, Object\> {
private final S3Client s3Client;
public App() {
s3Client = DependencyFactory.s3Client();
}
@Override
public Object handleRequest(final Object input, final Context context) {
ListBucketResponse response = s3Client.listBuckets();
// Process the response.
}
}
Lorsque vous utilisez le kit SDK AWS pour vous connecter à d’autres services AWS, vous pouvez accélérer le chargement des bibliothèques lors de l’initialisation en effectuant des appels d’API factices en dehors du gestionnaire de fonctions. Ces appels factices initialiseront toutes les parties de la bibliothèque chargées paresseusement et les processus supplémentaires tels que les poignées de main TLS.
public class App implements RequestHandler < Object, Object \> {
private final DynamoDbClient ddb = DynamoDbClient.builder()
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.region(System.getenv("AWS\_REGION"))
.httpClientBuilder(UrlConnectionHttpClient.builder())
.build();
private static final String tableName = System.getenv("DYNAMODB\_TABLE\_NAME");
public App() {
DescribeTableRequest request = DescribeTableRequest.builder()
.tableName(tableName)
.build();
try {
TableDescription tableInfo = ddb.describeTable(request).table();
if (tableInfo != null) {
System.out.println("Table found:" + tableInfo.tableArn());
}
} catch (DynamoDbException e) {
System.out.println(e.getMessage());
}
}
@Override
public Object handleRequest(final Object input, final Context context) {
//Handler code
}
}
Tirer parti des frameworks cloud natifs avec GraalVM
GraalVM est une machine virtuelle universelle qui prend en charge les langages basés sur JVM, tels que Java, Scala et Kotlin, ainsi que les langages dynamiques, tels que Python, JavaScript et les langages basés sur LLVM, tels que C et C++. GraalVM permet la compilation Ahead-of-Time (AoT) de programmes Java dans un exécutable natif autonome, appelé image native. L’exécutable est optimisé et contient tout le nécessaire pour exécuter l’application, et il a un temps de démarrage plus rapide et une empreinte mémoire de tas plus petite par rapport à une JVM.
Les frameworks cloud modernes tels que Micronaut et Quarkus (et la récente version Spring Boot 3) prennent en charge la création d’images natives GraalVM dans le cadre du processus de construction.
En conclusion, vous avez vu les différentes approches pour optimiser les applications Java sur AWS Lambda. D’autres ressources liées ci-dessous approfondissent les suggestions ci-dessus, ainsi qu’un atelier lié ci-dessous que vous pouvez suivre pour effectuer les optimisations ci-dessus et plus sur un exemple d’application Spring Boot.
Les références
- Présentation de la sécurité du livre blanc AWS Lambda
- Exploitation Lambda : Optimisation des performances – Partie 1
- Exploitation Lambda : Optimisation des performances – Partie 2
- Configuration de la simultanéité provisionnée
- Réduction des démarrages à froid Java sur les fonctions AWS Lambda avec SnapStart
- Java sur AWS Lambda