OpenTelemetry: agent, Collector, sampling, propagation

4 câu hỏi phỏng vấn~30 phútTừ khóa: OpenTelemetry, OTLP, Java agent, SDK, auto-instrumentation, Collector, receiver, processor, exporter, DaemonSet, gateway, Tempo, Jaeger, head sampling, tail sampling, parentbased, W3C traceparent, tracestate, baggage, resource attributes

Tiếp đi: OpenTelemetry. Mình đã có Micrometer Tracing gửi span sang Zipkin rồi, giờ team muốn chuyển sang OTel và Tempo. Nên làm thế nào?

Phần traceId/spanId và cách Spring Boot sinh span đã ở Distributed tracing. Bài này nói về đường ống phía sau: app gửi tín hiệu theo chuẩn nào, đi qua đâu, giữ lại trace nào, và làm sao không bị gắn chặt vào một vendor.

1. Bài toán

shop-app hiện tại:

order-service   ──Zipkin format──► Zipkin       (100% sampling)
payment-service ──Zipkin format──► Zipkin
inventory       ──chưa có tracing──
Kafka consumer (gửi email xác nhận) ──không nối được vào trace──

Hóa đơn lưu trace tăng mạnh mùa sale. Muốn đổi sang Grafana Tempo, sau này có thể sang vendor khác. Inventory là service cũ, team không muốn sửa code.

2. Vấn đề

3. Giải pháp: OTel = chuẩn + SDK + Collector

Thành phầnVai trò
API/SDKTạo span, metric, log trong app. Spring Boot 3 dùng qua Micrometer bridge micrometer-tracing-bridge-otel
Java agent-javaagent:opentelemetry-javaagent.jar: tự instrument hơn 100 thư viện (Spring MVC, JDBC, Kafka, RestClient) không cần sửa code
OTLPGiao thức chuẩn (gRPC 4317 / HTTP 4318) cho trace, metric, log
CollectorProcess riêng: receivers → processors → exporters. Nhận, lọc, lấy mẫu, gắn thêm metadata, gửi đi nhiều backend
BackendTempo, Jaeger, vendor (Datadog, New Relic...). App không cần biết là cái nào

4. Code từng bước

Bước 1: order/payment (Spring Boot 3, Micrometer + OTLP exporter)

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
management:
  tracing:
    sampling:
      probability: 1.0          # app gửi hết, Collector quyết định giữ cái nào (tail)
  otlp:
    tracing:
      endpoint: http://${OTEL_COLLECTOR_HOST:otel-collector.monitoring}:4318/v1/traces

Bước 2: inventory (service cũ, không sửa code) dùng Java agent

# Deployment inventory-service
containers:
- name: inventory
  image: shop/inventory:1.8
  env:
  - name: JAVA_TOOL_OPTIONS
    value: "-javaagent:/otel/opentelemetry-javaagent.jar"
  - name: OTEL_SERVICE_NAME
    value: inventory-service
  - name: OTEL_EXPORTER_OTLP_ENDPOINT
    value: http://otel-collector.monitoring:4318
  - name: OTEL_RESOURCE_ATTRIBUTES
    value: deployment.environment=prod,service.version=1.8
  - name: OTEL_TRACES_SAMPLER
    value: parentbased_always_on    # theo quyết định của service gọi tới
  volumeMounts: [ { name: otel-agent, mountPath: /otel } ]
# initContainer copy agent jar vào volume, hoặc dùng OTel Operator:
#   annotation instrumentation.opentelemetry.io/inject-java: "true"

Bước 3: Collector pipeline với tail sampling

# otel-collector.yaml
receivers:
  otlp:
    protocols: { grpc: { endpoint: 0.0.0.0:4317 }, http: { endpoint: 0.0.0.0:4318 } }
processors:
  memory_limiter: { check_interval: 1s, limit_percentage: 80 }
  k8sattributes: {}                 # gắn k8s.pod.name, namespace, deployment
  tail_sampling:
    decision_wait: 10s              # chờ đủ span của trace rồi mới quyết định
    policies:
    - { name: errors,  type: status_code, status_code: { status_codes: [ERROR] } }
    - { name: slow,    type: latency,     latency: { threshold_ms: 800 } }
    - { name: checkout-baseline, type: probabilistic, probabilistic: { sampling_percentage: 5 } }
  batch: {}
exporters:
  otlp/tempo: { endpoint: tempo.monitoring:4317, tls: { insecure: true } }
service:
  pipelines:
    traces:
      receivers:  [otlp]
      processors: [memory_limiter, k8sattributes, tail_sampling, batch]
      exporters:  [otlp/tempo]

Bước 4: Span tự viết, Kafka, và kiểm tra

@Service
@RequiredArgsConstructor
public class ReservationService {
    private final ObservationRegistry registry;

