- การออกแบบระบบที่ดี คือการออกแบบที่ดูไม่ซับซ้อนและแทบไม่ก่อปัญหาเป็นเวลานาน
- ส่วนที่ยากที่สุดของการออกแบบระบบคือการจัดการ สถานะ (state) และสิ่งสำคัญคือควรลดจำนวนคอมโพเนนต์ที่เก็บสถานะให้น้อยที่สุดเท่าที่ทำได้
- ฐานข้อมูลมักเป็นที่เก็บสถานะหลัก จึงต้องให้ความสำคัญกับ การออกแบบสคีมาและการทำดัชนี รวมถึงการแก้ปัญหาคอขวด
- การแคช การประมวลผลอีเวนต์ และงานเบื้องหลัง ควรถูกนำมาใช้อย่างระมัดระวังเพื่อประสิทธิภาพและการดูแลรักษา และไม่ควรใช้มากเกินไป
- แกนสำคัญของการสร้าง ระบบที่ยั่งยืนและเสถียร คือการเลือกใช้คอมโพเนนต์และวิธีการที่เรียบง่ายซึ่งผ่านการพิสูจน์มาเพียงพอ แทนการออกแบบที่ซับซ้อน
นิยามของการออกแบบระบบและแนวทางโดยรวม
- หากการออกแบบซอฟต์แวร์คือการประกอบโค้ด การออกแบบระบบ ก็คือกระบวนการประกอบบริการหลายประเภทเข้าด้วยกัน
- องค์ประกอบหลักของการออกแบบระบบ ได้แก่ แอปเซิร์ฟเวอร์ ฐานข้อมูล แคช คิว อีเวนต์บัส และพร็อกซี
- การออกแบบที่ดีมักทำให้เกิดปฏิกิริยาอย่าง "ไม่มีปัญหาอะไรเป็นพิเศษ" "จบได้ง่ายกว่าที่คิด" หรือ "ส่วนนี้ไม่ต้องกังวลก็ได้"
- ในทางกลับกัน การออกแบบที่ซับซ้อนและสะดุดตาอาจกำลังซ่อนปัญหารากฐาน หรือสะท้อนถึงการออกแบบที่มากเกินความจำเป็น
- แทนที่จะนำระบบซับซ้อนมาใช้ตั้งแต่ต้น ควรเริ่มจาก โครงสร้างเรียบง่ายขั้นต่ำที่ใช้งานได้จริง แล้วค่อย ๆ พัฒนาเพิ่ม จะได้ผลดีกว่า
การแยกความต่างระหว่าง state และ stateless
- ส่วนที่ยากที่สุดในงานออกแบบซอฟต์แวร์ก็คือ การจัดการสถานะ
- บริการที่ไม่เก็บข้อมูลและคืนผลลัพธ์ทันที เช่น การเรนเดอร์ PDF ของ GitHub ถือเป็นแบบ stateless
- ในทางกลับกัน บริการที่เขียนข้อมูลลงฐานข้อมูลคือบริการที่จัดการสถานะ
- ควรลดจำนวนคอมโพเนนต์ที่เก็บสถานะภายในระบบให้มากที่สุด เพราะช่วยลด ความซับซ้อนและโอกาสเกิดความขัดข้อง
- โครงสร้างที่แนะนำคือให้มีเพียงบริการเดียวที่รับผิดชอบการจัดการสถานะ ส่วนบริการอื่นให้เน้นบทบาทแบบ stateless เช่น การเรียก API หรือการส่งอีเวนต์
การออกแบบฐานข้อมูลและจุดคอขวด
การออกแบบสคีมาและดัชนี
- เพื่อจัดเก็บข้อมูล ควรมี การออกแบบสคีมาที่มนุษย์อ่านเข้าใจง่าย
- สคีมาที่ยืดหยุ่นเกินไป (เช่น เก็บทุกอย่างไว้ในคอลัมน์ JSON) อาจเพิ่มภาระให้ทั้งโค้ดแอปพลิเคชันและประสิทธิภาพ
- ควรกำหนด ดัชนีที่เหมาะสม ตามคอลัมน์ที่มีการคิวรีบ่อย การใส่ดัชนีให้ทุกอย่างกลับทำให้เกิดโอเวอร์เฮดโดยไม่จำเป็น
วิธีแก้ปัญหาคอขวด
- การเข้าถึงฐานข้อมูลมักเป็นคอขวดหนัก อยู่บ่อยครั้ง
- หากเป็นไปได้ การจัดการข้อมูลที่ซับซ้อนด้วย JOIN ภายในฐานข้อมูลแทนการทำในแอปพลิเคชันมักให้ประสิทธิภาพดีกว่า
- เมื่อใช้ ORM ต้องระวังความผิดพลาดจากการทำให้เกิดคิวรีภายในลูป
- ในบางกรณี การแยกคิวรีออกเป็นหลายส่วนเพื่อปรับภาระของฐานข้อมูลหรือความซับซ้อนของคิวรีก็เป็นอีกวิธีหนึ่ง
- กลยุทธ์ กระจาย read query ไปยัง read replica มีประสิทธิภาพในการลดภาระของโหนดหลักที่รับงานเขียน
- เมื่อมีคิวรีจำนวนมากถาโถมเข้ามา ธุรกรรมและงานเขียนอาจทำให้ฐานข้อมูลโอเวอร์โหลดได้ง่าย จึงควรพิจารณา การ throttle หรือจำกัดคิวรี
การแยกงานช้ากับงานเร็ว
- งานที่ผู้ใช้โต้ตอบด้วยต้องการ การตอบสนองภายในระดับไม่กี่ร้อยมิลลิวินาที
- สำหรับงานที่ใช้เวลานาน (เช่น การแปลง PDF ขนาดใหญ่) แนวทางที่มีประสิทธิภาพคือ ให้ส่วนที่จำเป็นน้อยที่สุดกับหน้าบ้านทันที แล้วส่งงานที่เหลือไปทำเบื้องหลัง
- งานเบื้องหลัง มักทำงานร่วมกันระหว่างคิว (เช่น Redis) และตัวรันงาน
- สำหรับงานที่ตั้งเวลาไว้ไกลล่วงหน้า การใช้ตารางใน DB แยกต่างหากและรันด้วย scheduler มักใช้งานได้จริงมากกว่า Redis
การแคช
- การแคช ช่วยลดต้นทุนและเพิ่มประสิทธิภาพเมื่อมีการคำนวณแบบเดิมหรือมีราคาแพงซ้ำ ๆ
- โดยทั่วไป วิศวกรจูเนียร์ที่เพิ่งเรียนเรื่องแคช มักอยากแคชทุกอย่าง ขณะที่วิศวกรที่มีประสบการณ์จะระมัดระวังมากกว่าในการนำแคชมาใช้
- แคชคือการเพิ่มสถานะรูปแบบใหม่ จึงมี ความเสี่ยงเรื่องการซิงก์ ความผิดพลาด และข้อมูล stale
- แนวทางที่เหมาะสมคือควรลองปรับปรุงประสิทธิภาพก่อน เช่น เพิ่มดัชนีให้คิวรี แล้วค่อยใช้การแคช
- สำหรับแคชขนาดใหญ่ อาจใช้วิธี บันทึกลง document storage เป็นระยะ เช่น S3/Azure Blob Storage แทน Redis/Memcached ได้เช่นกัน
การประมวลผลอีเวนต์
- บริษัทส่วนใหญ่มักมี event hub (เช่น Kafka) และให้บริการต่าง ๆ กระจายการประมวลผลบนพื้นฐานของอีเวนต์
- แทนที่จะใช้อีเวนต์พร่ำเพรื่อ การออกแบบ API แบบ request–response ที่เรียบง่ายมัก มีประโยชน์กว่าในด้านการบันทึกล็อกและการแก้ปัญหา
- การประมวลผลแบบอิงอีเวนต์เหมาะเมื่อผู้ส่งไม่จำเป็นต้องสนใจพฤติกรรมของผู้รับ หรือใน สถานการณ์ปริมาณสูงและยอมรับความหน่วงได้
วิธีส่งผ่านข้อมูล: Push และ Pull
- การส่งข้อมูลมี 2 แบบคือ Pull (ขอแล้วค่อยตอบ) และ Push (ส่งอัตโนมัติเมื่อมีการเปลี่ยนแปลง)
- แบบ Pull นั้นเรียบง่าย แต่มีปัญหาเรื่องการร้องขอซ้ำและโอเวอร์โหลดได้
- แบบ Push จะส่งข้อมูลไปยังไคลเอนต์ทันทีเมื่อข้อมูลฝั่งเซิร์ฟเวอร์เปลี่ยน จึง มีประสิทธิภาพและเหมาะกับการรักษาข้อมูลให้เป็นปัจจุบัน
- หากต้องรองรับไคลเอนต์จำนวนมาก ก็จำเป็นต้องขยายอินฟราสตรักเจอร์ให้เหมาะกับแต่ละรูปแบบ (เช่น event queue, cache server หลายตัว เป็นต้น)
โฟกัสที่ฮอตพาธ (Hot Paths)
- ฮอตพาธ หมายถึงเส้นทางที่สำคัญที่สุดในระบบและเป็นจุดที่ข้อมูลไหลผ่านจำนวนมาก
- ฮอตพาธมีทางเลือกน้อย และหากออกแบบผิดพลาดอาจก่อให้เกิด ปัญหาร้ายแรงต่อทั้งบริการ ดังนั้นต้องออกแบบอย่างรอบคอบ
- แทนที่จะทุ่มทรัพยากรให้ฟีเจอร์ย่อยที่มีตัวเลือกมากมาย ควร โฟกัสทรัพยากรด้านการออกแบบและการทดสอบไปที่ฮอตพาธ จะมีประสิทธิภาพกว่า
การบันทึกล็อก เมตริก และการติดตาม
- เพื่อวิเคราะห์สาเหตุเมื่อเกิดปัญหา ควรบันทึก ล็อกแบบละเอียดสำหรับเส้นทางที่ไม่ปกติ (unhappy path) อย่างจริงจัง
- จำเป็นต้องเก็บ ตัวชี้วัดด้าน observability พื้นฐาน เช่น ทรัพยากรระบบ (CPU/หน่วยความจำ) ขนาดคิว และเวลาของคำขอ/งาน
- ไม่ควรดูแค่ค่าเฉลี่ยเท่านั้น แต่ต้องสังเกต ตัวชี้วัดการกระจายอย่างค่า latency แบบ p95, p99 ด้วย เพราะคำขอที่ช้าเพียงส่วนน้อยอาจเป็นปัญหาสำคัญของผู้ใช้หลัก
คิลสวิตช์ การรีทราย และการกู้คืนความขัดข้อง
- การใช้ คิลสวิตช์ (หยุดระบบชั่วคราว) และการรีทรายอย่างมีกลยุทธ์เป็นสิ่งสำคัญ
- การรีทรายแบบไม่ยั้งคิดมีแต่จะเพิ่มภาระให้บริการอื่น ดังนั้นต้องควบคุมคำขอล่วงหน้าด้วย circuit breaker เป็นต้น จึงจะได้ผล
- การนำ Idempotency Key มาใช้ช่วยป้องกันงานซ้ำเมื่อมีการประมวลผลคำขอเดิมอีกครั้ง
- ในบางสถานการณ์ความขัดข้อง จำเป็นต้องเลือกว่าจะใช้ fail open หรือ fail closed ตัวอย่างเช่น rate limiting ควรเอนไปทาง fail open (ยอมให้ผ่าน) เพื่อลดผลกระทบต่อผู้ใช้ ขณะที่การยืนยันตัวตนจำเป็นต้องเป็น fail closed
สรุป
- แม้จะละหัวข้อบางอย่างไป เช่น การแยกบริการ การใช้คอนเทนเนอร์ VM หรือ tracing แต่การใช้ คอมโพเนนต์ที่ผ่านการพิสูจน์แล้ว ให้เหมาะกับจุดที่ควรใช้ ยังคงเป็นหนทางสู่การสร้างระบบที่เสถียรที่สุดในระยะยาว
- การออกแบบที่พิเศษทางเทคนิคนั้นเกิดขึ้นจริงน้อยมาก และ การออกแบบที่เรียบง่ายจนแทบน่าเบื่อ กลับเป็นสิ่งที่ถูกใช้ในงานจริงบ่อยที่สุด
- โดยแก่นแท้แล้ว การออกแบบระบบที่ดีคือกระบวนการประกอบวิธีการที่ผ่านการพิสูจน์เพียงพอเข้าด้วยกันอย่างปลอดภัยโดยไม่ทำให้ตัวมันเองโดดเด่นเกินไป
ยังไม่มีความคิดเห็น