🧵 Tracing ở mức khái niệm

By Better Wise

Bài trước bạn đã dựng alert dựa trên triệu chứng người dùng: alert chỉ kêu khi người dùng thật sự bị ảnh hưởng. Nhưng alert chỉ nói "có gì đó đang sai". Nó không nói sai ở đâu. Đó là lúc tracing trả lời câu hỏi mà log rời rạc không trả lời nổi.

Sau bài này, bạn có thể:

  • Giải thích trace là gì bằng một câu, không cần thuật ngữ nặng.
  • Mô tả span và cách các span lồng nhau thành một cây.
  • Nói được trace_id dùng để làm gì và tại sao phải truyền nó đi.
  • Chọn đúng công cụ: khi nào nhìn trace, khi nào quay lại đọc log.

3 giờ sáng: alert kêu, log im lặng

Alert báo: "Trang thanh toán chậm, p95 latency 4 giây". Bạn mở log. Mỗi service ghi log đẹp, có cấu trúc, có level, có user_id. Nhưng log của API gateway, của order service và của payment service nằm ở ba nơi, không có gì nối chúng lại. Bạn biết có chuyện chậm. Bạn không biết chậm ở chặng nào.

Trace sinh ra đúng cho khoảng trống đó.

Trace: cả chuyến đi của một request

Trace là toàn bộ hành trình của một request, từ lúc nó vào hệ thống đến lúc trả response, xuyên qua mọi service, mọi lời gọi database, mọi API bên ngoài.

Một trace gồm nhiều span. Mỗi span là một đơn vị công việc có điểm bắt đầu, có thời lượng (duration), và có status (ok hoặc error).

sequenceDiagram
    participant B as Trình duyệt
    participant G as API Gateway
    participant O as Order Service
    participant P as Payment Service
    participant D as Database
    B->>G: POST /checkout
    G->>O: tạo đơn hàng
    O->>D: INSERT order (120ms)
    O->>P: gọi thanh toán (1800ms)
    P->>D: INSERT payment (40ms)
    P-->>O: 200 OK
    O-->>G: 201 Created
    G-->>B: 201 Created (tổng 2100ms)

Nhìn hình là thấy ngay điều mà log không cho bạn: 1800ms nằm ở chặng gọi sang payment service, không phải ở database của bạn.

Span lồng nhau thành cây

Span có span cha. GET /checkout là span gốc, payment.charge là span con của nó, db.insert là span con của payment.charge. Mỗi span mang trace_id (chung cho cả chuyến đi) và parent_span_id (trỏ về span cha). Nhờ đó hệ thống tracing dựng lại được cây và bạn thấy ngay span nào chiếm nhiều thời gian nhất.

Note: Span không chỉ đo thời gian. Nó còn mang status. Một span có status error là một chặng đã thất bại, giúp bạn khoanh vùng lỗi nhanh hơn nhiều so với việc grep log.

Bài tập nhanh. Cho trace tóm tắt sau, span nào là bottleneck và độ trễ thật sự nằm ở đâu?

GET /checkout        2.4s
  auth.verify        0.05s
  cart.load          0.12s
  payment.charge     2.05s
    db.insert        0.03s
  email.send         0.04s

Xem gợi ý đáp án

payment.charge chiếm 2.05s trên tổng 2.4s, tức khoảng 85%. Nhưng span con db.insert bên trong nó chỉ 0.03s, nên phần lớn thời gian là chờ bên ngoài: network hoặc đối tác thanh toán. Kết luận đúng sẽ là "chậm do gọi payment service", không phải "database chậm". Đây chính là kiểu kết luận mà đọc log thuần sẽ rất khó rút ra.

trace_id: sợi chỉ xuyên suốt

trace_id là một chuỗi ID duy nhất cho mỗi request. Nó được sinh ở điểm vào đầu tiên (thường là API gateway hoặc service đầu tiên nhận request), rồi được truyền theo mọi lời gọi tiếp: qua HTTP header, qua metadata của message queue, qua context nội bộ.

Quy tắc duy nhất cần nhớ: nếu service A gọi service B mà không truyền trace_id, chuỗi bị đứt và trace của bạn chỉ còn một nửa.

Đây cũng là lý do bài học về structured logging có ích ngay lúc này. Nếu log của bạn có thêm field trace_id, bạn nối được log vào trace: xem trace để biết chặng nào chậm, rồi lọc log theo trace_id để biết chính xác chuyện gì đã xảy ra trong chặng đó.

Một _____ là toàn bộ hành trình của một request qua các service, còn mỗi chặng nhỏ bên trong là một _____. Mọi span của cùng một request mang cùng một _____. Các span nối với nhau thành cây thông qua trường _____. Nếu service gọi không _____ sang service được gọi, chuỗi trace sẽ bị đứt. Ở hệ thống lớn, tracing thường dùng _____ nên bạn chỉ thấy một phần request, không phải tất cả.

Bài tập nhanh. Bạn thêm tracing vào order service nhưng quên cấu hình cho payment service gửi trace_id ngược lại. Triệu chứng bạn sẽ thấy trên trace là gì?

Xem gợi ý đáp án

