Microservice Chassis
ทุก service ที่คุณ build ต้องมีเครื่องจักรพื้น ๆ ชุดเดียวกันก่อนถึงจะทำงานที่มีประโยชน์ได้ ต้องโหลด configuration ตั้ง structured logging พร้อม request correlation เปิด metric ในรูปแบบที่ระบบ monitoring คาดหวัง ตอบ health และ readiness probe ให้ orchestrator แล้ว register ตัวเองเพื่อให้ตัวอื่นค้นเจอ ไม่มีข้อไหนเป็น business logic เลย แต่ทุกข้อคือเงื่อนไขที่ทำให้ business logic รันได้อย่างปลอดภัย
ตอนคุณเขียน service เดียว คุณประกอบ plumbing นี้ครั้งเดียว พอถึง service ที่สิบ คุณสังเกตว่า code หลายร้อยบรรทัดเดียวกันถูก copy, paste และค่อย ๆ แตกต่างไปเงียบ ๆ: service หนึ่งเปิดเผย /healthz อีกตัว /health ตัวหนึ่ง log เป็น JSON อีกตัวเป็น plain text metric ถูกตั้งชื่อไม่สอดคล้องกัน cross-cutting baseline ถูก implement ใหม่ ต่างกันเล็กน้อย ในทุก service
คุณต้องการให้ทุก service ใหม่เริ่มต้นชีวิตด้วย configuration, logging, metric, health check และ discovery ที่เดินสายไว้แล้วและสอดคล้องกันทั้งกองทัพ — เพื่อให้ service เปิดเผย health endpoint เดียวกัน ปล่อย log ในรูปทรงเดียวกัน และรายงาน metric ด้วยชื่อเดียวกันกับทุก service อื่น คุณต้องการให้ developer ที่สร้าง service ใหม่เขียน business logic ตั้งแต่วันแรก ไม่ใช่ใช้เวลาหนึ่งสัปดาห์ประกอบ boilerplate ใหม่ และคุณต้องการ upgrade baseline นั้นในที่เดียวแทนที่จะใน repository ยี่สิบแห่ง
สิ่งที่ต้องการจึงเป็น operational baseline ชุดเดียวที่สอดคล้องกันและพร้อมใช้ในทุก service โดยลงแรงต่อ service ให้น้อยที่สุด และมีที่เดียวสำหรับพัฒนาต่อ
วิธีแก้
หัวข้อที่มีชื่อว่า “วิธีแก้”build microservice chassis ขึ้นมา ซึ่งคือ framework, library หรือฐานที่รวม cross-cutting concerns ไว้ให้ service ใหม่หยิบไปใช้ได้ทันที chassis รับหน้าที่ดูแล plumbing ทั้งหมด ทั้งอ่าน configuration ตั้ง structured logging ลงทะเบียน metric endpoint ติดตั้ง health และ readiness probe และจัดการ discovery registration แล้วเปิดผิวสัมผัสเล็ก ๆ ไว้ให้คุณเสียบ business logic เข้าไป
งานของ developer หดเหลือ: เริ่มจาก chassis เพิ่ม route และ domain logic แล้วส่งมอบ ทุกอย่างด้าน operation มีอยู่แล้วและเหมือนกับทุก service อื่นที่ build บน chassis chassis มักจับคู่กับ service template — repository ตั้งต้น, generator หรือโครงร่าง — เพื่อให้การสร้าง service ใหม่หมายถึงการ generate จาก template และเติม business code
เพราะ baseline อยู่ใน component ร่วมตัวเดียว การปรับปรุงอย่างเพิ่ม metric field ใหม่ ทำ log format ให้เข้มขึ้น หรือทำ health check ให้ดีขึ้น จึงเป็นการแก้ครั้งเดียว แล้ว roll out ด้วยการเลื่อนเวอร์ชัน chassis ในแต่ละ service แทนที่จะไล่แก้ทีละ repository ด้วยมือ
flowchart TB
subgraph Service[A service built on the chassis]
direction TB
Biz[Business logic: routes and domain rules]
subgraph Chassis[Microservice chassis]
Cfg[Config loading]
Log[Structured logging]
Met[Metrics endpoint]
Health[Health and readiness probes]
Disc[Service discovery registration]
end
Biz --> Chassis
end
Chassis --> Plat[Platform: orchestrator, monitoring, registry] ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่างนี้คือ service ที่สร้างบน chassis การเรียก bootstrap ครั้งเดียวต่อสาย config, logging, metric, health และ discovery ให้ครบ สิ่งเดียวที่ developer ต้องเพิ่มคือ route ที่มีความหมายเชิงธุรกิจ แต่ละตัวอย่างเขียนตามสไตล์ของภาษานั้น ๆ
import { createService } from '@acme/chassis';
// The chassis loads config, sets up logging, metrics,// /health, /ready, and registers the service for discovery.const app = createService({ name: 'order-service' });
// Developer adds only the business logic.app.get('/orders/:id', async (req, res) => { const order = await orderRepo.find(req.params.id); res.json(order);});
app.start();from acme_chassis import create_service
# The chassis wires config, logging, metrics,# /health, /ready, and discovery registration.app = create_service(name="order-service")
# Developer adds only the business logic.@app.get("/orders/{order_id}")async def get_order(order_id: str): return await order_repo.find(order_id)
app.start()package main
import "github.com/acme/chassis"
func main() { // The chassis sets up config, logging, metrics, // /health, /ready, and discovery registration. app := chassis.NewService("order-service")
// Developer adds only the business logic. app.Get("/orders/{id}", func(c *chassis.Ctx) error { order, err := orderRepo.Find(c.Param("id")) if err != nil { return err } return c.JSON(order) })
app.Start()}use acme_chassis::Service;
#[tokio::main]async fn main() { // The chassis wires config, logging, metrics, // /health, /ready, and discovery registration. let mut app = Service::new("order-service");
// Developer adds only the business logic. app.get("/orders/:id", |ctx| async move { let order = order_repo.find(ctx.param("id")).await?; ctx.json(order) });
app.start().await;}ผลลัพธ์ที่ตามมา
หัวข้อที่มีชื่อว่า “ผลลัพธ์ที่ตามมา”สิ่งที่คุณได้:
- ได้ความสอดคล้องมาฟรี ๆ ทุก service เปิด health endpoint รูปแบบ log และชื่อ metric ชุดเดียวกัน เพราะได้มาจาก component ร่วมตัวเดียว observability และ operability เลิกเป็นเรื่องที่แต่ละทีมเดาเอาเอง
- สร้าง service ได้เร็วขึ้น service ใหม่เริ่มต้นด้วย operational baseline ที่มีอยู่แล้ว ดังนั้น developer จึงเขียน domain code แทนการประกอบ plumbing ใหม่
- ที่เดียวให้พัฒนา baseline การปรับปรุง logging, metric หรือ health ลงใน chassis และ roll out ด้วยการเลื่อนเวอร์ชัน ไม่ใช่การแก้ทุก repository
สิ่งที่คุณต้องเสีย:
- ต้องลงแรงแยกตามภาษา ระบบแบบ polyglot ต้องมี chassis หนึ่งตัวต่อหนึ่งภาษา และการดูแลให้ทุกตัวมี feature เท่าเทียมกันคืองานที่ต้องทำต่อเนื่องไม่จบ
- การ coupling และแรงเสียดทานด้านการทำเวอร์ชัน ทุก service พึ่งพา chassis การเปลี่ยนแบบ breaking บังคับให้ต้อง upgrade แบบประสานงานกัน และ chassis ที่เก่าค้างสามารถรั้ง service ไว้ได้
- เสี่ยงบวม chassis ที่พยายามทำทุกอย่างจะหนักและบังคับสไตล์มากเกินไป ให้โฟกัสเฉพาะ cross-cutting concerns จริง ๆ ไม่ใช่ helper เชิงธุรกิจ
ข้อสังเกตที่ใช้ได้จริง: chassis จัดการเรื่อง in-process (config, logging, metric, health) ส่วน Service Mesh จัดการ networking between-process หลายทีมใช้ทั้งสองอย่าง
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”- Externalized Configuration — chassis มักเป็นตัวที่อ่านและใช้ externalized config
- Service Mesh — จัดการ networking cross-cutting concerns ที่ chassis ปล่อยให้ sidecar ทำ
- Service per Container — service ที่ build บน chassis ก็ยังถูก package และรันแบบหนึ่ง service ต่อหนึ่ง container
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| cross-cutting concerns (logging, health check, metrics) ทำครั้งเดียว | chassis กลายเป็น dependency ที่ทุก service ผูกกัน |
| ทุก service มี baseline ของ observability และ security เหมือนกัน | อัปเดต chassis ต้อง rebuild และ redeploy ทุก service |
| new service เริ่มต้นได้เร็ว — clone chassis แล้วเพิ่ม business logic | chassis อาจ bloat ถ้าใส่ฟีเจอร์มากเกิน |
| standard ชัดเจน — ทุกทีมทำงานแบบเดียวกัน | หลาย language stack = หลาย chassis |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Fat Chassis — ใส่ business logic ลงใน chassis อาการ:
- chassis มี auth logic, pricing logic, หรือ domain rule
- ทุก service ได้รับ code ที่ส่วนใหญ่ไม่ต้องการ
- chassis กลายเป็น monolith ที่แชร์ข้าม service
Chassis ที่ไม่ Update — chassis เก่าแต่ service ใหม่ยังคง base บน version เก่า อาการ:
- vulnerability ใน logging library แก้ใน chassis ใหม่ แต่ service เก่ายังใช้ version เก่า
- ไม่มี automated update mechanism
💡 ตัวอย่างจากของจริง
Netflix OSS:
- สร้าง Spring Cloud Netflix เป็น chassis สำหรับ Java service
- รวม Eureka (discovery), Hystrix (circuit breaker), Ribbon (load balancing) ไว้ใน chassis
- ทีมใหม่ใช้ starter project แล้วได้ baseline observability ทันที
Uber:
- ใช้ Fx (DI framework ของ Uber) เป็นฐานของ chassis สำหรับ Go service
- ทุก service ใหม่เริ่มจาก template ที่มี logging, tracing, metrics พร้อม