ข้ามไปยังเนื้อหา

Distributed Tracing

log ที่รวบรวมและเชื่อมโยงไว้แล้วช่วยให้คุณค้นทุกบรรทัดของ request หนึ่งมาอ่านเรียงตามลำดับได้ ซึ่งตอบคำถามว่า เกิดอะไรขึ้น แต่มีอีกคำถามที่ log ตอบได้ไม่ดีเลยคือ เวลาหายไปไหน checkout ที่ใช้เวลาสี่วินาทีวิ่งผ่าน gateway, order service, payment service และ inventory service โดยแต่ละตัวอาจเรียก database หรือ cache ต่อไปอีก ในต้นไม้ของการเรียกที่ซ้อนกันนั้น มีสามในสี่วินาทีหายไปที่จุดใดจุดหนึ่ง แต่รายการ log แบบแบน ๆ ต่อให้เชื่อมโยงกันได้สมบูรณ์แบบ ก็ไม่แสดงรูปร่างของต้นไม้หรือระยะเวลาของแต่ละกิ่งให้ดูอยู่ดี

correlation id บอกได้แค่ว่า log ชุดนี้เป็นพวกเดียวกัน แต่ไม่บอกว่า เกี่ยวข้องกันอย่างไร ไม่บอกว่าการเรียก payment เกิดขึ้น ภายใน การเรียก order ไม่บอกว่าการเรียก inventory รัน หลังจาก payment คืนค่ากลับมา และไม่บอกว่า database query ใต้ inventory กินเวลาไป 2.8 จาก 4 วินาทีของ request นั้น ถ้าไม่มีโครงสร้างเชิงสาเหตุที่มี timing กำกับ คุณก็ต้องกลับไปเพ่ง timestamp ข้าม service เพื่อเดา critical path เอาเอง ทั้งที่ timestamp จากคนละเครื่องยังไม่ตรงกันเป๊ะด้วยซ้ำ

แล้วคุณจะสร้าง nested call tree เต็มของคำขอหนึ่งขึ้นใหม่ได้อย่างไร พร้อมระยะเวลาของทุก hop ข้ามบริการที่แต่ละตัวเห็นเฉพาะส่วนของตัวเองเท่านั้น

Distributed tracing มองหนึ่ง request เป็นหนึ่ง trace ที่ประกอบขึ้นจาก span หลายอัน trace คือเส้นทางทั้งหมดของ request จากภายนอกหนึ่งครั้ง ส่วน span คือหน่วยงานหนึ่งหน่วยภายในนั้น เช่น request ขาเข้าที่ถูกจัดการ การเรียกขาออกหนึ่งครั้ง หรือ query หนึ่ง query แต่ละ span บันทึก start time, duration และชื่อ พร้อมพก id สองตัวคือ trace id ที่ทุก span ใน request เดียวกันใช้ร่วมกัน และ span id เฉพาะตัว นอกจากนี้แต่ละ span ยังระบุ parent span id ไว้ด้วย และ parent link เหล่านี้เองคือสิ่งที่ใช้ประกอบต้นไม้ขึ้นมาใหม่

กลไกที่ทำให้ข้ามขอบเขต process ได้คือ context propagation เวลา service เรียกออกไปข้างนอก จะฉีด trace id ปัจจุบันกับ span id ของตัวเองลงไปใน request ปกติส่งเป็น header ตามมาตรฐาน W3C Trace Context (traceparent) ฝั่ง service ที่รับก็อ่าน header เหล่านั้น ถือว่า span id ขาเข้าเป็น parent แล้วเปิด span ใหม่ใต้ parent นั้น ไล่ต่อกันไปเรื่อย ๆ ตามต้นไม้ ทุก service รายงาน span ของตัวเองไปยัง collector กลาง ซึ่งจะเย็บทุกอย่างกลับเข้าด้วยกันด้วย trace id และ parent link ออกมาเป็น waterfall พร้อม timing ชุดเดียว

sequenceDiagram
  participant GW as Gateway
  participant O as Order Service
  participant P as Payment Service
  participant I as Inventory Service
  GW->>O: POST /checkout\ntraceparent: trace=abc, span=1
  Note over O: start span 2 (parent 1)
  O->>P: charge()\ntraceparent: trace=abc, span=2
  Note over P: start span 3 (parent 2)
  P-->>O: ok (span 3 ends, 120ms)
  O->>I: reserve()\ntraceparent: trace=abc, span=2
  Note over I: start span 4 (parent 2)
  I-->>O: ok (span 4 ends, 2800ms)
  O-->>GW: 200 (span 2 ends, 2950ms)
แต่ละ hop อ่าน traceparent ขาเข้า, เริ่ม child span และ propagate trace พร้อม span id ของตัวเองต่อไป — collector สร้างต้นไม้ขึ้นใหม่จาก trace id และ parent link

หัวใจของ tracing คือ propagation ให้อ่าน trace context จาก request ขาเข้า แล้วฉีดลงไปในทุกการเรียกขาออก ตัวอย่างข้างล่างอ่าน traceparent header ขาเข้าแล้วส่งต่อ ที่เป็นขั้นต่ำที่ทำให้ trace ไม่ขาดตอนข้าม hop

