การส่งอีเมลแบบอะซิงโครนัสผ่าน AWS SES

เผยแพร่แล้ว: 2022-03-10
สรุปโดยย่อ ↬ การส่งอีเมลธุรกรรมจำนวนมากในคราวเดียว หากไม่ได้รับการออกแบบอย่างเหมาะสม อาจกลายเป็นปัญหาคอขวดสำหรับแอปพลิเคชันและทำให้ประสบการณ์ผู้ใช้ลดลง ส่วนหนึ่งของปัญหาคือการเชื่อมต่อกับเซิร์ฟเวอร์ SMTP จากภายในแอปพลิเคชันแบบซิงโครนัส ในบทความนี้ เราจะสำรวจวิธีการส่งอีเมลจากภายนอกแอปพลิเคชันแบบอะซิงโครนัส โดยใช้ AWS S3, Lambda และ SES ร่วมกัน

แอปพลิเคชันส่วนใหญ่ส่งอีเมลเพื่อสื่อสารกับผู้ใช้ อีเมลธุรกรรมคืออีเมลที่เกิดจากการโต้ตอบของผู้ใช้กับแอปพลิเคชัน เช่น เมื่อต้อนรับผู้ใช้ใหม่หลังจากลงทะเบียนในไซต์ ให้ลิงก์แก่ผู้ใช้เพื่อรีเซ็ตรหัสผ่าน หรือการแนบใบแจ้งหนี้หลังจากที่ผู้ใช้ทำการซื้อ กรณีก่อนหน้านี้ทั้งหมดจะต้องส่งอีเมลถึงผู้ใช้เพียงฉบับเดียว ในบางกรณี แอปพลิเคชันจำเป็นต้องส่งอีเมลจำนวนมากขึ้น เช่น เมื่อผู้ใช้โพสต์เนื้อหาใหม่บนไซต์ และผู้ติดตามทั้งหมดของเธอ (ซึ่งในแพลตฟอร์มเช่น Twitter อาจมีผู้ใช้หลายล้านคน) จะได้รับ การแจ้งเตือน ในสถานการณ์หลังนี้ การออกแบบไม่ถูกต้อง การส่งอีเมลอาจกลายเป็นปัญหาคอขวดในแอปพลิเคชัน

นั่นคือสิ่งที่เกิดขึ้นในกรณีของฉัน ฉันมีไซต์ที่อาจต้องส่งอีเมล 20 ฉบับหลังจากที่ผู้ใช้ทริกเกอร์การกระทำบางอย่าง (เช่น การแจ้งเตือนผู้ใช้ไปยังผู้ติดตามของเธอทั้งหมด) ในขั้นต้น มันอาศัยการส่งอีเมลผ่านผู้ให้บริการ SMTP บนคลาวด์ยอดนิยม (เช่น SendGrid, Mandrill, Mailjet และ Mailgun) อย่างไรก็ตาม การตอบกลับไปยังผู้ใช้จะใช้เวลาไม่กี่วินาที เห็นได้ชัดว่าการเชื่อมต่อกับเซิร์ฟเวอร์ SMTP เพื่อส่งอีเมล 20 ฉบับนั้นทำให้กระบวนการช้าลงอย่างมาก

หลังจากตรวจสอบแล้ว ฉันพบสาเหตุของปัญหา:

  1. การเชื่อมต่อแบบซิงโครนัส
    แอปพลิเคชันเชื่อมต่อกับเซิร์ฟเวอร์ SMTP และรอการตอบรับแบบซิงโครนัสก่อนที่จะดำเนินการตามกระบวนการต่อไป
  2. เวลาแฝงสูง
    ในขณะที่เซิร์ฟเวอร์ของฉันตั้งอยู่ในสิงคโปร์ ผู้ให้บริการ SMTP ที่ฉันใช้อยู่นั้นมีเซิร์ฟเวอร์อยู่ในสหรัฐอเมริกา ทำให้การเชื่อมต่อไปกลับใช้เวลานานมาก
  3. ไม่สามารถใช้ซ้ำได้ของการเชื่อมต่อ SMTP
    เมื่อเรียกใช้ฟังก์ชันเพื่อส่งอีเมล ฟังก์ชันจะส่งอีเมลทันที สร้างการเชื่อมต่อ SMTP ใหม่ในขณะนั้น (ไม่ได้เสนอให้รวบรวมอีเมลทั้งหมดแล้วส่งรวมกันเมื่อสิ้นสุดคำขอ ภายใต้ SMTP เดียว การเชื่อมต่อ).