Trace sẽ dừng ở span gọi sang payment, và mọi span bên trong payment service sẽ nằm ở một trace khác (hoặc không có trace). Bạn thấy 1800ms nhưng không thấy bên trong 1800ms đó có gì. Cách sửa: lan truyền trace_id qua header và khởi tạo span mới từ context nhận được.

Khi nào trace hữu ích hơn đọc log

Ba công cụ trả lời ba câu hỏi khác nhau. Đừng bắt một công cụ làm việc của công cụ khác.

Câu hỏi Công cụ
Có vấn đề không? Bao nhiêu? Từ lúc nào? metric
Chậm hoặc lỗi ở chặng nào, service nào? trace
Chính xác chuyện gì xảy ra, lỗi gì, payload ra sao? log

Metric báo có vấn đề, trace chỉ ra ở đâu, log nói chính xác chuyện gì.

Một cách nhớ khác: nếu câu hỏi của bạn có chữ "chặng nào", "service nào", "mất bao lâu", hãy mở trace. Nếu câu hỏi là "giá trị này bằng bao nhiêu", "exception gì", hãy mở log.

Bài tập nhanh. Chọn công cụ cho ba tình huống sau:

  1. Bạn cần biết 20 phút qua error rate của /checkout tăng bao nhiêu.
  2. Một request cụ thể trả 500, bạn cần đọc exception và stack trace.
  3. Người dùng báo trang chậm, bạn không biết chặng nào.

Xem gợi ý đáp án

  1. metric (error rate theo thời gian).
  2. log, lọc theo trace_id của request đó nếu có.
  3. trace: tìm request chậm, xem span nào chiếm nhiều thời gian nhất.

Bốn cái bẫy khi mới bật tracing

  • Trace không có trace_id trong log. Bạn thấy span chậm nhưng không đọc được chi tiết. Thêm một field trace_id vào structured log là bước rẻ nhất và đáng làm nhất.
  • Chỉ bật tracing ở một service. Chuỗi đứt, trace vô dụng. Tracing là chuyện của cả đường đi, không phải của một service đẹp nhất.
  • Nghĩ trace thay thế được log hoặc metric. Không. Trace trả lời "ở đâu", không trả lời "bao nhiêu" hay "exception gì".
  • Quên sampling. Thu thập mọi request rất tốn kém. Ở hệ thống lớn người ta lấy mẫu, thường 1 đến 10 phần trăm, và luôn ưu tiên giữ lại các trace có error. Vì vậy đôi khi bạn không tìm thấy trace của một request cụ thể, đó là bình thường.

Một mẹo thực tế: đừng bật tracing toàn hệ thống ngay. Bật cho một luồng quan trọng nhất (ví dụ luồng thanh toán), thấy giá trị rồi mở rộng.

Chốt lại và bước tiếp

  • Trace là cả chuyến đi của một request; span là từng chặng, lồng nhau thành cây.
  • trace_id là sợi chỉ nối mọi span và mọi log của cùng một request, và phải được truyền qua mọi lời gọi.
  • Trace trả lời "chậm ở đâu", log trả lời "chính xác chuyện gì", metric trả lời "bao nhiêu".

Bài sau, bạn sẽ dùng AI như một người trợ giúp: đặt log đúng chỗ, đúng field, kèm trace_id, thay vì rải log khắp nơi rồi hy vọng.

Front: Trace là gì? Back: Toàn bộ hành trình của một request xuyên qua các service, từ lúc nhận đến lúc trả response.

Front: Span là gì? Back: Một đơn vị công việc trong trace, có thời điểm bắt đầu, thời lượng và status.

Front: trace_id dùng để làm gì? Back: Gom mọi span và log của cùng một request lại với nhau.

Front: trace_id được sinh ở đâu? Back: Ở điểm vào đầu tiên (gateway hoặc service đầu tiên nhận request), rồi truyền đi tiếp.

Front: Quan hệ giữa trace và span? Back: Một trace gồm nhiều span lồng nhau tạo thành cây.

Front: Span nối vào cây nhờ trường nào? Back: parent_span_id, đi kèm trace_id.

Front: Span có status error cho bạn biết gì? Back: Chặng đó đã thất bại, giúp khoanh vùng lỗi nhanh.

Front: Metric trả lời câu hỏi gì? Back: Có vấn đề không, mức bao nhiêu, từ lúc nào (latency, error rate).

Front: Trace trả lời câu hỏi gì? Back: Chậm hoặc lỗi ở chặng nào, service nào.

Front: Log trả lời câu hỏi gì? Back: Chính xác chuyện gì xảy ra: exception, giá trị, payload.

Front: Khi service A gọi service B, cần làm gì với trace_id? Back: Truyền trace_id sang, thường qua HTTP header hoặc metadata của message.

Front: Vì sao hệ thống lớn dùng sampling khi trace? Back: Thu thập mọi request quá tốn kém; lấy mẫu vẫn đủ thấy pattern.

Front: Làm sao tìm bottleneck từ một trace? Back: Tìm span có thời lượng lớn nhất trong cây, rồi kiểm tra các span con của nó.

Front: Log có trace_id nhưng trace thì không, thiếu gì? Back: Thiếu cấu hình lan truyền trace_id giữa các service, chuỗi bị đứt.