Ecrire dans une base RDS dans une infrastructure AWS serverless orientée évènement
Dans AWS, la cohabitation de l’écosystème serverless et des bases de données relationnelles (bdd) n’est pas sans poser de problèmes. Dans une infrastructure évènementielle, il existe cependant des solutions si on est pret à quelques compromis.
Contexte
Pour illustrer le problème prenons une architecture basique avec une api rest (api gateway) qui sauvegarde en base la donnée transmise via une lambda.
La base de donnée utilisée est une simple base RDS MySQL avec 2 CPU et 2 GiB de RAM (instance class: db.t3.small).
Il y a des accès en lecture peu fréquent (quelques centaines par jour) et des accès en écriture très fréquent: entre 300 000 et 400 000 requêtes en quelques heures.
Idéalement, nous souhaitions rester dans une tarification abordable.

Contraintes
Nous avons été confrontés à plusieurs problèmes dès le départ:
- Avec une volumétrie importante, la lambda qui est attachée à l‘Api REST est mise à l’échelle rapidement (scaling) et on se retrouve avec un nombre important de lambda en concurrences. Ce qui innonde la base de données de connexions et surcharge fortement le CPU et déclenche très vite des erreurs puisque chaque instance de la lambda cherche à communiquer avec la bdd.
- Choisir une classe d’instance plus puissante pour la bdd fait automatiquement gonfler la facture AWS.
- Utiliser RDS proxy pour la gestion du pool de connexion entre les lambda et la base provoque également une hausse non négligeable de la tarification.
- Les bases de données Aurora serverless v2 s’adaptent à la charge mais sont de facto très chères et nécessite en plus d’utiliser RDS proxy.
Solutions
Pool de connexion: serverless-mysql
Il s’agit d’une librairie npm qui permet de gérer les connexion des lambda aux bases MySQL. Elle encapsule la librairie native JavaScript MySQL et fonctionne de la manière suivante:
- Analyse les connexions en cours à la base et permet un partage de ces connexions aux différentes lambda en concurrence pour éviter un cycle couteux en temps de connexion / deconnexion et limite donc le nombre de nouvelles connexions.
- Supprime automatiquement les connexions dites “zombies” qui ne sont plus attachées à aucune lambda.
- Propose un mécanisme de rejeu (retry) avec des itérations espacées dans le temps (backoff algorithm) pour retenter une connexion à la base à différents instants.
Malgré une nette amélioration sur le nombre de connexions, lorsque la volumétrie est trop importante sur un temps très court, le nombre de connexions maximum peut quand même être atteint et le CPU de la base MySQL plafonne à 100%.
Et si on pouvait agir au niveau de lambda ?
Lambda: Mémoire, expiration et concurrence réservée
L’autre levier sur lequel nous avons joué est de diminuer le temps de traitement de notre lambda car elle sera plus rapide et génèrera moins de scaling et donc moins de connexions/opérations simultanés à la bdd.
- Mémoire: Par défaut à 128MB dans AWS, augmenté à 512MB nous a permis de gagner quelques millisecondes sur le temps de traitement
- Expiration (timeout): Par défaut à 6 secondes, si la bdd est indisponible (et mets plus de 6 secondes à répondre) la lambda sera en erreur. Augmenter le timeout nous à permis de générer moins d’erreurs mais cela maintient plus longtemps la lambda en vie ce qui ne résoud pas le problème de concurrence.
- Concurrence réservée (Reserved concurrency): Permet de limiter le nombre de lambda qui vont être éxecutées en parallèle et donc de limiter le nombre d’intéractions avec la BDD. Si cette option résoud le problème de traffic vers la base de données elle ne résoud pas vraiment notre problème. En effet si on limite à 5 le nombre de lambda et que le nombre d’appels API est trop important la limite des 5 lambdas sera très vite atteinte et provoquera des erreurs du type 429: Rate exceeded” et “Too Many request exception” => traduction: il n’y a pas assez de lambda disponibles.
Et si on utilisait une file SQS ?
SQS: Gestion par lot et rejeu