เนื่องจาก #1 เวลาที่ผู้ใช้ต้องรอการตอบสนองจึงผูกกับเวลาที่ใช้ในการส่งอีเมล เนื่องจาก #2 เวลาในการส่งอีเมลหนึ่งฉบับจึงค่อนข้างสูง และด้วยเหตุที่ #3 เวลาในการส่งอีเมล 20 ฉบับจึงเป็นเวลา 20 เท่าในการส่งอีเมลหนึ่งฉบับ แม้ว่าการส่งอีเมลเพียงฉบับเดียวอาจไม่ได้ทำให้แอปพลิเคชันช้าลงอย่างมาก แต่การส่งอีเมล 20 ฉบับย่อมส่งผลต่อประสบการณ์ของผู้ใช้อย่างแน่นอน

เรามาดูกันว่าเราจะแก้ปัญหานี้ได้อย่างไร

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

ใส่ใจกับธรรมชาติของอีเมลธุรกรรม

ก่อนสิ่งอื่นใด เราต้องสังเกตว่าอีเมลบางฉบับมีความสำคัญไม่เท่ากัน เราสามารถจัดหมวดหมู่อีเมลอย่างกว้างๆ ออกเป็นสองกลุ่ม: อีเมลที่มีลำดับความสำคัญและไม่สำคัญ ตัวอย่างเช่น หากผู้ใช้ลืมรหัสผ่านเพื่อเข้าถึงบัญชี ผู้ใช้จะรออีเมลพร้อมลิงก์รีเซ็ตรหัสผ่านในกล่องจดหมายทันที นั่นคืออีเมลสำคัญ ในทางตรงกันข้าม การส่งอีเมลแจ้งว่ามีคนที่เราติดตามโพสต์เนื้อหาใหม่ไม่จำเป็นต้องมาถึงกล่องจดหมายของผู้ใช้ในทันที นั่นคืออีเมลที่ไม่มีลำดับความสำคัญ

โซลูชันต้องปรับวิธีการส่งอีเมลทั้งสองประเภทนี้ให้เหมาะสม สมมติว่าจะมีการส่งอีเมลที่มีลำดับความสำคัญเพียงไม่กี่ (อาจเป็น 1 หรือ 2) ระหว่างกระบวนการ และอีเมลจำนวนมากจะไม่ใช่อีเมลที่มีลำดับความสำคัญ จากนั้นเราจึงออกแบบโซลูชันดังนี้:

  • อีเมลที่มีลำดับความสำคัญ สามารถหลีกเลี่ยงปัญหาเวลาแฝงสูงได้โดยใช้ผู้ให้บริการ SMTP ที่ตั้งอยู่ในภูมิภาคเดียวกับที่มีการปรับใช้แอปพลิเคชัน นอกเหนือจากการวิจัยที่ดีแล้ว สิ่งนี้ยังรวมถึงการรวมแอปพลิเคชันของเราเข้ากับ API ของผู้ให้บริการอีกด้วย
  • อีเมลที่ไม่มีความสำคัญสามารถส่งอีเมล แบบอะซิงโครนัสและเป็นกลุ่มที่มีการส่งอีเมลหลายฉบับพร้อมกัน นำไปใช้ในระดับแอปพลิเคชัน มันต้องมีกองเทคโนโลยีที่เหมาะสม

มากำหนดกลุ่มเทคโนโลยีเพื่อส่งอีเมลแบบอะซิงโครนัสกันต่อไป

กำหนดกองเทคโนโลยี

หมายเหตุ: ฉันได้ตัดสินใจใช้สแต็คของฉันบนบริการของ AWS เนื่องจากเว็บไซต์ของฉันโฮสต์บน AWS EC2 แล้ว มิฉะนั้น ฉันจะมีค่าใช้จ่ายในการย้ายข้อมูลระหว่างเครือข่ายของบริษัทต่างๆ อย่างไรก็ตาม เราสามารถใช้โซลูชันของเราโดยใช้ผู้ให้บริการระบบคลาวด์รายอื่นได้เช่นกัน