    public void reserve(String orderId, List<Item> items) {
        Observation.createNotStarted("inventory.reserve", registry)
            .lowCardinalityKeyValue("warehouse", "HN")     // vào metric + span
            .highCardinalityKeyValue("order.id", orderId)  // chỉ vào span
            .observe(() -> doReserve(items));
    }
}
# Kafka: bật observation để traceparent đi theo header của message
spring.kafka.template.observation-enabled: true
spring.kafka.listener.observation-enabled: true

# Thử: gửi request kèm traceparent tự tạo, rồi tìm trong Tempo
curl -X POST localhost:8080/orders \
  -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
  -H 'Content-Type: application/json' -d '{"orderId":"1234","amount":99.0,"items":[]}'
# Grafana → Explore → Tempo → TraceQL:
{ resource.service.name = "payment-service" && status = error && duration > 800ms }

5. Cách hoạt động

W3C traceparent:
  00 - 4bf92f3577b34da6a3ce929d0e0e4736 - 00f067aa0ba902b7 - 01
  ver   trace-id (16 byte)                 parent span-id     flags (01 = sampled)
tracestate: vendor riêng.  baggage: key=value nghiệp vụ (vd tenant=vn) đi theo mọi hop.

order ──HTTP traceparent──► payment ──HTTP──► bank-gateway
  │
  └─Kafka header traceparent──► email-consumer (10 phút sau, cùng traceId)

Triển khai Collector 2 tầng:
 Pod order ─┐
 Pod payment├─OTLP─► Collector agent (DaemonSet, mỗi node)   ← gắn k8s metadata, batch
 Pod inv.  ─┘                │ loadbalancing exporter (route theo traceId)
                             ▼
                   Collector gateway (Deployment, N replica)  ← tail_sampling
                             │ toàn bộ span của 1 trace về CÙNG 1 replica
                             ▼
                     Tempo / Jaeger / vendor

6. So sánh: head vs tail sampling

Head samplingTail sampling
Quyết định lúcĐầu request, ở service đầu tiênSau khi trace kết thúc, ở Collector
Biết trace lỗi/chậm?KhôngCó
Chi phí appThấp (span không chọn thì không gửi)App gửi 100% span
Hạ tầngKhông cần gì thêmCollector có state, giữ span trong RAM, cần route theo traceId
Hợp vớiLưu lượng rất lớn, ngân sách chặtMuốn giữ mọi trace lỗi/chậm

Thực tế hay kết hợp: head sampling 20–50% ở app để giảm tải mạng, rồi tail sampling ở Collector giữ lỗi/chậm. Đổi lại, trace lỗi rơi vào phần head bỏ đi thì vẫn mất. Dùng parentbased để các service sau tôn trọng quyết định của service đầu, tránh trace đứt đoạn.

So với cấu hình cũ: Micrometer Tracing là facade (giống SLF4J), OTel là chuẩn + implementation. Spring Boot 3 dùng Micrometer API ở code, OTel ở dưới, nên đổi backend chỉ là đổi Collector exporter.

7. Khi nào nên dùng / không nên dùng

Bật tail sampling rồi mà trong Tempo có trace bị thiếu span, chỉ có order mà không có payment. Sao vậy nhỉ?

Đúng triệu chứng kinh điển khi chạy nhiều replica Collector. Ba nguyên nhân hay gặp:

  1. Span của một trace về nhiều replica khác nhau. Replica A thấy span order (lỗi → giữ), replica B thấy span payment (không thấy lỗi → bỏ). Sửa: tầng trước dùng loadbalancing exporter với routing_key: traceID để cả trace về cùng một gateway.
  2. decision_wait ngắn hơn trace. Payment gọi bank 15s, nhưng Collector quyết định sau 10s, span đến sau bị coi là trace mới. Tăng decision_wait (tốn RAM hơn) hoặc chấp nhận.
  3. Head sampling không nhất quán. Payment dùng sampler traceidratio riêng thay vì parentbased_*, nên tự bỏ span dù order đã chọn. Dùng parent-based ở mọi service trừ service đầu.
# Tầng agent
exporters:
  loadbalancing:
    routing_key: traceID
    protocol: { otlp: { tls: { insecure: true } } }
    resolver:
      k8s: { service: otel-gateway-headless.monitoring }

Request đi qua Ingress NGINX và một service Node.js của team frontend thì trace có nối được không?

Được, đó chính là lợi ích của chuẩn W3C traceparent: bất kỳ thành phần nào hiểu header này đều nối vào được, không phân biệt ngôn ngữ. Điều kiện:

Browser ─► Ingress NGINX ─► bff-node ─► order-service ─► payment-service
           traceparent ─────────────────────────────────────────►
           (cùng trace-id; mỗi hop tạo span con, cập nhật parent-id)