Ajouter une file SQS dans l’architecture existante permet de:
- Gérer la donnée par lot (batch) : La lambda traite une liste de données (records) à insérer en base et non plus une seule. Cela revient à insérer / mettre à jour une liste de valeurs en une seule requête SQL ce qui est plus optimisé en terme de ratio connexion/reqûetes. Il y aura donc moins d’intéractions avec la base mais chaque requête va insérer un nombre plus importants de données.
- Mécanisme de rejeu (retry): Par défaut AWS renvoie les messages dans le file SQS si une erreur est survenue dans la lambda et procède à 3 tentatives avant d’envoyer le message dans une DLQ en cas d’echec complet. C’est une gestion automatique du rejeu (retry) non négligeable lorsque l’on fait face à des erreurs “aléatoires” notamment à cause de la disponibilité de la base de données.
- Gestion des erreurs: La DLQ permet de ne pas perdre les données en erreur et de rejouer les messages en les poussant vers la file SQS source (redrive)
Malgré un lissement de la charge, la file SQS et la gestion par lot des données, cela n’empeche pas une surcharge de la bdd. Il y a trop de lambdas en concurrence.
SQS: Ajout d’un délai
Les file SQS possèdent un paramètre Delay configurable au niveau de la file elle même ou sur chaque message (record). Il permet d’ajouter un délai pendant lequel le message n’est pas accessible aux consommateurs (dans notre cas la lambda). Il est évident que mettre le même délai pour tous les message ne résoud rien et ne fait que reporter dans le temps le problème.
L’astuce consiste à mettre un délai aléatoire sur chaque message au moment ou ils sont poussés dans la file SQS pour décaler dans le temps la disponibilité de chacun des messages et permettre de lisser la charge. Le délai maximum est de 15 minutes ce qui offre une fenêtre assez large pour traiter un nombre conséquent de messages.
Cest un procédé qui fonctionne bien mais n’est pas sans poser certain problèmes:
- Si le volume de messages à traiter est très important (une file SQS peut contenir jusqu’a 120 000 messages à un instant T) on peut quand même se retrouver dans une situation ou le nombre de message à traiter est trop élevé et donc surcharger la base. C’est aléatoire et donc pas très pérènne.
- Cela peut ralonger de manière significative le temps de traitement des messages si on exploite les 15 minutes de délai disponibles. De plus nous n’avons pas la main sur le temps total de traitement des messages qui peut complètement varier d’un jour à l’autre.
- On est obligé de faire un calcul savant pour adapter le délai maximum en fonction du nombre de messages total à traiter ce qui peut très vite devenir complexe.
SQS et Lambda: Concurrence maximum
Quand une lambda est attachée à une file SQS, on parle alors de trigger (déclencheur) SQS pour la lambda, et ce dernier offre une configuration intéréssante:
- Batch size: Permet de controler le nombre de messages (records) pour chaque batch envoyé à la lambda.
- Batch window: Nombre de temps à attendre (en secondes) avant de déclencher la lambda et pendant lequel les messages sont collectés.
- Maximum concurrency: Nombre maximum de lambda concurrentes pour lire les messages de la file SQS.
Quelle différence entre maximum concurrency et reserved concurrency ? Dans le premier cas c’est le nombre maximum d’occurrences d’une lambda qui ecoute une file SQS. Dans le deuxième cas c’est c’est le nombre maximum d’occurrences d’une lambda sans aucun lien avec un trigger ou autres services AWS.
Et c’est bien le paramètre maximum concurrency sur la relation SQS / lambda qui change la donne car dans ce cas précis, le nombre d’instances de la lambda en conccurrence ne dépassera jamais le maximum concurrency et ne génèrera as d’erreur de type 429: Rate exceeded” et “Too Many request exception”.
Comment est-ce possible ? C’est lié au mode de fonctionnement entre la lambda qui fait un pull régulier des messages de la file SQS (et non la file SQS qui pousse (push) les messages vers la lambda). Les différentes instances de la lambda vont donc lires en parrallèle les messages de la file à leur rythme et cela permet de lissée dans le temps la charge et de soulager la base de données.
Conclusion
L’ensemble des fonctionnalités citées sont faciles à mettre en place mais n’ont de sens qu’avec les contraintes suivantes:
- Nous somme dans une infrastructure évènementielle (event driven) ou les requêtes http ne requièrent pas une réponse immédiate (mode request / response).
- Cela suppose aussi qu’un délai est acceptable pour, comme dans notre exemple, sauvegarder de la donnée en base puisque la restriction du nombre de lambda concurrente va ralentir le temps nécessaire à la lecture de tous les messages présents dans la file SQS.
- C’est principalement des opérations d’écriture et ponctuellement de lecture.
On peut voir sur le schéma ci dessous que la nombre de connexions maximum à la base est ridicule et laisse encore de la marge.

On constate également que l’utilisation du CPU chute drastiquement et dépasse rarement les 30% pour une lambda qui traite en moyenne 130 000 messages en 4 minutes (lecture SQS + insertion en bdd), et entre 800 000 et 900 000 requêtes SQL sont exécutées chaque jour sur la base RDS (lecture et écriture).

Enfin concernant la tarification on est autour des 52$ pour un mois classique.