วิธีแรกของฉันคือการตั้งค่าคิว ผ่านคิวฉันสามารถให้แอปพลิเคชันไม่ส่งอีเมลอีกต่อไป แต่แทนที่จะเผยแพร่ข้อความที่มีเนื้อหาอีเมลและข้อมูลเมตาในคิว จากนั้นให้กระบวนการอื่นรับข้อความจากคิวและส่งอีเมล

อย่างไรก็ตาม เมื่อตรวจสอบบริการคิวจาก AWS ที่เรียกว่า SQS ฉันตัดสินใจว่าไม่ใช่วิธีแก้ปัญหาที่เหมาะสม เนื่องจาก:

  • การติดตั้งค่อนข้างซับซ้อน
  • ข้อความคิวมาตรฐานสามารถเก็บข้อมูลได้สูงสุด 256 kb ซึ่งอาจไม่เพียงพอหากอีเมลมีไฟล์แนบ (เช่น ใบแจ้งหนี้) และถึงแม้จะแยกข้อความขนาดใหญ่ออกเป็นข้อความขนาดเล็กได้ แต่ความซับซ้อนก็ยิ่งเพิ่มมากขึ้น

จากนั้นฉันก็ตระหนักว่าฉันสามารถเลียนแบบพฤติกรรมของคิวได้อย่างสมบูรณ์แบบผ่านการผสมผสานระหว่างบริการอื่นๆ ของ AWS, S3 และ Lambda ซึ่งตั้งค่าได้ง่ายกว่ามาก S3 ซึ่งเป็นโซลูชันการจัดเก็บอ็อบเจ็กต์บนคลาวด์สำหรับจัดเก็บและเรียกข้อมูล สามารถทำหน้าที่เป็นที่เก็บสำหรับการอัปโหลดข้อความ และ Lambda ซึ่งเป็นบริการประมวลผลที่รันโค้ดเพื่อตอบสนองต่อเหตุการณ์ สามารถเลือกข้อความและดำเนินการกับข้อความได้

กล่าวอีกนัยหนึ่ง เราสามารถตั้งค่ากระบวนการส่งอีเมลได้ดังนี้:

  1. แอปพลิเคชันจะอัปโหลดไฟล์ที่มีเนื้อหาอีเมล + ข้อมูลเมตาไปยังบัคเก็ต S3
  2. เมื่อใดก็ตามที่ไฟล์ใหม่ถูกอัปโหลดไปยังบัคเก็ต S3 S3 จะทริกเกอร์เหตุการณ์ที่มีเส้นทางไปยังไฟล์ใหม่
  3. ฟังก์ชัน Lambda จะเลือกเหตุการณ์ อ่านไฟล์ และส่งอีเมล

สุดท้าย เราต้องตัดสินใจว่าจะส่งอีเมลอย่างไร เราสามารถใช้ผู้ให้บริการ SMTP ที่เรามีอยู่แล้วได้ ให้ฟังก์ชัน Lambda โต้ตอบกับ API ของตน หรือใช้บริการ AWS เพื่อส่งอีเมลที่เรียกว่า SES การใช้ SES มีทั้งข้อดีและข้อเสีย:

ประโยชน์:

  • ใช้งานง่ายมากจากภายใน AWS Lambda (ใช้โค้ดเพียง 2 บรรทัด)
  • ราคาถูกกว่า: ค่าธรรมเนียม 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 เป็นฟังก์ชันที่เสียบได้ ดังนั้นเราจึงสามารถแทนที่ได้ง่ายๆ โดยใช้ฟังก์ชันใหม่ที่มีลายเซ็นเดียวกันในไฟล์ functions.php ของเรา ในขั้นต้น จะมีโค้ดเดียวกันกับฟังก์ชัน wp_mail ดั้งเดิม ซึ่งอยู่ในไฟล์ wp-includes/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... }

