แนวคิด Distributed Tracing
เสาที่สามและซับซ้อนที่สุด แต่ทรงพลังมากในโลก microservices ลองนึกภาพปัญหานี้:
ลูกค้าบ่นว่า "กดสั่งซื้อแล้วช้า 3 วินาที" — request วิ่งผ่าน 8 service คุณจะรู้ได้ยังไงว่า service ตัวไหน ที่ทำให้ช้า? metric บอกแค่ว่า "ระบบช้า", log แต่ละ service ก็กระจัดกระจาย...
นี่คือปัญหาที่ distributed tracing เกิดมาแก้
ทำไม metrics + logs ไม่พอ
- Metrics บอกว่า ระบบโดยรวม ช้า แต่ไม่บอกว่า request ไหน หรือ service ไหน (มันเป็นค่า aggregate)
- Logs มีรายละเอียดแต่กระจัดกระจายไปตาม service ต้องมานั่งต่อจิ๊กซอว์เองว่า log ไหนของ request เดียวกัน
Tracing เติมช่องว่างนี้: ตามรอย 1 request ตลอดเส้นทาง ข้ามทุก service พร้อมเวลาที่ใช้ในแต่ละจุด
2 คำที่ต้องเข้าใจ: Trace และ Span
Span = หน่วยงานหนึ่งชิ้น (operation เดียว) มี:
- ชื่อ (เช่น
GET /order,SELECT users) - เวลาเริ่ม + ระยะเวลา (duration)
- attributes (metadata: http.method, db.statement, ...)
- สถานะ (ok / error)
Trace = การเดินทางทั้งหมดของ 1 request = ชุดของ span ที่เชื่อมกันเป็น tree ด้วยความสัมพันธ์ parent-child
Trace: "สั่งซื้อสินค้า" (trace_id: a1b2c3, รวม 820ms)
│
├─ span: POST /checkout [gateway] ├──────────────────────┤ 820ms
│ ├─ span: verify auth [auth-service] ├─┤ 12ms
│ ├─ span: create order [order-service] │ ├──┤ 40ms
│ │ └─ span: INSERT orders [postgres] │ ├─┤ 30ms
│ └─ span: charge payment [payment-service] │ ├──────────────┤ 760ms ⚠️
│ └─ span: call bank API [external] │ ├─────────────┤ 740ms ⚠️
พอเห็น waterfall แบบนี้ ตอบได้ทันทีว่าช้าที่ call bank API (740ms) ไม่ใช่โค้ดเรา — นี่คือพลังของ tracing
แต่ละ span มี
trace_idเดียวกัน (ผูกทั้ง trace) +span_idของตัวเอง +parent_span_idชี้ไป span แม่ โครงสร้าง 3 id นี้แหละที่ประกอบขึ้นเป็น tree
Context Propagation — กุญแจของทั้งระบบ
คำถามสำคัญ: service B จะรู้ได้ยังไงว่า request ที่มันได้รับ เป็นส่วนหนึ่งของ trace เดียวกับที่ service A เริ่มไว้?
คำตอบคือ context propagation — ส่ง trace context (trace_id, span_id ปัจจุบัน) ต่อไปกับทุก request ข้าม service ผ่าน HTTP header:
service A เรียก service B → แนบ header:
traceparent: 00-a1b2c3d4...-e5f6a7b8...-01
│ └─trace_id┘ └span_id┘ └flags (เช่น sampled)
└─version
service B อ่าน header นี้ → รู้ว่าตัวเองอยู่ใน trace a1b2c3 และ span ที่มันสร้างมี parent เป็น span จาก A
traceparentเป็นมาตรฐาน W3C Trace Context — ทำให้ tool ต่างเจ้าคุยกันได้ นี่คือสิ่งที่ทำให้ tracing ข้ามภาษา/ข้ามระบบได้ และเป็นสิ่งที่ OpenTelemetry (บทหน้า) จัดการให้อัตโนมัติ
ถ้า context propagation ขาดตอนแม้แต่ service เดียว (เช่น service เก่าที่ไม่ได้ instrument, หรือผ่าน message queue โดยไม่ส่ง context ต่อ) — trace จะ "ขาด" เป็นสองท่อน หา root cause ได้ยากขึ้น การ propagate ให้ครบทุก hop จึงสำคัญมาก
Sampling — เก็บทุก trace ไม่ไหว
จำจากบทที่ 01: traces แพงที่สุดในสามเสา ถ้าเก็บทุก request ในระบบ traffic สูง = ข้อมูลมหาศาล จึงต้อง sampling (เก็บแค่บางส่วน):
| กลยุทธ์ | ทำงานยังไง | ข้อดี/ข้อเสีย |
|---|---|---|
| Head-based | ตัดสินใจตั้งแต่ต้น trace (เช่น สุ่มเก็บ 10%) | ง่าย, เบา / อาจพลาด trace ที่ error |
| Tail-based | รอ trace จบแล้วค่อยตัดสิน (เก็บถ้า error/ช้า) | เก็บของสำคัญครบ / ซับซ้อน, ต้อง buffer |
กลยุทธ์ยอดนิยม: sample แบบ head 1-10% สำหรับ trace ปกติ แต่เก็บ 100% ของ trace ที่ error หรือช้าผิดปกติ (tail-based) — เพราะ trace ที่มีปัญหาคือของที่เราอยากดูที่สุด
Tracing เชื่อมกับ pillar อื่น
- Trace ↔ Log: ใส่
trace_idลงใน log (บทที่ 30) → จาก span กระโดดไปอ่าน log ของ span นั้นได้ - Trace ↔ Metric: metric อย่าง RED duration บอกว่า "ช้า" → เปิด trace ดูว่าช้าตรงไหน (flow บทที่ 01)
- Exemplars: จุดบนกราฟ metric ที่ลิงก์ตรงไปยัง trace ตัวอย่าง (บทที่ 50)
สรุป
- Tracing ตามรอย 1 request ตลอดเส้นทางข้าม service — เติมช่องว่างที่ metric/log ทำไม่ได้
- Span = 1 operation, Trace = ชุด span เชื่อมเป็น tree (ผ่าน trace_id/span_id/parent_span_id)
- Waterfall view ชี้ได้ทันทีว่าช้าที่ service ไหน
- Context propagation (W3C
traceparentheader) ส่ง trace context ข้าม service — ขาดไม่ได้แม้ hop เดียว - Sampling จำเป็นเพราะ trace แพง — เก็บ error/slow 100%, ปกติ sample
- ผูกกับ log ผ่าน
trace_id
บทหน้า: OpenTelemetry — มาตรฐานกลางที่ทำเรื่องพวกนี้ให้อัตโนมัติ และรวม 3 เสาเป็นหนึ่งเดียว