การจัดการข้อมูลใน 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 ห้า 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