Self-Contained Service
service ของคุณมีเส้นแบ่งที่ดี แต่ละตัวเป็นเจ้าของข้อมูลของตัวเอง ตอนนี้ถึงเวลาต้องทำงานร่วมกัน เวลาจะแสดง order สักใบ Order service ต้องใช้ชื่อลูกค้าและชื่อสินค้า — ข้อมูลที่ Customer และ Catalog service เป็นเจ้าของ ทางที่นึกออกทันทีคือเรียก service เหล่านั้นแบบ synchronous ทุกครั้งที่ render order ตอน dev ทุกอย่างดูดี เพราะทุก service เปิดอยู่และเครือข่ายเป็น loopback แล้วคุณก็ขึ้น production
ทันทีที่ service หนึ่งต้องเรียก service อื่นแบบ synchronous เพื่อทำงานของตัวเอง ก็เท่ากับรับ availability ของ service นั้นมาแบกไว้ พร้อมกับ latency ของตัวเองด้วย ต่อการเรียกแบบนี้สักสองสามชั้น ตัวเลขก็เริ่มไม่เข้าข้างคุณ สมมติสี่ service แต่ละตัวมี availability สวยงามที่ 99.9% request ที่ต้องพึ่งทั้งสี่ตัวแบบ synchronous จะพร้อมใช้งานเพียงราว 99.6% ของเวลา และ dependency ที่ช้าเพียงตัวเดียวก็ลาก latency ของทั้งสายให้สูงตาม ความล้มเหลวหรือความช้าที่จุดใดจุดหนึ่งกลายเป็นความล้มเหลวของทุกอย่างที่อยู่เหนือขึ้นไป สุดท้ายคุณได้ service ที่ deploy แยกกันได้ แต่ รัน แยกกันไม่ได้ — distributed system ที่เปราะบางกว่า monolith ที่เข้ามาแทนที่เสียอีก
แรงที่ขัดกันจึงเป็นแบบนี้ service หนึ่งต้องการข้อมูลที่ service อื่นเป็นเจ้าของ แต่ต้องไม่ยอมยก availability และความตอบสนองของตัวเองไปฝากไว้กับ service นั้นทุกครั้งที่ให้บริการ request
วิธีแก้
หัวข้อที่มีชื่อว่า “วิธีแก้”self-contained service จัดการแต่ละ request ให้จบในตัวเอง ไม่เรียก service อื่นแบบ synchronous บนเส้นทางของ request เลย วิธีการคือเก็บ สำเนาในที่ (local replica) ของข้อมูลที่ต้องใช้จากที่อื่น — เอาเฉพาะส่วนที่ใช้จริง — แล้วอัปเดตสำเนานั้น แบบ asynchronous ด้วยการ subscribe event ที่ service เจ้าของข้อมูลเผยแพร่
service เจ้าของยังคงเป็น source of truth เมื่อ Customer service เปลี่ยนชื่อลูกค้า ก็เผยแพร่ event CustomerUpdated ออกมา ฝั่ง Order service consume event นั้นแล้วอัปเดตสำเนาชื่อลูกค้าเล็ก ๆ ของตัวเอง ทีนี้พอ Order service ต้องให้บริการ request ข้อมูลที่ต้องใช้ก็นอนรออยู่ใน database ของตัวเองแล้ว ไม่มีการเรียกข้ามเครือข่ายบน hot path เลย request จึงสำเร็จแม้ Customer service จะปิดเพื่อบำรุงรักษาอยู่
การแลกนี้ตั้งใจแลก คุณยอมรับว่าสำเนาในที่เป็น eventually consistent — อาจตามหลัง source อยู่ชั่วครู่ — เพื่อแลกกับการที่ service ตอบ request ได้ด้วยตัวเอง สำหรับ read path ส่วนใหญ่ ชื่อที่เก่าไปสองสามวินาทีถือว่าคุ้มมากเมื่อแลกกับการที่ระบบยังใช้งานได้
flowchart TB
subgraph Chain[Synchronous chain - fragile]
direction LR
Req1[Request] --> O1[Order Service]
O1 -->|sync call| C1[Customer Service]
O1 -->|sync call| P1[Catalog Service]
note1[If Customer or Catalog is down or slow,<br/>the request fails or hangs]
end
subgraph Self[Self-contained - resilient]
direction LR
Req2[Request] --> O2[Order Service]
O2 --> R2[(Local replica:<br/>names, titles)]
C2[Customer Service] -. CustomerUpdated event .-> R2
P2[Catalog Service] -. ProductUpdated event .-> R2
note2[Request served from local replica;<br/>events update it in the background]
end ผลลัพธ์ที่ตามมา
หัวข้อที่มีชื่อว่า “ผลลัพธ์ที่ตามมา”สิ่งที่คุณได้:
- availability ไม่ถูกคูณลดลง service ตอบ request ได้โดยไม่พึ่งตัวอื่นตอน runtime เพื่อนบ้านล่มหรือช้าก็ไม่ลากให้ล่มตามอีกต่อไป availability เลิกเป็นผลคูณของ availability ของทุก dependency
- latency ต่ำลงและคาดเดาได้มากขึ้น ไม่มี network hop แบบ synchronous บน hot path จึงไม่มีเซอร์ไพรส์เรื่อง tail latency ที่นำเข้ามาจาก dependency ที่ช้า
- coupling ตอน runtime หลวมลง service ทำงานร่วมกันผ่าน event แบบ asynchronous แทนการเรียกแบบ synchronous แต่ละตัวจึง deploy และ restart ได้โดยไม่ต้องนัดเวลา downtime กัน
สิ่งที่คุณต้องจ่าย:
- eventual consistency สำเนาในที่อาจตามหลัง source of truth ได้ ใช้แสดงชื่อไม่มีปัญหา แต่รับไม่ได้กับการตัดสินใจที่ต้องใช้ค่าทางการและทันต่อวินาที เช่น ยอดเงินในบัญชี ณ ขณะที่โอน งานพวกนี้ยังต้องถาม service เจ้าของข้อมูลเสมอ
- ข้อมูลที่ replicate มาแล้วต้องดูแล คุณต้องเก็บและดูแลสำเนาข้อมูลของ service อื่น แปลว่าใช้ storage มากขึ้น ต้องมีวินัยในการเก็บสำเนาให้เล็กที่สุด และต้องจัดการ event ที่มาผิดลำดับหรือซ้ำแบบ idempotent
- event backbone ที่คุณต้องรันเอง pattern นี้ตั้งอยู่บนสมมติฐานว่ามีการเผยแพร่และ consume event ที่เชื่อถือได้ — ทั้ง message broker และวุฒิภาวะเชิงปฏิบัติการที่จะดูแลให้ระบบนั้นแข็งแรง นี่คือ infrastructure จริงที่ต้องสร้างและรับผิดชอบเอง
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”- Decompose by Subdomain — bounded context ที่ชัดเจนทำให้เห็นชัดว่าส่วนใดของข้อมูลภายนอกที่ service ต้องทำสำเนาจริง ๆ
- Saga — สไตล์ asynchronous ที่ขับเคลื่อนด้วย event ซึ่ง self-contained service ใช้จัดการการเขียนข้าม service
- CQRS — การสร้าง local read model จาก event คือกลไกเดียวกับที่ทำให้สำเนาของ self-contained service ทันสมัยอยู่เสมอ
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| ตอบ request ได้เองโดยไม่ block รอ service อื่น | ต้อง replicate ข้อมูลจาก service อื่น — eventual consistency |
| resilient — service อื่นล้มไม่กระทบ request | data sync complexity สูงขึ้น |
| latency ต่ำกว่าเพราะไม่มี synchronous hop | ข้อมูลที่ replicate มาอาจ stale |
| scale ได้อิสระจาก service ที่เป็น dependency | ใช้ storage เพิ่มจาก data replication |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Premature Self-containment — replicate ข้อมูลทุกอย่างโดยไม่มีเหตุผล อาการ:
- service replicate data จาก 10 service เพราะ “อยากเป็น self-contained”
- sync overhead สูงกว่า synchronous call ที่หลีกเลี่ยงได้
Self-contained แต่ Data เก่า — ไม่มี mechanism sync data ที่ดี อาการ:
- service แสดง price เก่าเพราะ replication lag
- user เห็น stock มีแต่พอ checkout ก็หมด
💡 ตัวอย่างจากของจริง
Netflix:
- recommendation service เป็น self-contained — มี local copy ของ catalog metadata
- ถ้า catalog service ล้ม recommendation ยังทำงานได้ด้วยข้อมูลเก่า
Grab:
- pricing service cache rider data ไว้ local
- ทำให้คำนวณ fare ได้เองโดยไม่ต้องเรียก rider service ทุก request