Có một điểm cần quyết định: có tin traceparent từ Internet không. Kẻ xấu có thể gửi flag 01 để ép bạn lưu trace. Thường Ingress/gateway bỏ header từ ngoài và tạo trace mới.

Phỏng vấn hỏi về OpenTelemetry thì trả lời sao?

Q1. OpenTelemetry là gì, giải quyết vấn đề gì?

🇻🇳 Tiếng Việt:

OpenTelemetry là chuẩn mở của CNCF cho trace, metric và log, gồm API, SDK cho nhiều ngôn ngữ, giao thức OTLP và Collector. Nó tách việc thu thập khỏi backend: app instrument một lần theo chuẩn, rồi đổi giữa Tempo, Jaeger hay vendor thương mại chỉ bằng cấu hình Collector. Trong Spring Boot 3 em dùng Micrometer Observation ở code và OTel bridge cùng OTLP exporter bên dưới.

🇬🇧 English:

OpenTelemetry is a CNCF open standard for traces, metrics and logs, made of APIs, SDKs for many languages, the OTLP protocol and the Collector. It decouples collection from the backend: you instrument once against the standard and switch between Tempo, Jaeger or a commercial vendor just by reconfiguring the Collector. In Spring Boot 3 I use Micrometer Observation in code with the OTel bridge and OTLP exporter underneath.

Q2. Vì sao nên có OTel Collector thay vì gửi thẳng từ app?

🇻🇳 Tiếng Việt:

Collector là một tầng trung gian có pipeline receiver, processor, exporter. Nó cho phép đổi backend mà không deploy lại app, gắn metadata Kubernetes, lọc dữ liệu nhạy cảm, batch và retry, giới hạn memory, và làm tail sampling. App chỉ cần biết một endpoint OTLP. Em thường chạy một DaemonSet agent trên mỗi node và một gateway Deployment cho xử lý nặng.

🇬🇧 English:

The Collector is an intermediate layer with a receiver, processor and exporter pipeline. It lets you switch backends without redeploying apps, add Kubernetes metadata, scrub sensitive data, batch and retry, cap memory, and do tail sampling. Apps only need one OTLP endpoint. I usually run a DaemonSet agent on each node plus a gateway Deployment for heavier processing.

Q3. Head sampling và tail sampling khác nhau thế nào?

🇻🇳 Tiếng Việt:

Head sampling quyết định giữ trace ngay đầu request, rẻ nhưng không biết trace đó có lỗi hay chậm. Tail sampling quyết định ở Collector sau khi đủ span, nên giữ được mọi trace lỗi và chậm cộng một phần trăm trace bình thường, đổi lại app phải gửi hết span và Collector phải giữ state, cần route theo traceId. Em dùng parent-based sampler để mọi service theo cùng quyết định.

🇬🇧 English:

Head sampling decides at the start of a request, which is cheap but blind to whether the trace will error or be slow. Tail sampling decides in the Collector once all spans arrive, so it can keep every error and slow trace plus a percentage of normal ones, at the cost of apps sending all spans and a stateful Collector that must route by trace ID. I use parent-based samplers so every service follows the same decision.

Q4. Java agent hay instrument thủ công?

🇻🇳 Tiếng Việt:

Java agent tự instrument các thư viện phổ biến qua bytecode, không sửa code, hợp với service cũ hoặc muốn phủ nhanh, nhưng tăng thời gian khởi động và khó kiểm soát chi tiết. Instrument qua Micrometer hoặc SDK cho phép thêm span nghiệp vụ và attribute có ý nghĩa. Thực tế em kết hợp: auto cho HTTP, JDBC, Kafka; thủ công cho bước nghiệp vụ quan trọng, và tránh để hai cơ chế cùng instrument một thư viện.

🇬🇧 English:

The Java agent instruments popular libraries via bytecode with no code changes, which suits legacy services or fast coverage, but it adds startup time and gives less control. Instrumenting through Micrometer or the SDK lets you add meaningful business spans and attributes. In practice I combine them: automatic for HTTP, JDBC and Kafka, manual for key business steps, and I make sure two mechanisms don't instrument the same library.

Cách nhớ ngắn gọn


Tự kiểm tra

Flag 01 cuối traceparent nghĩa là gì?

Trace đã được chọn lấy mẫu (sampled) ở upstream.

Muốn giữ 100% trace lỗi, dùng head hay tail sampling?

Tail sampling ở Collector (policy status_code ERROR).

Vì sao tail sampling nhiều replica cần loadbalancing exporter?

Để mọi span của một trace về cùng replica, quyết định trên trace đầy đủ.

Service cũ dùng B3, service mới W3C. Trace có nối không?

Không, trừ khi cấu hình propagator cả hai kiểu.

Instrument service không được sửa code bằng gì?

OTel Java agent qua JAVA_TOOL_OPTIONS=-javaagent:... hoặc OTel Operator inject.