Patreon ขยายระบบการแจ้งเตือนด้วย fanout ได้อย่างไร
(patreon.com)- ระบบเดิมที่ใช้งานมานาน 13 ปี และรองรับการแจ้งเตือนระดับหลายพันล้านครั้งต่อปี เริ่มชนปัญหา timeout เมื่อครีเอเตอร์รายใหญ่เพียงรายเดียวสร้างการแจ้งเตือนได้เป็นหลักล้านรายการ ทำให้ Patreon ออกแบบใหม่ด้วยชั้น fanout ที่สร้างการแจ้งเตือนล่วงหน้าต่อผู้รับแต่ละราย
- ใช้ fanout สองขั้นตอน โดยแบ่งรายชื่อผู้รับเป็นแบตช์เล็ก ๆ และประมวลผลฟีดในแอป, พุช, และอีเมลแยกจากกัน เพื่อให้ขยายแนวนอนได้และแยกผลกระทบเมื่อช่องทางใดช่องทางหนึ่งล่ม
- สำหรับครีเอเตอร์รายใหญ่ ฟีดในแอปและพุช เร็วขึ้น 80% ส่วนอีเมล เร็วขึ้น 55% และเมื่อจำนวนผู้ชมเพิ่มขึ้น 4 เท่า อัตราการเพิ่มขึ้นของ latency แบบ end-to-end ก็ลดจากเดิม 186% เหลือ 33% และ 60% ตามลำดับ
- ใช้ Notification Factory และ API เดียว
send_fanout_notificationsเพื่อทำให้การกรอง, การตรวจสอบสิทธิ์, และการสร้าง payload เป็นมาตรฐานเดียวกัน พร้อมติดตามเวลาในแต่ละขั้นและผลลัพธ์รายผู้รับ ทำให้การปฏิบัติการและการดีบักง่ายขึ้น - มี 10 ทีมและวิศวกรมากกว่า 30 คนช่วยกันย้ายการแจ้งเตือนกว่า 200 รายการ โดย การสร้างดราฟต์ด้วย AI และเส้นตายที่ผู้นำกำหนดอย่างชัดเจนช่วยลดงานซ้ำและลดการต้องรันสองระบบคู่ขนานเป็นเวลานาน
ครีเอเตอร์รายใหญ่เผยให้เห็นข้อจำกัดด้านการขยายระบบ
- Patreon ส่ง การแจ้งเตือนหลายพันล้านครั้งต่อปี ให้แฟน ๆ เมื่อครีเอเตอร์เผยแพร่โพสต์หรือเริ่มไลฟ์สตรีม
- จากการเปิดตัวสมาชิกฟรีและการเติบโต ทำให้จำนวนแฟนต่อครีเอเตอร์พุ่งสูงขึ้น และต้นปี 2025 ก็เริ่มเห็นคอขวดด้านประสิทธิภาพอย่างรุนแรง โดยเฉพาะกับครีเอเตอร์รายใหญ่
- เดิมการแจ้งเตือนโพสต์ทำงานด้วยงานอะซิงก์เดียวที่ดึงผู้รับทั้งหมดมา กรองตามการตั้งค่ารายผู้ใช้ สร้าง payload ให้แต่ละคน แล้วส่งต่อไปยังระบบส่งฟีดในแอป, พุช, และอีเมล
- สำหรับครีเอเตอร์รายใหญ่ งานเดียวต้องรับผิดชอบการสร้าง การแจ้งเตือนหลายล้านรายการ จึงเกิด timeout อย่างต่อเนื่อง และทำให้ส่งการแจ้งเตือนไปยังผู้ชมจำนวนมากได้อย่างเสถียรยากขึ้น
- โฟลว์เดิมมีข้อจำกัดเชิงโครงสร้างอยู่สองอย่าง
- ต้นทุนการสร้างต่อผู้รับถูกรวมอยู่ในงานอะซิงก์งานเดียว ทำให้ ขยายแนวนอน ไม่ได้ และมีเพดาน throughput ที่ชัดเจน
- ฟีดในแอป, พุช, และอีเมลเชื่อมกันแน่นเกินไป ความล้มเหลวของช่องทางหนึ่งจึงสามารถขัดขวางช่องทางอื่นได้ด้วย
- หลายทีมผลิตภัณฑ์แก้ปัญหาระยะสั้นด้วยการสร้างวิธีขยายเฉพาะสำหรับการแจ้งเตือนปริมาณสูงแต่ละแบบ แต่สิ่งนี้ทำให้แพลตฟอร์มซับซ้อนขึ้น และทำให้การพัฒนา, การปฏิบัติการ, และการดีบักยากขึ้น
- จึงตัดสินใจคงระบบส่งเดิมไว้ แต่เพิ่ม ชั้น fanout กลางไว้ข้างหน้าเพื่อให้ได้ความสามารถในการขยายแนวนอน, การแยกช่องทาง, และการสังเกตการณ์ระบบ
เงื่อนไขการออกแบบ 6 ข้อของแพลตฟอร์ม fanout
- ชั้นใหม่ถูกออกแบบให้แบ่งรายชื่อผู้รับขนาดใหญ่เป็นแบตช์เล็ก ๆ สร้างการแจ้งเตือนต่อผู้รับ แล้วส่งต่อไปยังระบบฟีดในแอป, พุช, และอีเมลเดิม
- แพลตฟอร์มนี้กำหนดข้อกำหนดไว้ 6 ข้อดังนี้
- เป็นมิตรกับนักพัฒนา: ไม่ควรต้องแก้ปัญหาเรื่องการขยายและความเชื่อถือได้ใหม่ทั้งหมดทุกครั้งที่มีการแจ้งเตือนปริมาณสูง และควรลดการใช้งานผิดพลาดพร้อมรองรับกรณีใช้งานที่หลากหลาย
- ขยายแนวนอน: ต้องแบ่งรายชื่อผู้รับเป็นแบตช์เล็ก ๆ โดยอัตโนมัติและประมวลผลแบบอะซิงก์ได้
- แยกช่องทาง: ต้องให้ฟีดในแอป, พุช, และอีเมลทำงานอย่างอิสระ เพื่อไม่ให้ปัญหาจากช่องทางหนึ่งลามไปอีกช่องทาง
- จัดการตามลำดับความสำคัญ: การแจ้งเตือนที่ไวต่อเวลาต้องใช้คิวและ worker pool เฉพาะ แยกจากการแจ้งเตือนทั่วไป
- สังเกตการณ์ได้: ต้องตรวจสอบผลการกรองและการส่ง รวมถึงวัด latency ตลอดวงจรชีวิตของการแจ้งเตือนได้
- ขยายต่อได้: ต้องเพิ่มความสามารถในอนาคต เช่น การสร้างรายชื่อผู้รับ โดยไม่ต้องรีแฟกเตอร์ครั้งใหญ่ทั้งระบบอีก
แยก business logic ด้วย Notification Factory
- เพื่อแยก orchestration ของแพลตฟอร์มออกจาก business logic เฉพาะของแต่ละการแจ้งเตือน จึงนำ abstraction ชื่อ Notification Factory มาใช้
- แต่ละ factory ทำหน้าที่เป็นพิมพ์เขียวของการประมวลผลการแจ้งเตือน โดยกำหนดสิ่งต่อไปนี้
- ลำดับความสำคัญของการประมวลผล
- การตั้งค่าการแจ้งเตือนที่ต้องใช้
- วิธีกรองผู้รับ
- การตรวจสอบสิทธิ์เฉพาะของการแจ้งเตือน
- วิธีสร้าง payload แยกตามช่องทาง
- แพตเทิร์นนี้อิงจาก เอนจินงานแบบกระจายภายในของ Patreon และใช้แนวคิดร่วมกันหลายส่วน
- โมเดล Notification กำหนดอาร์กิวเมนต์ของงานที่เก็บข้อมูลเฉพาะของการแจ้งเตือนนั้นและใช้ร่วมกันโดยผู้รับทั้งหมด
- คลาสย่อยของ BaseNotificationFactory ที่เชื่อมต่อกันจะทำหน้าที่เป็นตัวประมวลผลงาน พร้อมมีการตั้งค่าและเมธอดสำหรับกรองผู้รับและสร้างการแจ้งเตือน
- เพื่อให้งาน fanout ซึ่งรับเพียง payload การแจ้งเตือนที่ถูก serialize แล้ว สามารถค้นหา business logic ที่ต้องรันได้ จึงใช้ registry สำหรับแมประหว่างโมเดล Notification แต่ละตัวกับคลาส factory ที่สอดคล้องกัน
- ระหว่างรองรับประเภทการแจ้งเตือนที่หลากหลาย ก็ยังต้องป้องกันข้อผิดพลาดในการติดตั้งใช้งานที่พบบ่อยและการดึงข้อมูลที่ไม่มีประสิทธิภาพด้วย
- Patreon มีทั้งชนิดของเรกคอร์ดผู้ใช้และวิธีกรองหลายแบบ จึงออกแบบอินเทอร์เฟซผู้รับให้เป็นแบบทั่วไป
- และมี hook ที่ชัดเจนสำหรับการใช้งานการตั้งค่าผู้ใช้และการตรวจสอบสิทธิ์เฉพาะของการแจ้งเตือน
API เดียวและ fanout สองขั้นตอน
- ในอดีต แต่ละช่องทางใช้ API สำหรับส่งแยกกัน ทำให้โฟลว์ของฟีดในแอป, พุช, และอีเมลต่างกันได้ และทีมผลิตภัณฑ์ก็ต้องคิดถึงพฤติกรรมแยกตามช่องทางเร็วเกินไป
- หลังการออกแบบใหม่ จุดเริ่มต้นถูกรวมเป็น API เดียวคือ
send_fanout_notifications- ฝั่งที่เรียกใช้จะส่ง payload ของการแจ้งเตือน, รายชื่อผู้รับ, และ timestamp ที่ใช้วัดความหน่วงของการส่ง
- API จะค้นหา factory ของการแจ้งเตือนนั้น กำหนดขนาดแบตช์และลำดับความสำคัญ จากนั้นกระจายงานแยกตามช่องทางอย่างอิสระ
- ความล้มเหลวหรือความล่าช้าของช่องทางหนึ่งจะไม่ขัดขวางช่องทางอื่น
- ขั้นแรกมีหน้าที่ ขยายการสร้างการแจ้งเตือน
- แบ่งรายชื่อผู้รับทั้งหมดออกเป็นแบตช์เล็ก ๆ
- แต่ละแบตช์ถูกประมวลผลโดยงาน
FanoutNotifications - งานนี้จะกรองผู้รับ, รันการตรวจสอบสิทธิ์เฉพาะของการแจ้งเตือน, แล้วใช้ Notification Factory เพื่อสร้าง payload แยกตามช่องทาง
- ขั้นที่สองรับผิดชอบ ขยายการส่งต่อเพื่อส่งมอบ
- นำการแจ้งเตือนที่สร้างแล้วไป fanout ต่ออีกครั้งเป็นงานส่งเฉพาะสำหรับฟีดในแอป, พุช, และอีเมล
- ตัวประมวลผลงาน
FanoutNotificationsซึ่งเป็นแกนหลัก อยู่ระหว่าง API และระบบส่งตามช่องทางเดิม โดยเปลี่ยนงานขนาดยักษ์งานเดียวให้เป็นงานเล็กจำนวนมากที่ประมวลผลแยกกันได้
การสังเกตการณ์และการดีบักระดับผู้รับ
- การแจ้งเตือนอาจล้มเหลวหรือถูกข้ามได้จากหลายเหตุผล เช่น ผู้ใช้ปิดการตั้งค่าไว้, ไม่ผ่านการตรวจสอบสิทธิ์, หรือไม่สามารถส่งไปยังอีเมลนั้นได้
- บนแพลตฟอร์มเดิม หากต้องการตรวจสอบผลลัพธ์ของการแจ้งเตือนรายการหนึ่ง จำเป็นต้องนำล็อกและข้อมูลจากหลายระบบมาประกอบกัน และยังไม่มีการติดตามการทำงานรายผู้รับแบบครบถ้วน ทำให้การสืบสวนอาจใช้เวลาหลายชั่วโมง
- แพลตฟอร์มใหม่ถูกออกแบบมาเพื่อตอบคำถามต่อไปนี้ได้
- การส่งการแจ้งเตือนใช้เวลานานเท่าไร
- รายชื่อผู้รับเริ่มต้นมีขนาดเท่าไร
- มีกี่คนที่ถูกกรองออก และเพราะเหตุใด
- แต่ละช่องทางส่งสำเร็จกี่รายการ
- เพราะเหตุใดผู้ใช้คนหนึ่งจึงได้รับหรือไม่ได้รับการแจ้งเตือนรายการหนึ่ง
- มีการนำ โมเดลข้อมูลด้านเวลาและล็อก ที่ส่งต่อได้ตลอดทั้งแพลตฟอร์มมาใช้
- บันทึก timestamp ในแต่ละขั้นของโฟลว์การแจ้งเตือน
- สร้าง metrics และ events ทั้งสำหรับผลรวมและผลลัพธ์รายผู้รับ
- นักพัฒนาสามารถแนบคุณสมบัติการล็อกเพื่อรวมเอนทิตีที่เกี่ยวข้องไว้ในการติดตามการทำงานและ events ได้
- เมื่อปรับปรุงด้านการสังเกตการณ์, การแจ้งเตือน, แดชบอร์ด, และเครื่องมือดีบักแล้ว วิศวกรผลิตภัณฑ์และทีมซัพพอร์ตก็สามารถตรวจสอบปัญหาได้มากขึ้นโดยไม่ต้องพึ่งวิศวกรแพลตฟอร์ม
ย้ายการแจ้งเตือนมากกว่า 200 รายการ
- เนื่องจากแพตเทิร์น Notification Factory เปลี่ยนวิธีพัฒนาและดูแลการแจ้งเตือนโดยตรง จึงไม่สามารถย้ายด้วยแค่ เครื่องมือแปลงโค้ดอัตโนมัติ (codemod) ได้
- จำเป็นต้องคง business logic, การพึ่งพาข้อมูล, กฎด้านสิทธิ์, และพฤติกรรมแยกตามช่องทางของการแจ้งเตือนแต่ละรายการไว้เหมือนเดิม
- จึงสร้าง AI skill ขึ้นจากเอกสารการย้ายและ PR ตัวอย่างการย้ายแบบทำมือที่เป็นต้นแบบ
- นักพัฒนาสามารถรัน
/migrate-notif-fanout <notif_name>เพื่อสร้างผลลัพธ์การย้ายรอบแรกได้ - มันช่วยได้มากกับงานซ้ำ ๆ เช่น การสร้างโมเดลการแจ้งเตือน, การเชื่อม factory, การแก้จุดเรียกใช้, และการใช้แพตเทิร์นของแพลตฟอร์ม
- นักพัฒนาสามารถรัน
- AI ไม่ได้แทนที่การตัดสินใจของวิศวกร เพราะมันอาจพลาด business logic ที่ละเอียดอ่อน, เกิดภาพหลอน, หรือสร้างโค้ดซ้ำที่ยังต้องเก็บงานต่อ
- มีการย้ายการแจ้งเตือนมากกว่า 200 รายการ และด้วยความร่วมมือขนาดใหญ่รวมถึงการใช้ AI ก็ทำให้ส่วนที่เหลืออีก 80% เสร็จภายใน 6 สัปดาห์
ผลลัพธ์ด้านประสิทธิภาพและผลต่อการพัฒนาผลิตภัณฑ์
- เมื่อย้ายการสร้างการแจ้งเตือนต่อผู้รับไปไว้ในงานที่ขยายแนวนอนได้ ก็สามารถสร้าง การแจ้งเตือนหลายล้านรายการ ต่อครีเอเตอร์หนึ่งรายได้อย่างเสถียรโดยไม่เกิด timeout
- ประสิทธิภาพของการแจ้งเตือนปริมาณสูงสำหรับครีเอเตอร์รายใหญ่ดีขึ้นอย่างมาก
- การแจ้งเตือนแบบพุชและฟีดในแอป เร็วขึ้น 80%
- การแจ้งเตือนทางอีเมล เร็วขึ้น 55%
- เมื่อจำนวนผู้ชมเพิ่มขึ้น 4 เท่า latency แบบ end-to-end ของพุชและฟีดในแอปเพิ่มขึ้น 33% ส่วนอีเมลเพิ่มขึ้น 60%
- ภายใต้เงื่อนไขเดียวกัน แพลตฟอร์มเดิมมีอัตราการเพิ่มขึ้นของ latency ถึง 186%
- ทีมผลิตภัณฑ์ไม่จำเป็นต้องออกแบบโซลูชันขยายเฉพาะสำหรับการแจ้งเตือนปริมาณสูงแต่ละแบบอีกต่อไป และความเร็วในการพัฒนาก็ดีขึ้น
- แพลตฟอร์มใหม่นี้รองรับการเปิดตัวสำคัญในปี 2025 เช่น Lives, Quips และการดึงครีเอเตอร์รายใหญ่เข้ามา
- หากไม่มีการออกแบบ fanout ใหม่ การเปิดตัวเหล่านี้คงชนกับคอขวดเดิม และต้องใช้เวลาวิศวกรรมเพิ่มเพื่อสร้างโซลูชันขยายแบบเฉพาะกิจ
- metrics ด้านเวลาในแต่ละขั้นช่วยให้เห็นว่ามีการใช้เวลาไปกับส่วนใด และช่วยปรับโครงสร้างพื้นฐานให้สมดุลระหว่างความเร็วกับต้นทุนได้
การรันคู่ขนาน 9 เดือนและการประสานงานระดับองค์กร
- ส่วนที่ยากที่สุดคือการแก้ระบบ legacy ที่กำลังใช้งานอยู่ พร้อมประสานการย้ายของหลายทีมไปพร้อมกัน
- มีการ รันระบบการแจ้งเตือนสองระบบคู่ขนานเป็นเวลา 9 เดือน
- การเปลี่ยนแปลงทุกอย่างบนแพลตฟอร์มต้องเข้ากันได้ทั้งกับเส้นทางเดิมและเส้นทาง fanout ใหม่
- ขอบเขตที่อาจเกิดบั๊กและเหตุขัดข้องจึงกว้างขึ้น และยิ่งย้ายนาน ความเสี่ยงในการปฏิบัติการก็ยิ่งเพิ่ม
- มีเหตุขัดข้องขนาดเล็กเกิดขึ้นหลายครั้งจากปัญหาความเข้ากันได้ จึงยิ่งจำเป็นต้องย้ายให้เสร็จอย่างรวดเร็วและปลอดภัย
- ช่วงแรกใช้แนวทางจากล่างขึ้นบน โดยให้แต่ละทีมผลิตภัณฑ์จัดลำดับความสำคัญของการย้ายการแจ้งเตือนเอง
- ผ่านไป 6 เดือน ย้ายได้เพียงประมาณ 20%
- ความคืบหน้าช้าเพราะถูกเบียดด้วยโรดแมปผลิตภัณฑ์และลำดับความสำคัญอื่นของแต่ละทีม
- หลังจากผู้นำฝ่ายวิศวกรรมตกลงกันว่าจะให้เสร็จใน ไตรมาส 1 ปี 2026 และกำหนดเส้นตายที่ชัดเจนพร้อมยกระดับเป็นลำดับความสำคัญทั้งองค์กร การย้ายที่เหลือก็เสร็จภายใน 6 สัปดาห์
- งานทั้งหมดมี 10 ทีมและวิศวกรมากกว่า 30 คน เข้าร่วม
- ใช้ตัวติดตามใน Notion และระบบอัตโนมัติในการจัดการความคืบหน้า
- สื่อสารกับผู้มีส่วนได้ส่วนเสียอย่างสม่ำเสมอ และกระจายงานไปหลายทีม
- ทีมแพลตฟอร์มทำการย้ายประมาณครึ่งหนึ่ง ก่อนส่งต่อการ deploy และเก็บงานให้ทีมเจ้าของการแจ้งเตือน
- กระจายภาระงานเพื่อไม่ให้ทีมที่มีการแจ้งเตือนจำนวนมากรับภาระหนักเกินไป
- มีการจัด สัปดาห์ย้ายการแจ้งเตือน พร้อม office hours, ลีดเดอร์บอร์ด, ของรางวัล, และ happy hour เพื่อเร่งความคืบหน้าและฉลองการทำงานเสร็จ
- ยังเผื่อ buffer โดยทำการส่งมอบให้เสร็จก่อนเส้นตายรวม 2 สัปดาห์
- เพื่อช่วยทีมที่ยังต้องการการสนับสนุนเพิ่มเติม และค้นหางานที่ตกหล่นใน codebase
- ในการตรวจรอบสุดท้ายพบงานย้ายใหม่ที่จำเป็นอีก 4 รายการและรายการค้างอีกหลายจุด แต่ก็ปิดงานได้โดยกระทบกำหนดการเพียงเล็กน้อย
บทเรียนจากการย้ายแพลตฟอร์มขนาดใหญ่
- หากไม่มี การจัดลำดับความสำคัญอย่างชัดเจน การย้ายงานข้ามหลายทีมจะต้องไปแข่งขันกับโรดแมปของแต่ละทีมและถูกเลื่อนออกไปได้ง่าย
- เห็นชัดเป็นพิเศษในช่วง long tail ของการแจ้งเตือนปริมาณต่ำ ที่ผลด้านการขยายระบบอาจไม่โดดเด่นนัก
- การเห็นพ้องของผู้นำ, ลำดับความสำคัญร่วมกัน, และเส้นตายที่ชัดเจน มีความสำคัญต่อการทำให้การย้ายเสร็จภายในเวลาที่เหมาะสม
-
งานแพลตฟอร์มไม่ได้จบแค่ที่ API
- ก่อนหน้านี้ การตอบคำถามเรื่องการแจ้งเตือนจากทีมอื่นและการสืบสวนปัญหาการส่งกินสัดส่วนมากของงาน on-call ของวิศวกรแพลตฟอร์ม
- จึงต้องปรับปรุงการสังเกตการณ์, การแจ้งเตือน, แดชบอร์ด, และเครื่องมือดีบักไปพร้อมกัน เพื่อให้ทีมผลิตภัณฑ์และซัพพอร์ตตรวจสอบได้เอง และลดภาระ on-call ของทีมแพลตฟอร์ม
- ต้องคิดถึงไม่ใช่แค่คอขวดปัจจุบัน แต่รวมถึง คอขวดถัดไป ด้วย
- โปรเจกต์เดียวไม่อาจแก้ปัญหาการขยายของการแจ้งเตือนทั้งหมดได้
- การออกแบบให้รองรับความสามารถในอนาคตช่วยลดโอกาสที่จะต้องรีแฟกเตอร์ครั้งใหญ่ใหม่อีกเมื่อปริมาณการแจ้งเตือนเพิ่มขึ้น
คอขวดถัดไปและแผนในอนาคต
- แม้การสร้างการแจ้งเตือนจะขยายได้แล้ว แต่คอขวดถัดไปคือ การสร้างรายชื่อผู้รับ
- เมื่อกลุ่มเป้าหมายของการแจ้งเตือนยังคงใหญ่ขึ้น การดึงผู้รับจำนวนหลายล้านคนอย่างมีประสิทธิภาพก็จะยิ่งสำคัญขึ้น
- มีแผนจะรวมการสร้างรายชื่อผู้รับเข้าไว้ใน Notification Factory เพื่อให้แพลตฟอร์มรับผิดชอบโฟลว์การแจ้งเตือนแบบ end-to-end ได้มากขึ้น
- การตั้งค่าการแจ้งเตือนก็จะได้รับการปรับปรุงทั้งสำหรับนักพัฒนาและแฟน ๆ
- นักพัฒนาจะได้รับการสนับสนุนให้สร้างและจัดการการตั้งค่าใหม่ได้ง่ายขึ้น
- ส่วนแฟน ๆ จะเข้าใจและควบคุมความชอบด้านการแจ้งเตือนได้ง่ายขึ้น
ยังไม่มีความคิดเห็น