文章

OpenTelemetry 可观测性实践

OpenTelemetry 可观测性实践

概述

OpenTelemetry(OTel)是 CNCF 中活跃度仅次于 Kubernetes 的项目,提供了一套厂商中立的可观测性标准——统一的 SDK、统一的采集协议(OTLP)、统一的 Collector。它不替代 Prometheus / Loki / Tempo,而是取代它们各自专属的采集器和 agent,用一个 Collector 统一处理 Trace、Metric、Log 三种信号。

一句话:Prometheus 的 “ServiceMonitor → Prometheus → Grafana” 在 OTel 体系里变成 “OTel SDK / Auto-Instrumentation → OTel Collector → 任选后端(Prometheus / Tempo / ClickHouse / Datadog / …)”。后端是可替换的,采集层是统一的。

与 LGTM 栈的本质区别

LGTM 路线(三个 Agent 三条管线):
  Promtail ──→ Loki       (日志)
  Jaeger Agent ─→ Tempo   (链路)
  node-exporter → Prometheus(指标)

OTel 路线(一个 Collector 三条管线):
  ┌── Receiver (OTLP) ─→ Processor ─→ Exporter → Tempo/Jaeger     (traces)

  │  OTel Collector (DaemonSet + Deployment)

  ├── Receiver (prometheus) ─→ Processor ─→ Exporter → Prometheus

  └── Receiver (filelog) ─→ Processor ─→ Exporter → Loki / ClickHouse
维度LGTMOpenTelemetry
接入方式每种信号独立 agent统一 Collector
协议各自协议(Prometheus scrape、Loki push、Jaeger thrift)统一 OTLP(gRPC/HTTP)
厂商锁定Grafana 生态绑定无绑定(后端可热换)
自动探针Prometheus exporter / Jaeger client 手动埋点Auto-Instrumentation(Java/Python/Go/.NET 零代码注入)
应用代码改动需要引入特定库OTel SDK 一行不改即可迁移后端
CNCF 地位Prometheus 毕业、Loki/Tempo 沙箱毕业(2024)
kagent 集成需额外配置原生 OTel

三大核心概念

Signals:三种信号的统一模型

OTel 为三种观测信号定义了统一的数据模型,都用一组标准的 Resource + Attribute 来标记”这个信号从哪来”:

Signal用途OTel 数据对象
Trace请求在分布式系统中的传播路径Span(含 TraceID、SpanID、ParentSpanID)
Metric聚合的数值指标Counter、Histogram、Gauge、UpDownCounter
Log离散的事件记录LogRecord(SeverityText、Body、TraceID 关联)

关键特性:三种信号通过 W3C Trace Context 自动关联。同一个 TraceID 出现在 Metric 的 Exemplar、Log 的 Attribute 和 Trace 的 Span 中,Grafana / Jaeger 可以一键从 Metric 跳转到对应 Trace。

OTLP:统一采集协议

OTLP(OpenTelemetry Protocol)是 OTel 定义的标准传输协议,替代了 Prometheus scrape + Jaeger thrift + Fluentd forward 三个协议的拼凑。

应用 SDK 侧:
  OTLP exporter (gRPC) → grpc://otel-collector:4317

Collector 侧:
  OTLP receiver (gRPC :4317 / HTTP :4318)
    → processor pipeline
    → OTLP exporter → 另一个 Collector / 后端

OTLP 支持 gRPC(高性能二进制)和 HTTP(防火墙友好)两种传输。gRPC 模式下原生支持流式传输(Trace 的 Span 实时推送、Metric 的 Delta 推送)。

Collector:统一处理管线

Collector 是 OTel 的”中间件”,由三个组件组成:

Receiver (收) → Processor (处理) → Exporter (发)
     ↓               ↓                ↓
  接收数据      转换/过滤/采样      发送到后端
  OTLP /        batch /            Prometheus /
  Prometheus /  filter /           Tempo /
  filelog /     memory_limiter /   ClickHouse /
  k8s_events    k8sattributes      Datadog / ...

Pipeline 示例(一个 Collector 同时处理三种信号):

# otel-collector-config.yaml
receivers:
  otlp:                          # OTLP 接收(应用 SDK 推送)
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  prometheus:                    # 抓取 Prometheus exporter
    config:
      scrape_configs:
        - job_name: 'node-exporter'
          scrape_interval: 30s
          static_configs:
            - targets: ['localhost:9100']

  filelog:                       # 收集容器日志
    include: [/var/log/pods/*/*/*.log]
    operators:
      - type: json_parser

  k8s_events:                    # K8s Event 收集
    namespaces: [health, bigdata]

