إرسال رسائل البريد الإلكتروني بشكل غير متزامن من خلال AWS SES
نشرت: 2022-03-10ترسل معظم التطبيقات رسائل بريد إلكتروني للتواصل مع مستخدميها. رسائل البريد الإلكتروني للمعاملات هي تلك التي يتم تشغيلها من خلال تفاعل المستخدم مع التطبيق ، مثل عند الترحيب بمستخدم جديد بعد التسجيل في الموقع ، أو إعطاء المستخدم رابطًا لإعادة تعيين كلمة المرور ، أو إرفاق فاتورة بعد قيام المستخدم بالشراء. ستتطلب كل هذه الحالات السابقة عادةً إرسال بريد إلكتروني واحد فقط إلى المستخدم. ومع ذلك ، في بعض الحالات الأخرى ، يحتاج التطبيق إلى إرسال المزيد من رسائل البريد الإلكتروني ، مثل عندما ينشر المستخدم محتوى جديدًا على الموقع ، وسيتلقى جميع متابعيه (الذين قد يصل عددهم إلى ملايين المستخدمين ، في نظام أساسي مثل Twitter) تنبيه. في هذه الحالة الأخيرة ، إذا لم تتم هندستها بشكل صحيح ، فقد يصبح إرسال رسائل البريد الإلكتروني عقبة في التطبيق.
هذا ما حدث في حالتي. لدي موقع قد يحتاج إلى إرسال 20 بريدًا إلكترونيًا بعد بعض الإجراءات التي يقوم بها المستخدم (مثل إشعارات المستخدم لجميع متابعيها). في البداية ، اعتمدت على إرسال رسائل البريد الإلكتروني من خلال مزود SMTP مشهور قائم على السحابة (مثل SendGrid و Mandrill و Mailjet و Mailgun) ، ومع ذلك فإن الرد على المستخدم سيستغرق ثوانٍ. من الواضح أن الاتصال بخادم SMTP لإرسال رسائل البريد الإلكتروني العشرين هذه أدى إلى إبطاء العملية بشكل كبير.
بعد الفحص اكتشفت مصادر المشكلة:
- اتصال متزامن
يتصل التطبيق بخادم SMTP وينتظر إقرارًا بشكل متزامن قبل متابعة تنفيذ العملية. - زمن انتقال مرتفع
بينما يقع خادمي في سنغافورة ، فإن موفر SMTP الذي كنت أستخدمه لديه خوادمه الموجودة في الولايات المتحدة ، مما يجعل الاتصال ذهابًا وإيابًا يستغرق وقتًا طويلاً. - لا يمكن إعادة استخدام اتصال SMTP عند استدعاء الوظيفة لإرسال بريد إلكتروني ، ترسل الوظيفة البريد الإلكتروني على الفور ، مما يؤدي إلى إنشاء اتصال SMTP جديد في تلك اللحظة (لا يعرض جمع كل رسائل البريد الإلكتروني وإرسالها معًا في نهاية الطلب ، ضمن SMTP واحد الإتصال).
بسبب # 1 ، فإن الوقت الذي يجب على المستخدم انتظار الرد فيه مرتبط بالوقت الذي يستغرقه إرسال رسائل البريد الإلكتروني. بسبب # 2 ، يكون وقت إرسال بريد إلكتروني واحد مرتفعًا نسبيًا. وبسبب # 3 ، فإن الوقت اللازم لإرسال 20 رسالة بريد إلكتروني هو 20 مرة من الوقت المستغرق لإرسال بريد إلكتروني واحد. في حين أن إرسال بريد إلكتروني واحد فقط قد لا يجعل التطبيق أبطأ بشكل رهيب ، فإن إرسال 20 رسالة بريد إلكتروني بالتأكيد يؤثر على تجربة المستخدم.
دعونا نرى كيف يمكننا حل هذه المشكلة.
الانتباه إلى طبيعة رسائل البريد الإلكتروني الخاصة بالمعاملات
قبل أي شيء ، يجب أن نلاحظ أنه ليست كل رسائل البريد الإلكتروني متساوية في الأهمية. يمكننا تصنيف رسائل البريد الإلكتروني على نطاق واسع إلى مجموعتين: رسائل البريد الإلكتروني ذات الأولوية وغير ذات الأولوية. على سبيل المثال ، إذا نسي المستخدم كلمة المرور للوصول إلى الحساب ، فسوف يتوقع البريد الإلكتروني الذي يحتوي على رابط إعادة تعيين كلمة المرور على الفور في صندوق الوارد الخاص به ؛ هذا بريد إلكتروني ذو أولوية. في المقابل ، فإن إرسال بريد إلكتروني لإعلامنا بأن شخصًا ما نتابعه قد نشر محتوى جديدًا لا يحتاج إلى الوصول إلى صندوق الوارد الخاص بالمستخدم على الفور ؛ هذا بريد إلكتروني ليس له أولوية.
يجب أن يعمل الحل على تحسين كيفية إرسال هاتين الفئتين من رسائل البريد الإلكتروني. بافتراض أنه لن يكون هناك سوى عدد قليل (ربما 1 أو 2) من رسائل البريد الإلكتروني ذات الأولوية التي سيتم إرسالها أثناء العملية ، وأن الجزء الأكبر من رسائل البريد الإلكتروني سيكون رسائل غير ذات أولوية ، فإننا نصمم الحل على النحو التالي:
- يمكن أن تتجنب رسائل البريد الإلكتروني ذات الأولوية ببساطة مشكلة وقت الاستجابة المرتفع باستخدام موفر SMTP الموجود في نفس المنطقة التي يتم فيها نشر التطبيق. بالإضافة إلى البحث الجيد ، يتضمن ذلك دمج تطبيقنا مع واجهة برمجة التطبيقات الخاصة بالمزود.
- يمكن إرسال رسائل البريد الإلكتروني غير ذات الأولوية بشكل غير متزامن وفي مجموعات حيث يتم إرسال العديد من رسائل البريد الإلكتروني معًا. يتم تنفيذه على مستوى التطبيق ، ويتطلب مجموعة تقنية مناسبة.
دعنا نحدد مجموعة التكنولوجيا لإرسال رسائل البريد الإلكتروني بشكل غير متزامن بعد ذلك.
تحديد كومة التكنولوجيا
ملاحظة: لقد قررت أن أسند مجموعتي إلى خدمات AWS لأن موقع الويب الخاص بي مستضاف بالفعل على AWS EC2. خلاف ذلك ، سيكون لديّ مصروفات نقل البيانات بين شبكات العديد من الشركات. ومع ذلك ، يمكننا تنفيذ حلنا باستخدام موفري الخدمات السحابية الآخرين أيضًا.
كان أسلوبي الأول هو إعداد قائمة انتظار. من خلال قائمة انتظار ، كان بإمكاني ألا يرسل التطبيق رسائل البريد الإلكتروني بعد الآن ، ولكن بدلاً من ذلك ينشر رسالة تحتوي على محتوى البريد الإلكتروني والبيانات الوصفية في قائمة انتظار ، ثم يكون لدي عملية أخرى تلتقط الرسائل من قائمة الانتظار وترسل رسائل البريد الإلكتروني.
ومع ذلك ، عند التحقق من خدمة قائمة الانتظار من AWS ، والتي تسمى SQS ، قررت أنها ليست حلاً مناسبًا ، للأسباب التالية:
- إنه معقد نوعًا ما ؛
- يمكن لرسالة قائمة الانتظار القياسية تخزين أعلى 256 كيلوبايت فقط من المعلومات ، والتي قد لا تكون كافية إذا كان البريد الإلكتروني يحتوي على مرفقات (فاتورة على سبيل المثال). وعلى الرغم من إمكانية تقسيم رسالة كبيرة إلى رسائل أصغر ، فإن التعقيد يزداد أكثر.
ثم أدركت أنه يمكنني تقليد سلوك قائمة الانتظار تمامًا من خلال مجموعة من خدمات AWS الأخرى ، S3 و Lambda ، والتي يسهل إعدادها كثيرًا. يمكن أن يعمل S3 ، وهو حل تخزين كائن سحابي لتخزين البيانات واستردادها ، كمستودع لتحميل الرسائل ، ويمكن لـ Lambda ، وهي خدمة حوسبة تشغل التعليمات البرمجية استجابة للأحداث ، اختيار رسالة وتنفيذ عملية معها.
بمعنى آخر ، يمكننا إعداد عملية إرسال البريد الإلكتروني لدينا على النحو التالي:
- يقوم التطبيق بتحميل ملف يحتوي على محتوى البريد الإلكتروني + البيانات الوصفية إلى حاوية S3.
- عندما يتم تحميل ملف جديد في حاوية S3 ، يقوم S3 بتشغيل حدث يحتوي على المسار إلى الملف الجديد.
- تقوم وظيفة Lambda باختيار الحدث وقراءة الملف وإرسال البريد الإلكتروني.
أخيرًا ، علينا أن نقرر كيفية إرسال رسائل البريد الإلكتروني. يمكننا إما الاستمرار في استخدام مزود SMTP الموجود لدينا بالفعل ، أو جعل وظيفة Lambda تتفاعل مع واجهات برمجة التطبيقات الخاصة بهم ، أو استخدام خدمة AWS لإرسال رسائل البريد الإلكتروني ، والتي تسمى SES. استخدام SES له مزايا وعيوب:
فوائد:
- سهل الاستخدام للغاية من داخل AWS Lambda (لا يتطلب الأمر سوى سطرين من التعليمات البرمجية).
- إنه أرخص: يتم حساب رسوم Lambda بناءً على مقدار الوقت الذي يستغرقه تنفيذ الوظيفة ، لذا فإن الاتصال بـ SES من داخل شبكة AWS سيستغرق وقتًا أقصر من الاتصال بخادم خارجي ، مما يجعل المهمة تنتهي في وقت مبكر وتكلفة أقل . (ما لم يكن SES غير متوفر في نفس المنطقة التي يتم فيها استضافة التطبيق ؛ في حالتي ، نظرًا لأن SES غير متوفر في منطقة آسيا والمحيط الهادئ (سنغافورة) ، حيث يوجد خادم EC2 الخاص بي ، فقد يكون من الأفضل الاتصال ببعض مزود SMTP خارجي ومقره آسيا).
عيوب:
- لا يتم توفير العديد من الإحصائيات الخاصة بمراقبة رسائل البريد الإلكتروني المرسلة لدينا ، كما أن إضافة المزيد منها يتطلب جهدًا إضافيًا (على سبيل المثال: تتبع النسبة المئوية لرسائل البريد الإلكتروني التي تم فتحها ، أو الروابط التي تم النقر عليها ، والتي يجب إعدادها من خلال AWS CloudWatch).
- إذا واصلنا استخدام مزود SMTP لإرسال رسائل البريد الإلكتروني ذات الأولوية ، فلن يكون لدينا إحصائياتنا معًا في مكان واحد.
للتبسيط ، في الكود أدناه سنستخدم SES.
قمنا بعد ذلك بتعريف منطق العملية والمكدس على النحو التالي: يرسل التطبيق رسائل البريد الإلكتروني ذات الأولوية كالمعتاد ، ولكن بالنسبة للرسائل غير ذات الأولوية ، يقوم بتحميل ملف يحتوي على محتوى بريد إلكتروني وبيانات وصفية إلى S3 ؛ تتم معالجة هذا الملف بشكل غير متزامن بواسطة وظيفة Lambda ، والتي تتصل بـ SES لإرسال البريد الإلكتروني.
لنبدأ في تنفيذ الحل.
التفريق بين رسائل البريد الإلكتروني ذات الأولوية وغير ذات الأولوية
باختصار ، كل هذا يتوقف على التطبيق ، لذلك نحن بحاجة لاتخاذ قرار بشأن أساس البريد الإلكتروني عن طريق البريد الإلكتروني. سأصف حلاً قمت بتطبيقه لـ WordPress ، والذي يتطلب بعض الاختراقات حول القيود من الوظيفة wp_mail
. بالنسبة للأنظمة الأساسية الأخرى ، ستعمل الاستراتيجية أدناه أيضًا ، ولكن من المحتمل جدًا أن تكون هناك استراتيجيات أفضل ، والتي لا تتطلب الاختراق للعمل.
تتمثل طريقة إرسال بريد إلكتروني في WordPress في استدعاء الوظيفة wp_mail
، ولا نرغب في تغيير ذلك (على سبيل المثال: عن طريق استدعاء إما الوظيفتين wp_mail_synchronous
أو wp_mail_asynchronous
) ، لذلك سيحتاج تطبيقنا لـ wp_mail
إلى التعامل مع الحالات المتزامنة وغير المتزامنة ، وسيحتاج إلى معرفة المجموعة التي ينتمي إليها البريد الإلكتروني. لسوء الحظ ، لا يقدم wp_mail
أي معلمة إضافية يمكننا من خلالها تقييم هذه المعلومات ، كما يمكن رؤيتها من توقيعها:
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() )
بعد ذلك ، من أجل معرفة فئة البريد الإلكتروني ، نضيف حل اختراق: بشكل افتراضي ، نقوم بإنشاء بريد إلكتروني ينتمي إلى المجموعة ذات الأولوية ، وإذا كان $to
يحتوي على بريد إلكتروني معين (على سبيل المثال: [email protected]) ، أو إذا بدأ $subject
بسلسلة خاصة (على سبيل المثال: "[Non-priority!]") ، فإنه ينتمي إلى المجموعة غير ذات الأولوية (ونقوم بإزالة البريد الإلكتروني أو السلسلة المقابلة من الموضوع). wp_mail
هي وظيفة قابلة للتوصيل ، لذا يمكننا تجاوزها ببساطة عن طريق تنفيذ وظيفة جديدة بنفس التوقيع على ملف function.php الخاص بنا. في البداية ، يحتوي على نفس رمز وظيفة wp_mail
الأصلية ، الموجودة في ملف wp-include / pluggable.php ، لاستخراج جميع المعلمات:
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;
ثم نتحقق مما إذا كانت غير ذات أولوية ، وفي هذه الحالة نتفرع بعد ذلك إلى منطق منفصل ضمن الوظيفة send_asynchronous_mail
أو ، إذا لم يكن الأمر كذلك ، فإننا نستمر في تنفيذ نفس الكود كما في وظيفة 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 // ... }
في send_asynchronous_mail
، بدلاً من تحميل البريد الإلكتروني مباشرةً إلى S3 ، نقوم ببساطة بإضافة البريد الإلكتروني إلى المتغير العالمي $emailqueue
، والذي يمكننا من خلاله تحميل جميع رسائل البريد الإلكتروني معًا إلى S3 في اتصال واحد في نهاية الطلب:
function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { global $emailqueue; if (!$emailqueue) { $emailqueue = array(); } // Add email to queue. Code continues below... }
يمكننا تحميل ملف واحد لكل بريد إلكتروني ، أو يمكننا تجميعها بحيث تحتوي في ملف واحد على العديد من رسائل البريد الإلكتروني. نظرًا لأن $headers
تحتوي على بيانات تعريف البريد الإلكتروني (من ، ونوع المحتوى ، ومجموعة الأحرف ، و CC ، و BCC ، وحقول الرد) ، يمكننا تجميع رسائل البريد الإلكتروني معًا عندما يكون لديهم نفس $headers
. بهذه الطريقة ، يمكن تحميل جميع رسائل البريد الإلكتروني هذه في نفس الملف إلى S3 ، وسيتم تضمين المعلومات الوصفية $headers
مرة واحدة فقط في الملف ، بدلاً من مرة واحدة لكل بريد إلكتروني:

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 }
أخيرًا ، الدالة send_asynchronous_mail
ترجع true
. يرجى ملاحظة أن هذا الرمز متطفل: فعادة ما يعني true
أن البريد الإلكتروني قد تم إرساله بنجاح ، ولكن في هذه الحالة ، لم يتم إرساله حتى الآن ، وقد يفشل تمامًا. لهذا السبب ، يجب ألا تتعامل الوظيفة التي تستدعي wp_mail
مع الاستجابة true
على أنها "تم إرسال البريد الإلكتروني بنجاح" ، بل يجب أن تتعامل مع الإقرار بأنه قد تم وضعه في قائمة الانتظار. لهذا السبب من المهم قصر هذه التقنية على رسائل البريد الإلكتروني غير ذات الأولوية بحيث إذا فشلت ، يمكن أن تستمر العملية في إعادة المحاولة في الخلفية ، ولن يتوقع المستخدم أن يكون البريد الإلكتروني موجودًا بالفعل في صندوق الوارد الخاص به:
function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { // Continued from above... // That's it! return true; }
تحميل رسائل البريد الإلكتروني إلى S3
في مقالتي السابقة "مشاركة البيانات بين خوادم متعددة من خلال AWS S3" ، وصفت كيفية إنشاء حاوية في S3 وكيفية تحميل الملفات إلى الحاوية من خلال SDK. تستمر جميع التعليمات البرمجية أدناه في تنفيذ حل لبرنامج WordPress ، ومن ثم فإننا نتصل بـ AWS باستخدام SDK لـ PHP.
يمكننا أن نمتد من الفئة المجردة AWS_S3
(المقدمة في مقالتي السابقة) للاتصال بـ S3 وتحميل رسائل البريد الإلكتروني إلى مجموعة "رسائل بريد إلكتروني غير متزامنة" في نهاية الطلب (يتم تشغيلها من خلال خطاف wp_footer
). يرجى ملاحظة أنه يجب علينا الاحتفاظ بقائمة التحكم في الوصول "خاصة" لأننا لا نريد أن تتعرض رسائل البريد الإلكتروني للإنترنت:
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();
نبدأ في التكرار من خلال أزواج الرؤوس => بيانات البريد الإلكتروني المحفوظة في المتغير الشامل $emailqueue
، ونحصل على تكوين افتراضي من الوظيفة get_default_email_meta
إذا كانت الرؤوس فارغة. في الكود أدناه ، أسترد الحقل "من" فقط من الرؤوس (يمكن نسخ الكود لاستخراج جميع الرؤوس من الوظيفة الأصلية 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... } } }
أخيرًا ، نقوم بتحميل رسائل البريد الإلكتروني إلى S3. نقرر عدد رسائل البريد الإلكتروني المراد تحميلها لكل ملف بهدف توفير المال. يتم احتساب رسوم وظائف Lambda بناءً على مقدار الوقت الذي يحتاجون إليه للتنفيذ ، محسوبًا على فترات تبلغ 100 مللي ثانية. كلما تطلبت الوظيفة المزيد من الوقت ، أصبحت أكثر تكلفة.
إرسال جميع رسائل البريد الإلكتروني عن طريق تحميل ملف واحد لكل بريد إلكتروني ، إذن ، يكون أكثر تكلفة من تحميل ملف واحد لكل العديد من رسائل البريد الإلكتروني ، حيث يتم حساب النفقات العامة من تنفيذ الوظيفة مرة واحدة لكل بريد إلكتروني ، بدلاً من مرة واحدة فقط للعديد من رسائل البريد الإلكتروني ، وأيضًا بسبب إرسال العديد من رسائل البريد الإلكتروني معًا يملأ 100 مللي ثانية بشكل أكثر شمولاً.
لذلك نقوم بتحميل العديد من رسائل البريد الإلكتروني لكل ملف. كم عدد رسائل البريد الإلكتروني؟ وظائف Lambda لها أقصى وقت تنفيذ (3 ثوانٍ افتراضيًا) ، وإذا فشلت العملية ، فسوف تستمر في إعادة المحاولة من البداية ، وليس من حيث فشلت. لذلك ، إذا كان الملف يحتوي على 100 رسالة بريد إلكتروني ، وتمكنت Lambda من إرسال 50 رسالة بريد إلكتروني قبل انتهاء الوقت الأقصى ، فإنها تفشل وتعيد تنفيذ العملية مرة أخرى ، وإرسال أول 50 رسالة بريد إلكتروني مرة أخرى. لتجنب ذلك ، يجب أن نختار عددًا من رسائل البريد الإلكتروني لكل ملف ونحن على ثقة من أنها كافية لمعالجتها قبل انتهاء الحد الأقصى للوقت. في حالتنا ، يمكننا اختيار إرسال 25 بريدًا إلكترونيًا لكل ملف. يعتمد عدد رسائل البريد الإلكتروني على التطبيق (سيستغرق إرسال رسائل البريد الإلكتروني الأكبر وقتًا أطول ، وسيعتمد وقت إرسال بريد إلكتروني على البنية التحتية) ، لذلك يجب علينا إجراء بعض الاختبارات للتوصل إلى الرقم الصحيح.
محتوى الملف هو ببساطة كائن JSON ، يحتوي على meta للبريد الإلكتروني ضمن الخاصية "meta" ، وجزء من رسائل البريد الإلكتروني ضمن الخاصية "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), ]); } } } }
للتبسيط ، في الكود أعلاه ، لا أقوم بتحميل المرفقات إلى S3. إذا كانت رسائل البريد الإلكتروني الخاصة بنا بحاجة إلى تضمين مرفقات ، فيجب علينا استخدام وظيفة SES SendRawEmail
بدلاً من SendEmail
(المستخدمة في نص Lambda أدناه).
بعد إضافة المنطق لتحميل الملفات برسائل البريد الإلكتروني إلى S3 ، يمكننا الانتقال إلى جانب تشفير وظيفة Lambda.
ترميز سيناريو لامدا
يُطلق على وظائف Lambda أيضًا وظائف بدون خادم ، ليس لأنها لا تعمل على خادم ، ولكن لأن المطور لا يحتاج إلى القلق بشأن الخادم: يوفر المطور البرنامج النصي ببساطة ، وتتولى السحابة توفير الخادم والنشر و تشغيل البرنامج النصي. ومن ثم ، كما ذكرنا سابقًا ، يتم فرض رسوم على وظائف Lambda بناءً على وقت تنفيذ الوظيفة.
يقوم البرنامج النصي Node.js التالي بالمهمة المطلوبة. تم الاستدعاء من خلال حدث "وضع" في S3 ، والذي يشير إلى إنشاء كائن جديد في الحاوية ، الوظيفة:
- يحصل على مسار الكائن الجديد (تحت
srcKey
المتغير) والحاوية (ضمنsrcBucket
المتغير). - يقوم بتنزيل الكائن من خلال
s3.getObject
. - يوزع محتوى الكائن ، من خلال
JSON.parse(response.Body.toString())
، ويستخرج رسائل البريد الإلكتروني وميتا البريد الإلكتروني. - يتكرر عبر جميع رسائل البريد الإلكتروني ويرسلها عبر
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); }); };
بعد ذلك ، يجب علينا تحميل وتكوين وظيفة Lambda إلى AWS ، والتي تتضمن:
- إنشاء دور تنفيذي يمنح Lambda أذونات للوصول إلى S3.
- إنشاء حزمة .zip تحتوي على جميع الكود ، أي وظيفة Lambda التي نقوم بإنشائها + جميع وحدات Node.js المطلوبة.
- تحميل هذه الحزمة إلى AWS باستخدام أداة CLI.
كيفية القيام بهذه الأشياء موضحة بشكل صحيح على موقع AWS ، في البرنامج التعليمي حول استخدام AWS Lambda مع Amazon S3.
ربط S3 بوظيفة Lambda
أخيرًا ، بعد إنشاء الدلو ووظيفة Lambda ، نحتاج إلى ربطهما معًا ، بحيث عندما يكون هناك كائن جديد تم إنشاؤه في الحاوية ، فسيؤدي ذلك إلى تشغيل حدث لتنفيذ وظيفة Lambda. للقيام بذلك ، نذهب إلى لوحة القيادة S3 ونضغط على صف الحاوية ، والذي سيظهر خصائصه:

