Comment optimiser ses fonctions lambdas grâce à AWS Lambda power tuning

Que l’on soit débutant ou confirmé sur AWS, le point de départ d’un projet qui embarque du serverless n’a jamais été l’optimisation des fonctions. C’est normal, à la genèse d’un projet, on souhaite quelque chose de fonctionnel, pas forcément quelque chose d'optimisée (en tout cas, pas de suite). On se retrouve donc souvent avec une ou plusieurs fonctions lambda qui tournent, mais qui pourraient soit tourner plus vite, soit tourner pour moins cher !

J’ai l’occasion de travailler sur un projet personnel avec une architecture 100% serverless. Sur ce projet, j’ai notamment plusieurs fonctions qui vont faire des appels API en parallèle. On parle ici de fonctions qui font plusieurs milliers voire des dizaines de milliers d’appels. Au tout départ, nous n'étions pas loin des quotas pour une lambda en termes de temps (15 minutes d'exécution). Nous avons donc tout d’abord ajouté du multithreading pour gérer les appels API (au lieu de les faire 1 par 1, on les fait désormais en parallèle). Nous avons réussi à diviser le temps par 2 voire 3 pour certaines fonctions. Et avec les lambdas, le temps c’est de l’argent.

Time is money gif

Étant satisfait de l’amélioration, nous avons décidé de laisser les fonctions telles quelles et de nous concentrer sur d’autres fonctionnalités. Au bout de quelques mois, j’ai remarqué que nous avions la même mémoire par défaut sur chacune de nos lambdas (1024 MB), et je me suis posé la question suivante : est-il possible d’augmenter ou de baisser cette mémoire sur les “grosses” lambdas et avoir une diminution du coût (et même une augmentation de la performance) ?

Je me suis souvenu à ce moment-là de cet outil dont j’avais entendu parler dans un épisode du podcast AWS en français : AWS Lambda Power Tuning.

C’est de cet utilitaire que je vais vous parler aujourd’hui, celui qui m’a permis d’économiser plus de 30% de ma facture sur le service AWS Lambda.

AWS Lambda power tuning : comment ça marche ?

Cet outil, qui est open source et disponible ici, se présente sous la forme d'une Step Function que l'on déploie sur son compte AWS. Cette Step Function a pour but de faire tourner votre lambda avec différentes configurations en termes de mémoire plusieurs fois et de ressortir un comparatif sous forme de graphique (ou de JSON) pour essayer de trouver le point optimal entre coût et temps d'exécution. Il existe trois modes d'optimisation possibles, soit par les coûts, soit par le temps d'exécution, ou alors un mode "balanced" où il va essayer de trouver un équilibre entre les deux.

Pour la déployer sur son compte, il existe six façons différentes de le faire :

Vous pouvez retrouver tous les tutoriels pour déployer l’application ici : https://github.com/alexcasalboni/aws-lambda-power-tuning/blob/master/README-DEPLOY.md

L’outil est ensuite très simple à utiliser. Il faut vous rendre dans le service AWS Step Functions, trouver la step function avec un nom qui doit commencer par “powerTuningStateMachine-” et appuyer sur le bouton “start execution”.

Une fois que vous avez appuyé sur le bouton, vous allez vous retrouver avec cette interface :

alt_text

La partie qui nous intéresse est la partie input, où nous allons paramétrer ce que nous voulons tester. Voici la liste des inputs possibles pour notre exécution :

  • lambdaARN : ARN de la lambda à exécuter
  • powerValues : Liste sous forme de tableau représentant les configurations de mémoire à tester
  • num : Le nombre d’invocations de lambda à faire pour chaque configuration (5 minimum, recommandation entre 10 et 100)
  • payload : Les paramètres d’entrée de la fonction Lambda
  • payloadS3 : Les paramètres d’entrée de la fonction Lambda (si trop gros pour la lambda)
  • parallelInvocation : Permet de faire les invocations de la lambda en parallèle (attention au throttling si activé…)
  • strategy : La stratégie dont j’ai parlé plus haut (coût, temps ou balanced)
  • balancedWeight : Paramètre représentant ce que vous souhaitez optimiser (0 correspond à la stratégie cost et 1 à la stratégie temps)
  • autoOptimize : Applique automatiquement la meilleure configuration à la fin de l'exécution de la step function
  • autoOptimizeAlias : Crée ou met à jour l’alias avec la nouvelle configuration
  • dryRun : Permet de tester le fonctionnement et l’appel à la fonction lambda (droit IAM par exemple)
  • preProcessorARN : ARN d’une lambda à exécuter avant chaque exécution de la lambda à tester
  • postProcessorARN : ARN d’une lambda à exécuter après chaque exécution de la lambda à tester
  • discardTopBottom : Permet de supprimer les 20% des plus rapides et des moins rapides (pour avoir une représentation fiable de la réalité => supprimer les cold starts par exemple)
  • sleepBetweenRunsMs : Temps entre chaque exécution de la fonction à tester

Il y a beaucoup d’informations ici, mais nous pouvons nous concentrer sur lambdaARN, num et strategy pour lancer notre première invocation. Le reste peut être exploré après une première optimisation si vous n’êtes pas encore satisfait.

Un input ressemblerait à ceci, par exemple :


