Asynchroniczne wysyłanie e-maili przez AWS SES
Opublikowany: 2022-03-10Większość aplikacji wysyła e-maile, aby komunikować się ze swoimi użytkownikami. E-maile transakcyjne to te, które są wyzwalane przez interakcję użytkownika z aplikacją, na przykład podczas powitania nowego użytkownika po zarejestrowaniu się w serwisie, podania mu linku do zresetowania hasła lub dołączenia faktury po dokonaniu przez użytkownika zakupu. Wszystkie te poprzednie przypadki będą zazwyczaj wymagały wysłania tylko jednego e-maila do użytkownika. Jednak w niektórych innych przypadkach aplikacja musi wysłać znacznie więcej wiadomości e-mail, na przykład gdy użytkownik opublikuje nową treść na stronie, a wszyscy jej obserwatorzy (co na platformie takiej jak Twitter może liczyć miliony użytkowników) otrzymają powiadomienie. W tej drugiej sytuacji, nieodpowiednio zaprojektowane, wysyłanie e-maili może stać się wąskim gardłem w aplikacji.
Tak właśnie stało się w moim przypadku. Mam witrynę, która może wymagać wysłania 20 e-maili po wykonaniu pewnych działań wywołanych przez użytkownika (takich jak powiadomienia użytkownika do wszystkich jej obserwatorów). Początkowo polegał na wysyłaniu wiadomości e-mail za pośrednictwem popularnego dostawcy SMTP działającego w chmurze (takiego jak SendGrid, Mandrill, Mailjet i Mailgun), jednak odpowiedź z powrotem do użytkownika zajęłaby kilka sekund. Najwyraźniej połączenie z serwerem SMTP w celu wysłania tych 20 e-maili znacznie spowolniło proces.
Po oględzinach znalazłem źródła problemu:
- Połączenie synchroniczne
Aplikacja łączy się z serwerem SMTP i synchronicznie czeka na potwierdzenie przed kontynuowaniem wykonywania procesu. - Wysoka latencja
Chociaż mój serwer znajduje się w Singapurze, dostawca SMTP, z którego korzystałem, ma swoje serwery zlokalizowane w USA, co sprawia, że połączenie w obie strony zajmuje dużo czasu. - Brak możliwości ponownego wykorzystania połączenia SMTP Podczas wywoływania funkcji wysyłania e-maila funkcja wysyła e-mail natychmiast, tworząc w tym momencie nowe połączenie SMTP (nie oferuje zebrania wszystkich e-maili i wysłania ich wszystkich razem na końcu żądania, w ramach jednego SMTP połączenie).
Ze względu na #1, czas, jaki użytkownik musi czekać na odpowiedź, jest powiązany z czasem potrzebnym na wysłanie wiadomości e-mail. Z powodu punktu 2 czas na wysłanie jednego e-maila jest stosunkowo długi. A ze względu na 3, czas wysłania 20 e-maili jest 20 razy dłuższy niż czas wysłania jednego e-maila. Podczas gdy wysyłanie tylko jednego e-maila może nie spowalniać aplikacji, wysłanie 20 e-maili z pewnością to robi, wpływając na wrażenia użytkownika.
Zobaczmy, jak możemy rozwiązać ten problem.
Zwracanie uwagi na charakter transakcyjnych wiadomości e-mail
Przede wszystkim musimy zauważyć, że nie wszystkie e-maile mają jednakowe znaczenie. Możemy ogólnie podzielić e-maile na dwie grupy: e-maile priorytetowe i niepriorytetowe. Na przykład, jeśli użytkownik zapomni hasła dostępu do konta, będzie oczekiwał wiadomości e-mail z linkiem do resetowania hasła natychmiast w swojej skrzynce odbiorczej; to jest e-mail priorytetowy. W przeciwieństwie do tego, wysłanie wiadomości e-mail z powiadomieniem, że ktoś, kogo obserwujemy, opublikował nową treść, nie musi natychmiast trafiać do skrzynki odbiorczej użytkownika; to jest wiadomość e-mail niepriorytetowa.
Rozwiązanie musi zoptymalizować sposób wysyłania tych dwóch kategorii wiadomości e-mail. Zakładając, że w trakcie procesu zostanie wysłanych tylko kilka (może 1 lub 2) priorytetowych wiadomości e-mail, a większość e-maili będzie niepriorytetowych, projektujemy rozwiązanie w następujący sposób:
- Priorytetowe wiadomości e-mail mogą po prostu uniknąć problemu z dużymi opóźnieniami, korzystając z dostawcy SMTP znajdującego się w tym samym regionie, w którym aplikacja jest wdrożona. Oprócz dobrego researchu wiąże się to z integracją naszej aplikacji z API dostawcy.
- Wiadomości e-mail niepriorytetowe mogą być wysyłane asynchronicznie oraz w partiach, w których wiele wiadomości e-mail jest wysyłanych razem. Wdrożony na poziomie aplikacji, wymaga odpowiedniego stosu technologicznego.
Zdefiniujmy stos technologii, aby następnie wysyłać wiadomości e-mail asynchronicznie.
Definiowanie stosu technologii
Uwaga: Zdecydowałem się oprzeć swój stos na usługach AWS, ponieważ moja witryna jest już hostowana na AWS EC2. W przeciwnym razie miałbym obciążenie związane z przenoszeniem danych między sieciami kilku firm. Możemy jednak wdrożyć nasze rozwiązanie, korzystając również z innych dostawców usług w chmurze.
Moim pierwszym podejściem było ustawienie kolejki. Przez kolejkę mógłbym sprawić, że aplikacja nie będzie już wysyłać wiadomości e-mail, ale zamiast tego opublikuje wiadomość z treścią i metadanymi wiadomości e-mail w kolejce, a następnie inny proces pobierze wiadomości z kolejki i wyśle je.
Jednak sprawdzając usługę kolejek z AWS o nazwie SQS uznałem, że nie jest to właściwe rozwiązanie, ponieważ:
- Konfiguracja jest dość skomplikowana;
- Wiadomość w standardowej kolejce może przechowywać maksymalnie 256 kb informacji, co może nie wystarczyć, jeśli wiadomość e-mail zawiera załączniki (na przykład faktura). I chociaż możliwe jest podzielenie dużej wiadomości na mniejsze wiadomości, złożoność rośnie jeszcze bardziej.
Wtedy zdałem sobie sprawę, że mogę doskonale naśladować zachowanie kolejki dzięki połączeniu innych usług AWS, S3 i Lambda, które są znacznie prostsze w konfiguracji. S3, rozwiązanie do przechowywania obiektów w chmurze do przechowywania i pobierania danych, może działać jako repozytorium do przesyłania komunikatów, a Lambda, usługa obliczeniowa, która uruchamia kod w odpowiedzi na zdarzenia, może wybrać komunikat i wykonać na nim operację.
Innymi słowy, możemy skonfigurować nasz proces wysyłania wiadomości e-mail w następujący sposób:
- Aplikacja przesyła plik z treścią e-maila + metadane do zasobnika S3.
- Za każdym razem, gdy nowy plik jest przesyłany do zasobnika S3, S3 wyzwala zdarzenie zawierające ścieżkę do nowego pliku.
- Funkcja Lambda wybiera zdarzenie, odczytuje plik i wysyła wiadomość e-mail.
Na koniec musimy zdecydować, jak wysyłać e-maile. Możemy albo nadal korzystać z dostawcy SMTP, którego już mamy, mając funkcję Lambda w interakcji z ich API, albo skorzystać z usługi AWS do wysyłania e-maili, zwanej SES. Korzystanie z SES ma zarówno zalety, jak i wady:
Korzyści:
- Bardzo prosty w obsłudze z poziomu AWS Lambda (wystarczy 2 linijki kodu).
- Jest tańszy: Opłaty Lambda są obliczane na podstawie czasu potrzebnego na wykonanie funkcji, więc połączenie z SES z poziomu sieci AWS zajmie mniej czasu niż połączenie z serwerem zewnętrznym, dzięki czemu funkcja kończy się wcześniej i kosztuje mniej . (O ile SES nie jest dostępny w tym samym regionie, w którym jest hostowana aplikacja; w moim przypadku, ponieważ SES nie jest oferowany w regionie Azji i Pacyfiku (Singapur), w którym znajduje się mój serwer EC2, może lepiej połączyć się z niektórymi Zewnętrzny dostawca SMTP z siedzibą w Azji).
Wady:
- Dostępnych jest niewiele statystyk do monitorowania wysyłanych przez nas e-maili, a dodawanie bardziej zaawansowanych wymaga dodatkowego wysiłku (np. śledzenie, jaki procent e-maili zostało otwartych lub jakie linki zostały kliknięte, należy ustawić za pomocą AWS CloudWatch).
- Jeśli nadal będziemy korzystać z dostawcy SMTP do wysyłania priorytetowych wiadomości e-mail, nie będziemy mieć wszystkich naszych statystyk w jednym miejscu.
Dla uproszczenia w poniższym kodzie będziemy używać SES.
Następnie zdefiniowaliśmy logikę procesu i stosu w następujący sposób: Aplikacja jak zwykle wysyła priorytetowe wiadomości e-mail, ale w przypadku niepriorytetowych przesyła plik z treścią i metadanymi wiadomości e-mail do S3; plik ten jest asynchronicznie przetwarzany przez funkcję Lambda, która łączy się z SES w celu wysłania wiadomości e-mail.
Zacznijmy implementację rozwiązania.
Rozróżnianie wiadomości e-mail priorytetowych i niepriorytetowych
Krótko mówiąc, wszystko zależy od aplikacji, więc musimy zdecydować się na e-mail przez e-mail. Opiszę rozwiązanie, które zaimplementowałem dla WordPressa, które wymaga kilku hacków wokół ograniczeń funkcji wp_mail
. W przypadku innych platform poniższa strategia również będzie działać, ale całkiem możliwe, że będą lepsze strategie, które nie wymagają hacków do działania.
Sposobem na wysłanie wiadomości e-mail w WordPressie jest wywołanie funkcji wp_mail
i nie chcemy tego zmieniać (np. przez wywołanie funkcji wp_mail_synchronous
lub wp_mail_asynchronous
), więc nasza implementacja wp_mail
będzie musiała obsługiwać zarówno przypadki synchroniczne, jak i asynchroniczne, i będzie musiał wiedzieć, do której grupy należy e-mail. Niestety wp_mail
nie oferuje żadnego dodatkowego parametru, na podstawie którego moglibyśmy ocenić te informacje, co widać po jego podpisie:
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() )
Następnie, aby ustalić kategorię wiadomości e-mail dodajemy hakerskie rozwiązanie: domyślnie przypisujemy wiadomość e-mail do grupy priorytetowej, a jeśli $to
zawiera konkretny e-mail (np.: [email protected]) lub jeśli $subject
zaczyna się od specjalnego ciągu znaków (np.: „[Non-priority!]”), to należy do grupy niepriorytetowej (i usuwamy odpowiedni e-mail lub ciąg z tematu). wp_mail
jest funkcją wtykową, więc możemy ją zastąpić, po prostu implementując nową funkcję z tym samym podpisem w naszym pliku functions.php. Początkowo zawiera ten sam kod oryginalnej funkcji wp_mail
, znajdującej się w pliku wp-includes/pluggable.php, aby wyodrębnić wszystkie parametry:
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;
A potem sprawdzamy, czy nie jest to priorytet, w takim przypadku rozwidlamy się do osobnej logiki pod funkcją send_asynchronous_mail
lub, jeśli nie, kontynuujemy wykonywanie tego samego kodu, co w oryginalnej funkcji wp_mail
:
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 // ... }
W naszej funkcji send_asynchronous_mail
, zamiast przesyłać e-mail bezpośrednio do S3, po prostu dodajemy e-mail do zmiennej globalnej $emailqueue
, z której możemy przesłać wszystkie e-maile razem do S3 w jednym połączeniu na końcu żądania:
function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { global $emailqueue; if (!$emailqueue) { $emailqueue = array(); } // Add email to queue. Code continues below... }
Możemy przesłać jeden plik na wiadomość e-mail lub połączyć je tak, aby w jednym pliku zawierało wiele wiadomości e-mail. Ponieważ $headers
zawiera meta e-maili (od, typ treści i zestaw znaków, CC, BCC i pola odpowiedzi), możemy grupować e-maile, gdy mają te same $headers
. W ten sposób wszystkie te e-maile można przesłać w tym samym pliku do S3, a metainformacje $headers
będą zawarte tylko raz w pliku, a nie raz na 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 }
Wreszcie funkcja send_asynchronous_mail
zwraca true
. Proszę zauważyć, że ten kod jest chytry: true
normalnie oznaczałoby, że wiadomość e-mail została wysłana pomyślnie, ale w tym przypadku nie została jeszcze wysłana i może się nie powieść. Z tego powodu funkcja wywołująca wp_mail
nie może traktować true
odpowiedzi jako „wiadomość e-mail została wysłana pomyślnie”, ale potwierdzenie, że została umieszczona w kolejce. Dlatego ważne jest, aby ograniczyć tę technikę do niepriorytetowych wiadomości e-mail, aby w przypadku niepowodzenia proces mógł nadal ponawiać próbę w tle, a użytkownik nie spodziewał się, że wiadomość e-mail będzie już w jej skrzynce odbiorczej:
function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { // Continued from above... // That's it! return true; }
Przesyłanie wiadomości e-mail do S3
W moim poprzednim artykule „Udostępnianie danych między wieloma serwerami za pośrednictwem AWS S3” opisałem, jak utworzyć zasobnik w S3 i jak przesyłać pliki do zasobnika za pomocą SDK. Cały poniższy kod kontynuuje implementację rozwiązania dla WordPressa, stąd łączymy się z AWS za pomocą SDK for PHP.
Możemy rozszerzyć klasę abstrakcyjną AWS_S3
(przedstawioną w moim poprzednim artykule), aby połączyć się z S3 i przesłać wiadomości e-mail do zasobnika „async-emails” na końcu żądania (wywoływanego przez hak wp_footer
). Proszę zauważyć, że musimy zachować ACL jako „prywatną”, ponieważ nie chcemy, aby e-maile były udostępniane w Internecie:
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();
Rozpoczynamy iterację przez pary nagłówków => dane email zapisane w zmiennej globalnej $emailqueue
i pobieramy domyślną konfigurację z funkcji get_default_email_meta
, jeśli nagłówki są puste. W poniższym kodzie pobieram tylko pole „from” z nagłówków (kod do wyodrębnienia wszystkich nagłówków można skopiować z oryginalnej funkcji 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... } } }
Na koniec przesyłamy e-maile do S3. Decydujemy o tym, ile e-maili wgrać na plik z zamiarem zaoszczędzenia pieniędzy. Funkcje lambda są naliczane na podstawie czasu, jaki potrzebują na wykonanie, obliczonego dla rozpiętości 100ms. Im więcej czasu wymaga funkcja, tym droższa staje się.
Wysyłanie wszystkich e-maili przez przesłanie 1 pliku na e-mail jest zatem droższe niż przesyłanie 1 pliku na wiele e-maili, ponieważ obciążenie związane z wykonaniem funkcji jest obliczane raz na e-mail, a nie tylko raz na wiele e-maili, a także dlatego, że wysyłanie wielu e-maili razem wypełnia zakresy 100ms dokładniej.
Dlatego przesyłamy wiele e-maili na plik. Ile e-maili? Funkcje lambda mają maksymalny czas wykonania (domyślnie 3 sekundy), a jeśli operacja się nie powiedzie, będzie ponawiać próbę od początku, a nie od miejsca, w którym się nie udało. Tak więc, jeśli plik zawiera 100 e-maili, a Lambda zdoła wysłać 50 e-maili przed upływem maksymalnego czasu, to się nie powiedzie i ponawia operację, ponownie wysyłając pierwsze 50 e-maili. Aby tego uniknąć, musimy wybrać liczbę wiadomości e-mail na plik, co do których jesteśmy pewni, że wystarczy je przetworzyć, zanim upłynie maksymalny czas. W naszej sytuacji mogliśmy wysłać 25 e-maili na plik. Liczba e-maili zależy od aplikacji (większe e-maile zostaną wysłane dłużej, a czas wysłania e-maila będzie zależał od infrastruktury), dlatego powinniśmy przeprowadzić testy, aby znaleźć odpowiednią liczbę.
Zawartość pliku to po prostu obiekt JSON, zawierający meta e-mail we właściwości „meta” i fragment wiadomości e-mail we właściwości „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), ]); } } } }
Dla uproszczenia w powyższym kodzie nie przesyłam załączników do S3. Jeśli nasze e-maile muszą zawierać załączniki, musimy użyć funkcji SES SendRawEmail
zamiast SendEmail
(który jest używany w skrypcie Lambda poniżej).
Po dodaniu logiki wgrywania plików z e-mailami do S3, możemy przejść do kodowania funkcji Lambda.
Kodowanie skryptu lambda
Funkcje lambda są również nazywane funkcjami bezserwerowymi, nie dlatego, że nie działają na serwerze, ale dlatego, że programista nie musi się martwić o serwer: programista po prostu dostarcza skrypt, a chmura zajmuje się udostępnianiem serwera, wdrażaniem i uruchomienie skryptu. W związku z tym, jak wspomniano wcześniej, funkcje Lambda są rozliczane na podstawie czasu wykonania funkcji.
Poniższy skrypt Node.js wykonuje wymagane zadanie. Wywoływana przez zdarzenie S3 „Put”, które wskazuje, że w zasobniku został utworzony nowy obiekt, funkcja:
- Uzyskuje ścieżkę nowego obiektu (pod zmienną
srcKey
) i zasobnik (pod zmiennąsrcBucket
). - Pobiera obiekt za pośrednictwem
s3.getObject
. - Analizuje zawartość obiektu za pomocą
JSON.parse(response.Body.toString())
i wyodrębnia wiadomości e-mail oraz meta wiadomości e-mail. - Iteruje przez wszystkie e-maile i wysyła je przez
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); }); };
Następnie musimy wgrać i skonfigurować funkcję Lambda do AWS, co wiąże się z:
- Utworzenie roli wykonawczej przyznającej Lambda uprawnienia dostępu do S3.
- Stworzenie paczki .zip zawierającej cały kod, czyli tworzona przez nas funkcja Lambda + wszystkie wymagane moduły Node.js.
- Przesyłanie tego pakietu do AWS za pomocą narzędzia CLI.
Jak to zrobić, jest odpowiednio wyjaśnione na stronie AWS, w samouczku dotyczącym używania AWS Lambda z Amazon S3.
Podłączanie S3 z funkcją Lambda
Wreszcie, po utworzeniu wiaderka i funkcji Lambda, musimy połączyć je ze sobą, aby za każdym razem, gdy w wiadrze zostanie utworzony nowy obiekt, wywoła to zdarzenie wykonujące funkcję Lambda. W tym celu przechodzimy do dashboardu S3 i klikamy wiersz wiadra, który pokaże jego właściwości:
Następnie klikając Właściwości, przewijamy w dół do pozycji „Zdarzenia”, a tam klikamy Dodaj powiadomienie i wypełniamy następujące pola:
- Nazwa: nazwa powiadomienia, np.: „EmailSender”;
- Zdarzenia: „Put”, które jest zdarzeniem wyzwalanym, gdy w zasobniku tworzony jest nowy obiekt;
- Wyślij do: „Funkcja Lambda”;
- Lambda: nazwa naszej nowo utworzonej Lambdy, np.: „LambdaEmailSender”.
Na koniec możemy również ustawić zasobnik S3, aby po pewnym czasie automatycznie usuwał pliki zawierające dane e-mail. W tym celu przechodzimy do zakładki Zarządzanie zasobnika i tworzymy nową regułę cyklu życia, definiującą po ilu dniach wiadomości e-mail muszą wygasnąć:
Otóż to. Od tego momentu, dodając nowy obiekt na wiadrze S3 z treścią i meta dla e-maili, uruchomi funkcję Lambda, która odczyta plik i połączy się z SES w celu wysłania e-maili.
Wdrożyłem to rozwiązanie na swojej stronie i znów stało się szybkie: odciążając wysyłanie e-maili do zewnętrznego procesu, czy aplikacje wysyłają 20 czy 5000 e-maili nie ma znaczenia, odpowiedź dla użytkownika, który wywołał akcję, będzie natychmiastowy.
Wniosek
W tym artykule przeanalizowaliśmy, dlaczego wysyłanie wielu transakcyjnych wiadomości e-mail w jednym żądaniu może stać się wąskim gardłem w aplikacji i stworzyliśmy rozwiązanie problemu: zamiast łączyć się z serwerem SMTP z poziomu aplikacji (synchronicznie), możemy wysyłaj maile z funkcji zewnętrznej, asynchronicznie, w oparciu o stos AWS S3 + Lambda + SES.
Wysyłając e-maile asynchronicznie, aplikacja może wysłać tysiące e-maili, ale nie wpłynie to na odpowiedź użytkownika, który wywołał akcję. Jednak, aby upewnić się, że użytkownik nie będzie czekał na wiadomość e-mail w skrzynce odbiorczej, zdecydowaliśmy się również podzielić wiadomości e-mail na dwie grupy, priorytetową i niepriorytetową, i wysyłać tylko niepriorytetowe wiadomości e-mail asynchronicznie. Dostarczyliśmy implementację dla WordPressa, która jest dość zręczna ze względu na ograniczenia funkcji wp_mail
do wysyłania e-maili.
Lekcja z tego artykułu jest taka, że funkcje bezserwerowe w aplikacji opartej na serwerze działają całkiem dobrze: witryny działające w systemie CMS, takim jak WordPress, mogą poprawić swoją wydajność, wdrażając tylko określone funkcje w chmurze i uniknąć dużej złożoności wynikającej z migracji wysoce dynamiczne witryny do architektury w pełni bezserwerowej.