Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn đang là kỹ sư trực (on-call) cho một sàn thương mại điện tử. Đúng giờ cao điểm, khách hàng phàn nàn rằng trang thanh toán "quay quay" mãi không xong. Bạn mở dashboard lên: CPU của tất cả các service đều bình thường, không service nào báo lỗi 500, log của từng service riêng lẻ đọc vẫn "sạch". Nhưng request thì mất tới 8 giây mới trả về. Câu hỏi ám ảnh nhất lúc này là: chậm ở chỗ nào?
Trong một hệ thống monolith cũ, câu hỏi đó dễ trả lời — mọi thứ nằm trong một tiến trình, bạn bật profiler lên là thấy. Nhưng ở kiến trúc microservices hiện đại, một request "Đặt hàng" có thể đi qua API Gateway → Order Service → Inventory Service → Payment Service → Notification Service, mỗi service lại gọi database, cache, message queue riêng. Một request đơn giản với người dùng có thể tương ứng với 10 đến 100 "hop" (bước nhảy qua các thành phần) bên dưới. Khi tổng thời gian bị đội lên, bạn không thể chỉ nhìn log rời rạc của từng service để ghép lại bức tranh — vì log của service A và service B không biết chúng thuộc cùng một request.
Đây chính là bài toán mà Distributed Tracing (truy vết phân tán) sinh ra để giải quyết. Nó cho bạn khả năng "nhuộm màu" một request duy nhất và theo dõi nó xuyên suốt toàn bộ hành trình qua mọi service, để bạn nhìn thấy chính xác hop nào tốn 6 trong 8 giây. Trong bài này, chúng ta sẽ hiểu bản chất của tracing, ba công cụ chủ đạo trên cloud (AWS X-Ray, Google Cloud Trace, và Jaeger open-source), rồi thực hành từng bước để bạn tự tin triển khai.
Khái niệm cốt lõi
Trace, Span, và mối quan hệ cha–con
Ba khái niệm nền tảng bạn phải nắm:
- Trace: là toàn bộ hành trình của một request đi qua hệ thống. Mỗi trace có một
trace_idduy nhất — hãy hình dung nó như mã vận đơn của một kiện hàng, đi tới đâu cũng mang theo mã đó. - Span: là một "đoạn công việc" đơn lẻ trong trace — ví dụ "Order Service xử lý logic đặt hàng", hay "một truy vấn SQL tới database". Mỗi span có thời điểm bắt đầu, thời điểm kết thúc (nên biết được nó tốn bao nhiêu mili-giây), tên, và các thuộc tính (attributes/tags) như
http.status_code,db.statement. - Quan hệ cha–con (parent-child): các span liên kết với nhau qua
span_idvàparent_span_id. Span "gọi Payment Service" là cha của span "Payment Service truy vấn thẻ tín dụng". Nhờ vậy, khi ghép lại, ta có một cây (tree) hoặc biểu đồ thác nước (waterfall) thể hiện cái gì gọi cái gì, và cái gì chạy song song, cái gì chạy tuần tự.
Context propagation — trái tim của tracing
Điều khó nhất trong distributed tracing không phải là đo thời gian, mà là làm sao service B biết được nó đang phục vụ cùng một request với service A. Câu trả lời là context propagation (lan truyền ngữ cảnh): khi service A gọi service B qua HTTP, nó nhét trace_id và parent_span_id vào HTTP header của request. Service B đọc header đó, biết mình là "con" của span nào, và tiếp tục nhét vào header khi gọi service C.
Chuẩn công nghiệp cho việc này ngày nay là W3C Trace Context — header tên traceparent, ví dụ:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Trong đó có version, trace_id (32 ký tự hex), parent span_id (16 ký tự hex), và trace flags. Nếu một service trong chuỗi "quên" chuyển tiếp header này, trace của bạn sẽ bị đứt — đây là lỗi kinh điển ta sẽ bàn ở phần sau.
Sampling — không phải trace nào cũng cần lưu
Nếu hệ thống của bạn xử lý 50.000 request mỗi giây, việc lưu trace cho tất cả sẽ tạo ra khối lượng dữ liệu khổng lồ và chi phí lưu trữ đắt đỏ. Vì vậy có cơ chế sampling (lấy mẫu):
- Head-based sampling: quyết định lấy mẫu ngay ở đầu request (ví dụ chỉ giữ 5% ngẫu nhiên). Đơn giản, rẻ, nhưng có thể bỏ lỡ đúng cái request lỗi bạn cần.
- Tail-based sampling: đợi trace hoàn tất rồi mới quyết định — ưu tiên giữ lại những trace có lỗi hoặc chậm bất thường. Thông minh hơn nhưng tốn tài nguyên hơn vì phải buffer.
OpenTelemetry — chuẩn chung để không bị khóa
Trước đây mỗi công cụ có SDK riêng, đổi công cụ là phải viết lại code. OpenTelemetry (OTel) là chuẩn mở, thống nhất API và SDK để bạn "instrument" code một lần, rồi xuất dữ liệu đi bất kỳ backend nào — X-Ray, Cloud Trace, Jaeger, Datadog... Đây là điểm mấu chốt về tính di động (portability) mà một kỹ sư cloud cần ghi nhớ.
Ba công cụ chủ đạo
- AWS X-Ray: dịch vụ tracing được quản lý (managed) của AWS, tích hợp sâu với Lambda, ECS, API Gateway. Có Service Map trực quan hóa toàn bộ kiến trúc như một bản đồ các node kết nối.
- Google Cloud Trace: tương đương trên GCP, tích hợp chặt với Cloud Run, GKE, App Engine. Điểm mạnh là phân tích latency (phân bố phần trăm p50/p95/p99) và tự động phát hiện thay đổi bất thường.
- Jaeger: dự án open-source (gốc từ Uber, nay thuộc CNCF), self-hosted. Bạn tự vận hành, toàn quyền kiểm soát dữ liệu, không phụ thuộc nhà cung cấp cloud nào — phù hợp cả on-premise lẫn multi-cloud.
Tình huống thực tế
Ví dụ 1 — Sàn TMĐT "ChợViệt" và 8 giây bí ẩn
Một startup TMĐT tại TP.HCM (tạm gọi ChợViệt) chạy 12 microservices trên EKS. Vào đợt sale 12/12, thời gian đặt hàng đội lên trung bình 8 giây, tỷ lệ bỏ giỏ hàng tăng 30%. Team ban đầu đổ lỗi cho Payment Service vì "thanh toán vốn hay chậm".
Sau khi bật AWS X-Ray và mở Service Map, họ nhìn vào waterfall của một trace điển hình. Bất ngờ: Payment Service chỉ tốn 400ms. Thủ phạm thật là Inventory Service gọi database kiểm tra tồn kho — nhưng nó gọi tuần tự cho từng sản phẩm trong giỏ, mỗi truy vấn 250ms. Giỏ 30 sản phẩm = 7,5 giây chỉ riêng bước này. Waterfall cho thấy rõ 30 thanh ngang xếp nối đuôi nhau.
Bài học: Trực giác về "service hay chậm" thường sai. Trace cho bạn bằng chứng thay vì phỏng đoán. Fix ở đây là gộp 30 truy vấn thành một câu IN (...) — thời gian tụt còn 300ms.
Ví dụ 2 — Fintech "MoMoPay giả định" và trace bị đứt đoạn
Một công ty ví điện tử triển khai Jaeger cho hệ thống trên GKE. Họ hào hứng bật lên nhưng phát hiện các trace luôn kết thúc đột ngột ở Notification Service — như thể service đó "biến mất". Log của Notification vẫn chạy bình thường, nhưng nó không bao giờ xuất hiện trong bất kỳ trace nào.
Điều tra ra: Notification Service được viết bằng Go và dùng một HTTP client tự chế không chuyển tiếp header traceparent. Khi service gọi tới đây, context bị mất, nên Jaeger coi mọi thứ phía sau là một trace mới hoàn toàn (mồ côi). Sau khi thay bằng client có instrument OpenTelemetry, header được truyền đúng, và họ mới phát hiện Notification đang gọi một API SMS bên thứ ba mất tới 3 giây khi nhà mạng nghẽn.
Bài học: Trace chỉ hoàn chỉnh khi mọi service trong chuỗi đều truyền context. Một mắt xích đứt làm hỏng cả sợi dây. Luôn ưu tiên dùng thư viện auto-instrumentation chuẩn thay vì tự chế HTTP client.
Ví dụ 3 — SaaS Đông Nam Á và chi phí tracing vượt ngân sách
Một công ty SaaS phục vụ khách ở Singapore và Việt Nam bật Cloud Trace với sampling 100% "cho chắc". Tháng đầu, hóa đơn ingestion trace lên tới hàng nghìn USD vì họ có hàng chục triệu request/ngày, và nhiều span rác từ health-check nội bộ.
Họ điều chỉnh: giảm head-based sampling xuống 5% cho traffic bình thường, nhưng cấu hình giữ 100% các trace có lỗi qua tail-based sampling, đồng thời loại các endpoint health-check khỏi tracing. Chi phí giảm hơn 80% mà vẫn không mất khả năng debug sự cố quan trọng.
Bài học: Tracing là công cụ mạnh nhưng không miễn phí. Sampling thông minh (giữ cái lỗi, bỏ cái bình thường) là kỹ năng vận hành bắt buộc khi lên quy mô lớn.
Hướng dẫn từng bước
Dưới đây là quy trình triển khai tracing cho một hệ thống mới, theo cách trung lập với công cụ (dùng OpenTelemetry làm nền):
- Chọn backend lưu trữ trace. Nếu bạn đang all-in trên AWS, chọn X-Ray. Trên GCP thì Cloud Trace. Nếu muốn tự chủ, multi-cloud, hoặc on-premise, chọn Jaeger. Nguyên tắc: instrument bằng OTel để sau này đổi backend không phải sửa code.
- Instrument code bằng OpenTelemetry SDK. Với đa số ngôn ngữ (Java, Python, Node.js, Go, .NET) đều có auto-instrumentation — chỉ cần thêm agent/thư viện là các framework HTTP, database client, gRPC tự động tạo span. Ví dụ Java chỉ cần chạy kèm
-javaagent:opentelemetry-javaagent.jar.
- Đảm bảo context propagation. Kiểm tra rằng mọi lời gọi liên service đều dùng client đã được instrument, để header
traceparentđược truyền đi. Đây là bước dễ sai nhất — hãy test bằng cách nhìn một trace mẫu có đủ tất cả service không.
- Triển khai OpenTelemetry Collector. Đây là thành phần trung gian nhận span từ ứng dụng, xử lý (lọc, thêm metadata, sampling), rồi export tới backend. Dùng Collector giúp bạn tách biệt ứng dụng khỏi backend cụ thể — đổi từ Jaeger sang X-Ray chỉ là đổi cấu hình exporter của Collector.
- Cấu hình sampling hợp lý. Bắt đầu với head-based ở tỷ lệ vừa phải (ví dụ 10%), thêm quy tắc giữ 100% trace lỗi. Đừng để 100% mọi thứ ở production quy mô lớn.
- Thêm span thủ công cho logic nghiệp vụ quan trọng. Auto-instrumentation bắt được HTTP/DB, nhưng những đoạn logic quan trọng (ví dụ "tính toán khuyến mãi") thì bạn nên tự tạo span để nhìn thấy chúng trong waterfall.
- Gắn thuộc tính (attributes) hữu ích. Thêm
user.id,order.id,regionvào span để sau này lọc và tìm đúng trace của một khách hàng cụ thể khi họ khiếu nại.
- Kết nối trace với log và metric. Đưa
trace_idvào mỗi dòng log. Khi thấy một trace chậm, bạn nhảy thẳng sang log của đúng request đó — đây là sức mạnh của "observability" ba trụ cột (trace, log, metric).
Lỗi thường gặp & mẹo
- Trace bị đứt vì không truyền context: Như ví dụ 2. Luôn dùng thư viện HTTP/gRPC đã instrument. Với công việc bất đồng bộ (message queue như SQS, Kafka), nhớ nhét trace context vào message attributes, vì header HTTP không tự đi qua queue.
- Bật sampling 100% ở production: Gây tốn chi phí và có thể làm nghẽn Collector. Mẹo: dùng tail-based sampling để giữ trọn vẹn các trace lỗi/chậm, còn lại lấy mẫu thấp.
- Quên instrument tầng database: Nhiều người chỉ instrument HTTP mà bỏ query SQL. Thế là thấy "Order Service tốn 2s" nhưng không biết vì câu SQL nào. Hãy bật instrumentation cho cả DB client.
- Cardinality quá cao trong attributes: Đừng nhét những giá trị vô hạn dạng như timestamp đầy đủ hay UUID vào tên span (chỉ nên để trong attributes). Tên span cardinality cao làm hệ thống phân tích chậm và tốn kém.
- Đồng hồ các máy lệch nhau (clock skew): Nếu các node không đồng bộ NTP, thời gian span có thể trông "âm" hoặc lệch. Đảm bảo NTP chạy chuẩn trên mọi node.
- Nhầm tracing với logging: Trace trả lời "request này đi qua đâu, chậm ở đâu"; log trả lời "chi tiết chuyện gì xảy ra bên trong". Chúng bổ sung cho nhau, không thay thế. Mẹo vàng: nối chúng bằng
trace_id.
- Mẹo tiết kiệm khi mới bắt đầu: Đừng cố instrument toàn bộ 50 service ngay. Bắt đầu từ đường đi (path) quan trọng nhất về doanh thu — ví dụ luồng thanh toán — rồi mở rộng dần.
Bài tập thực hành
- Vẽ waterfall bằng tay: Cho một request "Đặt hàng" đi qua API Gateway (50ms) → Order Service (tổng 900ms), trong đó Order gọi Inventory (300ms) rồi gọi Payment (500ms) tuần tự, còn 100ms là logic riêng. Hãy vẽ biểu đồ waterfall với quan hệ cha–con và chỉ ra: nếu cho Inventory và Payment chạy song song thì tiết kiệm được bao nhiêu thời gian?
- Đọc header traceparent: Với header
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01, hãy tách ra trace_id và parent span_id. Giải thích service nhận header này sẽ dùng chúng thế nào.
- Thiết kế sampling: Hệ thống của bạn có 20.000 request/giây, trong đó 0,5% bị lỗi. Ngân sách chỉ cho phép lưu 1.000 trace/giây. Hãy đề xuất một chiến lược sampling giữ được toàn bộ trace lỗi mà vẫn nằm trong ngân sách. Tính xem còn lại bao nhiêu "chỗ trống" cho trace bình thường.
- Chẩn đoán trace đứt (tình huống): Bạn thấy tất cả trace kết thúc ở Service C, không bao giờ có Service D dù D vẫn chạy. Liệt kê 3 nguyên nhân khả dĩ và cách kiểm tra từng cái.
- So sánh công cụ: Lập bảng so sánh X-Ray, Cloud Trace, Jaeger theo các tiêu chí: managed hay self-hosted, khóa nhà cung cấp (vendor lock-in), phù hợp môi trường nào. Bạn sẽ chọn công cụ nào cho một công ty Việt Nam chạy hybrid (một phần on-premise, một phần AWS)? Vì sao?
Tóm tắt
Distributed tracing là "đèn pha" soi sáng hành trình của từng request qua rừng microservices. Những điểm cốt lõi cần nhớ:
- Một request người dùng có thể tương ứng 10–100 hop bên dưới; khi chậm, chỉ trace mới cho bạn biết hop nào là thủ phạm, thay vì phỏng đoán theo cảm tính.
- Trace gồm nhiều span liên kết cha–con; vẽ lên trục thời gian thành waterfall để nhìn ra điểm nghẽn.
- Context propagation qua header
traceparent(chuẩn W3C) là trái tim của tracing — một service quên truyền là trace đứt. - OpenTelemetry giúp bạn instrument một lần, xuất đi bất kỳ backend nào, tránh khóa nhà cung cấp.
- Ba lựa chọn backend: X-Ray (AWS), Cloud Trace (GCP), Jaeger (open-source, self-hosted).
- Sampling thông minh — giữ trace lỗi/chậm, bỏ trace bình thường — là kỹ năng bắt buộc để kiểm soát chi phí khi lên quy mô.