Distributed Tracing với Jaeger và Docker: Hướng dẫn Thực tế cho Microservices

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Khởi đầu nhanh: Chạy Jaeger trong vòng 5 phút

Sau ba tiếng vật lộn với sự cố production, cuộn qua hàng triệu dòng log, bạn sẽ nhận ra một bài học đắt giá: log cho thấy triệu chứng, nhưng trace mới chỉ ra nguyên nhân. Khi một cú click của người dùng kích hoạt năm lần gọi API và hai truy vấn cơ sở dữ liệu, bạn cần một bản đồ, chứ không phải một danh sách. Hãy cùng xây dựng bản đồ đó bằng Jaeger và Docker trong chưa đầy năm phút.

Chúng ta sẽ sử dụng file docker-compose.yml để khởi tạo image ‘all-in-one’ của Jaeger. Phiên bản này giống như một chiếc ‘dao đa năng’ cho các nhà phát triển, vì nó tích hợp sẵn UI, collector và query engine vào một container nhẹ duy nhất.

version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686" # Giao diện Jaeger UI
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
    environment:
      - COLLECTOR_OTLP_ENABLED=true

  service-a:
    image: node:18
    working_dir: /app
    volumes:
      - ./service-a:/app
    command: npm start
    environment:
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
    depends_on:
      - jaeger

Chạy lệnh docker-compose up -d và truy cập vào http://localhost:16686. Theo kinh nghiệm của tôi, thiết lập này là bước ngoặt cho hầu hết các đội ngũ. Nó biến việc debug từ trò chơi đoán mò thành một môn khoa học trực quan.

Đi sâu tìm hiểu: Cấu trúc của một Trace

Để làm chủ Jaeger, bạn cần hiểu hai khái niệm: Span và Trace. Hãy coi **Trace** là toàn bộ câu chuyện của một request. Còn **Span** là một chương đơn lẻ—chẳng hạn như một truy vấn database cụ thể hoặc một lệnh gọi API mất 200ms tới một dịch vụ hạ nguồn (downstream).

Logic của việc Quan sát

Lan truyền ngữ cảnh (Context propagation) chính là “bí thuật”. Khi Service A gọi Service B, nó phải chuyển đi một “Trace ID”. Jaeger không dùng phép thuật để liên kết các dịch vụ của bạn. Thay vào đó, code của bạn sẽ chèn ID này vào HTTP header, thường là theo tiêu chuẩn W3C traceparent.

Hãy quên các SDK Jaeger cũ đi; OpenTelemetry (OTel) hiện là tiêu chuẩn của ngành. Jaeger hỗ trợ nó nguyên bản, giúp hệ thống của bạn không bị lạc hậu trong tương lai. Đây là cách bạn cấu hình một dịch vụ Node.js để kết nối với instance Jaeger chạy trong Docker:

// instrumentation.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
  }),
  serviceName: 'order-service',
});

sdk.start();

Image Docker all-in-one rất tuyệt vì nó giúp bạn bỏ qua sự phức tạp khi cài đặt Cassandra hay Elasticsearch. Tuy nhiên, hãy nhớ: nó lưu trữ mọi thứ trong RAM. Nếu bạn khởi động lại container đó, dữ liệu của bạn sẽ biến mất. Hãy dùng nó cho máy cá nhân, nhưng tuyệt đối đừng mang vào môi trường production.

Sử dụng Nâng cao: Xử lý Traffic trên Production

Quy mô thay đổi mọi thứ. Việc truy vết tạo ra một lượng dữ liệu khổng lồ. Nếu hệ thống của bạn xử lý 1.000 request mỗi giây và mỗi request tạo ra 10 span, bạn sẽ có 10.000 span mỗi giây. Điều đó sẽ làm sập một hệ thống cơ bản chỉ trong vài phút.

Sampling thông minh

