บล็อก

การแจ้งเตือนที่ส่งครั้งเดียว ไม่ใช่ครั้งเดียวต่อเซิร์ฟเวอร์

หนึ่งในการแจ้งเตือนเชิงปฏิบัติการของเรา คือการแจ้ง "เครดิตเหลือน้อย" ของแอปที่ publish แล้วของผู้สร้าง ซึ่งตั้งใจให้ส่งถึงเราไม่เกินวันละครั้ง แต่บ่ายวันหนึ่งมันมาถึงห้าครั้ง สาเหตุคือแต่ละสำเนาของ API จำในหน่วยความจำของตัวเองว่าเคยส่งการแจ้งเตือนใดไปแล้ว ทำให้เซิร์ฟเวอร์สองเครื่องส่งซ้ำ และทุกครั้งที่ deploy ก็ล้างความจำนั้นและทำให้ส่งซ้ำได้อีก รูปแบบเดียวกันนี้ยังทำให้ background job ทำงานซ้ำสองเท่าด้วย ตอนนี้ทั้งสองส่วนประสานงานผ่านที่เก็บข้อมูลร่วมกัน และทั้งสองจะล้มเหลวในทางที่ปลอดภัยหากที่เก็บข้อมูลนั้นใช้งานไม่ได้

อาการ

Mythex ส่งการแจ้งเตือนไปยังทีมเมื่อมีบางอย่างที่ต้องตรวจสอบ เช่น บริการล่ม job ค้าง หรือยอดเครดิตของผู้สร้างใกล้หมดขณะที่แอปของเขา publish อยู่ การแจ้งเตือนแต่ละรายการมี dedupe window ซึ่งเป็นช่วงเวลาที่การแจ้งเตือนเดียวกันจะถูกส่งเพียงครั้งเดียว สำหรับการแจ้งเตือนเครดิตเหลือน้อย ช่วงนี้คือ 24 ชั่วโมง

การแจ้งเตือนเดียวกันห้าครั้งในบ่ายเดียวหมายความว่า window นั้นไม่ได้ทำงาน และที่แย่กว่านั้นคือมันมาเป็นคู่ ห่างกันประมาณ 11 นาที

เหตุใดจึงเกิดขึ้น

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

  • API replica สองตัว โปรดักชันรันสำเนาของ API สองตัว แต่ละตัวมีตารางของตัวเอง จึงส่งการแจ้งเตือนหนึ่งครั้งต่อตัว นั่นคือคู่ที่เห็น
  • Deploy ทุกครั้งที่ deploy จะแทนที่ replica ทั้งสองด้วยโปรเซสใหม่และตารางที่ว่างเปล่า หลัง deploy การแจ้งเตือนจึงถูกส่งอีกครั้ง ราวกับไม่เคยถูกส่งมาก่อน

ช่วงประมาณ 11 นาทีระหว่างการแจ้งเตือนสองครั้งในแต่ละคู่มาจาก job ที่สร้างการแจ้งเตือนนี้ มันรันบน replica ทั้งสองตัวและใช้ lock ดังนั้น replica จะได้ lock สลับกัน และแต่ละตัวจะสร้างการแจ้งเตือนเมื่อถึงคิวของตน

มีบั๊กที่สองที่เงียบกว่าในโค้ดชุดเดียวกัน เพื่อไม่ให้ตารางโตไปเรื่อยๆ มันจะลบตัวเองทุกครั้งที่มีคีย์ถึง 500 รายการ โดยลบทุกรายการที่เก่ากว่า 15 นาที ไม่ว่า window ของรายการนั้นจะยาวเท่าไร ภายใต้โหลดสูง การกันซ้ำ 24 ชั่วโมงอาจอยู่ได้เพียงหนึ่งในสี่ชั่วโมง

วิธีแก้

บันทึกร่วมหนึ่งรายการต่อการแจ้งเตือนหนึ่งครั้ง

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

คีย์และ window มาจากกฎเดียวกันที่แพ็กเกจการแจ้งเตือนใช้ ซึ่งตอนนี้ใช้ร่วมกันระหว่างทั้งสองชั้น ดังนั้นทั้งสองชั้นจึงไม่สามารถเห็นต่างกันว่าอะไรนับว่าเป็น "การแจ้งเตือนเดียวกัน"

ล้มเหลวแบบเปิด ไม่ใช่แบบปิด

การตัดสินใจที่สำคัญที่สุดคือจะทำอย่างไรเมื่อเข้าถึง Redis ไม่ได้ เราเลือกที่จะส่งการแจ้งเตือนต่อไป หาก Redis ล่ม หรือไม่ตอบภายใน 1.5 วินาที การแจ้งเตือนก็จะถูกส่งออกไป การแจ้งเตือนซ้ำคือสิ่งรบกวน การแจ้งเตือนที่หายไปคือการที่มีคนไม่ได้รับแจ้ง

