10 yeas of experience of building mission critical Fintech system handling extremely high load
Walkthroughs, tutorials, guides, and tips. This story will teach you how to do something new or how to do something better.
ความยืดหยุ่นในซอฟต์แวร์หมายถึงความสามารถของแอปพลิเคชันที่จะทำงานได้อย่างราบรื่นและเชื่อถือได้แม้จะเผชิญกับปัญหาหรือความล้มเหลวที่ไม่คาดคิด ในโครงการ Fintech ความยืดหยุ่นมีความสำคัญเป็นอย่างยิ่งเนื่องจากหลายสาเหตุ ประการแรก บริษัทต่างๆ มีหน้าที่ต้องปฏิบัติตามข้อกำหนดด้านกฎระเบียบ และหน่วยงานกำกับดูแลด้านการเงินเน้นย้ำถึงความยืดหยุ่นในการดำเนินงานเพื่อรักษาเสถียรภาพภายในระบบ ยิ่งไปกว่านั้น การแพร่กระจายของเครื่องมือดิจิทัลและการพึ่งพาผู้ให้บริการบุคคลที่สามทำให้ธุรกิจ Fintech เผชิญกับภัยคุกคามด้านความปลอดภัยที่เพิ่มมากขึ้น ความยืดหยุ่นยังช่วยบรรเทาความเสี่ยงของการหยุดทำงานที่เกิดจากปัจจัยต่างๆ เช่น ภัยคุกคามทางไซเบอร์ โรคระบาด หรือเหตุการณ์ทางภูมิรัฐศาสตร์ ปกป้องการดำเนินงานหลักของธุรกิจและสินทรัพย์ที่สำคัญ
จากรูปแบบการฟื้นตัว เราเข้าใจแนวทางปฏิบัติและกลยุทธ์ที่ดีที่สุดชุดหนึ่งที่ออกแบบมาเพื่อให้แน่ใจว่าซอฟต์แวร์สามารถทนต่อการหยุดชะงักและรักษาการทำงานไว้ได้ รูปแบบเหล่านี้ทำหน้าที่เสมือนตาข่ายนิรภัยที่จัดเตรียมกลไกสำหรับจัดการข้อผิดพลาด จัดการภาระงาน และกู้คืนจากความล้มเหลว จึงมั่นใจได้ว่าแอปพลิเคชันจะยังคงแข็งแกร่งและเชื่อถือได้ภายใต้เงื่อนไขที่ไม่เอื้ออำนวย
กลยุทธ์การฟื้นตัวที่พบได้บ่อยที่สุด ได้แก่ แบลคเฮด แคช ฟอลแบ็ค การลองใหม่ และเซอร์กิตเบรกเกอร์ ในบทความนี้ ฉันจะอธิบายกลยุทธ์เหล่านี้โดยละเอียดมากขึ้น พร้อมยกตัวอย่างปัญหาที่กลยุทธ์เหล่านี้สามารถช่วยแก้ไขได้
มาดูการตั้งค่าด้านบนกัน เรามีแอปพลิเคชันธรรมดาๆ ที่มีแบ็กเอนด์หลายตัวอยู่เบื้องหลังเพื่อรับข้อมูลบางส่วน มีไคลเอนต์ HTTP หลายตัวที่เชื่อมต่อกับแบ็กเอนด์เหล่านี้ ปรากฏว่าทั้งหมดใช้พูลการเชื่อมต่อเดียวกัน! และยังมีทรัพยากรอื่นๆ เช่น CPU และ RAM อีกด้วย
จะเกิดอะไรขึ้น หากแบ็กเอนด์ตัวใดตัวหนึ่งประสบปัญหาบางอย่างที่ส่งผลให้มีความล่าช้าในการร้องขอสูง เนื่องจากเวลาตอบสนองสูง พูลการเชื่อมต่อทั้งหมดจึงเต็มไปด้วยคำขอที่รอการตอบสนองจากแบ็กเอนด์ 1 เป็นผลให้คำขอที่ตั้งใจส่งไปยังแบ็กเอนด์ 2 และแบ็กเอนด์ 3 ที่ยังทำงานได้ปกติจะไม่สามารถดำเนินการต่อได้เนื่องจากพูลหมดลง ซึ่งหมายความว่าความล้มเหลวในแบ็กเอนด์ตัวใดตัวหนึ่งของเราอาจทำให้เกิดความล้มเหลวในแอปพลิเคชันทั้งหมดได้ ในอุดมคติ เราต้องการให้เฉพาะฟังก์ชันการทำงานที่เกี่ยวข้องกับแบ็กเอนด์ที่ล้มเหลวประสบกับความเสื่อมโทรม ในขณะที่แอปพลิเคชันที่เหลือยังคงทำงานได้ตามปกติ
Bulkhead Pattern คืออะไร?
คำว่า Bulkhead pattern มาจากคำว่า Bulkhead pattern ซึ่งมาจากการต่อเรือ โดยเกี่ยวข้องกับการสร้างช่องแยกหลายช่องภายในเรือ หากเกิดการรั่วไหลในช่องหนึ่ง น้ำจะไหลเข้าไป แต่ช่องอื่นๆ จะไม่ได้รับผลกระทบ การแยกส่วนนี้จะช่วยป้องกันไม่ให้เรือทั้งลำจมลงเนื่องจากเกิดการรั่วไหลเพียงครั้งเดียว
รูปแบบ Bulkhead สามารถใช้เพื่อแยกทรัพยากรประเภทต่างๆ ภายในแอปพลิเคชัน ป้องกันไม่ให้ความล้มเหลวในส่วนหนึ่งส่งผลกระทบต่อระบบทั้งหมด นี่คือวิธีที่เราสามารถนำไปใช้กับปัญหาของเรา:
ลองสมมติว่าระบบแบ็กเอนด์ของเรามีโอกาสพบข้อผิดพลาดทีละรายการต่ำ อย่างไรก็ตาม เมื่อการดำเนินการเกี่ยวข้องกับการสอบถามแบ็กเอนด์ทั้งหมดเหล่านี้แบบขนาน แบ็กเอนด์แต่ละอันสามารถส่งคืนข้อผิดพลาดได้โดยอิสระ เนื่องจากข้อผิดพลาดเหล่านี้เกิดขึ้นโดยอิสระ ความน่าจะเป็นโดยรวมของข้อผิดพลาดในแอปพลิเคชันของเราจึงสูงกว่าความน่าจะเป็นของข้อผิดพลาดของแบ็กเอนด์เดี่ยว ความน่าจะเป็นของข้อผิดพลาดสะสมสามารถคำนวณได้โดยใช้สูตร P_total=1−(1−p)^n โดยที่ n คือจำนวนระบบแบ็กเอนด์
ตัวอย่างเช่น หากเรามีแบ็กเอนด์ 10 รายการ โดยแต่ละรายการมีความน่าจะเป็นของข้อผิดพลาดเท่ากับ p=0.001 (สอดคล้องกับ SLA 99.9%) ความน่าจะเป็นของข้อผิดพลาดที่เกิดขึ้นจะเป็นดังนี้:
P_total=1−(1−0.001)^10=0.009955
ซึ่งหมายความว่า SLA รวมของเราลดลงเหลือประมาณ 99% ซึ่งแสดงให้เห็นว่าความน่าเชื่อถือโดยรวมลดลงเมื่อทำการสอบถามแบ็กเอนด์หลายรายการพร้อมกัน เพื่อบรรเทาปัญหานี้ เราสามารถใช้แคชในหน่วยความจำได้
แคชในหน่วยความจำทำหน้าที่เป็นบัฟเฟอร์ข้อมูลความเร็วสูง จัดเก็บข้อมูลที่เข้าถึงบ่อยครั้ง และขจัดความจำเป็นในการดึงข้อมูลจากแหล่งที่มาที่อาจทำงานช้าทุกครั้ง เนื่องจากแคชที่เก็บไว้ในหน่วยความจำมีโอกาสเกิดข้อผิดพลาด 0% เมื่อเทียบกับการดึงข้อมูลผ่านเครือข่าย จึงเพิ่มความน่าเชื่อถือของแอปพลิเคชันของเราได้อย่างมาก นอกจากนี้ การแคชยังช่วยลดปริมาณการรับส่งข้อมูลบนเครือข่าย ทำให้โอกาสเกิดข้อผิดพลาดลดลงอีกด้วย ดังนั้น การใช้แคชในหน่วยความจำจึงช่วยลดอัตราข้อผิดพลาดในแอปพลิเคชันของเราได้เมื่อเทียบกับระบบแบ็กเอนด์ นอกจากนี้ แคชในหน่วยความจำยังช่วยให้ดึงข้อมูลได้เร็วกว่าการดึงข้อมูลผ่านเครือข่าย จึงช่วยลดเวลาแฝงของแอปพลิเคชัน ซึ่งถือเป็นข้อได้เปรียบที่สำคัญ
สำหรับข้อมูลส่วนบุคคล เช่น โปรไฟล์ผู้ใช้หรือคำแนะนำ การใช้แคชในหน่วยความจำก็มีประสิทธิภาพสูงเช่นกัน แต่เราต้องแน่ใจว่าคำขอทั้งหมดจากผู้ใช้จะไปยังอินสแตนซ์แอปพลิเคชันเดียวกันอย่างสม่ำเสมอเพื่อใช้ข้อมูลที่แคชไว้สำหรับพวกเขา ซึ่งจำเป็นต้องมีเซสชันคงที่ การนำเซสชันคงที่ไปใช้อาจมีความท้าทาย แต่สำหรับสถานการณ์นี้ เราไม่จำเป็นต้องมีกลไกที่ซับซ้อน การปรับสมดุลปริมาณการรับส่งข้อมูลเล็กน้อยเป็นที่ยอมรับได้ ดังนั้นอัลกอริทึมการปรับสมดุลโหลดที่เสถียร เช่น การแฮชที่สม่ำเสมอจะเพียงพอ
ยิ่งไปกว่านั้น ในกรณีที่โหนดล้มเหลว การแฮชที่สม่ำเสมอจะรับประกันว่าเฉพาะผู้ใช้ที่เชื่อมโยงกับโหนดที่ล้มเหลวเท่านั้นที่จะได้รับการสมดุลใหม่ ซึ่งช่วยลดการหยุดชะงักของระบบให้เหลือน้อยที่สุด แนวทางนี้ช่วยลดความซับซ้อนในการจัดการแคชส่วนบุคคล และเพิ่มเสถียรภาพและประสิทธิภาพโดยรวมของแอปพลิเคชันของเรา
หากข้อมูลที่เราตั้งใจจะแคชนั้นมีความสำคัญและใช้ในคำขอทุกรายการที่ระบบของเราจัดการ เช่น นโยบายการเข้าถึง แผนการสมัครใช้งาน หรือเอนทิตีสำคัญอื่นๆ ในโดเมนของเรา แหล่งที่มาของข้อมูลนี้อาจก่อให้เกิดจุดล้มเหลวที่สำคัญในระบบของเราได้ เพื่อรับมือกับความท้าทายนี้ วิธีหนึ่งคือการจำลองข้อมูลนี้ทั้งหมดโดยตรงในหน่วยความจำของแอปพลิเคชันของเรา
ในสถานการณ์นี้ หากปริมาณข้อมูลในแหล่งข้อมูลสามารถจัดการได้ เราสามารถเริ่มกระบวนการโดยดาวน์โหลดสแน็ปช็อตของข้อมูลนี้เมื่อเริ่มต้นแอปพลิเคชันของเรา จากนั้น เราจะสามารถรับเหตุการณ์การอัปเดตเพื่อให้แน่ใจว่าข้อมูลที่เก็บไว้ในแคชยังคงซิงโครไนซ์กับแหล่งข้อมูลได้ การนำวิธีนี้มาใช้จะช่วยเพิ่มความน่าเชื่อถือในการเข้าถึงข้อมูลสำคัญนี้ เนื่องจากการดึงข้อมูลแต่ละครั้งเกิดขึ้นโดยตรงจากหน่วยความจำโดยมีความน่าจะเป็นของข้อผิดพลาด 0% นอกจากนี้ การดึงข้อมูลจากหน่วยความจำยังรวดเร็วเป็นพิเศษ จึงช่วยเพิ่มประสิทธิภาพการทำงานของแอปพลิเคชันของเรา กลยุทธ์นี้ช่วยลดความเสี่ยงที่เกี่ยวข้องกับการพึ่งพาแหล่งข้อมูลภายนอกได้อย่างมีประสิทธิภาพ ทำให้มั่นใจได้ว่าการเข้าถึงข้อมูลสำคัญสำหรับการทำงานของแอปพลิเคชันของเรามีความสม่ำเสมอและเชื่อถือได้
อย่างไรก็ตาม ความจำเป็นในการดาวน์โหลดข้อมูลเมื่อเริ่มต้นแอปพลิเคชัน ส่งผลให้กระบวนการเริ่มต้นล่าช้า ถือเป็นการละเมิดหลักการหนึ่งของ "แอปพลิเคชัน 12 ปัจจัย" ที่สนับสนุนให้เริ่มต้นแอปพลิเคชันได้รวดเร็ว อย่างไรก็ตาม เราไม่ต้องการเสียประโยชน์จากการใช้แคช เพื่อแก้ไขปัญหานี้ เรามาดูวิธีแก้ปัญหาที่เป็นไปได้กัน
การเริ่มต้นอย่างรวดเร็วถือเป็นสิ่งสำคัญ โดยเฉพาะอย่างยิ่งสำหรับแพลตฟอร์มอย่าง Kubernetes ซึ่งต้องอาศัยการโยกย้ายแอปพลิเคชันอย่างรวดเร็วไปยังโหนดทางกายภาพต่างๆ โชคดีที่ Kubernetes สามารถจัดการแอปพลิเคชันที่เริ่มต้นช้าได้โดยใช้ฟีเจอร์ต่างๆ เช่น โพรบการเริ่มต้น
ความท้าทายอีกประการหนึ่งที่เราอาจเผชิญคือการอัปเดตคอนฟิกูเรชันในขณะที่แอปพลิเคชันกำลังทำงาน บ่อยครั้งจำเป็นต้องปรับเวลาแคชหรือระยะเวลาหมดเวลาของคำขอเพื่อแก้ไขปัญหาการผลิต แม้ว่าเราจะสามารถปรับใช้ไฟล์คอนฟิกูเรชันที่อัปเดตกับแอปพลิเคชันได้อย่างรวดเร็ว แต่การใช้การเปลี่ยนแปลงเหล่านี้มักต้องรีสตาร์ท เนื่องจากเวลาเริ่มต้นของแอปพลิเคชันแต่ละตัวขยายออกไป การรีสตาร์ทแบบต่อเนื่องอาจทำให้การปรับใช้การแก้ไขให้กับผู้ใช้ของเราล่าช้าอย่างมาก
เพื่อแก้ไขปัญหานี้ วิธีแก้ปัญหาอย่างหนึ่งคือการจัดเก็บการกำหนดค่าในตัวแปรที่ทำงานพร้อมกันและให้เธรดพื้นหลังอัปเดตเป็นระยะ อย่างไรก็ตาม พารามิเตอร์บางอย่าง เช่น การหมดเวลาคำขอ HTTP อาจต้องเริ่มต้นไคลเอนต์ HTTP หรือฐานข้อมูลใหม่เมื่อการกำหนดค่าที่เกี่ยวข้องเปลี่ยนแปลง ซึ่งอาจก่อให้เกิดความท้าทายได้ อย่างไรก็ตาม ไคลเอนต์บางตัว เช่น ไดรเวอร์ Cassandra สำหรับ Java รองรับการโหลดการกำหนดค่าใหม่โดยอัตโนมัติ ทำให้กระบวนการนี้ง่ายขึ้น
การนำการกำหนดค่าที่โหลดซ้ำได้มาใช้สามารถลดผลกระทบเชิงลบของเวลาเริ่มต้นแอปพลิเคชันที่ยาวนานและยังมอบผลประโยชน์เพิ่มเติม เช่น อำนวยความสะดวกในการนำฟีเจอร์แฟล็กมาใช้ แนวทางนี้ช่วยให้เราสามารถรักษาความน่าเชื่อถือและการตอบสนองของแอปพลิเคชันได้ในขณะที่จัดการการอัปเดตการกำหนดค่าได้อย่างมีประสิทธิภาพ
ตอนนี้มาดูปัญหาอื่นกันบ้าง: ในระบบของเรา เมื่อได้รับคำขอจากผู้ใช้และประมวลผลโดยส่งแบบสอบถามไปยังแบ็กเอนด์หรือฐานข้อมูล บางครั้งอาจได้รับการตอบสนองข้อผิดพลาดแทนข้อมูลที่คาดหวัง จากนั้น ระบบของเราจะตอบกลับผู้ใช้ด้วย "ข้อผิดพลาด"
อย่างไรก็ตาม ในหลายๆ สถานการณ์ อาจจะดีกว่าที่จะแสดงข้อมูลที่ล้าสมัยเล็กน้อยพร้อมกับข้อความที่ระบุว่ามีการล่าช้าในการรีเฟรชข้อมูล แทนที่จะปล่อยให้ผู้ใช้ต้องเจอกับข้อความแสดงข้อผิดพลาดสีแดงขนาดใหญ่
เพื่อแก้ไขปัญหานี้และปรับปรุงการทำงานของระบบ เราสามารถใช้รูปแบบ Fallback ได้ แนวคิดเบื้องหลังรูปแบบนี้เกี่ยวข้องกับการมีแหล่งข้อมูลรองซึ่งอาจมีข้อมูลที่มีคุณภาพต่ำกว่าหรือมีความสดใหม่กว่าเมื่อเทียบกับแหล่งข้อมูลหลัก หากแหล่งข้อมูลหลักไม่พร้อมใช้งานหรือส่งข้อผิดพลาดกลับมา ระบบจะกลับไปเรียกข้อมูลจากแหล่งข้อมูลรองนี้เพื่อให้แน่ใจว่ามีข้อมูลบางรูปแบบนำเสนอต่อผู้ใช้แทนที่จะแสดงข้อความแสดงข้อผิดพลาด
หากคุณลองดูภาพด้านบน คุณจะสังเกตเห็นความคล้ายคลึงกันระหว่างปัญหาที่เราเผชิญอยู่ในขณะนี้กับปัญหาที่เราพบในตัวอย่างแคช
เพื่อแก้ไขปัญหานี้ เราสามารถพิจารณาใช้รูปแบบที่เรียกว่าการลองใหม่ แทนที่จะพึ่งพาแคช ระบบสามารถออกแบบให้ส่งคำขอซ้ำโดยอัตโนมัติในกรณีที่เกิดข้อผิดพลาด รูปแบบการลองใหม่นี้เป็นทางเลือกที่ง่ายกว่าและสามารถลดโอกาสเกิดข้อผิดพลาดในแอปพลิเคชันของเราได้อย่างมีประสิทธิภาพ ซึ่งแตกต่างจากการแคชซึ่งมักต้องใช้กลไกการทำให้แคชไม่ถูกต้องที่ซับซ้อนเพื่อจัดการกับการเปลี่ยนแปลงข้อมูล การลองใหม่เมื่อคำขอล้มเหลวทำได้ค่อนข้างตรงไปตรงมา เนื่องจากการทำให้แคชไม่ถูกต้องถือเป็นหนึ่งในงานที่ท้าทายที่สุดในวิศวกรรมซอฟต์แวร์ การใช้กลยุทธ์การลองใหม่จะปรับปรุงการจัดการข้อผิดพลาดและปรับปรุงความยืดหยุ่นของระบบได้
อย่างไรก็ตาม การใช้กลยุทธ์การลองใหม่อีกครั้งโดยไม่คำนึงถึงผลที่อาจเกิดขึ้นอาจก่อให้เกิดภาวะแทรกซ้อนเพิ่มเติมได้
ลองนึกภาพว่าแบ็กเอนด์ของเรามีปัญหา ในสถานการณ์เช่นนี้ การเริ่มดำเนินการซ้ำกับแบ็กเอนด์ที่มีปัญหาอาจส่งผลให้ปริมาณการรับส่งข้อมูลเพิ่มขึ้นอย่างมาก ปริมาณการรับส่งข้อมูลที่เพิ่มขึ้นอย่างกะทันหันอาจทำให้แบ็กเอนด์รับมือไม่ไหว ส่งผลให้ความล้มเหลวรุนแรงขึ้น และอาจทำให้เกิดผลกระทบแบบลูกโซ่ไปทั่วทั้งระบบ
เพื่อรับมือกับความท้าทายนี้ สิ่งสำคัญคือต้องเสริมรูปแบบการลองใหม่ด้วยรูปแบบเซอร์กิตเบรกเกอร์ เซอร์กิตเบรกเกอร์ทำหน้าที่เป็นกลไกป้องกันที่ตรวจสอบอัตราข้อผิดพลาดของบริการปลายทาง เมื่ออัตราข้อผิดพลาดเกินเกณฑ์ที่กำหนดไว้ เซอร์กิตเบรกเกอร์จะขัดจังหวะคำขอไปยังบริการที่ได้รับผลกระทบเป็นระยะเวลาที่กำหนด ในช่วงเวลาดังกล่าว ระบบจะไม่ส่งคำขอเพิ่มเติมเพื่อให้บริการที่ล้มเหลวมีเวลาในการกู้คืน หลังจากช่วงเวลาที่กำหนด เซอร์กิตเบรกเกอร์จะอนุญาตให้คำขอจำนวนจำกัดผ่านไปอย่างระมัดระวัง เพื่อตรวจสอบว่าบริการได้เสถียรแล้วหรือไม่ หากบริการฟื้นตัวแล้ว ปริมาณการรับส่งข้อมูลปกติจะค่อยๆ กลับมาเป็นปกติ มิฉะนั้น เซอร์กิตเบรกเกอร์จะยังคงเปิดอยู่และบล็อกคำขอต่อไปจนกว่าบริการจะกลับสู่การทำงานปกติ ด้วยการรวมรูปแบบเซอร์กิตเบรกเกอร์เข้ากับตรรกะการลองใหม่ เราสามารถจัดการสถานการณ์ข้อผิดพลาดได้อย่างมีประสิทธิภาพและป้องกันภาระงานของระบบเกินในระหว่างที่แบ็คเอนด์ล้มเหลว
โดยสรุป การนำรูปแบบการฟื้นตัวเหล่านี้มาใช้จะทำให้เราสามารถเสริมความแข็งแกร่งให้กับแอปพลิเคชันของเราเพื่อรับมือกับเหตุการณ์ฉุกเฉิน รักษาความพร้อมใช้งานสูง และมอบประสบการณ์ที่ราบรื่นให้แก่ผู้ใช้ นอกจากนี้ ฉันต้องการเน้นย้ำว่าการวัดระยะไกลเป็นอีกเครื่องมือหนึ่งที่ไม่ควรละเลยเมื่อต้องจัดหาการฟื้นตัวของโครงการ บันทึกและเมตริกที่ดีสามารถปรับปรุงคุณภาพของบริการได้อย่างมาก และให้ข้อมูลเชิงลึกที่มีค่าเกี่ยวกับประสิทธิภาพของบริการ ช่วยให้ตัดสินใจอย่างรอบรู้เพื่อปรับปรุงบริการให้ดียิ่งขึ้น