Saga Pattern: Fundamentals
Saga ถูกพูดถึงครั้งแรกในปี 1987 (a 1987 research paper) โดย Hector Garcia-Molina และ Kenneth Salem พวกเขาเสนอวิธีการแก้ปัญหา LLTs (Long Lived Transactions) และถูกพูดถึงอีกครั้งในปี 2015 ในบริบทของ Distributed Systems (2015 distributed sagas paper)
ขอคั่นโฆษณาด้วยช่วง “คำนี้ดี๊ดี”
คำนี้ดี๊ดีเสนอคำว่า Transaction
ภาษาที่มนุษย์ใช้สื่อสารกับ Relational Database คือ SQL (Structured Query Language) เราสามารถพิมพ์ผ่านคีย์บอร์ดหรือจะร่ายมนต์ผ่าน Control C + Control V เพื่อสั่งให้ฐานข้อมูลสร้าง ลบ ค้นหาหรืออัปเดตข้อมูล คำสั่งเหล่านี้เรียกว่า SQL statements ซึ่งคำว่า Transaction แปลเป็นภาษาไทยหมายถึง “ธุรกรรม” ในบริบทของฐานข้อมูลจะหมายถึงกลุ่มของ SQL statements ที่ต้องทำงานร่วมกัน
ตัวอย่างเช่น: เราหยิบโทรศัพท์จากโต๊ะหัวเตียง เปิดแอพธนาคารกดโอนเงิน 1,500 บาท Transaction จะประกอบด้วยคำสั่งค้นหาข้อมูลตรวจสอบยอดเงิน อัปเดตยอดเงินและสร้างประวัติการทํารายการ
จากตัวอย่าง 1,500 ขณะที่เซิร์ฟเวอร์กำลังอัปเดตยอดเงินและสร้างประวัติการทํารายการเกิดปัญหาฮาร์ดดิสเต็มทำให้บางคำสั่งทำงานไม่สำเร็จและส่งผลกระทบทำให้ลูกค้าเห็นข้อมูลที่ผิด เช่น มีประวัติการทํารายการโอนเงิน 1,500 บาท แต่ยอดเงินในบัญชีคงเหลือเท่าเดิม ดังนั้นเราจึงต้องมีการการันตีข้อมูลบนฐานข้อมูลว่ามีความน่าเชื่อถือและมีความถูกต้อง หนึ่งในการการันตีที่ผม คุณและอับดุลรู้จักคือ ACID Transactions
What is ACID?
ACID ย่อมาจาก Atomicity, Consistency, Isolation และ Durability ทั้ง 4 คุณสมบัตินี้ทำให้ Transaction บน Database นั้นมีความน่าเชื่อถือมากขึ้น
- Atomicity: คือการการันตีว่า Transaction ที่ประกอบด้วย SQL Statements ไม่ว่าที่จะ Read, Write, Update หรือ Delete หากเกิดปัญหาในขั้นตอนใดขั้นตอนหนึ่งถือว่า Transaction นั่นทำงานไม่สมบูรณ์จะต้อง Undo Change ทั้งหมด ภาษาชาวบ้านเรียกว่า เอาทั้งหมดหรือไม่เอาเลย (all or nothing)
- Consistency: ในบริบทของ ACID หมายถึงการการันตีว่าข้อมูลจะถูกต้องตาม Database Rules ทั้งก่อนและหลังทำ Transaction (ตัว C ใน ACID เป็นคนละเรื่องกับ Consistency ใน CAP Theorem อย่าสับสนกันเด้อ)
ตัวอย่างเช่น ตารางEmployees
ต้องมีคอลัมน์EmployeeID
ที่ไม่ซ้ำกัน การอ้างอิงระหว่างตารางต้องมี Foreign Key อยู่ในตารางที่ถูกอ้างอิง หรือค่าของSalary
ต้องมากกว่าศูนย์ - Isolation: เมื่อมีการทำงานแบบ Multiple Transactions เกิดขึ้นในเวลาเดียวกัน จะต้องได้ผลลัพธ์เหมือนกันกับ Sequential Transactions และต้องการันตีได้ว่า Interim State Changes ของ Transaction หนึ่ง ไม่กระทบกับ Transactions อื่นๆ เพื่อป้องกันการเกิด Race Condition
- Durability: คือการการันตีว่าเมื่อ Transaction ทำงานสำเร็จ (committed) ไม่ว่าจะเกิดฝนตก ไฟดับ น้ำท่วม หรือ Server Restart ข้อมูลที่ถูกเก็บจะไม่หายไปหรือก็คือ เราควรจะมีการ Backup Data เพื่อป้องกันกรณีเหล่านี้
ACID กับ Monolithic
ในโลก Monolithic การจัดการ ACID Transactions เป็นเรื่องที่ค่อนข้างตรงไปตรงมาเนื่องจาก Database Transaction สามารถครอบคลุมได้ทุกตารางในฐานข้อมูล ทำให้สามารถจัดการเรื่องของ ACID ได้ง่ายมาก ๆ
ตัวอย่างเช่น: ระบบจองตั๋วเครื่องบิน ลูกค้าต้องการเลื่อน Flight หลังจากจองและชำระเงินสำเร็จ สิ่งที่ระบบต้องทำคือการตรวจข้อมูล Flight, Hotel และ Car Rental ในวันที่ลูกค้าต้องเลื่อนว่าสามารถทำรายการได้หรือไม่ ในส่วนของการเขียนโปรแกรมเราสามารถสั่งค้นหาข้อมูลและตรวจสอบให้แน่ใจก่อนจะ commitTransaction
หรือหากมีขั้นตอนใดขั้นตอนหนึ่งล้มเหลว เช่น วันที่ลูกค้าเลือกไม่มีรถให้เช่า เราก็สามารถ rollbackTransaction
เพื่อทำให้มั่นใจว่าไม่มีการแก้ไขข้อมูลใด ๆ หาก Transaction ไม่สำเร็จตามเงื่อนไขที่เราต้องการ
ACID กับ Microservices
ในโลกของ Microservices ที่เราแยก Service ตาม Boundary Context จากตัวอย่างระบบจองตั๋วเครื่องบิน เราอาจจะแบ่งได้ประมาณ FlightReservation Service, Payment Service, HotelBooking Service และ CarRental Service ถ้าออกแบบโดยอ้างอิงตาม Database-per-Service Pattern ที่แต่ละ Service มีฐานข้อมูลของตัวเอง จากรูปด้านล่างจะเห็นได้ว่า Database Transaction จะครอบคลุมได้แค่ Local Transactions หรือก็คือมีผลกับแค่ฐานข้อมูลใน Service ตัวเอง สิ่งนี้ทำให้คุณสมบัติ ACID Transactions เป็นเรื่องที่ท้าทายมาก
จากตัวอย่างระบบจองตั๋วเครื่องบิน ในกรณีเดียวกันคือ ลูกค้าต้องการเลื่อน Flight หลังจากจองและชำระเงินสำเร็จ สมมุติว่าระบบสามารถหา Flight และจอง Hotel ได้ แต่พอถึงขึ้นตอนการเช่ารถ ไม่สามารถเช่ารถในวันที่ลูกค้าระบุได้ เราจะไม่สามารถสั่ง Undo Change ของ Transaction ทั้งหมดได้เนื่องจากฐานข้อมูลของ Hotel Booking และ Flight Reservation เป็นคนละตัวกับ Car Rental นั่นหมายความว่าคุณสมบัติตัว A⇒ Atomicity ที่บอกว่าต้องสำเร็จทั้งหมดถ้างั้นไม่เอาเลยไม่เป็นจริงแล้ว เพราะข้อมูล Hotel และ Flight ถูกบันทึกลงฐานข้อมูลเรียบร้อยแล้ว ในโลกของ Microservices ที่ Atomicity ได้หายไปเราจำเป็นต้องหาวิธีอื่นเพื่อช่วยในการการันตีความน่าเชื่อถือของข้อมูล
ขอจบการนำเสนอช่วงคำนี้ดี๊ดีแต่เพียงเท่านี้ โดยอีก 1 คุณสมบัติที่หายไลน์ไปไม่ตอบก็คือเรื่องของ Isolation เดี๋ยวเราจะมาพูดถึงในบทความต่อไป
What is the Saga Pattern?
“ไม่ช้อปคือโกรธ งานนี้แจกสะบัด ลดแรงแซงโค้ง 12.12” ระบบของแพลตฟอร์มอีคอมเมิร์ซเจ้าใหญ่ประกาศโปรโมชั่นเพื่อเพิ่มยอดผู้ใช้งานในระบบ เมื่อผู้ใช้งานทำการเก็บโค้ดส่วนลด เลือกสั่งสินค้าและกดชำระเงิน ได้รับการแจ้งเตือนจากระบบว่าผู้ขายเตรียมสินค้า ขนส่งรับและจัดส่งสินค้าสำเร็จแล้ว กระบวนการเหล่านี้เรียกว่า Operation
Saga Pattern คือการออกแบบ Workflow ให้กับ Operation ต่าง ๆ ที่มีการแก้ไขข้อมูลหรือ Immutable data เราจะเห็นว่าใน Happy path จากรูปด้านบนที่เป็นกล่องสีเขียว ถ้าหากมี Operation ชำระเงินสำเร็จ ก็ควรมี Operation กรณีชำระเงินไม่สำเร็จเช่นกัน การออกแบบ Operation ช่วยเราในกรณีที่เกิดข้อพลาด ให้สามารถ Undo Change ของข้อมูลให้กลับไปสู่สถานะที่ควรจะเป็นในเชิงของธุรกิจ เช่น ผู้ขายเตรียมสินค้าไม่ได้เนื่องจากสินค้าขาดสต๊อก
ในแง่มุมของ Technical เจ้า Saga คือเข้ามาช่วยทำเรื่อง Sequence of Local Transactions อธิบายง่าย ๆ เราแบ่ง Microservices ตาม Boundary Context ในแต่ละ Service จะมีฐานข้อมูลของตัวเอง เราจะนิยาม 2 สิ่งนี้รวมกันว่า Local Transactions เมื่อ Local Transactions แรกทำงานเสร็จจะส่ง Message/Event เพื่อ Trigger Local Transaction ตัวถัดไป ในกรณีเกิดข้อผิดพลาดใน Service ใด Service หนึ่ง Saga จะมีสิ่งที่เรียกว่า Compensating Transaction
Compensating Transaction
ทุกคนน่าจะรู้จัก Food Delivery ดี เราเลือกร้านจากแอปพลิเคชันที่ได้ส่วนลดเยอะที่สุด กดสั่งอาหาร ใส่คูปองส่วนลดและกดชำระเงิน หลังชำระเงินสำเร็จหน้าจอแสดงข้อความกําลังค้นหาไรเดอร์ …A few minutes later… หน้าจอแสดงไม่พบไรเดอร์!! แอปพลิเคชันทำการยกเลิกคำสั่งซื้อและคืนคูปองส่วนลดพร้อมยอดเงินที่ตัดให้กับเรา
ถ้าทีมคุณทำ Scrum ในวันที่ทีมทำ Product Backlog Refinement เหล่า Product Developer จะนั่งจับเข่าคุยเพื่อทำความเข้าใจเกี่ยวกับ Operation ต่าง ๆ ในธุรกิจที่พวกเขากำลังทำกับเหล่า Stakeholder หรือ Domain Expert พวกเขาอาจจะจิบกาแฟหนึ่งครั้งก่อนเริ่มถามคำถามประมาณว่า กรณีไม่พบไรเดอร์ต้องทำอย่างไร ถ้าชำระเงินไม่สำเร็จจะเกิดอะไรขึ้น หรือถ้าร้านอาหารหมูกรอบหมด!! ทำให้แม่ครัวต้องกดยกเลิกคำสั่ง พี่อยากให้ระบบแสดงผลอย่างไร การออกแบบ Operation เหล่านี้คือความหมายของคำว่า Compensating Transaction
มี 2 เรื่องที่อยากจะเน้นย้ำก่อนทำ Compensating Transaction
- Idempotency ก็คือไม่ว่า Operation เกิดขึ้นกี่ครั้งซ้ำ ๆ กัน ถ้าเป็นข้อมูลชุดเดิมผลลัพธ์ที่ได้ต้องเหมือนเดิมเพื่อที่จะทำให้สามารถ Retry ในกรณีที่เกิดปัญหา
- Compensating Transaction คือการจัดการกรณีของ Business failures ไม่ใช่ Technical failures
ตัวอย่างเช่น เมื่อลูกค้ากดชำระเงิน แต่ยอดเงินไม่พอ สิ่งนี้เรียก Business failure ในทางกลับกัน Payment 504 Gateway time-out หรือ 500 Internal Service Error สิ่งนี้เรียกว่า Technical failure ดังนั่นต้องให้ Business ช่วยในการตัดสินใจว่าควรจะเป็นอย่างไร อ้าว!! แล้วกรณีของ 504 กับ 500 เราจะจัดการอย่างไรล่ะ?เดี๋ยวมาเล่าให้ฟังในบทความถัดไป
ในโลกความเป็นจริงต้องเข้าใจว่าบาง Operation ก็ไม่สามารถย้อนกลับได้ ตัวอย่างเช่น ระบบส่ง Email บอกรายละเอียดว่าคำสั่งซื้อกําลังดําเนินการ แต่สักพักแอปพลิเคชันแจ้งว่าไรเดอร์ยกเลิกคำสั่ง เราไม่สามารถยกเลิก Email ที่ส่งไปแล้วได้
และบาง Operation เกิดขึ้นแล้วสามารถย้อนกลับได้แต่ไม่สามารถทำได้แบบอัตโนมัติ ต้องมีคนเข้ามาช่วย ตัวอย่างเช่น ระบบจองตั๋วเครื่องบิน กรณีเที่ยวบินของลูกค้าถูกเลื่อนเราไม่สามารถเลือกเที่ยวบินให้ลูกค้าเองได้ เราต้องติดต่อกับทางลูกค้าเพื่อยืนยันวันที่กับลูกค้าก่อน ดังนั้นการออกแบบก็จะขึ้นอยู่กับลักษณะของ Operation เรามาสามารถ Mixing fail-backward และ fail-forward เพื่อให้เข้ากับธุรกิจของเราได้
Conclusion
ถ้าอ่านมาถึงจุดนี้จะทำให้เราเข้าใจว่า Saga ไม่สามารถทดแทนคุณสมบัติของ Atomicity ใน ACID ได้ แต่สิ่งที่ Saga ให้ได้คือเราสามารถรู้ว่า Transactions อยู่ในสถานะใด ซึ่งช่วยให้เราสามารถจัดการกับผลกระทบของการขาดตัว A => Atomicity ได้
ถึงแม้ Saga เป็น Great Failure Management Pattern ที่น่าสนใจ แต่สิ่งที่สำคัญคือ ต้องถามตัวเองเยอะ ๆ ว่าระบบที่เรากำลังดูแลจำเป็นต้องใช้ Distributed Transaction จริง ๆ ใช่ไหม เรามีตัวเลือกอื่นไหม และสุดท้ายทุก Design Patterns เกิดมาเพื่อแก้ปัญหาทางธุรกิจบางอย่าง เราควรเข้าใจธุรกิจของเรา ทีมของเราและวัฒนธรรมขององค์กรของเรา ก่อนจะนำเทคโนโลยีเหล่านี้เข้ามาช่วยให้พวกเรามีเวลานอนเพิ่มขึ้น
Implementing Saga pattern
- Event Modeling
- Orchestration-based saga
- Choreography-based saga
- Limits of Saga pattern
Coming soon …
Refs:
- Building Microservices: Chapter 6. Workflow
- Building Event-Driven Microservices:Chapter 8. Building Workflows with Microservice
- https://microservices.io/patterns/data/saga.html