processors:
  batch:                         # 批量发送(减少网络开销)
    send_batch_size: 8192
    timeout: 5s

  memory_limiter:                # 内存保护
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128

  k8sattributes:                 # 自动附加 K8s 元数据
    extract:
      metadata:
        - k8s.pod.name
        - k8s.namespace.name
        - k8s.deployment.name
        - k8s.node.name
    pod_association:
      - sources:
          - from: resource_attribute
            name: k8s.pod.ip

  filter:                        # 丢弃不需要的数据
    metrics:
      metric:
        - 'name == "http_client_duration_ms" and type == HISTOGRAM'

  tail_sampling:                 # 尾采样(只保留错误 + 慢请求的 trace)
    decision_wait: 10s
    policies:
      - name: errors-and-slow
        type: and
        and:
          and_sub_policy:
            - name: status-error
              type: status_code
              status_code: { status_codes: [ERROR] }
            - name: latency
              type: latency
              latency: { threshold_ms: 1000 }

exporters:
  otlp/tempo:
    endpoint: tempo.monitoring:4317
    tls:
      insecure: true

  prometheusremotewrite:         # 写入 Prometheus / VictoriaMetrics
    endpoint: "http://prometheus.monitoring:9090/api/v1/write"

  loki:
    endpoint: "http://loki.monitoring:3100/loki/api/v1/push"

service:
  pipelines:
    traces:                      # trace 管线
      receivers:  [otlp]
      processors: [memory_limiter, k8sattributes, tail_sampling, batch]
      exporters:  [otlp/tempo]

    metrics:                     # metrics 管线
      receivers:  [otlp, prometheus]
      processors: [memory_limiter, k8sattributes, batch]
      exporters:  [prometheusremotewrite]

    logs:                        # logs 管线
      receivers:  [otlp, filelog, k8s_events]
      processors: [memory_limiter, k8sattributes, batch]
      exporters:  [loki]

关键 Processor 详解

Processor作用推荐
batch批量压缩后发送,减少 backend 压力必须
memory_limiterCollector 内存超限时丢弃数据而非 OOM必须
k8sattributes自动附加 Pod/Namespace/Deployment/Node 标签必须(K8s 集群)
tail_sampling只保留错误+慢请求的 trace,砍掉 90% 正常 trace高流量必装
filter丢弃不需要的 metrics(降低高基数)按需
attributes增删改属性(如 mask 敏感字段)安全合规场景
resource修改 resource 属性多集群统一命名时用
transformOTTL(OpenTelemetry Transformation Language)通用数据转换复杂逻辑时用

K8s 部署方案

部署模式:DaemonSet + Deployment

OTel Collector 在 K8s 上采用双层部署:

模式 1: DaemonSet(节点级)
  每个节点一个 Collector Pod
  → hostNetwork + hostPID
  → 收集宿主机容器日志 (filelog receiver)
  → 收集 kubelet metrics (kubeletstats receiver)
  → 收集节点 metrics (hostmetrics receiver)
  → 高吞吐,本地 network latency 为零

模式 2: Deployment(集群级)
  多副本 Collector Service
  → 接收应用 SDK 推送的 OTLP 数据
  → 做 tail_sampling(需要全局视图)
  → HA:多副本 + gRPC 负载均衡

OpenTelemetry Operator

# 安装 Operator
kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml

# Operator 管理 Collector CR
# 同时支持 Auto-Instrumentation(自动注入 Java/Python/Go/.NET SDK)
# OpenTelemetryCollector CR(DaemonSet)
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: otel-daemonset
  namespace: monitoring
spec:
  mode: daemonset
  hostNetwork: true
  env:
    - name: K8S_NODE_NAME
      valueFrom:
        fieldRef:
          fieldPath: spec.nodeName
  config: |
    receivers:
      prometheus:
        config:
          scrape_configs:
            - job_name: kubelet
              kubernetes_sd_configs:
                - role: node
              scheme: https
              tls_config:
                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
                insecure_skip_verify: true
              bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token

      kubeletstats:
        collection_interval: 30s
        auth_type: serviceAccount
        endpoint: "https://${env:K8S_NODE_NAME}:10250"

      hostmetrics:
        collection_interval: 30s
        scrapers:
          cpu: {}
          memory: {}
          disk: {}
          network: {}
          load: {}

      filelog:
        include: [/var/log/pods/*/*/*.log]
        start_at: beginning
        include_file_path: true
        operators:
          - type: container
            id: container-parser

    processors:
      batch:
        send_batch_size: 8192
        timeout: 10s
      memory_limiter:
        check_interval: 1s
        limit_mib: 1024
      k8sattributes:
        extract:
          metadata: [k8s.namespace.name, k8s.pod.name, k8s.deployment.name, k8s.node.name]

    exporters:
      otlp:
        endpoint: otel-gateway.monitoring:4317
        tls:
          insecure: true

    service:
      pipelines:
        metrics:
          receivers: [prometheus, kubeletstats, hostmetrics]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [otlp]
        logs:
          receivers: [filelog]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [otlp]
