Asynchrones Senden von E-Mails über AWS SES
Veröffentlicht: 2022-03-10Die meisten Anwendungen senden E-Mails, um mit ihren Benutzern zu kommunizieren. Transaktions-E-Mails werden durch die Interaktion des Benutzers mit der Anwendung ausgelöst, z. B. wenn ein neuer Benutzer nach der Registrierung auf der Website begrüßt wird, dem Benutzer ein Link zum Zurücksetzen des Passworts gegeben wird oder eine Rechnung angehängt wird, nachdem der Benutzer einen Kauf getätigt hat. Alle diese vorherigen Fälle erfordern normalerweise das Senden nur einer E-Mail an den Benutzer. In einigen anderen Fällen muss die Anwendung jedoch viel mehr E-Mails senden, z. B. wenn ein Benutzer neue Inhalte auf der Website veröffentlicht und alle seine Follower (die auf einer Plattform wie Twitter Millionen von Benutzern ausmachen können) eine E-Mail erhalten Benachrichtigung. In dieser letzteren Situation kann das Senden von E-Mails, wenn es nicht richtig aufgebaut ist, zu einem Engpass in der Anwendung werden.
Das ist in meinem Fall passiert. Ich habe eine Website, die nach einigen vom Benutzer ausgelösten Aktionen (z. B. Benutzerbenachrichtigungen an alle ihre Follower) möglicherweise 20 E-Mails senden muss. Ursprünglich verließ es sich darauf, die E-Mails über einen beliebten Cloud-basierten SMTP-Anbieter (wie SendGrid, Mandrill, Mailjet und Mailgun) zu senden, die Antwort an den Benutzer dauerte jedoch Sekunden. Offensichtlich verlangsamte die Verbindung zum SMTP-Server zum Senden dieser 20 E-Mails den Prozess erheblich.
Nach der Inspektion fand ich die Ursachen des Problems heraus:
- Synchrone Verbindung
Die Anwendung verbindet sich mit dem SMTP-Server und wartet synchron auf eine Bestätigung, bevor sie mit der Ausführung des Prozesses fortfährt. - Hohe Latenz
Während sich mein Server in Singapur befindet, befinden sich die Server des von mir verwendeten SMTP-Anbieters in den USA, wodurch die Roundtrip-Verbindung viel Zeit in Anspruch nimmt. - Keine Wiederverwendbarkeit der SMTP-Verbindung Wenn Sie die Funktion zum Senden einer E-Mail aufrufen, sendet die Funktion die E-Mail sofort und erstellt in diesem Moment eine neue SMTP-Verbindung (sie bietet nicht an, alle E-Mails zu sammeln und am Ende der Anfrage alle zusammen unter einem einzigen SMTP zu senden Verbindung).
Aufgrund von #1 ist die Zeit, die der Benutzer auf die Antwort warten muss, an die Zeit gebunden, die zum Senden der E-Mails benötigt wird. Aufgrund von #2 ist die Zeit zum Versenden einer E-Mail relativ hoch. Und wegen Nr. 3 ist die Zeit zum Versenden von 20 E-Mails 20 Mal so lang wie die Zeit, die zum Versenden einer E-Mail benötigt wird. Während das Senden von nur einer E-Mail die Anwendung möglicherweise nicht furchtbar langsamer macht, tut es das Senden von 20 E-Mails sicherlich, was die Benutzererfahrung beeinträchtigt.
Mal sehen, wie wir dieses Problem lösen können.
Achten Sie auf die Art von Transaktions-E-Mails
Vor allem müssen wir feststellen, dass nicht alle E-Mails gleich wichtig sind. Wir können E-Mails grob in zwei Gruppen einteilen: Prioritäts- und Nicht-Prioritäts-E-Mails. Wenn der Benutzer beispielsweise das Passwort für den Zugriff auf das Konto vergessen hat, erwartet er die E-Mail mit dem Link zum Zurücksetzen des Passworts sofort in seinem Posteingang. das ist eine Prioritäts-E-Mail. Im Gegensatz dazu muss das Versenden einer E-Mail mit der Benachrichtigung, dass jemand, dem wir folgen, neue Inhalte gepostet hat, nicht sofort im Posteingang des Benutzers ankommen; das ist eine E-Mail ohne Priorität.
Die Lösung muss den Versand dieser beiden E-Mail-Kategorien optimieren. Unter der Annahme, dass während des Prozesses nur wenige (vielleicht 1 oder 2) E-Mails mit Priorität gesendet werden und der Großteil der E-Mails keine Priorität hat, entwerfen wir die Lösung wie folgt:
- Prioritäts-E-Mails können das Problem der hohen Latenz einfach vermeiden, indem Sie einen SMTP-Anbieter verwenden, der sich in derselben Region befindet, in der die Anwendung bereitgestellt wird. Dazu gehört neben guter Recherche auch die Integration unserer Anwendung mit der API des Anbieters.
- E-Mails ohne Priorität können asynchron und in Stapeln gesendet werden, wenn viele E-Mails zusammen gesendet werden. Auf Anwendungsebene implementiert, erfordert es einen geeigneten Technologie-Stack.
Als Nächstes definieren wir den Technologie-Stack zum asynchronen Senden von E-Mails.
Definieren des Technologie-Stacks
Hinweis: Ich habe mich entschieden, meinen Stack auf AWS-Services aufzubauen, da meine Website bereits auf AWS EC2 gehostet wird. Andernfalls hätte ich einen Overhead durch das Verschieben von Daten zwischen den Netzwerken mehrerer Unternehmen. Wir können unsere Lösungen jedoch auch mit anderen Cloud-Dienstleistern implementieren.
Mein erster Ansatz bestand darin, eine Warteschlange einzurichten. Durch eine Warteschlange könnte ich die Anwendung veranlassen, die E-Mails nicht mehr zu senden, sondern stattdessen eine Nachricht mit dem E-Mail-Inhalt und den Metadaten in einer Warteschlange zu veröffentlichen und dann einen anderen Prozess die Nachrichten aus der Warteschlange abholen und die E-Mails senden zu lassen.
Als ich jedoch den Warteschlangendienst von AWS namens SQS überprüfte, entschied ich, dass dies keine geeignete Lösung war, weil:
- Es ist ziemlich komplex einzurichten;
- Eine Standard-Warteschlangennachricht kann nur bis zu 256 KB an Informationen speichern, was möglicherweise nicht ausreicht, wenn die E-Mail Anhänge enthält (z. B. eine Rechnung). Und obwohl es möglich ist, eine große Nachricht in kleinere Nachrichten aufzuteilen, wächst die Komplexität noch weiter.
Dann wurde mir klar, dass ich das Verhalten einer Warteschlange durch eine Kombination anderer AWS-Dienste, S3 und Lambda, die viel einfacher einzurichten sind, perfekt imitieren kann. S3, eine Cloud-Objektspeicherlösung zum Speichern und Abrufen von Daten, kann als Repository zum Hochladen der Nachrichten fungieren, und Lambda, ein Computerdienst, der Code als Reaktion auf Ereignisse ausführt, kann eine Nachricht auswählen und eine Operation damit ausführen.
Mit anderen Worten, wir können unseren E-Mail-Sendeprozess wie folgt einrichten:
- Die Anwendung lädt eine Datei mit dem E-Mail-Inhalt und den Metadaten in einen S3-Bucket hoch.
- Immer wenn eine neue Datei in den S3-Bucket hochgeladen wird, löst S3 ein Ereignis aus, das den Pfad zu der neuen Datei enthält.
- Eine Lambda-Funktion wählt das Ereignis aus, liest die Datei und sendet die E-Mail.
Schließlich müssen wir entscheiden, wie wir E-Mails versenden. Wir können entweder weiterhin den SMTP-Anbieter verwenden, den wir bereits haben, und die Lambda-Funktion mit ihren APIs interagieren lassen, oder den AWS-Service zum Senden von E-Mails namens SES verwenden. Die Verwendung von SES hat sowohl Vor- als auch Nachteile:
Leistungen:
- Sehr einfach in AWS Lambda zu verwenden (es sind nur 2 Codezeilen erforderlich).
- Es ist billiger: Die Lambda-Gebühren werden basierend auf der Zeit berechnet, die für die Ausführung der Funktion benötigt wird, sodass die Verbindung zu SES innerhalb des AWS-Netzwerks kürzer dauert als die Verbindung zu einem externen Server, wodurch die Funktion früher beendet wird und weniger kostet . (Es sei denn, SES ist nicht in derselben Region verfügbar, in der die Anwendung gehostet wird; in meinem Fall, da SES in der Region Asien-Pazifik (Singapur), wo sich mein EC2-Server befindet, nicht angeboten wird, wäre es vielleicht besser, mich mit einigen zu verbinden in Asien ansässiger externer SMTP-Anbieter).
Nachteile:
- Es werden nicht viele Statistiken zur Überwachung unserer gesendeten E-Mails bereitgestellt, und das Hinzufügen aussagekräftigerer erfordert zusätzlichen Aufwand (z. B.: Das Nachverfolgen, wie viel Prozent der E-Mails geöffnet oder auf welche Links geklickt wurde, muss über AWS CloudWatch eingerichtet werden).
- Wenn wir weiterhin den SMTP-Anbieter zum Senden der Prioritäts-E-Mails verwenden, haben wir unsere Statistiken nicht alle zusammen an einem Ort.
Der Einfachheit halber verwenden wir im folgenden Code SES.
Wir haben dann die Logik des Prozesses und des Stapels wie folgt definiert: Die Anwendung sendet E-Mails mit Priorität wie gewohnt, aber für E-Mails ohne Priorität lädt sie eine Datei mit E-Mail-Inhalt und Metadaten auf S3 hoch; Diese Datei wird asynchron von einer Lambda-Funktion verarbeitet, die eine Verbindung zu SES herstellt, um die E-Mail zu senden.
Beginnen wir mit der Implementierung der Lösung.
Unterscheidung zwischen Prioritäts- und Nicht-Prioritäts-E-Mails
Kurz gesagt, das hängt alles von der Anwendung ab, also müssen wir E-Mail für E-Mail entscheiden. Ich werde eine Lösung beschreiben, die ich für WordPress implementiert habe, die einige Hacks um die Einschränkungen der Funktion wp_mail
. Für andere Plattformen wird die folgende Strategie auch funktionieren, aber möglicherweise wird es bessere Strategien geben, die keine Hacks erfordern, um zu funktionieren.
Der Weg, eine E-Mail in WordPress zu senden, ist durch Aufrufen der Funktion wp_mail
, und wir wollen das nicht ändern (z. B. durch Aufrufen der Funktion wp_mail_synchronous
oder wp_mail_asynchronous
), also muss unsere Implementierung von wp_mail
sowohl synchrone als auch asynchrone Fälle behandeln, und müssen wissen, zu welcher Gruppe die E-Mail gehört. Unglücklicherweise bietet wp_mail
keinen zusätzlichen Parameter, anhand dessen wir diese Informationen auswerten könnten, wie aus seiner Signatur ersichtlich ist:
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() )
Um die Kategorie der E-Mail herauszufinden, fügen wir dann eine Hacky-Lösung hinzu: Standardmäßig machen wir eine E-Mail zur Prioritätsgruppe, und wenn $to
eine bestimmte E-Mail enthält (z. B.: [email protected]), oder wenn $subject
mit einem speziellen String beginnt (z. B.: „[Non-priority!]“), dann gehört es zur Non-Priority-Gruppe (und wir entfernen die entsprechende E-Mail oder String aus dem Betreff). wp_mail
ist eine austauschbare Funktion, sodass wir sie einfach überschreiben können, indem wir eine neue Funktion mit derselben Signatur in unserer Datei functions.php implementieren. Anfänglich enthält es denselben Code der ursprünglichen wp_mail
-Funktion, die sich in der Datei wp-includes/pluggable.php befindet, um alle Parameter zu extrahieren:
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;
Und dann prüfen wir, ob es sich um keine Priorität handelt. In diesem Fall verzweigen wir dann zu einer separaten Logik unter der Funktion send_asynchronous_mail
oder, wenn dies nicht der Fall ist, führen wir weiterhin denselben Code wie in der ursprünglichen wp_mail
Funktion aus:
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 // ... }
In unserer Funktion send_asynchronous_mail
fügen wir die E-Mail, anstatt die E-Mail direkt in S3 hochzuladen, einfach zu einer globalen Variable $emailqueue
, von der aus wir alle E-Mails zusammen in einer einzigen Verbindung am Ende der Anfrage nach S3 hochladen können:
function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { global $emailqueue; if (!$emailqueue) { $emailqueue = array(); } // Add email to queue. Code continues below... }
Wir können eine Datei pro E-Mail hochladen oder sie so bündeln, dass wir in einer Datei viele E-Mails enthalten. Da $headers
E-Mail-Meta enthält (von, Inhaltstyp und Zeichensatz, CC, BCC und Antwortfelder), können wir E-Mails gruppieren, wenn sie die gleichen $headers
haben. Auf diese Weise können diese E-Mails alle in derselben Datei in S3 hochgeladen werden, und die $headers
Metainformationen werden nur einmal in die Datei aufgenommen, anstatt einmal pro 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 }
Schließlich gibt die Funktion send_asynchronous_mail
true
zurück. Bitte beachten Sie, dass dieser Code hacky ist: true
würde normalerweise bedeuten, dass die E-Mail erfolgreich gesendet wurde, aber in diesem Fall wurde sie noch nicht einmal gesendet und könnte perfekt fehlschlagen. Aus diesem Grund darf die Funktion, die wp_mail
, eine true
Antwort nicht als „die E-Mail wurde erfolgreich gesendet“ behandeln, sondern als Bestätigung, dass sie in die Warteschlange gestellt wurde. Aus diesem Grund ist es wichtig, diese Technik auf E-Mails ohne Priorität zu beschränken, damit der Prozess im Falle eines Fehlschlags im Hintergrund weiter versuchen kann und der Benutzer nicht erwartet, dass sich die E-Mail bereits in seinem Posteingang befindet:
function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { // Continued from above... // That's it! return true; }
Hochladen von E-Mails auf S3
In meinem vorherigen Artikel „Daten zwischen mehreren Servern über AWS S3 teilen“ habe ich beschrieben, wie man einen Bucket in S3 erstellt und Dateien über das SDK in den Bucket hochlädt. Der gesamte Code unten setzt die Implementierung einer Lösung für WordPress fort, daher verbinden wir uns mit AWS über das SDK für PHP.
Wir können von der abstrakten Klasse AWS_S3
(eingeführt in meinem vorherigen Artikel) ausgehen, um eine Verbindung zu S3 herzustellen und die E-Mails am Ende der Anfrage (ausgelöst durch wp_footer
Hook) in einen Bucket „async-emails“ hochzuladen. Bitte beachten Sie, dass wir die ACL als „privat“ halten müssen, da wir nicht möchten, dass die E-Mails dem Internet ausgesetzt werden:
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();
Wir beginnen mit der Iteration durch die Kopfzeilenpaare => emaildata gespeichert in der globalen Variablen $emailqueue
und erhalten eine Standardkonfiguration von der Funktion get_default_email_meta
für den Fall, dass die Kopfzeilen leer sind. Im folgenden Code rufe ich nur das „from“-Feld aus den Headern ab (der Code zum Extrahieren aller Header kann aus der ursprünglichen Funktion wp_mail
kopiert werden):
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... } } }
Schließlich laden wir die E-Mails in S3 hoch. Wir entscheiden, wie viele E-Mails pro Datei hochgeladen werden, um Geld zu sparen. Lambda-Funktionen werden basierend auf der Zeit berechnet, die sie für die Ausführung benötigen, berechnet auf Spannen von 100 ms. Je mehr Zeit eine Funktion benötigt, desto teurer wird sie.
Das Versenden aller E-Mails durch das Hochladen von 1 Datei pro E-Mail ist dann teurer als das Hochladen von 1 Datei pro vielen E-Mails, da der Overhead für die Ausführung der Funktion einmal pro E-Mail berechnet wird, anstatt nur einmal für viele E-Mails, und auch weil viele E-Mails gesendet werden zusammen füllt die 100-ms-Spanne gründlicher aus.
Wir laden also viele E-Mails pro Datei hoch. Wie viele E-Mails? Lambda-Funktionen haben eine maximale Ausführungszeit (standardmäßig 3 Sekunden), und wenn die Operation fehlschlägt, wird sie von Anfang an wiederholt, nicht von dort, wo sie fehlgeschlagen ist. Wenn also die Datei 100 E-Mails enthält und Lambda es schafft, 50 E-Mails zu senden, bevor die maximale Zeit abgelaufen ist, schlägt es fehl und es versucht erneut, die Operation auszuführen, wobei die ersten 50 E-Mails erneut gesendet werden. Um dies zu vermeiden, müssen wir eine Anzahl von E-Mails pro Datei auswählen, von der wir sicher sind, dass sie ausreicht, um sie zu verarbeiten, bevor die maximale Zeit abgelaufen ist. In unserer Situation könnten wir uns dafür entscheiden, 25 E-Mails pro Datei zu senden. Die Anzahl der E-Mails hängt von der Anwendung ab (das Versenden größerer E-Mails dauert länger, und die Zeit zum Versenden einer E-Mail hängt von der Infrastruktur ab), daher sollten wir einige Tests durchführen, um die richtige Anzahl zu ermitteln.
Der Inhalt der Datei ist einfach ein JSON-Objekt, das das E-Mail-Meta unter der Eigenschaft „meta“ und den Teil der E-Mails unter der Eigenschaft „emails“ enthält:
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), ]); } } } }
Der Einfachheit halber lade ich im obigen Code die Anhänge nicht in S3 hoch. Wenn unsere E-Mails Anhänge enthalten müssen, müssen wir die SES-Funktion SendRawEmail
anstelle von SendEmail
verwenden (das im Lambda-Skript unten verwendet wird).
Nachdem wir die Logik zum Hochladen der Dateien mit E-Mails in S3 hinzugefügt haben, können wir mit der Codierung der Lambda-Funktion fortfahren.
Codieren des Lambda-Skripts
Lambda-Funktionen werden auch serverlose Funktionen genannt, nicht weil sie nicht auf einem Server laufen, sondern weil sich der Entwickler nicht um den Server kümmern muss: Der Entwickler stellt einfach das Skript zur Verfügung, und die Cloud kümmert sich um die Bereitstellung des Servers, das Deployment und Ausführen des Skripts. Daher werden, wie bereits erwähnt, Lambda-Funktionen basierend auf der Ausführungszeit der Funktion berechnet.
Das folgende Node.js-Skript erledigt die erforderliche Aufgabe. Die Funktion wird vom S3-Ereignis „Put“ aufgerufen, das angibt, dass ein neues Objekt im Bucket erstellt wurde:
- Ruft den Pfad des neuen Objekts (unter Variable
srcKey
) und Bucket (unter VariablesrcBucket
) ab. - Lädt das Objekt über
s3.getObject
. - Analysiert den Inhalt des Objekts über
JSON.parse(response.Body.toString())
und extrahiert die E-Mails und das E-Mail-Meta. - Durchläuft alle E-Mails und sendet sie über
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); }); };
Als nächstes müssen wir die Lambda-Funktion in AWS hochladen und konfigurieren, was Folgendes beinhaltet:
- Erstellen einer Ausführungsrolle, die Lambda-Berechtigungen für den Zugriff auf S3 gewährt.
- Erstellen eines .zip-Pakets, das den gesamten Code enthält, dh die Lambda-Funktion, die wir erstellen, + alle erforderlichen Node.js-Module.
- Hochladen dieses Pakets auf AWS mit einem CLI-Tool.
Wie Sie diese Dinge tun, wird auf der AWS-Website im Tutorial zur Verwendung von AWS Lambda mit Amazon S3 ausführlich erklärt.
Anschließen von S3 mit der Lambda-Funktion
Nachdem der Bucket und die Lambda-Funktion erstellt wurden, müssen wir schließlich beide miteinander verknüpfen, sodass jedes Mal, wenn ein neues Objekt im Bucket erstellt wird, ein Ereignis zur Ausführung der Lambda-Funktion ausgelöst wird. Dazu gehen wir zum S3-Dashboard und klicken auf die Bucket-Zeile, die ihre Eigenschaften anzeigt:
Klicken Sie dann auf Eigenschaften, scrollen Sie nach unten zum Punkt „Ereignisse“, klicken Sie dort auf Benachrichtigung hinzufügen und füllen Sie die folgenden Felder aus:
- Name: Name der Benachrichtigung, zB: „EmailSender“;
- Ereignisse: „Put“, das ist das Ereignis, das ausgelöst wird, wenn ein neues Objekt im Bucket erstellt wird;
- Senden an: „Lambda-Funktion“;
- Lambda: Name unseres neu erstellten Lambda, zB: „LambdaEmailSender“.
Schließlich können wir den S3-Bucket auch so einstellen, dass er die Dateien mit den E-Mail-Daten nach einiger Zeit automatisch löscht. Dazu gehen wir auf die Registerkarte Management des Buckets und erstellen eine neue Lifecycle-Regel, die definiert, nach wie vielen Tagen die E-Mails ablaufen müssen:
Das ist es. Ab diesem Moment wird beim Hinzufügen eines neuen Objekts zum S3-Bucket mit Inhalt und Meta für die E-Mails die Lambda-Funktion ausgelöst, die die Datei liest und eine Verbindung zu SES herstellt, um die E-Mails zu senden.
Ich habe diese Lösung auf meiner Website implementiert, und es wurde wieder schnell: Durch die Auslagerung des E-Mail-Versands an einen externen Prozess spielt es keine Rolle, ob die Anwendungen 20 oder 5000 E-Mails senden, die Antwort an den Benutzer, der die Aktion ausgelöst hat, wird sein sofortig.
Fazit
In diesem Artikel haben wir analysiert, warum das Senden vieler Transaktions-E-Mails in einer einzigen Anfrage zu einem Engpass in der Anwendung werden kann, und eine Lösung entwickelt, um das Problem zu lösen: Anstatt sich aus der Anwendung heraus (synchron) mit dem SMTP-Server zu verbinden, können wir dies tun Senden Sie die E-Mails asynchron von einer externen Funktion, basierend auf einem Stack aus AWS S3 + Lambda + SES.
Durch das asynchrone Senden von E-Mails kann die Anwendung Tausende von E-Mails senden, ohne dass die Antwort an den Benutzer, der die Aktion ausgelöst hat, beeinträchtigt wird. Um jedoch sicherzustellen, dass der Benutzer nicht darauf wartet, dass die E-Mail im Posteingang ankommt, haben wir uns auch entschieden, E-Mails in zwei Gruppen aufzuteilen, Priorität und Nicht-Priorität, und nur die Nicht-Prioritäts-E-Mails asynchron zu senden. Wir haben eine Implementierung für WordPress bereitgestellt, die aufgrund der Einschränkungen der Funktion wp_mail
zum Versenden von E-Mails ziemlich hackig ist.
Eine Lektion aus diesem Artikel ist, dass serverlose Funktionen in einer serverbasierten Anwendung ziemlich gut funktionieren: Websites, die auf einem CMS wie WordPress ausgeführt werden, können ihre Leistung verbessern, indem sie nur bestimmte Funktionen in der Cloud implementieren und viel Komplexität vermeiden, die durch die Migration entsteht hochdynamische Sites bis hin zu einer vollständig serverlosen Architektur.