Envoi d'e-mails de manière asynchrone via AWS SES
Publié: 2022-03-10La plupart des applications envoient des e-mails pour communiquer avec leurs utilisateurs. Les e-mails transactionnels sont ceux déclenchés par l'interaction de l'utilisateur avec l'application, par exemple lors de l'accueil d'un nouvel utilisateur après s'être enregistré sur le site, en donnant à l'utilisateur un lien pour réinitialiser le mot de passe ou en joignant une facture après que l'utilisateur ait effectué un achat. Tous ces cas précédents nécessiteront généralement l'envoi d'un seul e-mail à l'utilisateur. Dans certains autres cas cependant, l'application doit envoyer beaucoup plus d'e-mails, comme lorsqu'un utilisateur publie un nouveau contenu sur le site, et tous ses abonnés (qui, sur une plate-forme comme Twitter, peuvent représenter des millions d'utilisateurs) recevront un notification. Dans cette dernière situation, mal architecturée, l'envoi d'e-mails peut devenir un goulot d'étranglement dans l'application.
C'est ce qui s'est passé dans mon cas. J'ai un site qui peut avoir besoin d'envoyer 20 e-mails après certaines actions déclenchées par l'utilisateur (telles que les notifications de l'utilisateur à tous ses abonnés). Initialement, il reposait sur l'envoi des e-mails via un fournisseur SMTP populaire basé sur le cloud (tel que SendGrid, Mandrill, Mailjet et Mailgun), mais la réponse à l'utilisateur prenait quelques secondes. De toute évidence, la connexion au serveur SMTP pour envoyer ces 20 e-mails ralentissait considérablement le processus.
Après inspection, j'ai trouvé les sources du problème:
- Connexion synchrone
L'application se connecte au serveur SMTP et attend un accusé de réception, de manière synchrone, avant de poursuivre l'exécution du processus. - Latence élevée
Alors que mon serveur est situé à Singapour, le fournisseur SMTP que j'utilisais a ses serveurs situés aux États-Unis, ce qui fait que la connexion aller-retour prend un temps considérable. - Pas de réutilisabilité de la connexion SMTP Lors de l'appel de la fonction pour envoyer un e-mail, la fonction envoie l'e-mail immédiatement, créant une nouvelle connexion SMTP à ce moment-là (elle ne propose pas de collecter tous les e-mails et de les envoyer tous ensemble à la fin de la requête, sous un seul SMTP connexion).
En raison de #1, le temps que l'utilisateur doit attendre la réponse est lié au temps qu'il faut pour envoyer les e-mails. En raison de #2, le temps d'envoi d'un e-mail est relativement élevé. Et à cause du #3, le temps d'envoyer 20 e-mails est 20 fois le temps qu'il faut pour envoyer un e-mail. Bien que l'envoi d'un seul e-mail ne rende pas l'application terriblement plus lente, l'envoi de 20 e-mails le fait certainement, ce qui affecte l'expérience utilisateur.
Voyons comment résoudre ce problème.
Prêter attention à la nature des e-mails transactionnels
Avant toute chose, il faut remarquer que tous les emails ne se valent pas en importance. Nous pouvons globalement classer les e-mails en deux groupes : les e-mails prioritaires et non prioritaires. Par exemple, si l'utilisateur a oublié le mot de passe pour accéder au compte, il attendra l'e-mail contenant le lien de réinitialisation du mot de passe immédiatement dans sa boîte de réception ; c'est un email prioritaire. En revanche, l'envoi d'un e-mail notifiant qu'une personne que nous suivons a publié un nouveau contenu n'a pas besoin d'arriver immédiatement dans la boîte de réception de l'utilisateur ; c'est un email non prioritaire.
La solution doit optimiser l'envoi de ces deux catégories d'emails. En supposant qu'il n'y aura que quelques (peut-être 1 ou 2) e-mails prioritaires à envoyer au cours du processus, et que la majeure partie des e-mails seront non prioritaires, nous concevons la solution comme suit :
- Les e- mails prioritaires peuvent simplement éviter le problème de latence élevée en utilisant un fournisseur SMTP situé dans la même région où l'application est déployée. En plus d'une bonne recherche, cela implique d'intégrer notre application avec l'API du fournisseur.
- Les e-mails non prioritaires peuvent être envoyés de manière asynchrone et par lots où de nombreux e-mails sont envoyés ensemble. Mis en œuvre au niveau applicatif, il nécessite une pile technologique appropriée.
Définissons ensuite la pile technologique pour envoyer des e-mails de manière asynchrone.
Définir la pile technologique
Remarque : j'ai décidé de baser ma pile sur les services AWS car mon site Web est déjà hébergé sur AWS EC2. Sinon, j'aurais un surcoût dû au déplacement des données entre les réseaux de plusieurs entreprises. Cependant, nous pouvons également mettre en œuvre notre solution en utilisant d'autres fournisseurs de services cloud.
Ma première approche a été de créer une file d'attente. Grâce à une file d'attente, je pourrais faire en sorte que l'application n'envoie plus les e-mails, mais publie à la place un message avec le contenu et les métadonnées de l'e-mail dans une file d'attente, puis qu'un autre processus récupère les messages de la file d'attente et envoie les e-mails.
Cependant, lors de la vérification du service de file d'attente d'AWS, appelé SQS, j'ai décidé que ce n'était pas une solution appropriée, car :
- C'est assez complexe à mettre en place ;
- Un message de file d'attente standard ne peut stocker que 256 Ko d'informations, ce qui peut ne pas être suffisant si l'e-mail contient des pièces jointes (une facture par exemple). Et même s'il est possible de diviser un gros message en messages plus petits, la complexité augmente encore plus.
Ensuite, j'ai réalisé que je pouvais parfaitement imiter le comportement d'une file d'attente grâce à une combinaison d'autres services AWS, S3 et Lambda, qui sont beaucoup plus faciles à configurer. S3, une solution de stockage d'objets dans le cloud pour stocker et récupérer des données, peut servir de référentiel pour télécharger les messages, et Lambda, un service informatique qui exécute du code en réponse à des événements, peut sélectionner un message et exécuter une opération avec celui-ci.
En d'autres termes, nous pouvons configurer notre processus d'envoi d'e-mails comme ceci :
- L'application télécharge un fichier avec le contenu de l'e-mail + les métadonnées dans un compartiment S3.
- Chaque fois qu'un nouveau fichier est téléchargé dans le compartiment S3, S3 déclenche un événement contenant le chemin d'accès au nouveau fichier.
- Une fonction Lambda sélectionne l'événement, lit le fichier et envoie l'e-mail.
Enfin, nous devons décider comment envoyer des e-mails. Nous pouvons soit continuer à utiliser le fournisseur SMTP que nous avons déjà, en faisant interagir la fonction Lambda avec leurs API, soit utiliser le service AWS pour envoyer des e-mails, appelé SES. L'utilisation de SES présente à la fois des avantages et des inconvénients :
Avantages:
- Très simple à utiliser depuis AWS Lambda (il suffit de 2 lignes de code).
- C'est moins cher : les frais Lambda sont calculés en fonction du temps nécessaire à l'exécution de la fonction, de sorte que la connexion à SES depuis le réseau AWS prendra moins de temps que la connexion à un serveur externe, ce qui permet à la fonction de se terminer plus tôt et coûte moins cher. . (À moins que SES ne soit pas disponible dans la même région où l'application est hébergée ; dans mon cas, étant donné que SES n'est pas proposé dans la région Asie-Pacifique (Singapour), où se trouve mon serveur EC2, je ferais peut-être mieux de me connecter à certains Fournisseur SMTP externe basé en Asie).
Désavantages:
- Peu de statistiques sont fournies pour surveiller nos e-mails envoyés, et en ajouter des plus puissantes nécessite des efforts supplémentaires (par exemple : le suivi du pourcentage d'e-mails ouverts ou des liens cliqués doit être configuré via AWS CloudWatch).
- Si nous continuons à utiliser le fournisseur SMTP pour envoyer les e-mails prioritaires, nous n'aurons pas toutes nos statistiques au même endroit.
Pour plus de simplicité, dans le code ci-dessous, nous utiliserons SES.
Nous avons ensuite défini la logique du processus et de la pile comme suit : l'application envoie les e-mails prioritaires comme d'habitude, mais pour les non prioritaires, elle télécharge un fichier avec le contenu et les métadonnées des e-mails vers S3 ; ce fichier est traité de manière asynchrone par une fonction Lambda, qui se connecte à SES pour envoyer l'e-mail.
Commençons à mettre en œuvre la solution.
Différencier les e-mails prioritaires et non prioritaires
Bref, tout dépend de l'application, il faut donc se décider sur une base mail par mail. Je vais décrire une solution que j'ai implémentée pour WordPress, qui nécessite quelques hacks autour des contraintes de la fonction wp_mail
. Pour d'autres plates-formes, la stratégie ci-dessous fonctionnera également, mais il y aura très probablement de meilleures stratégies, qui ne nécessitent pas de hacks pour fonctionner.
La façon d'envoyer un e-mail dans WordPress est d'appeler la fonction wp_mail
, et nous ne voulons pas changer cela (par exemple : en appelant la fonction wp_mail_synchronous
ou wp_mail_asynchronous
), donc notre implémentation de wp_mail
devra gérer à la fois les cas synchrones et asynchrones, et devra savoir à quel groupe appartient l'e-mail. Malheureusement, wp_mail
n'offre aucun paramètre supplémentaire à partir duquel nous pourrions évaluer ces informations, comme on peut le voir à partir de sa signature :
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() )
Ensuite, afin de connaître la catégorie de l'email on ajoute une solution hacky : par défaut, on fait appartenir un email au groupe prioritaire, et si $to
contient un email particulier (ex : [email protected]), ou si $subject
commence par une chaîne spéciale (par exemple : "[Non-priority !]"), alors il appartient au groupe non prioritaire (et nous supprimons l'e-mail ou la chaîne correspondante du sujet). wp_mail
est une fonction enfichable, nous pouvons donc la remplacer simplement en implémentant une nouvelle fonction avec la même signature sur notre fichier functions.php. Initialement, il contient le même code que la fonction wp_mail
d'origine, située dans le fichier wp-includes/pluggable.php, pour extraire tous les paramètres :
if ( !function_exists( 'wp_mail' ) ) : function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) { $atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'message', 'headers', 'attachments' ) ); if ( isset( $atts['to'] ) ) { $to = $atts['to']; } if ( !is_array( $to ) ) { $to = explode( ',', $to ); } if ( isset( $atts['subject'] ) ) { $subject = $atts['subject']; } if ( isset( $atts['message'] ) ) { $message = $atts['message']; } if ( isset( $atts['headers'] ) ) { $headers = $atts['headers']; } if ( isset( $atts['attachments'] ) ) { $attachments = $atts['attachments']; } if ( ! is_array( $attachments ) ) { $attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) ); } // Continue below... } endif;
Et puis nous vérifions s'il n'est pas prioritaire, auquel cas nous bifurquons vers une logique distincte sous la fonction send_asynchronous_mail
ou, si ce n'est pas le cas, nous continuons à exécuter le même code que dans la fonction wp_mail
d'origine :
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) { // Continued from above... $hacky_email = "[email protected]"; if (in_array($hacky_email, $to)) { // Remove the hacky email from $to array_splice($to, array_search($hacky_email, $to), 1); // Fork to asynchronous logic return send_asynchronous_mail($to, $subject, $message, $headers, $attachments); } // Continue all code from original function in wp-includes/pluggable.php // ... }
Dans notre fonction send_asynchronous_mail
, au lieu de télécharger l'e-mail directement vers S3, nous ajoutons simplement l'e-mail à une variable globale $emailqueue
, à partir de laquelle nous pouvons télécharger tous les e-mails ensemble vers S3 en une seule connexion à la fin de la requête :
function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { global $emailqueue; if (!$emailqueue) { $emailqueue = array(); } // Add email to queue. Code continues below... }
Nous pouvons télécharger un fichier par e-mail, ou nous pouvons les regrouper de sorte que dans 1 fichier, nous contiendrions de nombreux e-mails. Étant donné que $headers
contient des métadonnées d'e-mail (champs de, type de contenu et charset, CC, BCC et réponse à), nous pouvons regrouper les e-mails chaque fois qu'ils ont les mêmes $headers
. De cette façon, ces e-mails peuvent tous être téléchargés dans le même fichier vers S3, et les méta-informations $headers
ne seront incluses qu'une seule fois dans le fichier, au lieu d'une seule fois par e-mail :
function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { // Continued from above... // Add email to the queue $emailqueue[$headers] = $emailqueue[$headers] ?? array(); $emailqueue[$headers][] = array( 'to' => $to, 'subject' => $subject, 'message' => $message, 'attachments' => $attachments, ); // Code continues below }
Enfin, la fonction send_asynchronous_mail
renvoie true
. Veuillez noter que ce code est hacky : true
signifierait normalement que l'e-mail a été envoyé avec succès, mais dans ce cas, il n'a même pas encore été envoyé, et il pourrait parfaitement échouer. Pour cette raison, la fonction appelant wp_mail
ne doit pas traiter une true
réponse comme "l'e-mail a été envoyé avec succès", mais un accusé de réception qu'il a été mis en file d'attente. C'est pourquoi il est important de limiter cette technique aux e-mails non prioritaires afin qu'en cas d'échec, le processus puisse continuer à réessayer en arrière-plan et que l'utilisateur ne s'attende pas à ce que l'e-mail soit déjà dans sa boîte de réception :
function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { // Continued from above... // That's it! return true; }
Téléchargement d'e-mails sur S3
Dans mon article précédent "Partage de données entre plusieurs serveurs via AWS S3", j'ai décrit comment créer un compartiment dans S3 et comment télécharger des fichiers dans le compartiment via le SDK. Tout le code ci-dessous continue la mise en œuvre d'une solution pour WordPress, nous nous connectons donc à AWS en utilisant le SDK pour PHP.
Nous pouvons étendre la classe abstraite AWS_S3
(introduite dans mon article précédent) pour nous connecter à S3 et télécharger les e-mails dans un compartiment "async-emails" à la fin de la requête (déclenchée via le crochet wp_footer
). Veuillez noter que nous devons garder l'ACL comme "privé" car nous ne voulons pas que les e-mails soient exposés à Internet :
class AsyncEmails_AWS_S3 extends AWS_S3 { function __construct() { // Send all emails at the end of the execution add_action("wp_footer", array($this, "upload_emails_to_s3"), PHP_INT_MAX); } protected function get_acl() { return "private"; } protected function get_bucket() { return "async-emails"; } function upload_emails_to_s3() { $s3Client = $this->get_s3_client(); // Code continued below... } } new AsyncEmails_AWS_S3();
Nous commençons à parcourir les paires d'en-têtes => emaildata enregistrées dans la variable globale $emailqueue
, et obtenons une configuration par défaut de la fonction get_default_email_meta
si les en-têtes sont vides. Dans le code ci-dessous, je ne récupère que le champ "de" des en-têtes (le code pour extraire tous les en-têtes peut être copié depuis la fonction d'origine wp_mail
) :
class AsyncEmails_AWS_S3 extends AWS_S3 { public function get_default_email_meta() { // Code continued from above... return array( 'from' => sprintf( '%s <%s>', get_bloginfo('name'), get_bloginfo('admin_email') ), 'contentType' => 'text/html', 'charset' => strtolower(get_option('blog_charset')) ); } public function upload_emails_to_s3() { // Code continued from above... global $emailqueue; foreach ($emailqueue as $headers => $emails) { $meta = $this->get_default_email_meta(); // Retrieve the "from" from the headers $regexp = '/From:\s*(([^\<]*?) <)? ?\s*\n/i'; if(preg_match($regexp, $headers, $matches)) { $meta['from'] = sprintf( '%s <%s>', $matches[2], $matches[3] ); } // Code continued below... } } }
(.+?)>class AsyncEmails_AWS_S3 extends AWS_S3 { public function get_default_email_meta() { // Code continued from above... return array( 'from' => sprintf( '%s <%s>', get_bloginfo('name'), get_bloginfo('admin_email') ), 'contentType' => 'text/html', 'charset' => strtolower(get_option('blog_charset')) ); } public function upload_emails_to_s3() { // Code continued from above... global $emailqueue; foreach ($emailqueue as $headers => $emails) { $meta = $this->get_default_email_meta(); // Retrieve the "from" from the headers $regexp = '/From:\s*(([^\<]*?) <)? ?\s*\n/i'; if(preg_match($regexp, $headers, $matches)) { $meta['from'] = sprintf( '%s <%s>', $matches[2], $matches[3] ); } // Code continued below... } } }
Enfin, nous téléchargeons les e-mails sur S3. Nous décidons du nombre d'e-mails à télécharger par fichier dans le but d'économiser de l'argent. Les fonctions Lambda sont facturées en fonction du temps dont elles ont besoin pour s'exécuter, calculé sur des durées de 100 ms. Plus une fonction prend de temps, plus elle devient coûteuse.
L'envoi de tous les e-mails en téléchargeant 1 fichier par e-mail est donc plus coûteux que de télécharger 1 fichier pour plusieurs e-mails, car la surcharge liée à l'exécution de la fonction est calculée une fois par e-mail, au lieu d'une seule fois pour de nombreux e-mails, et aussi parce que l'envoi de nombreux e-mails remplit ensemble les durées de 100 ms de manière plus approfondie.
Nous téléchargeons donc de nombreux e-mails par fichier. Combien d'e-mails ? Les fonctions Lambda ont un temps d'exécution maximal (3 secondes par défaut), et si l'opération échoue, elle recommencera depuis le début, et non à partir de l'endroit où elle a échoué. Ainsi, si le fichier contient 100 e-mails et que Lambda parvient à envoyer 50 e-mails avant la fin du délai maximal, il échoue et il réessaye d'exécuter l'opération, en envoyant à nouveau les 50 premiers e-mails. Pour éviter cela, nous devons choisir un nombre d'e-mails par fichier dont nous sommes convaincus qu'il suffit de les traiter avant que le temps maximum ne soit écoulé. Dans notre situation, nous pourrions choisir d'envoyer 25 e-mails par fichier. Le nombre d'e-mails dépend de l'application (les e-mails plus volumineux prendront plus de temps à être envoyés et le temps d'envoi d'un e-mail dépendra de l'infrastructure), nous devons donc faire des tests pour trouver le bon nombre.
Le contenu du fichier est simplement un objet JSON, contenant la méta de l'e-mail sous la propriété "meta" et le bloc d'e-mails sous la propriété "emails":
class AsyncEmails_AWS_S3 extends AWS_S3 { public function upload_emails_to_s3() { // Code continued from above... foreach ($emailqueue as $headers => $emails) { // Code continued from above... // Split the emails into chunks of no more than the value of constant EMAILS_PER_FILE: $chunks = array_chunk($emails, EMAILS_PER_FILE); $filename = time().rand(); for ($chunk_count = 0; $chunk_count < count($chunks); $chunk_count++) { $body = array( 'meta' => $meta, 'emails' => $chunks[$chunk_count], ); // Upload to S3 $s3Client->putObject([ 'ACL' => $this->get_acl(), 'Bucket' => $this->get_bucket(), 'Key' => $filename.$chunk_count.'.json', 'Body' => json_encode($body), ]); } } } }
Pour plus de simplicité, dans le code ci-dessus, je ne télécharge pas les pièces jointes sur S3. Si nos e-mails doivent inclure des pièces jointes, nous devons utiliser la fonction SES SendRawEmail
au lieu de SendEmail
(qui est utilisée dans le script Lambda ci-dessous).
Après avoir ajouté la logique pour télécharger les fichiers avec les e-mails sur S3, nous pouvons passer ensuite au codage de la fonction Lambda.
Codage du script Lambda
Les fonctions Lambda sont également appelées fonctions sans serveur, non pas parce qu'elles ne s'exécutent pas sur un serveur, mais parce que le développeur n'a pas à se soucier du serveur : le développeur fournit simplement le script, et le cloud s'occupe de provisionner le serveur, de déployer et exécutant le script. Par conséquent, comme mentionné précédemment, les fonctions Lambda sont facturées en fonction du temps d'exécution de la fonction.
Le script Node.js suivant effectue le travail requis. Invoquée par l'événement "Put" S3, qui indique qu'un nouvel objet a été créé sur le bucket, la fonction :
- Obtient le chemin du nouvel objet (sous la variable
srcKey
) et le bucket (sous la variablesrcBucket
). - Télécharge l'objet, via
s3.getObject
. - Analyse le contenu de l'objet, via
JSON.parse(response.Body.toString())
, et extrait les e-mails et la méta e-mail. - Parcourt tous les e-mails et les envoie via
ses.sendEmail
.
var async = require('async'); var aws = require('aws-sdk'); var s3 = new aws.S3(); exports.handler = function(event, context, callback) { var srcBucket = event.Records[0].s3.bucket.name; var srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")); // Download the file from S3, parse it, and send the emails async.waterfall([ function download(next) { // Download the file from S3 into a buffer. s3.getObject({ Bucket: srcBucket, Key: srcKey }, next); }, function process(response, next) { var file = JSON.parse(response.Body.toString()); var emails = file.emails; var emailsMeta = file.meta; // Check required parameters if (emails === null || emailsMeta === null) { callback('Bad Request: Missing required data: ' + response.Body.toString()); return; } if (emails.length === 0) { callback('Bad Request: No emails provided: ' + response.Body.toString()); return; } var totalEmails = emails.length; var sentEmails = 0; for (var i = 0; i < totalEmails; i++) { var email = emails[i]; var params = { Destination: { ToAddresses: email.to }, Message: { Subject: { Data: email.subject, Charset: emailsMeta.charset } }, Source: emailsMeta.from }; if (emailsMeta.contentType == 'text/html') { params.Message.Body = { Html: { Data: email.message, Charset: emailsMeta.charset } }; } else { params.Message.Body = { Text: { Data: email.message, Charset: emailsMeta.charset } }; } // Send the email var ses = new aws.SES({ "region": "us-east-1" }); ses.sendEmail(params, function(err, data) { if (err) { console.error('Unable to send email due to an error: ' + err); callback(err); } sentEmails++; if (sentEmails == totalEmails) { next(); } }); } } ], function (err) { if (err) { console.error('Unable to send emails due to an error: ' + err); callback(err); } // Success callback(null); }); };
Ensuite, nous devons charger et configurer la fonction Lambda sur AWS, ce qui implique :
- Création d'un rôle d'exécution accordant des autorisations Lambda pour accéder à S3.
- Création d'un package .zip contenant tout le code, c'est-à-dire la fonction Lambda que nous créons + tous les modules Node.js requis.
- Téléchargement de ce package sur AWS à l'aide d'un outil CLI.
La procédure à suivre est correctement expliquée sur le site AWS, dans le didacticiel sur l'utilisation d'AWS Lambda avec Amazon S3.
Connecter S3 avec la fonction Lambda
Enfin, après avoir créé le compartiment et la fonction Lambda, nous devons les relier ensemble, de sorte que chaque fois qu'un nouvel objet est créé sur le compartiment, il déclenche un événement pour exécuter la fonction Lambda. Pour ce faire, nous allons dans le tableau de bord S3 et cliquez sur la ligne du compartiment, qui affichera ses propriétés :
Ensuite, en cliquant sur Propriétés, nous faisons défiler jusqu'à l'élément "Événements", et là nous cliquons sur Ajouter une notification, et saisissons les champs suivants :
- Nom : nom de la notification, ex : « EmailSender » ;
- Événements : "Put", qui est l'événement déclenché lorsqu'un nouvel objet est créé sur le bucket ;
- Envoyer à : "Fonction Lambda" ;
- Lambda : nom de notre Lambda nouvellement créé, par exemple : "LambdaEmailSender".
Enfin, nous pouvons également configurer le compartiment S3 pour supprimer automatiquement les fichiers contenant les données de messagerie après un certain temps. Pour cela, on se rend dans l'onglet Management du bucket, et on crée une nouvelle règle Lifecycle, définissant après combien de jours les emails doivent expirer :
C'est ça. A partir de ce moment, lors de l'ajout d'un nouvel objet sur le bucket S3 avec le contenu et la méta des e-mails, il déclenchera la fonction Lambda, qui lira le fichier et se connectera à SES pour envoyer les e-mails.
J'ai implémenté cette solution sur mon site, et c'est redevenu rapide : en déchargeant l'envoi d'emails vers un processus externe, que les applications envoient 20 ou 5000 emails ne fait aucune différence, la réponse à l'utilisateur qui a déclenché l'action sera immédiat.
Conclusion
Dans cet article, nous avons analysé pourquoi l'envoi de nombreux e-mails transactionnels en une seule requête peut devenir un goulot d'étranglement dans l'application, et avons créé une solution pour résoudre le problème : au lieu de nous connecter au serveur SMTP depuis l'application (de manière synchrone), nous pouvons envoyer les emails depuis une fonction externe, de manière asynchrone, basée sur une pile AWS S3 + Lambda + SES.
En envoyant des e-mails de manière asynchrone, l'application peut parvenir à envoyer des milliers d'e-mails, mais la réponse à l'utilisateur qui a déclenché l'action ne sera pas affectée. Cependant, pour s'assurer que l'utilisateur n'attend pas que l'e-mail arrive dans la boîte de réception, nous avons également décidé de diviser les e-mails en deux groupes, prioritaires et non prioritaires, et d'envoyer uniquement les e-mails non prioritaires de manière asynchrone. Nous avons fourni une implémentation pour WordPress, qui est plutôt hacky en raison des limitations de la fonction wp_mail
pour l'envoi d'e-mails.
Une leçon de cet article est que les fonctionnalités sans serveur sur une application basée sur un serveur fonctionnent plutôt bien : les sites fonctionnant sur un CMS comme WordPress peuvent améliorer leurs performances en implémentant uniquement des fonctionnalités spécifiques sur le cloud, et éviter une grande complexité liée à la migration. des sites hautement dynamiques à une architecture entièrement sans serveur.