Distributed Tracing — rượt tìm lỗi chậm trong hệ thống microservice
Khi hệ thống còn là một app đơn khối (monolith), để tìm chỗ nào chậm thì bật log, nhìn vô một chỗ là ra. Nhưng hồi mà chuyển sang microservice rồi mới thấy cực hình: một request khách bấm xong mà tốn 1.2 giây, thì cái 1.2 giây đó nằm ở service nào? Auth? Order? Payment? Hay là cái database phía sau?
Đoán mò kiểu đó làm anh em debug cả ngày mà chưa chắc đã đúng. Đó là lúc distributed tracing (tracing phân tán) lên tiếng. Trong bài này em nói rõ khái niệm, cách cài OpenTelemetry với Go, và một cái bug thật tụi em gặp mà nhờ trace mới vỡ lẽ.
Trace, Span, Context — ba chữ nghe to mà hiểu được liền
Đừng lo mấy thuật ngữ, nó đơn giản lắm:
- Trace: toàn bộ hành trình của một request, từ lúc vào gateway tới lúc trả về client.
- Span: một "chặn" bên trong hành trình đó — ví dụ gọi HTTP tới service X là một span, query database là một span nhỏ hơn.
- Context: thông tin (trace ID, span ID) được truyền dọc từ service này sang service kia, để service sau biết mình thuộc trace nào.
Cái chìa khoá nằm ở chữ context. Request đi qua 3 service mà không ai chịu truyền trace ID qua HTTP header thì trace đứt đoạn, nhìn không ra bức tranh chung.
OpenTelemetry — chuẩn chung ai cũng dùng
Bây giờ không còn cảnh mỗi hãng một SDK riêng (Jaeger, Zipkin, DataDog, ...). OpenTelemetry (gọi tắt OTel) thành chuẩn: bạn instrument ứng dụng một lần, rồi export cho bất kỳ backend nào. Muốn đổi vendor chỉ cần đổi exporter, code giữ nguyên.
Setup trong Go ngắn gọn như vầy:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
exporter, _ := otlptrace.New(ctx, otlptracegrpc.NewClient(
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure(),
))
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.Default()),
)
otel.SetTracerProvider(tp)
return tp, nil
}
Xong tới phần tạo span trong từng handler tụi em gọi tới service khác:
func handleOrder(w http.ResponseWriter, r *http.Request) {
tracer := otel.Tracer("order-service")
ctx, span := tracer.Start(r.Context(), "order.process")
defer span.End()
// gọi sang service payment, PHẢI truyền ctx vô
resp, err := callPayment(ctx, payload)
if err != nil {
span.RecordError(err) // trace sẽ note chỗ này fail
span.SetAttributes(attribute.String("order.id", payload.ID))
http.Error(w, "payment failed", http.StatusInternalServerError)
return
}
_ = resp
}
Chi tiết cực kỳ quan trọng: lúc gọi HTTP tới service khác phải set header trace. Client của OTel làm tự động nhờ propagation, nhưng nếu tự viết HTTP thì nhớ thêm:
// bên client khi gọi service kế
oteltracehttp.Inject(ctx, &req.Header) // đẩy traceparent vào header
Nếu quên bước này, span bên service kia sẽ không nối vô trace của mình — trace nhìn ra hai mảnh rời, chả ích gì.
Cái bug thật tụi em gặp
Có đợt user kêu API tạo order khi thì 200ms, khi thì 900ms, rất hên xui. Không có trace thì chắc dò log từng service mệt nghỉ. Có trace rồi mới thấy đường đi:
gateway → order (500ms) → inventory (300ms) → database inventory: 180ms chờ lock
Hoá ra inventory dùng lock cấp row quá lâu, mấy request tạo order giờ cao điểm chờ nhau. Trước đó có index ngon lành nhưng không ai theo dõi thời gian chờ lock — cái này nằm bên dưới tầng query đơn lẻ, log thường không thấy. Trace phô bày một đường timeline từ đầu tới cuối, thấy ngay cổ chai không phải ở code mà ở cạnh tranh DB.
Kinh nghiệm thực chiến
- Instrument từ sớm, đừng chờ tới lúc sập. Thêm trace sau khi hệ thống chậm rồi thì vừa mệt vừa thiếu dữ liệu quá khứ.
- Bắt đầu từ "thượng nguồn": gateway hoặc entry point. Span ở đây mới thấy toàn cảnh; span trong một service lẻ giống như log mà thôi.
- Đừng trace mọi thứ. Sampling 100% request khối lượng lớn sẽ tốn bộ nhớ. Sample kiểu tail-based cho đúng: trace áp suất cao lưu đầy đủ, request bình thường lấy mẫu thưa.
- Kèm attribute có ích: order id, user id, error message. Không thì trace đẹp mà không search được.
Tracing không làm app nhanh lên, nhưng nó bảo cho mình biết tiền vàng rơi ở đâu để còn mà rót sức. Mấy cái metric tổng (P95 latency theo service) chỉ nói được "hệ thống chậm", còn trace mới nói chính xác nó chậm ở con đường nào.