# OpenTelemetryCollector CR(Deployment — 集群级 Gateway)
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: otel-gateway
  namespace: monitoring
spec:
  mode: deployment
  replicas: 2
  config: |
    receivers:
      otlp:                     # 从 DaemonSet Collector 接收
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
    processors:
      tail_sampling:            # 全局采样(只在 Gateway 做)
        decision_wait: 10s
        num_traces: 50000
        policies:
          - name: errors
            type: status_code
            status_code: { status_codes: [ERROR] }
          - name: slow
            type: latency
            latency: { threshold_ms: 2000 }
      batch:
        timeout: 5s
    exporters:
      otlp/tempo:
        endpoint: tempo.monitoring:4317
        tls: { insecure: true }
      prometheusremotewrite:
        endpoint: "http://victoriametrics.monitoring:8428/api/v1/write"
      loki:
        endpoint: "http://loki.monitoring:3100/loki/api/v1/push"
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [tail_sampling, batch]
          exporters: [otlp/tempo]
        metrics:
          receivers: [otlp]
          processors: [batch]
          exporters: [prometheusremotewrite]
        logs:
          receivers: [otlp]
          processors: [batch]
          exporters: [loki]

Auto-Instrumentation —— 零代码注入

# Instrumentation CR:声明哪些 Pod 自动注入 OTel SDK
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: java-auto-instr
  namespace: health
spec:
  exporter:
    endpoint: http://otel-gateway.monitoring:4318
  propagators:
    - tracecontext
    - baggage
  java:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
  sampler:
    type: parentbased_traceidratio
    argument: "0.1"           # 10% 采样率

---
# 目标 Pod 加 annotation 即可注入
apiVersion: apps/v1
kind: Deployment
metadata:
  name: health-ack
spec:
  template:
    metadata:
      annotations:
        instrumentations.opentelemetry.io/inject-java: "health/java-auto-instr"

支持的自动探针语言:Java、Python、Go(eBPF)、.NET、Node.js、Ruby

后端替换的灵活性

OTel 最大的卖点:改一行 Exporter 配置,后端全换,应用代码毫不知情

场景切换示例(只改 Collector 的 exporters 和 pipelines):

今天是:
  traces → otlp → Tempo
  metrics → prometheusremotewrite → Prometheus
  logs → loki → Loki

明天(成本优化):
  traces → otlp → ClickHouse(列存,成本降为 1/10)
  metrics → prometheusremotewrite → VictoriaMetrics(单机替代集群)
  logs → otlp → ClickHouse

后天(接入 Datadog):
  traces → datadog → Datadog
  metrics → datadog → Datadog
  logs → datadog → Datadog

这就是 OTel 的统一管线的价值——采集标准统一后,后端随便换。

kagent + OTel 集成

kagent 原生将 OpenTelemetry 注入到每个 Agent Pod 的生命周期中:

# kagent Agent CRD(追踪配置)
apiVersion: kagent.dev/v1alpha2
kind: Agent
spec:
  declarative:
    tracing:
      enabled: true
      endpoint: http://otel-gateway.monitoring:4317
      sampler:
        type: TraceIdRatioBased
        ratio: 0.1

kagent 自动为每个 Agent Pod 做:

  • Prompt → LLM API 调用的 Span(记 token 数 + 延迟)
  • 工具调用 Span(MCP tools/call 的延迟和结果)
  • HITL 审批 Span(人工等待时间)
  • A2A 跨 Agent 通信 Span(W3C Trace Context 透传)

这些 span 全部打到 OTel Collector,在 Tempo/Jaeger 中可以看到一个用户请求 → kagent → LLM → MCP Tool Server 的完整调用链。

选型建议

场景推荐方案
新集群、从零搭建OTel Collector(DaemonSet + Gateway)→ VictoriaMetrics + Tempo + Loki
已有 Prometheus 集群、不想动OTel Collector 仅用于 Trace + Log,Metric 保留 Prometheus scrape
GPU 训练集群(kagent + NCCL)OTel Collector + kagent OTel 配置,全链路 Trace 覆盖训练 Job
需要更换后端(成本/合规)OTel 是不二之选(换 Exporter 即可)
团队已熟悉 PromQL / LogQL保留 LGTM 后端,用 OTel Collector 替换采集层
小集群,运维力量有限直接用 kube-prometheus-stack(LGTM 一体化 Helm),等规模上来再引入 OTel

关联知识

参考资源

学习时间

阶段时间备注
OTel 体系2026-07-02OTLP 协议、Collector Pipeline、K8s 部署双层模式、Auto-Instrumentation、后端替换、kagent 集成

状态: 🌱 学习中 下次复习日期: 2026-07-09