Microservice Pattern — 2 Phase Commit ฉบับย่อ
เรื่องของ Distributed transaction ใน Microservice มี 2 solutions ที่นิยมกัน ได้แก่
- 2PC (two-phase commit)
- Saga Pattern
โดยในบทความนี้จะขอเล่าความเป็นมาของ 2PC กันก่อนครับ ว่าแนวคิด implementation ตัวนี้เป็นอย่างไร และก่อนที่จะทำความรู้จัก 2PC เราจำเป็นต้องรู้ก่อนว่าระบบ transaction คืออะไร เกี่ยวข้องกับเรื่องอะไรบ้างที่เราควรรู้ไว้
อย่างแรกเลย ACID คุณสมบัติของเรื่อง concurrency control แต่ละตัวประกอบด้วยคุณสมบัติ ดังนี้
- Atomicity = all or nothing คือ transaction ใดๆจะสมบูรณ์ได้นั้นเกิดจาก ทุก commit success หรือถ้าหาก fail ก็ให้ยกเลิก transaction ทั้งหมดเลย
- Consistency = ข้อมูลต้องมีความสอดคล้องก่อนและหลังการทำธุรกรรมโดยไม่มีขั้นตอนที่ขาดหายไป
- Isolation = แต่ละ transaction ต้องอิสระต่อกัน
- Durability = ระบบมีความทนทานที่ดีพอ ที่จะไม่ทำให้ transaction ที่สำเร็จไปแล้วสูญหาย
ผมขอสรุปสั้นๆนะครับเรื่อง ACID ซึ่งประเด็นหลักที่เราจะโฟกัสนั่นคือ Atomicity
Usecase: Customer order
มาดูความต้องการของระบบนี้กันครับ
มี 2 microservices ที่ทำงานร่วมกัน โดยแต่ละ services ก็มี database ของตัวเอง โจทย์ คือ ทำอย่างไรให้ atomicity
- customer service
- order service
ความต้องการของระบบ คือ
- ระบบเช็คยอดเงินคงเหลือของลูกค้า
- ระบบสร้างออร์เดอร์ที่ผูกกับลูกค้า
- ระบบหักเงินสำเร็จ หลังจากนั้นออร์เดอร์ถึงจะสร้างสำเร็จ
หากเส้นใดเส้นนึงพังระบบจะต้อง rollback กลับไป state ก่อนหน้า
🌱 2PC Solution
Actors
- Coordinator = ทำหน้าที่เป็นคนกลางคอยประสานงานเรื่อง transaction
- CustomerService = เก็บข้อมูลลูกค้า กระเป๋าเงิน เป็นต้น
- OrderService = จัดการออร์เดอร์
วิธีการทำงาน
Coordinator
สร้าง global transaction id (tid)Coordinator
เรียกCustomerService
เพื่อเตรียมอัพเดตยอดเงินของลูกค้า ตามจำนวนราคาของไอเทมที่กำลังจะเปิดออร์เดอร์ (ลอจิกจริงๆมีเยอะกว่านี้นะครับ เช่น เช็คยอดเงินคงเหลือ)- ถ้า (2) โอเคแล้ว
Coordinator
กำหนด preared state และlock database object
Coordinator
เรียกOrderService
เพื่อเตรียมสร้างออร์เดอร์ใหม่สำหรับลูกค้าคนนี้ และ กำหนดไอเทมในออร์เดอร์- ถ้า (4) โอเคแล้ว
Coordinator
กำหนด preared state และlock database object
- ดูเหมือนว่า
prepared
ทั้ง 2 services OK Coordinator
commit เพื่ออัพเตดยอดเงินของลูกค้าในCustomerService
CustomerService
ตอบกลับหาCoordinator
ว่า successCoordinator
commit เพื่อสร้างออร์เดอร์และไอเทมOrderService
OrderService
ตอบกลับหาCoordinator
ว่า success- ดูเหมือนว่า
Success
ทั้ง 2 servicesCoordinator
ก็จะตัดสินว่า transaction ของ request นี้สมบูรณ์ และunlock database object
🐛 ถ้าเกิดข้อผิดพลาดของระบบ หรือ ยอดเงินไม่พอต้องทำอย่างไร?
Coordinator
ต้องรีบ abort transaction นี้ทันที และ ย้อนกลับไป state ก่อนหน้า
Coordinator
สร้าง global transaction id (tid)Coordinator
เรียกCustomerService
เพื่อเตรียมอัพเดตยอดเงินของลูกค้า ตามจำนวนราคาของไอเทมที่กำลังจะเปิดออร์เดอร์- ถ้า (2) โอเคแล้ว
Coordinator
กำหนด preared state และlock database object
Coordinator
เรียกOrderService
เพื่อเตรียมสร้างออร์เดอร์ใหม่สำหรับลูกค้าคนนี้ และ กำหนดไอเทมในออร์เดอร์- ❌ เกิดข้อผิดพลาดบางอย่างเกิดขึ้นในตอนเรียก
OrderService
Coordinator
ต้องทำการabort(tid)
ไปที่CustomerService
เพราะว่า prepare state (3) ทำไปแล้ว- ถ้าทุกอย่าง
abort
เรียบร้อยหมดแล้วCoordinator
ทำการ rollback และ unlock database object ที่เกี่ยวข้อง
ข้อสังเกตุเกี่ยวกับ 2PC
- โซลูชันนี้ strong consistency มาก เหมาะกับระบบที่มีความซับซ้อนน้อย จำนวน microservices ไม่เยอะมาก
- เป็นระบบที่ต้องการความแม่นยำสูง และ รับได้ถ้าระบบทำงานแบบ synchronous (sequential call)
- ระบบต้อง lock database resource เช่น rows หรือ document object ที่ทำงานอยู่ หรือ ต้องสร้าง global previous state เก็บไว้ เพื่อย้อนกลับให้ได้
สำหรับบทความหน้าจะพูดถึง Saga Pattern เพื่อใช้แก้จุดอ่อนของ 2PC กันครับ