ในระบบแบบ monolith การ debug request ที่ช้าหมายถึงการเปิดอ่าน log ไฟล์เดียว และอาจต่อ profiler เพิ่มอีกหน่อย แต่ในสถาปัตยกรรม microservice การคลิกเพียงครั้งเดียวอาจกระจายไปยัง service นับสิบตัว ฐานข้อมูลสามตัว และ message broker อีกหนึ่งตัว เมื่อ request นั้นใช้เวลาสี่วินาที คำถามจึงกลายเป็นว่า เวลาหายไปอยู่ตรงไหน?
Monitoring บอกคุณได้ ว่า มีบางอย่างผิดปกติ ส่วน observability คือความสามารถในการถามว่า ทำไม ซึ่งรวมถึงคำถามที่คุณไม่เคยคาดไว้ล่วงหน้า โดยใช้ข้อมูลที่ระบบผลิตออกมาอยู่แล้ว ข้อมูลเหล่านี้มีอยู่สามรูปแบบที่เสริมกันและกัน
สัญญาณสามแบบ และแต่ละแบบเก่งเรื่องอะไร
| สัญญาณ | เก่งเรื่อง | จุดอ่อน |
|---|---|---|
| Metrics | ดูแนวโน้ม, ตั้ง alert, ตอบว่า "ระบบพังอยู่ไหม?" | รายละเอียดน้อย อธิบาย request เดี่ยว ๆ ไม่ได้ |
| Traces | ติดตาม request หนึ่งตัวข้ามหลาย service | ถูก sample และเก็บทั้งหมดได้แพง |
| Logs | ให้บริบทละเอียดของเหตุการณ์เฉพาะ | เชื่อมโยงกันยากถ้าไม่มี ID และมีต้นทุนสูงเมื่อระบบใหญ่ |
คุณค่าที่แท้จริงเกิดจาก การเชื่อมโยงสัญญาณเหล่านี้เข้าด้วยกัน: alert ดังขึ้นจาก metric คุณกระโดดไปดูตัวอย่าง trace ในช่วงเวลานั้น แล้วจาก span ที่ช้าก็กระโดดต่อไปยัง log ของ request ตัวนั้นได้ทันที
Distributed tracing
Trace คือเส้นทางของ request หนึ่งตัวที่วิ่งผ่านระบบ ประกอบขึ้นจาก span หลายตัว โดยแต่ละ span คือการทำงานหนึ่งช่วงที่มีการจับเวลา (เช่น HTTP handler, DB query หรือการเช็ก cache) และมี span แม่ เมื่อแสดงผลเป็น waterfall ตัว trace จะบอกได้ทันทีว่า hop ไหนช้าหรือล้มเหลว
Context propagation
เพื่อให้ span จาก service ต่าง ๆ รวมอยู่ใน trace เดียวกันได้ ทุก service ต้องส่งต่อ trace context ไปด้วย โดยมาตรฐานที่ใช้คือ header traceparent ของ W3C:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01ไลบรารี instrumentation จะ inject header นี้ตอนเรียกออกไปข้างนอก และ extract ออกมาตอนมี request เข้ามา สำหรับ message broker ให้ใส่ไว้ใน message header เพื่อให้ hop ที่เป็น asynchronous ยังเชื่อมต่อกันอยู่
การทำ instrumentation ด้วย OpenTelemetry
OpenTelemetry (OTel) คือมาตรฐานกลางที่ไม่ผูกกับ vendor รายใด สำหรับ traces, metrics และ logs ทำ instrumentation ครั้งเดียวแล้วส่งข้อมูลไปยัง backend ไหนก็ได้ ตัวอย่างในภาษา Go:
func (h *Handler) Checkout(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "checkout")
defer span.End()
span.SetAttributes(
attribute.String("cart.id", cartID),
attribute.Int("cart.items", len(items)),
)
if err := h.payments.Charge(ctx, total); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "payment failed")
http.Error(w, "payment failed", http.StatusBadGateway)
return
}
}คุณค่าส่วนใหญ่มาจาก auto-instrumentation ได้แก่ middleware สำหรับ HTTP server และ client, gRPC interceptor และ wrapper ของ database driver ส่วน span แบบ manual ให้เพิ่มเฉพาะรอบ ๆ การทำงานทางธุรกิจที่มีความหมายเท่านั้น
Sampling
การเก็บทุก span จากระบบที่มี traffic สูงนั้นแพงมาก กลยุทธ์ที่นิยมใช้มีดังนี้
- Head sampling: ตัดสินใจตั้งแต่ต้น request เช่น เก็บไว้ 5% ต้นทุนต่ำ แต่อาจพลาด request ที่ช้าซึ่งเกิดขึ้นไม่บ่อย
- Tail sampling: buffer trace ที่สมบูรณ์ไว้ใน collector แล้วเลือกเก็บเฉพาะตัวที่น่าสนใจ เช่น error ทั้งหมด, อะไรก็ตามที่เกิน 1 วินาที และ traffic ปกติแบบสุ่มอีกส่วนเล็ก ๆ ดูแลยากกว่า แต่มีประโยชน์กว่ามาก
Metrics: วัดสิ่งที่ผู้ใช้รู้สึก
Metrics เก็บได้ถูกและ query ได้เร็ว จึงเหมาะเป็นพื้นฐานของ dashboard และ alert มีสองกรอบคิดที่ช่วยให้เลือกได้ว่าควรวัดอะไร
RED สำหรับ service ที่ขับเคลื่อนด้วย request
- Rate: จำนวน request ต่อวินาที
- Errors: จำนวน request ที่ล้มเหลวต่อวินาที
- Duration: การกระจายตัวของ latency (ใช้ histogram ไม่ใช่ค่าเฉลี่ย)
USE สำหรับทรัพยากร (CPU, pool, queue)
- Utilization: ทรัพยากรถูกใช้งานหนักแค่ไหน
- Saturation: มีงานรอคิวอยู่เท่าไร
- Errors: จำนวนความล้มเหลว
Latency ต้องดูเป็น percentile
ค่าเฉลี่ยซ่อนความเจ็บปวดไว้ ถ้า p50 อยู่ที่ 80ms แต่ p99 อยู่ที่ 3 วินาที แปลว่าผู้ใช้ 1 ใน 100 คนเจอประสบการณ์ที่แย่มาก และผู้ใช้ที่ยิง 50 request ต่อ session แทบจะต้องเจอแน่นอน ให้บันทึก latency เป็น histogram เพื่อจะคำนวณ p50, p95 และ p99 ได้ตอน query
ระวังเรื่อง cardinality
ทุกชุดค่าของ label ที่ไม่ซ้ำกันจะสร้าง time series ใหม่ขึ้นมาหนึ่งเส้น label อย่าง method และ status_code ใช้ได้ไม่มีปัญหา แต่ label อย่าง user_id หรือ URL path ดิบที่มี ID ปนอยู่ อาจสร้าง series นับล้านเส้นจนทำให้ metrics backend ล่มได้ ควร normalize path ให้เป็น route template (/orders/:id) ก่อนบันทึก
Structured logs
Log แบบข้อความธรรมดาเหมาะกับมนุษย์ที่อ่านทีละบรรทัด ส่วน structured log ซึ่งก็คือคู่ key-value ที่มักเข้ารหัสเป็น JSON นั้นเหมาะกับเครื่องที่ต้องค้นหาในหลายล้านบรรทัด:
logger.InfoContext(ctx, "payment captured",
slog.String("order_id", orderID),
slog.Int64("amount_cents", amount),
slog.String("provider", "stripe"),
slog.Duration("latency", elapsed),
){"time":"2026-09-24T10:12:03Z","level":"INFO","msg":"payment captured",
"order_id":"ord_8812","amount_cents":4599,"provider":"stripe",
"latency":"212ms","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736"}แนวปฏิบัติที่ทำให้ log มีประโยชน์จริง:
- ใส่
trace_idและspan_idในทุกบรรทัดของ log field เดียวนี้คือตัวเชื่อม log เข้ากับ trace - ใช้ชื่อ field ให้สอดคล้องกัน ทุก service ถ้า service หนึ่ง log เป็น
userIdแต่อีกตัวใช้user_idการ query ข้าม service จะลำบากมาก - Log เป็นเหตุการณ์ ไม่ใช่เล่าเรื่อง "Payment captured" พร้อม field ต่าง ๆ ดีกว่า "entering function…" ห้าบรรทัด
- ห้าม log ข้อมูลลับหรือข้อมูลส่วนบุคคลเด็ดขาด ให้ปิดบัง token, password และหมายเลขบัตรตั้งแต่ระดับ logger
- เลือก level อย่างตั้งใจ
ERRORควรหมายถึงมีคนอาจต้องลงมือแก้ไข ถ้า log error ให้กับเหตุการณ์ที่คาดไว้อยู่แล้วอย่าง 404 คนจะเรียนรู้ที่จะเมิน level นี้ไปเอง
SLO: เปลี่ยนสัญญาณให้เป็นการตัดสินใจ
Service Level Objective (SLO) คือการระบุว่า "ดีพอ" หมายถึงอะไรในมุมมองของผู้ใช้ เช่น "99.5% ของ request ที่ checkout ต้องสำเร็จภายในเวลาไม่เกิน 800ms ในช่วง 28 วัน"
อีก 0.5% ที่เหลือคือ error budget ของคุณ ซึ่งเปลี่ยนวิธีคุยกันในทีม:
- Budget ยังเหลือ? ปล่อยฟีเจอร์และรับความเสี่ยงได้ในระดับที่สมเหตุสมผล
- Budget ถูกเผาเร็ว? ชะลอลงและให้ความสำคัญกับความเสถียรก่อน
ให้ตั้ง alert จาก burn rate ซึ่งก็คืออัตราที่ budget ถูกใช้ไป แทนการ alert จาก metric ที่กระโดดขึ้นชั่วครู่ วิธีนี้ช่วยลดการปลุกคนกลางดึกเพราะความผิดปกติเล็ก ๆ ที่ไม่สำคัญ แต่ยังจับ incident จริงได้เร็วเหมือนเดิม
แผนการเริ่มใช้งานจริง
- เริ่มจาก auto-instrumentation สำหรับ HTTP, gRPC และ database client ในทุก service
- Deploy OpenTelemetry Collector เป็น pipeline กลางเพียงตัวเดียวสำหรับการ batch, sampling และ export
- กำหนดมาตรฐาน ชื่อ service, tag ของ environment และชื่อ field ใน log
- สร้าง RED dashboard หนึ่งอันต่อหนึ่ง service จาก template ที่ใช้ร่วมกัน
- กำหนด SLO ให้กับ user journey สำคัญสองถึงสามเส้นทาง และตั้ง alert จาก burn rate
- ใส่ trace ID ไว้ใน error response เพื่อให้ ticket ของทีม support ลิงก์ไปยัง trace ได้โดยตรง
สรุป
Metrics บอกว่ามีบางอย่างผิดปกติ traces ชี้ว่าผิดปกติที่ไหน และ logs อธิบายว่าทำไม ทำ instrumentation ครั้งเดียวด้วย OpenTelemetry เชื่อมสัญญาณทั้งสามเข้าด้วยกันด้วย trace ID ร่วม ควบคุม cardinality และ sampling และผูก alert เข้ากับ SLO ที่สะท้อนประสบการณ์จริงของผู้ใช้ เมื่อทำได้ถึงจุดนั้น checkout ที่ใช้เวลา 4 วินาทีก็จะกลายเป็นเรื่องที่ตรวจสอบได้อย่างรวดเร็ว
