Cơn ác mộng debug microservices lúc nửa đêm
Khoảng 6 tháng trước, team mình nhận một loạt vé báo lỗi khẩn cấp từ khách hàng. Thao tác bấm nút đặt hàng chập chờn, lúc mất 2 giây, lúc vọt lên tận 14 giây rồi ngắt kết nối với mã lỗi 504 Gateway Timeout. Cụm backend khi đó gồm 4 service Python (FastAPI và Flask) trao đổi qua REST API và Redis queue. Mỗi service vẫn ghi log dạng text truyền thống ra file riêng.
Mở terminal SSH vào từng cụm máy chủ, mình đối mặt với hơn 100.000 dòng log cuộn liên tục mỗi phút. Mỗi request của user nhảy qua 3-4 service. Không hề có một mã định danh nào xuyên suốt hành trình đó. Bạn không thể biết service thanh toán chậm vì nghẽn mạng hay do database Postgres đang lock bảng. Cách debug bằng grep log cổ điển lúc này hoàn toàn bất lực.
Để cứu vãn tình hình, team quyết định đưa OpenTelemetry (OTel) vào stack Python. Sau nửa năm chạy thực tế, thời gian khoanh vùng nguyên nhân gốc rễ (Root Cause Analysis) giảm từ hơn 45 phút xuống dưới 2 phút. Cả team nhìn rõ từng mili-giây của request ngay trên giao diện web.
Hiểu đúng về OpenTelemetry: Ba trụ cột Observability
Về bản chất, OpenTelemetry là chuẩn mã nguồn mở do CNCF quản lý, ra đời từ việc sáp nhập hai dự án OpenTracing và OpenCensus. OTel không phải là công cụ lưu trữ hay vẽ biểu đồ. Nhiệm vụ duy nhất của nó là chuẩn hóa việc sinh ra, thu thập và gửi dữ liệu giám sát ra ngoài.
Nhờ tính chuẩn hóa này, bạn không sợ bị vendor lock-in. Hôm nay bạn đẩy dữ liệu vào Jaeger, ngày mai muốn đổi sang Datadog, Grafana Tempo hay New Relic thì chỉ cần sửa đúng một dòng biến môi trường.
Dữ liệu giám sát của OTel xoay quanh ba thành phần:
- Traces: Bản đồ hành trình của một request. Mỗi trạm dừng bên trong được gọi là một Span (chẳng hạn như một câu query SQL, một lần gọi API ngoài). Mỗi span chứa thời gian chạy, status code và metadata tùy biến.
- Metrics: Các con số đo lường theo chu kỳ như RAM sử dụng, số lượng request/giây (RPS) hay tỷ lệ lỗi HTTP 5xx.
- Logs: Nhật ký sự kiện có timestamp, nhưng được gắn chặt với ngữ cảnh của trace.
Sức mạnh lớn nhất nằm ở tính liên kết: khi thấy một Span bị nghẽn 800ms, bạn có thể click mở ngay đoạn Log chính xác phát sinh trong 800ms đó nhờ trace_id đồng bộ.
Thực hành: Triển khai OpenTelemetry trong dự án Python
Trong Python, OTel hỗ trợ hai phương pháp: Auto-instrumentation (gắn tự động, không đụng vào code) và Manual instrumentation (tự viết code để đo logic nội bộ). Chúng ta sẽ đi qua cả hai.
1. Cài đặt các gói thư viện cần thiết
Tạo virtualenv sạch và cài bộ SDK cùng exporter theo chuẩn OTLP (OpenTelemetry Protocol):
python3 -m venv venv
source venv/bin/activate
# Cài đặt OTel SDK và công cụ tự động
pip install opentelemetry-api opentelemetry-sdk
pip install opentelemetry-distro
pip install opentelemetry-exporter-otlp
# Tự phát hiện các package hiện có (FastAPI, requests, SQLAlchemy...) để cài plugin tương thích
opentelemetry-bootstrap -a install
2. Khởi chạy Jaeger All-in-One bằng Docker
Để có ngay giao diện trực quan hóa trace tại local, hãy dựng một container Jaeger hỗ trợ sẵn endpoint OTLP:
docker run -d --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest
Hai cổng cần nhớ: 4317 chạy giao thức gRPC (nhanh và nhẹ hơn) và 4318 chạy HTTP/Protobuf.
3. Phương pháp 1: Auto-instrumentation (Zero-code change)
Giả sử bạn có file app.py chạy FastAPI xử lý đơn hàng đơn giản:
import time
import requests
from fastapi import FastAPI
app = FastAPI(title="Order Service")
@app.get("/orders/{order_id}")
def get_order(order_id: int):
time.sleep(0.05) # Giả lập xử lý nội bộ
# Gọi tiếp sang service khác hoặc bên thứ ba
resp = requests.get("https://httpbin.org/delay/0.2")
return {"order_id": order_id, "status": "success", "upstream_code": resp.status_code}
Thay vì sửa code, bạn wrap lệnh khởi chạy Uvicorn bằng CLI của OpenTelemetry:
export OTEL_SERVICE_NAME="order-service"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
export OTEL_EXPORTER_OTLP_INSECURE="true"
opentelemetry-instrument \
--traces_exporter otlp \
--metrics_exporter otlp \
--logs_exporter otlp \
uvicorn app:app --port 8000
Bắn thử vài request: curl http://localhost:8000/orders/402. Mở trình duyệt vào http://localhost:16686, bạn sẽ thấy ngay sơ đồ dạng thác nước (waterfall). Biểu đồ bóc tách rõ: 50ms tiêu tốn ở FastAPI và 210ms nằm ở request HTTP outbound.
4. Phương pháp 2: Manual instrumentation để đo logic nghiệp vụ
Auto-instrumentation rất tốt ở tầng biên (HTTP, DB query). Tuy nhiên, nếu bạn muốn giám sát sâu vào logic thanh toán, tính voucher hay thuật toán chống gian lận, bạn cần tạo Span thủ công.
Đoạn mã dưới đây khởi tạo TracerProvider với BatchSpanProcessor để không làm chậm luồng xử lý chính:
import time
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# Khởi tạo định danh service
resource = Resource(attributes={
SERVICE_NAME: "payment-service",
"deployment.environment": "production"
})
provider = TracerProvider(resource=resource)
# Dùng BatchSpanProcessor để gửi dữ liệu bất đồng bộ theo lô, giảm overhead dưới 1.5% CPU
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("payment.checker")
def process_payment(customer_id: str, amount: float):
with tracer.start_as_current_span("process_payment_flow") as parent_span:
parent_span.set_attribute("customer.id", customer_id)
parent_span.set_attribute("payment.amount", amount)
# Đo đạc đoạn validate input
with tracer.start_as_current_span("validate_account") as val_span:
if not customer_id.startswith("CUST_"):
val_span.record_exception(ValueError("Invalid customer ID"))
val_span.set_status(trace.StatusCode.ERROR, "Mã khách hàng sai format")
return False
val_span.set_attribute("validation.passed", True)
# Đo đạc đoạn trừ số dư ví
with tracer.start_as_current_span("deduct_balance"):
time.sleep(0.08) # Giả lập query DB
return True
if __name__ == "__main__":
process_payment("CUST_9912", 350000.0)
Trong quá trình xử lý logic validate dữ liệu đầu vào (ví dụ kiểm tra định dạng customer ID hay token), bạn sẽ thường xuyên dùng Regex. Để test nhanh các biểu thức chính quy phức tạp trước khi nhúng vào code, bạn có thể tham khảo công cụ regex tester tại toolcraft.app/vi/tools/developer/regex-tester. Giao diện trực quan trên browser giúp bạn bắt lỗi chuỗi ngay lập tức thay vì phải in log thủ công.
5. Tự động bơm Trace ID vào Log
Ghi log mà thiếu trace context thì giá trị giảm một nửa. Để mỗi dòng log phát ra đều chứa trace_id và span_id, bạn chỉ cần một dòng hook từ thư viện chuẩn:
import logging
from opentelemetry.instrumentation.logging import LoggingInstrumentor
# Tự động chèn trace_id, span_id vào định dạng log
LoggingInstrumentor().instrument(set_logging_format=True)
logger = logging.getLogger("checkout")
logging.basicConfig(level=logging.INFO)
with tracer.start_as_current_span("create_invoice"):
logger.info("Hóa đơn đang được lập")
# Output thực tế: [trace_id=4bf92f3577b34da6a3ce929d0e0e4736 span_id=00f067aa0ba902b7] Hóa đơn đang được lập
Khi đẩy log về Grafana Loki hoặc OpenSearch, kỹ sư trực ca chỉ cần copy chuỗi Trace ID dán vào Jaeger là thấy toàn bộ ngữ cảnh lỗi. Không còn cảnh đoán mò nguyên nhân.
Kinh nghiệm tối ưu khi chạy Production
Áp dụng OTel vào thực tế cần lưu ý hai điểm mấu chốt:
Thứ nhất, luôn dùng BatchSpanProcessor thay vì SimpleSpanProcessor. Bộ xử lý batch gom các span và gửi đi trong background thread, giúp độ trễ của ứng dụng gần như không bị ảnh hưởng (overhead thường dưới 2ms mỗi request).
Thứ hai, hãy thiết lập tỷ lệ lấy mẫu (Sampling Rate). Khi hệ thống đạt mốc vài nghìn request/giây, việc ghi nhận 100% trace sẽ gây tốn băng thông và dung lượng lưu trữ. Hãy cấu hình biến OTEL_TRACES_SAMPLER=parentbased_traceidratio và OTEL_TRACES_SAMPLER_ARG=0.1 để chỉ giữ lại 10% trace bình thường nhưng vẫn bắt trọn 100% trace chứa lỗi.
Lộ trình thực tế nhất cho bạn: bắt đầu ngay với opentelemetry-instrument để có cái nhìn tổng quan về network và database. Khi đã quen, hãy gắn thêm span thủ công cho những hàm xử lý nghiệp vụ trọng yếu.