ثم بالضغط على الخصائص ، نقوم بالتمرير لأسفل إلى العنصر "الأحداث" ، وهناك نضغط على إضافة إشعار ، ونقوم بإدخال الحقول التالية:
- الاسم: اسم الإعلام ، على سبيل المثال: "مرسل البريد الإلكتروني" ؛
- الأحداث: "وضع" ، وهو الحدث الذي يتم تشغيله عند إنشاء كائن جديد في الحاوية ؛
- أرسل إلى: "Lambda Function" ؛
- Lambda: اسم Lambda الذي أنشأناه حديثًا ، على سبيل المثال: "LambdaEmailSender".

أخيرًا ، يمكننا أيضًا تعيين دلو S3 لحذف الملفات التي تحتوي على بيانات البريد الإلكتروني تلقائيًا بعد مرور بعض الوقت. لهذا ، ننتقل إلى علامة تبويب الإدارة في الحاوية ، وننشئ قاعدة دورة حياة جديدة ، تحدد بعد عدد الأيام التي يجب أن تنتهي فيها رسائل البريد الإلكتروني:

هذا هو. من هذه اللحظة ، عند إضافة كائن جديد إلى حاوية S3 مع المحتوى والميتا لرسائل البريد الإلكتروني ، سيؤدي ذلك إلى تشغيل وظيفة Lambda ، والتي ستقرأ الملف وتتصل بـ SES لإرسال رسائل البريد الإلكتروني.
لقد قمت بتطبيق هذا الحل على موقعي ، وأصبح سريعًا مرة أخرى: عن طريق إلغاء إرسال رسائل البريد الإلكتروني إلى عملية خارجية ، سواء أكانت التطبيقات ترسل 20 أو 5000 رسالة بريد إلكتروني لا تحدث فرقًا ، فستكون الاستجابة للمستخدم الذي أطلق الإجراء مباشر.
خاتمة
في هذه المقالة ، قمنا بتحليل سبب تحول إرسال العديد من رسائل البريد الإلكتروني للمعاملات في طلب واحد إلى عنق الزجاجة في التطبيق ، وقمنا بإنشاء حل للتعامل مع المشكلة: بدلاً من الاتصال بخادم SMTP من داخل التطبيق (بشكل متزامن) ، يمكننا إرسال رسائل البريد الإلكتروني من وظيفة خارجية ، بشكل غير متزامن ، بناءً على مجموعة من AWS S3 + Lambda + SES.
من خلال إرسال رسائل البريد الإلكتروني بشكل غير متزامن ، يمكن للتطبيق إدارة إرسال آلاف رسائل البريد الإلكتروني ، ومع ذلك لن تتأثر الاستجابة للمستخدم الذي أطلق الإجراء. ومع ذلك ، للتأكد من أن المستخدم لا ينتظر وصول البريد الإلكتروني إلى البريد الوارد ، قررنا أيضًا تقسيم رسائل البريد الإلكتروني إلى مجموعتين ، ذات أولوية وغير ذات أولوية ، وإرسال رسائل البريد الإلكتروني غير ذات الأولوية فقط بشكل غير متزامن. لقد قدمنا تطبيقًا لبرنامج WordPress ، وهو أمر صعب نظرًا لقيود وظيفة wp_mail
لإرسال رسائل البريد الإلكتروني.
الدرس المستفاد من هذه المقالة هو أن الوظائف التي لا تحتاج إلى خادم في تطبيق قائم على الخادم تعمل بشكل جيد: يمكن للمواقع التي تعمل على CMS مثل WordPress تحسين أدائها من خلال تنفيذ ميزات محددة فقط على السحابة ، وتجنب قدر كبير من التعقيد الذي يأتي من الترحيل مواقع ديناميكية للغاية إلى بنية بدون خادم بالكامل.