เราสามารถอัปโหลดไฟล์ได้หนึ่งไฟล์ต่ออีเมล หรือรวมกลุ่มเพื่อให้มีอีเมลจำนวนมากใน 1 ไฟล์ เนื่องจาก $headers มีเมตาอีเมล (จาก ประเภทเนื้อหาและชุดอักขระ CC, BCC และฟิลด์ตอบกลับ) เราจึงสามารถจัดกลุ่มอีเมลไว้ด้วยกันทุกครั้งที่มี $headers เหมือนกัน ด้วยวิธีนี้ อีเมลเหล่านี้ทั้งหมดสามารถอัปโหลดในไฟล์เดียวกันไปยัง S3 ได้ และข้อมูลเมตา $headers จะถูกรวมไว้ในไฟล์เพียงครั้งเดียว แทนที่จะเป็น 1 ครั้งต่ออีเมล:

 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 และอัปโหลดอีเมลไปยังบัคเก็ต "async-emails" ที่ส่วนท้ายของคำขอ (เรียกใช้ผ่าน wp_footer hook) โปรดทราบว่าเราต้องเก็บ ACL เป็น "ส่วนตัว" เนื่องจากเราไม่ต้องการให้อีเมลถูกเปิดเผยบนอินเทอร์เน็ต:

 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();

เราเริ่มวนซ้ำผ่านคู่ของส่วนหัว => emaildata ที่บันทึกไว้ในตัวแปรส่วนกลาง $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 เรากำหนดจำนวนอีเมลที่จะอัปโหลดต่อไฟล์โดยมีจุดประสงค์เพื่อประหยัดเงิน ฟังก์ชันแลมบ์ดาคิดค่าใช้จ่ายตามระยะเวลาที่ต้องดำเนินการ โดยคำนวณจากช่วง 100 มิลลิวินาที ยิ่งต้องการเวลามากเท่าไร ฟังก์ชันก็จะยิ่งแพงขึ้นเท่านั้น

การส่งอีเมลทั้งหมดโดยอัปโหลด 1 ไฟล์ต่ออีเมลจึงมีราคาแพงกว่าการอัปโหลด 1 ไฟล์ต่ออีเมลหลายๆ ฉบับ เนื่องจากค่าใช้จ่ายจากการเรียกใช้ฟังก์ชันจะคำนวณเพียงครั้งเดียวต่ออีเมล แทนที่จะเป็นเพียงครั้งเดียวสำหรับอีเมลจำนวนมาก และเนื่องจากการส่งอีเมลจำนวนมาก ร่วมกันเติมช่วง 100ms ให้ละเอียดยิ่งขึ้น

ดังนั้นเราจึงอัปโหลดอีเมลจำนวนมากต่อไฟล์ กี่อีเมล? ฟังก์ชันแลมบ์ดามีเวลาดำเนินการสูงสุด (ค่าเริ่มต้นคือ 3 วินาที) และหากการดำเนินการล้มเหลว ก็จะลองใหม่อีกครั้งตั้งแต่ต้น ไม่ใช่จากจุดที่ล้มเหลว ดังนั้น หากไฟล์มีอีเมล 100 ฉบับ และแลมบ์ดาสามารถส่งอีเมลได้ 50 ฉบับก่อนที่เวลาสูงสุดจะหมดลง ก็จะล้มเหลวและลองดำเนินการใหม่อีกครั้ง โดยส่งอีเมล 50 ฉบับแรกอีกครั้ง เพื่อหลีกเลี่ยงปัญหานี้ เราต้องเลือกจำนวนอีเมลต่อไฟล์ที่เรามั่นใจว่าเพียงพอในการดำเนินการก่อนที่เวลาสูงสุดจะหมดลง ในสถานการณ์ของเรา เราสามารถเลือกที่จะส่ง 25 อีเมลต่อไฟล์ จำนวนอีเมลขึ้นอยู่กับแอปพลิเคชัน (อีเมลขนาดใหญ่จะใช้เวลาส่งนานกว่า และเวลาส่งอีเมลจะขึ้นอยู่กับโครงสร้างพื้นฐาน) ดังนั้นเราจึงควรทำการทดสอบเพื่อให้ได้จำนวนที่ถูกต้อง

เนื้อหาของไฟล์เป็นเพียงวัตถุ JSON ที่มีเมตาอีเมลภายใต้คุณสมบัติ "เมตา" และกลุ่มอีเมลภายใต้คุณสมบัติ "อีเมล":

 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 ได้

