Microservice Pattern — 2 Phase Commit ฉบับย่อ

เรื่องของ Distributed transaction ใน Microservice มี 2 solutions ที่นิยมกัน ได้แก่

  1. 2PC (two-phase commit)
  2. 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

  1. customer service
  2. order service

ความต้องการของระบบ คือ

  1. ระบบเช็คยอดเงินคงเหลือของลูกค้า
  2. ระบบสร้างออร์เดอร์ที่ผูกกับลูกค้า
  3. ระบบหักเงินสำเร็จ หลังจากนั้นออร์เดอร์ถึงจะสร้างสำเร็จ

หากเส้นใดเส้นนึงพังระบบจะต้อง rollback กลับไป state ก่อนหน้า

🌱 2PC Solution

Actors

  1. Coordinator = ทำหน้าที่เป็นคนกลางคอยประสานงานเรื่อง transaction
  2. CustomerService = เก็บข้อมูลลูกค้า กระเป๋าเงิน เป็นต้น
  3. OrderService = จัดการออร์เดอร์

วิธีการทำงาน

  1. Coordinator สร้าง global transaction id (tid)
  2. Coordinator เรียก CustomerService เพื่อเตรียมอัพเดตยอดเงินของลูกค้า ตามจำนวนราคาของไอเทมที่กำลังจะเปิดออร์เดอร์ (ลอจิกจริงๆมีเยอะกว่านี้นะครับ เช่น เช็คยอดเงินคงเหลือ)
  3. ถ้า (2) โอเคแล้ว Coordinator กำหนด preared state และ lock database object
  4. Coordinator เรียก OrderService เพื่อเตรียมสร้างออร์เดอร์ใหม่สำหรับลูกค้าคนนี้ และ กำหนดไอเทมในออร์เดอร์
  5. ถ้า (4) โอเคแล้ว Coordinator กำหนด preared state และ lock database object
  6. ดูเหมือนว่า prepared ทั้ง 2 services OK
  7. Coordinator commit เพื่ออัพเตดยอดเงินของลูกค้าใน CustomerService
  8. CustomerService ตอบกลับหา Coordinator ว่า success
  9. Coordinator commit เพื่อสร้างออร์เดอร์และไอเทม OrderService
  10. OrderService ตอบกลับหา Coordinator ว่า success
  11. ดูเหมือนว่า Successทั้ง 2 services Coordinator ก็จะตัดสินว่า transaction ของ request นี้สมบูรณ์ และ unlock database object

🐛 ถ้าเกิดข้อผิดพลาดของระบบ หรือ ยอดเงินไม่พอต้องทำอย่างไร?

Coordinator ต้องรีบ abort transaction นี้ทันที และ ย้อนกลับไป state ก่อนหน้า

  1. Coordinator สร้าง global transaction id (tid)
  2. Coordinator เรียก CustomerService เพื่อเตรียมอัพเดตยอดเงินของลูกค้า ตามจำนวนราคาของไอเทมที่กำลังจะเปิดออร์เดอร์
  3. ถ้า (2) โอเคแล้ว Coordinator กำหนด preared state และ lock database object
  4. Coordinator เรียก OrderService เพื่อเตรียมสร้างออร์เดอร์ใหม่สำหรับลูกค้าคนนี้ และ กำหนดไอเทมในออร์เดอร์
  5. ❌ เกิดข้อผิดพลาดบางอย่างเกิดขึ้นในตอนเรียก OrderService
  6. Coordinator ต้องทำการ abort(tid) ไปที่ CustomerService เพราะว่า prepare state (3) ทำไปแล้ว
  7. ถ้าทุกอย่าง 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 กันครับ

--

--

Teerapong Singthong 👨🏻‍💻

Engineering Manager, ex-Solution Engineering Lead at LINE | Tech | Team Building | System Design | Architecture | SWE | Large Scaling System