การแจ้งเตือนที่รุนแรงที่สุด คือ P0 จะไม่ถูกกันด้วยการกันซ้ำเลย

แต่ละรายการเก็บ window ของตัวเอง

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

เรื่องเดียวกัน ที่อื่น

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

การเขียนของมันเป็น idempotent คือการรันสองครั้งให้ผลเหมือนการรันหนึ่งครั้ง ดังนั้นจึงไม่มีอะไรพัง เพียงแต่ทำงานเป็นสองเท่า ตอนนี้แต่ละช่วงสองนาทีจะถูกจองใน Redis เช่นเดียวกับที่ตัววัดการใช้งานของเราจองของตัวเองแล้ว และมีเพียง replica ที่จองได้เท่านั้นที่รันรอบนั้น หาก Redis เข้าถึงไม่ได้ รอบนั้นจะรันต่อไป เพราะการทำงานซ้ำสองครั้งไม่เป็นอันตราย แต่การไม่ทำงานเลยเป็นอันตราย

วิธีที่เราทดสอบ

การทดสอบจำลองการตั้งค่าจริง โดยโหลดโมดูลการแจ้งเตือนสองครั้งในฐานะ "replica" สองตัวที่แยกกัน ชี้ทั้งสองไปยังที่เก็บข้อมูลร่วมตัวเดียว สร้างการแจ้งเตือนเดียวกันจากแต่ละตัว จากนั้นโหลดสำเนาใหม่อีกหนึ่งตัวเพื่อแทน "หลัง deploy" และสร้างการแจ้งเตือนนั้นอีกครั้ง ผลคือมีการแจ้งเตือนถูกส่งเพียงหนึ่งครั้ง การทดสอบอื่นตรวจว่าการแจ้งเตือนของผู้สร้างอีกคนยังคงถูกส่ง P0 ถูกส่งจากทั้งสอง replica และการแจ้งเตือนยังถูกส่งเมื่อที่เก็บข้อมูลล่มหรือค้างเกินเวลาที่กำหนด การแก้ไขการลบมีการทดสอบของตัวเอง: คีย์ 24 ชั่วโมงยังอยู่รอดหลังการแจ้งเตือนอื่นอีก 600 รายการเข้ามาหลังจาก 20 นาที

สำหรับ job การกระทบยอด replica สองตัวที่ tick พร้อมกัน บวกกับ tick ที่สามในช่วงเวลาเดียวกัน ให้ผลลัพธ์เป็นการแสดงรายการเพียงหนึ่งครั้ง

สิ่งที่เราได้เรียนรู้

  • สถานะในหน่วยความจำเป็นของโปรเซสนั้นๆ สิ่งใดที่ต้องเป็นจริงข้ามเซิร์ฟเวอร์ เช่น "ส่งแล้วครั้งหนึ่ง" หรือ "รันแล้วครั้งหนึ่ง" ต้องอยู่ในที่ที่พวกมันใช้ร่วมกัน
  • Deploy คือการรีสตาร์ต สถานะที่รอดชีวิตได้จนถึงการรีสตาร์ตครั้งถัดไปจะไม่รอดข้ามวันทำงานปกติ
  • ตัดสินใจว่าจะล้มเหลวทางไหน สำหรับการแจ้งเตือน หากที่เก็บข้อมูลร่วมล่ม ควรหมายถึงการแจ้งเตือนที่มากขึ้น ไม่ใช่น้อยลง
  • มองหารูปแบบ ไม่ใช่แค่อินสแตนซ์เดียว การแจ้งเตือนคือกรณีที่ดังที่สุด ส่วน job กระทบยอดที่ซ้ำสองเท่าคือบั๊กเดียวกันที่กินงานอย่างเงียบๆ

ไม่มีสิ่งนี้มองเห็นได้จากภายใน Mythex สำหรับข้อมูลเพิ่มเติมว่าเราทำให้แพลตฟอร์มมั่นคงอย่างไร ดู วิธีที่เราทำให้การ deploy เหลือศูนย์ข้อผิดพลาด 502 และ ไฟล์ swap ที่หายไป หากคุณยังใหม่กับแนวคิดการรันเซิร์ฟเวอร์มากกว่าหนึ่งตัวของบริการเดียว คู่มือของเราเรื่อง เซิร์ฟเวอร์คืออะไร ครอบคลุมพื้นฐาน

Mythex · Pricing · สร้างอะไรก็ได้ด้วย AI · เทมเพลตเว็บไซต์และแอป · เปรียบเทียบ Mythex · อภิธานศัพท์การสร้างแอปด้วย AI · เครื่องมือฟรีสำหรับสร้างแอปด้วย AI · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)