การเข้ารหัสสคริปต์แลมบ์ดา

ฟังก์ชันแลมบ์ดาเรียกอีกอย่างว่าฟังก์ชันไร้เซิร์ฟเวอร์ ไม่ใช่เพราะไม่ได้ทำงานบนเซิร์ฟเวอร์ แต่เนื่องจากนักพัฒนาไม่จำเป็นต้องกังวลเกี่ยวกับเซิร์ฟเวอร์: นักพัฒนาเพียงจัดเตรียมสคริปต์ และคลาวด์จะดูแลการจัดเตรียมเซิร์ฟเวอร์ การปรับใช้และ เรียกใช้สคริปต์ ดังนั้น ดังที่ได้กล่าวไว้ก่อนหน้านี้ ฟังก์ชันแลมบ์ดาจะถูกเรียกเก็บตามเวลาการทำงานของฟังก์ชัน

สคริปต์ Node.js ต่อไปนี้ทำงานที่จำเป็น เรียกใช้โดยเหตุการณ์ "วาง" ของ S3 ซึ่งระบุว่ามีการสร้างวัตถุใหม่บนบัคเก็ต ฟังก์ชัน:

  1. รับเส้นทางของวัตถุใหม่ (ภายใต้ตัวแปร srcKey ) และที่ฝากข้อมูล (ภายใต้ตัวแปร srcBucket )
  2. ดาวน์โหลดอ็อบเจ็กต์ผ่าน s3.getObject
  3. แยกวิเคราะห์เนื้อหาของวัตถุ ผ่าน JSON.parse(response.Body.toString()) และแยกอีเมลและเมตาของอีเมล
  4. ทำซ้ำผ่านอีเมลทั้งหมด และส่งผ่าน 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 ซึ่งเกี่ยวข้องกับ:

  1. การสร้างบทบาทการดำเนินการที่ให้สิทธิ์ Lambda ในการเข้าถึง S3
  2. การสร้างแพ็คเกจ .zip ที่มีโค้ดทั้งหมด เช่น ฟังก์ชัน Lambda ที่เรากำลังสร้าง + โมดูล Node.js ที่จำเป็นทั้งหมด
  3. การอัปโหลดแพ็คเกจนี้ไปยัง AWS โดยใช้เครื่องมือ CLI

วิธีการทำสิ่งเหล่านี้มีการอธิบายอย่างถูกต้องบนเว็บไซต์ AWS ในบทช่วยสอนเกี่ยวกับการใช้ AWS Lambda กับ Amazon S3

เชื่อมต่อ S3 ด้วยฟังก์ชัน Lambda

สุดท้าย เมื่อสร้างบัคเก็ตและฟังก์ชัน Lambda เราจำเป็นต้องเชื่อมต่อทั้งสองอย่างเข้าด้วยกัน เพื่อที่ว่าเมื่อใดก็ตามที่มีการสร้างออบเจกต์ใหม่บนบัคเก็ต มันจะทริกเกอร์เหตุการณ์เพื่อเรียกใช้ฟังก์ชัน Lambda ในการดำเนินการนี้ เราไปที่แดชบอร์ด S3 และคลิกที่แถวฝากข้อมูล ซึ่งจะแสดงคุณสมบัติของมัน:

การแสดงคุณสมบัติของบัคเก็ตภายในแดชบอร์ด S3
การคลิกที่แถวของที่เก็บข้อมูลจะแสดงคุณสมบัติของที่เก็บข้อมูล (ตัวอย่างขนาดใหญ่)

จากนั้นคลิกที่ Properties เราเลื่อนลงไปที่รายการ "Events" จากนั้นคลิกที่เพิ่มการแจ้งเตือนและป้อนข้อมูลในฟิลด์ต่อไปนี้:

  • ชื่อ: ชื่อของการแจ้งเตือน เช่น: “EmailSender”;
  • เหตุการณ์: “พุท” ซึ่งเป็นเหตุการณ์ที่ทริกเกอร์เมื่อมีการสร้างออบเจกต์ใหม่บนบัคเก็ต
  • ส่งไปที่: “ฟังก์ชันแลมบ์ดา”;
  • Lambda: ชื่อของ Lambda ที่สร้างขึ้นใหม่ของเรา เช่น: “LambdaEmailSender”