Bạn không cần phải ghi lại mọi request đơn lẻ. Trong môi trường production, chúng ta sử dụng **Probabilistic Sampling** (Lấy mẫu xác suất). Việc ghi lại chỉ 1% traffic thường là đủ để phát hiện các xu hướng hiệu năng mà không làm ảnh hưởng quá nhiều đến chi phí lưu trữ.

# Thiết lập lấy mẫu 1% qua biến môi trường
OTEL_TRACES_SAMPLER=parentbased_always_off
OTEL_TRACES_SAMPLER_ARG=0.01

Chuyển sang Lưu trữ Vĩnh viễn

Để triển khai thực thụ, bạn phải tách riêng dịch vụ Jaeger Collector và Query. Collector thu thập dữ liệu từ các ứng dụng và ghi vào Elasticsearch. Sau đó, dịch vụ Query sẽ lấy dữ liệu đó ra cho giao diện UI. Sự phân tách này cho phép bạn mở rộng các thành phần độc lập khi traffic tăng trưởng.

# Cấu hình lưu trữ sẵn sàng cho production
jaeger-collector:
  image: jaegertracing/jaeger-collector
  environment:
    - SPAN_STORAGE_TYPE=elasticsearch
    - ES_SERVER_URLS=http://elasticsearch:9200

jaeger-query:
  image: jaegertracing/jaeger-query
  environment:
    - SPAN_STORAGE_TYPE=elasticsearch
    - ES_SERVER_URLS=http://elasticsearch:9200

Bài học Thực tế từ “Chiến trường”

Tôi đã quản lý hàng trăm microservices. Dưới đây là những điều tôi ước mình biết sớm hơn trước khi bắt đầu with distributed tracing.

1. Custom Tag là “cứu cánh”

Các trace tiêu chuẩn thì tốt, nhưng các tag nghiệp vụ (business tag) còn tốt hơn. Hãy luôn gắn các ID như customer_id hoặc order_id vào các span của bạn. Khi một khách hàng VIP phàn nàn về việc thanh toán chậm lúc 2 giờ sáng, bạn có thể tìm thấy trace chính xác của họ chỉ trong vài giây.

const span = tracer.startSpan('process-payment');
span.setAttribute('order.id', '12345');
span.setAttribute('payment.provider', 'stripe');
span.end();

2. Lưu ý về “Thuế hiệu năng”

Truy vết không miễn phí, nhưng nó khá rẻ nếu bạn biết cách. Sử dụng gRPC (port 4317) hiệu quả hơn đáng kể so với HTTP. Trong các bài benchmark của chúng tôi, việc tích hợp OTel thường chỉ chiếm chưa đến 2% tài nguyên CPU. Chỉ cần đảm bảo các exporter của bạn là non-blocking để chúng không làm treo ứng dụng nếu Jaeger gặp sự cố.

3. Tin tưởng vào Đồ thị Phụ thuộc

Tab ‘Dependencies’ trong giao diện Jaeger là một viên ngọc quý bị ẩn giấu. Nó xây dựng một bản đồ kiến trúc thực tế dựa trên traffic thật. Điều này thường chính xác hơn nhiều so với bất kỳ file README hay sơ đồ kiến trúc nào mà team của bạn đã không cập nhật suốt sáu tháng qua.

4. Săn lùng các “Ghost” Span

Nếu Service A gọi Service B nhưng B không bao giờ xuất hiện trong trace, hãy kiểm tra middleware của bạn. Khả năng cao là một proxy hoặc load balancer đã loại bỏ các header. Hãy luôn xác minh rằng trace context còn tồn tại qua mỗi bước nhảy (hop), đặc biệt là khi đi từ gateway công khai vào mạng nội bộ.

Distributed tracing không còn là một thứ xa xỉ nữa. Nó là xương sống của hệ thống quan sát (observability) hiện đại. Hãy bắt đầu với Docker để làm quen, sau đó đưa nó sâu vào quy trình làm việc của bạn để tiêu diệt tận gốc những con bug “bất khả thi”.

Share: