Chez Local Logic, nous nous appuyons fortement sur les fonctions AWS lambda. Nous surveillons actuellement notre environnement avec Sentry via leur sdk et savons que nous pourrions alternativement utiliser la couche (layer) sentry. Comme un bon développeur, je me suis demandé quels étaient les avantages et les inconvénients. Évidemment, l’un d’entre eux est: quelle option est la plus rapide? Je les ai donc comparées et je partagerai les résultats dans ce billet.
Configuration
Comme toutes les bonnes expériences scientifiques, je vais présenter l’environnement d’installation pour la postérité.
- exécution : python 3.11
- mémoire: 1024MB
- architecture: x86_64
Pour faire un benchmark, j’ai utilisé une lambda réelle que nous avons. Je l’ai reconfigurée pour chacun des tests suivants :
- Sans sentry
- sentry-sdk 1.45.0 (La version que nous utilisons déjà)
- sentry-sdk 2.1.1 (La version dont je viens de découvrir l’existence)
- Couche sentry
arn:aws:lambda:us-east-2:943013980633:layer:SentryPythonServerlessSDK:112
Pour reproduire la couche sentry, le code python pour initialiser le sdk est le suivant
import os
import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
sentry_sdk.init(
dsn=settings.os.environ["SENTRY_DSN"],
integrations=[AwsLambdaIntegration(timeout_warning=true)],
traces_sample_rate=os.environ["SENTRY_TRACES_SAMPLE_RATE"],
environment=os.environ["STAGE"],
)
Pour pouvoir mesurer, j’ai modifié la lambda de la manière suivante :
- Défini le délai d’expiration (timeout) sur 5 secondes.
- Sur la toute première ligne de la fonction handler, logger, avec un logging structuré, « ready » avec la clé/valeur de
context.remaining_time_in_milis
(). C’était nécessaire pour mesurer le temps passé dans couche sentry pour invoquer notre code. - Sur la ligne suivante, exécuter
sleep(6)
. Cela permet de suspendre la lambda jusqu’à ce qu’elle s’arrête, ce qui facilite les tests car tous les appels effectués pendant cette période déclencheront une nouvelle lambda et donc un démarrage à froid (cold start).
Pour générer des résultats, j’ai exécuté, en bash, la boucle serrée suivante.
for i in {0..20}; do http -A bearer -a $BEARER_TOKEN https://<endpoint that targets the lambda> & done;
Le fait que j’utilise une interface http pour déclencher le test est impertinent. J’aurais pu l’appeler par invocation directe. Cela n’a pas d’importance. Ce qui compte c’est que la boucle envoie chaque appel http en arrière-plan, générant ainsi 21 appels http presque simultanés.
Pour collecter les données, j’ai utilisé AWS logs insights avec la requête suivante.
stats count(@initDuration), min(@initDuration), avg(@initDuration), max(@initDuration), stddev(@initDuration), count(remaining_time_in_milis), min(remaining_time_in_milis), avg(remaining_time_in_milis), max(remaining_time_in_milis), stddev(remaining_time_in_milis) by bin(5m)
Comme vous l’avez peut-être compris à la lecture de cette requête, je devais exécuter les tests à 5 minutes d’intervalle pour ne pas mélanger les résultats. Ça s’est fait implicitement car la mise en place du code, de la couche, des dépendances et le déploiement entre chaque test ont pris à peu près autant de temps. Pour ce qui est des count, comme les journaux sont éventuellement cohérents (eventually consistent), j’ai dû attendre qu’ils atteignent 21 pour m’assurer que j’avais les bons résultats.
Résultats
Configuration Sentry | Moyenne @initDuration ms (démarrage à froid) | Écart Type @initDuration ms | Moyenne temps jusqu’au handler ms (5 – remaining_time_in_milis) | Moyenne totale temps d’initialisation ms |
Sans sentry | 3101.31 7 | 223.769 | 0.809 | 3102.127 |
Sentry-sdk 1.45.0 | 3485.301 | 251.720 | 1.619 | 3486.920 |
Sentry-sdk 2.1.1 | 3377.584 | 226.086 | 1.762 | 3379.346 |
Sentry LayerServerless 112 | 963.46 0 | 79.456 | 3858.667 | 4822.127 |
J’ai omis les comptes, les valeurs minimales, maximales et l’écart-type de ce tableau, car il aurait été trop volumineux pour ce billet.
Interprétation
Éliminons d’emblée le résultat le plus surprenant: La couche est surprenamment… décevante. Avec un temps d’initialisation moyen total supérieur de ~1443ms. Le gagnant est … il n’y a pas de gagnant. En regardant uniquement la moyenne, on pourrait penser que sentry 2.1.1 est le vainqueur, mais en prenant en compte l’écart-type et la taille de l’échantillon, c’est statistiquement peu concluant.
A partir de ce tableau, la nécessité de mesurer le «temps jusqu’au handler» est évidente. Je ne peux que supposer que la couche sentry, au lieu de charger notre application dans la portion démarrage à froid, le fait à l’intérieur de son handler, et donc cela compte dans le temps d’exécution normal. Cela peut être un problème, et j’y ai été confronté en testant, car ce «temps d’exécution normal» compte aussi pour le délai d’expiration, ce qui n’est pas souhaitable. Lorsque j’ai commencé à tester, mon délai d’attente était fixé à 3 secondes et les tests de la couche sentry ont échoué parce qu’ils se sont arrêtés avant d’atteindre mon handler.
L’utilisation de sentry-sdk et son initialisation au niveau du module / espace de noms global, augmente le temps de démarrage à froid, ce qui est normal.
Si vous vous indignez de notre temps de démarrage à froid d’environ 3 secondes, tenez compte du contexte. Ils se produisent pour environ 0,2 % des requêtes. Vous pouvez alors vous demander si cela valait la peine de passer autant de temps sur ces benchmarks. Bien sûr que oui! Tout pour la science!
Mentions
Simon Lemieux a fourni des indications mathématiques sur la fiabilité statistique de «l’avantage» de 100 ms du sdk 2.1.1 par rapport au sdk 1.45.0.
L’image de couverture est Abstract Clock Vortex HD Wallpaper by Laxmonaut