การตั้งค่า S3 ด้วย Lambda
การเพิ่มการแจ้งเตือนใน S3 เพื่อทริกเกอร์เหตุการณ์สำหรับ Lambda (ตัวอย่างขนาดใหญ่)

สุดท้าย เรายังสามารถตั้งค่าบัคเก็ต S3 ให้ลบไฟล์ที่มีข้อมูลอีเมลโดยอัตโนมัติหลังจากผ่านไประยะหนึ่ง สำหรับสิ่งนี้ เราไปที่แท็บการจัดการของบัคเก็ต และสร้างกฎวงจรการใช้งานใหม่ โดยกำหนดหลังจากจำนวนวันที่อีเมลต้องหมดอายุ:

กฎวงจรชีวิต
การตั้งค่ากฎวงจรการใช้งานเพื่อลบไฟล์ออกจากบัคเก็ตโดยอัตโนมัติ (ตัวอย่างขนาดใหญ่)

แค่นั้นแหละ. นับจากนี้ไป เมื่อเพิ่มออบเจ็กต์ใหม่ในบัคเก็ต S3 ที่มีเนื้อหาและเมตาสำหรับอีเมล ระบบจะเรียกใช้ฟังก์ชัน Lambda ซึ่งจะอ่านไฟล์และเชื่อมต่อกับ SES เพื่อส่งอีเมล

ฉันใช้โซลูชันนี้ในไซต์ของฉัน และมันก็กลับมาเร็วอีกครั้ง โดยการลดภาระการส่งอีเมลไปยังกระบวนการภายนอก ไม่ว่าแอปพลิเคชันจะส่งอีเมล 20 หรือ 5,000 ฉบับไม่ได้สร้างความแตกต่าง การตอบสนองต่อผู้ใช้ที่ทริกเกอร์การดำเนินการจะเป็น ทันที

บทสรุป

ในบทความนี้ เราได้วิเคราะห์ว่าเหตุใดการส่งอีเมลธุรกรรมจำนวนมากในคำขอเดียวอาจกลายเป็นคอขวดในแอปพลิเคชัน และสร้างวิธีแก้ปัญหาเพื่อจัดการกับปัญหา: แทนที่จะเชื่อมต่อกับเซิร์ฟเวอร์ SMTP จากภายในแอปพลิเคชัน (พร้อมกัน) เราสามารถ ส่งอีเมลจากฟังก์ชันภายนอกแบบอะซิงโครนัสตามสแต็กของ AWS S3 + Lambda + SES

การส่งอีเมลแบบอะซิงโครนัสทำให้แอปพลิเคชันสามารถจัดการส่งอีเมลนับพันฉบับได้ แต่การตอบสนองต่อผู้ใช้ที่เรียกการดำเนินการจะไม่ได้รับผลกระทบ อย่างไรก็ตาม เพื่อให้แน่ใจว่าผู้ใช้จะไม่รอให้อีเมลมาถึงกล่องจดหมาย เราจึงตัดสินใจแยกอีเมลออกเป็นสองกลุ่ม ได้แก่ ลำดับความสำคัญและไม่ใช่ลำดับความสำคัญ และส่งอีเมลที่ไม่มีความสำคัญแบบอะซิงโครนัสเท่านั้น เราได้จัดเตรียมการใช้งานสำหรับ WordPress ซึ่งค่อนข้างจะแฮ็กเนื่องจากข้อจำกัดของฟังก์ชัน wp_mail สำหรับการส่งอีเมล

บทเรียนจากบทความนี้คือฟังก์ชันแบบไร้เซิร์ฟเวอร์บนแอปพลิเคชันที่ทำงานบนเซิร์ฟเวอร์นั้นทำงานได้ดีทีเดียว: ไซต์ที่ทำงานบน CMS เช่น WordPress สามารถปรับปรุงประสิทธิภาพได้โดยใช้คุณลักษณะเฉพาะบนคลาวด์เท่านั้น และหลีกเลี่ยงความซับซ้อนมากมายที่เกิดจากการโยกย้าย ไซต์ที่มีไดนามิกสูงไปจนถึงสถาปัตยกรรมแบบไร้เซิร์ฟเวอร์