import express from 'express';
import { fetch } from 'undici';
const app = express();
app.post('/checkout', async (req, res) => {
// Read incoming trace context (or start a new trace at the edge).
const incoming = req.header('traceparent') ?? newTraceparent();
const { traceId, parentSpanId } = parseTraceparent(incoming);
// This service's own span becomes the parent of downstream calls.
const mySpanId = randomSpanId();
const outgoing = formatTraceparent(traceId, mySpanId);
// Propagate on every outbound call.
await fetch('http://payment/charge', {
method: 'POST',
headers: { traceparent: outgoing },
});
res.sendStatus(200);
});

ในทางปฏิบัติคุณแทบไม่ต้องเขียน propagation นี้เองเลย instrumentation library อย่างที่สร้างบน OpenTelemetry จะฉีดและ extract trace context จับเวลา span และ export ให้เสร็จสรรพ ความเข้าใจกลไกนี้จะมีค่าที่สุดตอนที่ trace ขาดหายอย่างลึกลับที่ hop ใด hop หนึ่ง

สิ่งที่คุณได้รับ:

  • Critical path ที่ถูกทำให้มองเห็นได้ trace waterfall แสดงระยะเวลาของทุก hop ดังนั้นคุณจึงเห็นได้ในพริบตาว่า 2.8 จาก 4 วินาทีถูกใช้ไปใน inventory query หนึ่ง แทนการเดาข้าม log timestamp
  • โครงสร้างเชิงสาเหตุ ไม่ใช่แค่ correlation parent link สร้างขึ้นใหม่ว่า การเรียกใดเกิดขึ้นภายในการเรียกใด กู้คืนมุมมองแบบซ้อนที่ stack trace เดียวให้คุณใน monolith
  • ชี้ต้นตอของ error ข้าม service ได้ เมื่อ request ล้มเหลว trace จะชี้ตรงไปที่ span และ service ที่พังจริง ไม่ใช่แค่ 500 กว้าง ๆ จาก gateway

สิ่งที่คุณต้องจ่าย:

  • Propagation ต้องไม่ขาด hop เดียวที่ทำ trace context หล่นหาย — client ที่ไม่ได้ instrument, queue ที่ไม่พก header, ขอบเขตของ thread — จะแยก trace ออกเป็นสองและซ่อนต้นไม้ที่อยู่เลยจุดนั้นไป
  • Sampling มักเป็นข้อบังคับ การบันทึกทุก span ของทุกคำขอที่ทราฟฟิกสูงนั้นแพง คุณจึง sample เลือกกลยุทธ์ sampling ของคุณอย่างตั้งใจ เพราะ head-based sample อาจพลาดคำขอที่ล้มเหลวซึ่งหายากแต่คุณอยากเห็นที่สุด
  • ความพยายามในการ instrument ทุกบริการ, client library และขอบเขต async ต้องมีส่วนร่วม; ภาษาและ framework ที่ปะปนกันทำให้การครอบคลุมที่สม่ำเสมอเป็นงานจริง ๆ
  • Log Aggregation — ใส่ trace id ลงในทุกบรรทัด log แล้ว traces และ logs ก็อ้างอิงถึงกันและกัน
  • Application Metrics — metric บอกว่า latency เพิ่มขึ้น ส่วน trace บอกว่าเพิ่มขึ้น ตรงไหน
ข้อดีข้อแลกเปลี่ยน
เห็น request journey ข้าม service ทั้งหมดในที่เดียวsampling ที่ไม่ดีทำให้พลาด trace สำคัญ
ค้นหา bottleneck ใน distributed system ได้overhead ของ trace collection และ storage
correlate log และ metric กับ specific requestrequire instrument ทุก service — ถ้า service หนึ่งไม่ instrument trace ขาด
เห็น dependency map ของ service จาก trace dataทีมต้องเรียนรู้ tool เพิ่ม (Jaeger, Zipkin, Tempo)

Trace ทุก Request (100% Sampling) — ไม่ใช้ sampling ทำให้ storage พุ่ง อาการ:

  • trace storage cost สูงมากใน high-traffic system
  • ใช้ head-based หรือ tail-based sampling แทน

Trace ที่ขาดช่วง — service บางตัวไม่ propagate trace context อาการ:

  • เปิด trace แล้วเห็น hop ขาดหายไป
  • ไม่รู้ว่า service ไหนใช้เวลาเท่าไร
  • ต้อง instrument ทุก service และทุก async call

💡 ตัวอย่างจากของจริง

Google (Dapper):

  • เป็น paper ที่เป็นต้นแบบของ distributed tracing ทั้งอุตสาหกรรม (2010)
  • Zipkin (Twitter) และ Jaeger (Uber) ได้รับแรงบันดาลใจจาก Dapper

Uber (Jaeger):

  • สร้าง Jaeger เพื่อ trace request ข้าม 1000+ service
  • ปัจจุบัน CNCF graduated project — ใช้แพร่หลายที่สุดในอุตสาหกรรม
trace กับ span สัมพันธ์กันอย่างไร
อะไรคือสิ่งที่ใช้ประกอบ call tree แบบซ้อนกันของ trace ขึ้นมาใหม่
จะเกิดอะไรขึ้นถ้ามี hop หนึ่งไม่ propagate trace context ต่อ