ข้ามไปยังเนื้อหา

การจัดการข้อมูลใน Microservices

คุณได้แยก monolith ออกเป็นหลาย service แล้ว ตอนนี้แต่ละ service ทำงานเป็น deployable ของตัวเอง มีทีมของตัวเอง และมีจังหวะการ release ของตัวเอง การแยกส่วนแบบนี้ดูสะอาดตาบนกระดานไวท์บอร์ด — จนกระทั่งคุณไปถึงเรื่องฐานข้อมูล ใน monolith มี schema เดียว connection pool เดียว และขอบเขต transaction เดียว คำสั่ง BEGIN ... COMMIT ตัวเดียวสามารถแตะตาราง orders ตาราง inventory และบัญชีแยกประเภทของลูกค้าได้แบบ atomic และคำสั่ง SELECT ตัวเดียวที่มี join สามารถเย็บข้อมูลเหล่านั้นกลับเข้าด้วยกันเพื่อทำรายงานได้

นาทีที่ตารางเหล่านั้นย้ายไปอยู่หลัง service ที่แยกจากกัน รากฐานร่วมนั้นก็หายไป โมดูลนี้ว่าด้วยสิ่งที่จะเข้ามาแทนที่

ถ้าทุก service เป็นอิสระอย่างแท้จริง ก็ไม่มี service ใดที่จะเอื้อมเข้าไปในตารางของ service อื่นได้ แต่การดำเนินงานทางธุรกิจจริง ๆ มักไม่เคารพขอบเขตของ service การสั่งซื้อหนึ่งครั้งแตะทั้ง orders, inventory และ payments หน้า dashboard ของลูกค้าต้องการข้อมูลที่กระจายอยู่ตามครึ่งโหลของ service ดังนั้นคำถามหลักของโมดูลนี้คือ:

คุณจะรักษาข้อมูลให้สอดคล้องและ query ได้ข้าม service ได้อย่างไร เมื่อไม่มีฐานข้อมูลร่วม ไม่มี distributed join และไม่มี global transaction?

คำตอบไม่ใช่กลไกเดียว แต่เป็นกลุ่มเล็ก ๆ ของ pattern ที่ทำงานร่วมกัน ตัวแรกสร้างขอบเขตขึ้นมา ส่วนที่เหลือคืนความสามารถที่ขอบเขตนั้นพรากไป

แต่ละ service มีที่จัดเก็บข้อมูลส่วนตัวของตัวเอง service อื่นจะเข้าถึงข้อมูลนั้นได้ก็ต่อเมื่อผ่าน API ของ service เจ้าของ หรือผ่าน event ที่ service นั้น publish ออกมาเท่านั้น

flowchart TB
  Client[Client / Gateway] --> OAPI
  Client --> IAPI
  Client --> PAPI
  subgraph Orders[Order Service]
    OAPI[API]
    ODB[(Orders DB)]
    OAPI --> ODB
  end
  subgraph Inventory[Inventory Service]
    IAPI[API]
    IDB[(Inventory DB)]
    IAPI --> IDB
  end
  subgraph Payments[Payment Service]
    PAPI[API]
    PDB[(Payments DB)]
    PAPI --> PDB
  end
  OAPI -. events .-> IAPI
  OAPI -. events .-> PAPI
แต่ละ service เป็นเจ้าของฐานข้อมูลส่วนตัว การทำงานร่วมกันเกิดขึ้นผ่าน API และ event

ห้า pattern ในโมดูลนี้ประกอบเข้าด้วยกันแบบนี้:

  • Database per Service — ทุก service เป็นเจ้าของ database ส่วนตัวที่มีเจ้าของเท่านั้นแตะได้โดยตรง นี่คือรากฐานที่ทำให้ pattern ที่เหลือจำเป็นต้องมี
  • Saga — รักษาข้อมูลให้สอดคล้องข้าม service โดยไม่ใช้ distributed transaction ด้วยการร้อย local transaction ต่อกัน แล้วย้อนกลับด้วย compensating action เมื่อมีขั้นตอนไหนล้มเหลว
  • API Composition — ตอบ query ที่พาดข้ามหลาย service โดยเรียกแต่ละ service แล้ว join ผลลัพธ์เข้าด้วยกันใน memory
  • CQRS — แยกฝั่งเขียนออกจาก read model ที่สร้างมาเพื่อจุดประสงค์เฉพาะหนึ่งหรือหลายตัว ซึ่งถูกซิงค์ให้ตรงกันด้วย event ทำให้ query ข้าม service กลายเป็นการ lookup ในเครื่องที่รวดเร็ว
  • Event Sourcing — เก็บ state เป็น log ของ event แบบ append-only เล่น log ซ้ำเพื่อสร้าง state ขึ้นใหม่ และ publish event เดียวกันนั้นไปยังทุกคนอื่นอย่างน่าเชื่อถือ

เราเริ่มต้นด้วยขอบเขต (Database per Service) จากนั้นจัดการกับการเขียนที่พาดข้าม service (Saga) และการอ่านที่พาดข้าม service (API Composition และ CQRS) และจบด้วยโมเดลการจัดเก็บที่ทำให้การ publish event อย่างน่าเชื่อถือเป็นเรื่องธรรมชาติ (Event Sourcing) แต่ละบทเรียนมีรูปแบบเดียวกัน: สถานการณ์ที่นำไปสู่ pattern แรงที่ขัดแย้งกัน สิ่งที่ pattern ทำ ไดอะแกรม code ตัวอย่างที่รันได้ และมุมมองตรงไปตรงมาว่า pattern นั้นมีต้นทุนอะไรกับคุณบ้าง

  • Database per Service — เริ่มจากตัวนี้ เพราะเป็นรากฐานของทั้งโมดูล
  • Saga — pattern ตัวเด่นสำหรับการเขียนข้าม service
ทำไม transaction และ join ข้าม service จึงหายไปเมื่อคุณนำ microservices มาใช้?
pattern ใดเป็นรากฐานที่ทำให้อีกสี่ตัวจำเป็น?
การแลกเปลี่ยน (trade-off) หลักที่โมดูลนี้พูดถึงคืออะไร?