Sự Cố Khiến Tôi Xây Dựng Hệ Thống Này
Khoảng một năm trước, dịch vụ checkout của chúng tôi bắt đầu bị lỗi không liên tục trên môi trường production. Thời gian phản hồi tăng đột biến từ 200ms lên 8 giây — nhưng chỉ xảy ra với khoảng 3% số request. Log hiển thị lỗi, nhưng chúng nằm rải rác trên sáu dịch vụ khác nhau: API gateway, auth service, product catalog, cart, inventory và payment processor.
Chúng tôi mất bốn tiếng đồng hồ để đối chiếu các dòng log theo timestamp, cố gắng ghép lại những gì đã xảy ra. Cuối cùng chúng tôi tìm ra nguyên nhân — database connection pool bị cạn kiệt trong inventory service và lan rộng ngược lên upstream. Việc sửa chỉ mất 5 phút. Nhưng chẩn đoán mất nửa ngày làm việc.
Đó là lúc tôi bắt đầu nhìn nhận distributed tracing một cách nghiêm túc. Không phải là tính năng tùy chọn, mà là yêu cầu bắt buộc với bất kỳ hệ thống nào có hơn hai dịch vụ giao tiếp với nhau.
Tại Sao Chỉ Dùng Log Không Đủ Với Microservices
Vấn đề cốt lõi với log trong kiến trúc microservice là chúng bị cô lập. Mỗi dịch vụ ghi log riêng với context riêng của nó. Khi một request từ người dùng đi qua bảy dịch vụ, bạn có bảy luồng log riêng biệt mà không có cách nào gắn kết chúng lại một cách tự nhiên.
Bạn phải vắt óc suy nghĩ: đối chiếu timestamp, sao chép request ID qua các cửa sổ terminal, hy vọng rằng ai đó đã thực sự truyền trace ID header. Hầu hết thời gian, họ không làm vậy.
Distributed tracing giải quyết điều này bằng cách gán cho mỗi request một trace ID duy nhất theo dõi nó qua từng dịch vụ. Mỗi dịch vụ thêm vào một span — đơn vị công việc có thời gian — và tất cả kết nối thành một cây duy nhất cho thấy chính xác những gì đã xảy ra, theo thứ tự nào, và mỗi bước mất bao lâu. Đây là tầng visibility mà log đơn thuần không thể cung cấp.
So Sánh Các Lựa Chọn: Jaeger, Zipkin và Grafana Tempo
Trước khi chọn Grafana Tempo, tôi đã đánh giá các backend phổ biến để hiểu rõ sự đánh đổi.
Jaeger và Zipkin
Cả hai đều là các dự án trưởng thành, được tài liệu hóa tốt. Jaeger là dự án tốt nghiệp CNCF với sự hỗ trợ hệ sinh thái mạnh mẽ. Vấn đề là lưu trữ: theo mặc định chúng dùng Elasticsearch hoặc Cassandra làm backend, nghĩa là chi phí vận hành đáng kể. Với các dịch vụ lưu lượng cao, bạn sẽ thấy mình phải quản lý một cụm database nặng nề chỉ để lưu trữ dữ liệu trace mà bạn chỉ xem khi có sự cố.
Zipkin đơn giản hơn nhưng ít được phát triển tích cực và có hệ sinh thái nhỏ hơn. Với team chưa chạy Elasticsearch, chi phí vận hành của Jaeger có thể lớn hơn lợi ích mà nó mang lại.
Grafana Tempo
Tempo áp dụng cách tiếp cận hoàn toàn khác: nó lưu trace dưới dạng file phẳng trong object storage — S3, GCS, Azure Blob, hoặc ổ đĩa cục bộ — thay vì duy trì search index. Điều này giúp nó rẻ hơn đáng kể. Object storage thường có chi phí thấp hơn block storage khoảng 20 lần cho cùng dung lượng dữ liệu.
Điểm đánh đổi cần hiểu rõ: Tempo không hỗ trợ tìm kiếm trace theo tên dịch vụ hay các thuộc tính tùy ý một mình. Nó dựa vào việc tương quan trace ID tìm thấy trong log (qua Loki) hoặc metrics (qua Prometheus). Nếu bạn đã ở trong hệ sinh thái Grafana, điều này phù hợp tự nhiên. Nếu không, hãy tính đến công việc tích hợp.
Với hầu hết các team xây dựng trên Grafana stack, Tempo là lựa chọn rõ ràng để kiểm soát chi phí khi mở rộng quy mô.
Vấn Đề Chi Phí Thực Sự: Bạn Không Thể Lưu Mọi Thứ
Đây là điều không được thảo luận đủ khi các team mới thiết lập tracing: một dịch vụ bận rộn có thể tạo ra hàng triệu span mỗi phút. Ở mức độ đó, việc lưu từng trace đơn lẻ trở nên tốn kém rất nhanh, ngay cả với object storage rẻ tiền.
Giải pháp là sampling — chỉ ghi lại một phần nhỏ trace của bạn. Nhưng có hai chiến lược khác nhau về cơ bản, và chọn sai sẽ phá hỏng toàn bộ mục đích.
Head-Based Sampling
Với head-based sampling, quyết định ghi lại trace xảy ra ngay khi bắt đầu request, trước khi bất kỳ dữ liệu nào được thu thập. Cấu hình phổ biến là “lấy mẫu ngẫu nhiên 10% request.” Cách này đơn giản và không có overhead.
Vấn đề: bạn đang lấy mẫu một cách mù quáng. Cái request chậm mà bạn thực sự cần debug? Có 90% khả năng bạn không bắt được nó. Head-based sampling dễ triển khai nhưng rất kém trong việc giữ lại những trace thực sự quan trọng.
Tail-Based Sampling
Tail-based sampling đưa ra quyết định sau khi request hoàn thành. OpenTelemetry Collector buffer tất cả span, chờ trace đầy đủ, rồi quyết định giữ lại gì dựa trên kết quả: giữ tất cả lỗi, giữ tất cả request chậm hơn 2 giây, giữ 5% mọi thứ còn lại.
Tôi đã áp dụng cách tiếp cận này trong production và kết quả luôn ổn định — chi phí lưu trữ giảm đáng kể trong khi chúng tôi giữ lại 100% trace thực sự quan trọng cho việc debug. 5% ngẫu nhiên giữ đủ dữ liệu để theo dõi xu hướng hiệu suất mà không làm chi phí tăng vọt.
Thiết Lập Grafana Tempo với OpenTelemetry
Đây là cấu hình hoạt động sử dụng Docker Compose. Cách này phản ánh cách bạn triển khai lên VPS hoặc chuyển đổi cho Kubernetes.
Bước 1: Docker Compose cho Stack
Tạo file docker-compose.yml:
version: '3.8'
services:
tempo:
image: grafana/tempo:latest
command: [ "-config.file=/etc/tempo.yaml" ]
volumes:
- ./tempo.yaml:/etc/tempo.yaml
- tempo-data:/var/tempo
ports:
- "3200:3200"
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
command: [ "--config=/etc/otel-collector.yaml" ]
volumes:
- ./otel-collector.yaml:/etc/otel-collector.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
depends_on:
- tempo
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
volumes:
tempo-data:
Bước 2: Cấu Hình Tempo
Tạo file tempo.yaml:
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
storage:
trace:
backend: local
local:
path: /var/tempo/blocks
wal:
path: /var/tempo/wal
Trong môi trường production, thay backend: local bằng backend: s3 và thêm cấu hình S3 bucket của bạn. Ổ đĩa cục bộ hoạt động tốt cho môi trường phát triển và lưu lượng thấp.
Bước 3: OpenTelemetry Collector với Tail Sampling
Đây là nơi kiểm soát chi phí thực sự xảy ra. Tạo file otel-collector.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
tail_sampling:
decision_wait: 10s # Chờ 10 giây để trace hoàn tất trước khi quyết định
num_traces: 100000 # Buffer tối đa 100k trace đang chờ xử lý
policies:
- name: errors-policy
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-traces-policy
type: latency
latency: {threshold_ms: 2000}
- name: probabilistic-policy
type: probabilistic
probabilistic: {sampling_percentage: 5}
exporters:
otlp:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling]
exporters: [otlp]
Cấu hình này giữ 100% trace lỗi, 100% request chậm hơn 2 giây, và 5% mọi thứ còn lại. Điều chỉnh tỷ lệ phần trăm dựa trên lưu lượng và ngân sách lưu trữ — với lưu lượng rất cao (10k+ req/phút), ngay cả 1% trace bình thường cũng cho bạn đủ dữ liệu.
Bước 4: Instrumentation cho Ứng Dụng Python
Cài đặt các gói OpenTelemetry:
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
Khởi tạo tracing trong startup của ứng dụng:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
# Thêm span tùy chỉnh ở những chỗ cần theo dõi
def process_order(order_id: str):
with tracer.start_as_current_span("process-order") as span:
span.set_attribute("order.id", order_id)
result = run_inventory_check(order_id)
span.set_attribute("inventory.available", result)
return result
Trace ID được truyền tự động giữa các dịch vụ qua HTTP header (định dạng W3C TraceContext) khi bạn dùng OpenTelemetry HTTP instrumentation library. Riêng với FastAPI, thêm FastAPIInstrumentor().instrument_app(app) sau khi khởi tạo app và bạn sẽ có span tự động cho mọi endpoint mà không cần viết code thủ công.
Bước 5: Grafana Data Source
Tạo file grafana-datasources.yaml:
apiVersion: 1
datasources:
- name: Tempo
type: tempo
url: http://tempo:3200
isDefault: true
Kiểm Tra Cài Đặt
Khởi động stack với docker-compose up -d, gửi request qua dịch vụ đã được instrumented, rồi mở Grafana tại http://localhost:3000. Điều hướng đến Explore → chọn Tempo data source → duyệt trace gần đây hoặc tìm kiếm theo trace ID.
Bạn sẽ thấy giao diện waterfall hiển thị từng span xuyên suốt các dịch vụ, với thông tin thời gian đặt cạnh nhau. Nếu một request bị chậm, bạn sẽ ngay lập tức thấy dịch vụ nào là nút thắt cổ chai và nó đang xử lý dữ liệu gì.
Một điều cần hiệu chỉnh: giá trị decision_wait trong tail sampler cần dài hơn trace chậm nhất có thể của bạn. Nếu một trace mất 15 giây end-to-end nhưng cửa sổ quyết định là 10 giây, sampler sẽ đưa ra quyết định không đầy đủ và có thể bỏ mất span quan trọng. Với hầu hết HTTP API, 10–30 giây là cửa sổ an toàn. Với batch processing job, bạn có thể cần lâu hơn.
Điều Gì Cần Mong Đợi Khi Triển Khai Production
Sau khi triển khai cấu hình này trên ứng dụng Python bốn dịch vụ xử lý khoảng 500 request mỗi phút, lưu trữ vẫn dưới 2GB mỗi ngày. Không có sampling, cùng tải công việc đó sẽ tạo ra khoảng 40GB dữ liệu trace mỗi ngày. Chính sách probabilistic 5% với request bình thường cho chúng tôi đủ dữ liệu để theo dõi xu hướng hiệu suất và lập kế hoạch năng lực, trong khi các chính sách lỗi và latency đảm bảo chúng tôi không bao giờ bỏ lỡ sự cố đáng điều tra.
Sự thay đổi trong quy trình làm việc rất đáng kể. Thay vì đào bới log trong lúc xảy ra sự cố, kỹ sư mở Grafana, lấy trace ID từ báo cáo khách hàng hoặc cảnh báo, và trong vòng 30 giây họ biết chính xác sự chậm trễ xảy ra ở đâu và dữ liệu nào liên quan ở mỗi bước.
Bắt đầu với một dịch vụ, làm quen với việc đọc waterfall view, rồi mở rộng instrumentation từng dịch vụ một. Instrument database call và external HTTP client trước — đó là nơi những bất ngờ về latency hầu như luôn ẩn náu.