{

  "lambdaARN": "arn:aws:lambda:eu-west-1:xxxxxxxx:function:lambda-power-tuning-snapoleon-article",

  "powerValues": [

    700,

    1000,

    1500,

    2000,

    2500,

    3000

  ],

  "num": 30,

  "payload": {},

  "parallelInvocation": false,

  "strategy": "balanced"

}

Si tout se passe bien, vous devriez avoir un joli graphique d'état pour la step function comme celui-ci :

alt_text

Si vous cliquez sur la dernière étape "Optimizer", vous aurez accès à l'output de l'étape avec notamment les résultats de la step function (disponible également sous le volet Execution input and output). Vous aurez ici, sous format JSON, la sortie avec les résultats pour chaque configuration, ainsi qu'un lien vers un site web permettant de visualiser les résultats sous forme de graphique. Chaque résultat pour une configuration donnée ressemblera à ceci :


{

      "averagePrice": 0.00003504375000000001,

      "averageDuration": 2135.3594444444443,

      "totalCost": 0.0010591546875000002,

      "value": 1000

}

et le graphique ressemblera à quelque chose comme cela :

alt_text

Il est désormais temps de vous montrer la puissance de l’outil grâce à un exemple concret.

Optimiser sa fonction lambda en pratique

Un bon cas d'utilisation d'optimisation serait pour du calcul pur, avec une bibliothèque comme Pandas en Python. Nous allons donc essayer d'optimiser une fonction Lambda qui utilise la bibliothèque Pandas. Le code est très simple :


import json

import pandas as pd

import numpy as np

def lambda_handler(event, context):

    # Génère de la donnée au hasard

    data = np.random.randn(1500000, 10)

    df = pd.DataFrame(data)

    

    # Effectue des calculs sur ces données

    df = df.apply(lambda x: x**2)

    df = df.apply(lambda x: x + 10)

    

    df = df.apply(lambda x: x**2)

    df = df.apply(lambda x: x + 10)

    

    df = df.apply(lambda x: x**2)

    df = df.apply(lambda x: x + 10)

    

    # Affiche le résultat

    print(df)

    return {

        'statusCode': 200,

        'body': json.dumps('Please optimize me !')

    }

Si vous souhaitez effectuer le test par vous-même, vous devrez déployer la fonction et ne pas oublier d'ajouter le layer contenant les bibliothèques nécessaires (Pandas et Numpy). Voici l'ARN du layer utilisée ici : arn:aws:lambda:eu-west-1:336392948345:layer:AWSSDKPandas-Python39:5.

Une fois la fonction déployée et en supposant que vous avez déjà déployé AWS Lambda Power Tuning sur votre compte, il ne vous reste plus qu'à paramétrer l'exécution de la fonction :


{

  "lambdaARN": "arn:aws:lambda:eu-west-1:xxxxxxx:function:lambda-power-tuning-snapoleon-article",

  "powerValues": [

    700,

    1000,

    1500,

    2000,

    2500,

    3000

  ],

  "num": 30,

  "payload": {},

  "parallelInvocation": false,

  "strategy": "balanced"

}

Voici ce que j’ai utilisé pour notre exemple. Attention, la plupart du temps vous serez limité à 3008MB pour la mémoire, c’est une “soft limit” par défaut pour tous les comptes AWS. Vous pouvez demander une augmentation (jusqu’à 10 000 MB), mais il faudra faire une demande auprès du support.

Une fois les paramètres renseignés, on lance l'exécution et il ne reste plus qu'à attendre.

Deux minutes plus tard, tout est terminé et nous avons les résultats à disposition :

alt_text

On remarque que si l'on alloue 700 MB, le temps d'exécution est de 3097 ms pour un coût de 0,000036 $. En passant à 1000 MB de mémoire, nous pouvons réduire le temps d'exécution à 2135 ms pour un coût quasi similaire de 0,000035 $. Si l'on passe cette fois à 1500MB, le temps d'exécution descend à 1422 ms et le coût reste le même. À partir de 2000 MB, on atteint un plateau en ce qui concerne le temps d'exécution (on ne descendra pas en dessous de 1149 ms). Étant donné que le temps ne bougera plus à partir de ce moment, le prix ne fera que monter. Pour 2000 MB, on aura un coût de 0,000038 $, 0,000049 $ pour 2500 MB et 0,000059 $ pour 3000MB.

Nous pouvons tirer de cette exécution que le sweet spot semble se situer aux alentours de 1500 ou 2000 MB en fonction de ce que vous cherchez à optimiser. Si vous souhaitez économiser de l'argent, 1500 MB semble être un bon candidat. En revanche, si vous cherchez à optimiser le temps d'exécution, 2000 MB semble être le meilleur choix.

Pour conclure :

Cet outil est incontournable si vous souhaitez optimiser les coûts ou le temps d'exécution de vos fonctions Lambda. Il vous permettra de réaliser des économies considérables sur votre facture AWS si vous utilisez fréquemment le service Lambda.

Il est également important de noter que cet outil peut être intégré à une CI/CD et exécuté à chaque déploiement de votre application. Vous pouvez ainsi mettre à jour vos fonctions Lambda en continu via des processus DevOps (mais attention aux drifts dans l'IaC).