Chapter 10

生产线上的 OTel

从 demo 到生产隔着一条成本和稳定性的鸿沟。这一章给你生产部署清单、采样与高基数实战、LLM 可观测(2025 GenAI SemConv Stable),以及一份排障经验集。

生产部署清单

1. 至少 Agent + Gateway 两层
Agent 在节点就近收,Gateway 做集中采样/路由——不要让 SDK 直连公网后端
2. Collector 资源配额
Gateway 至少 3 副本;CPU 和 memory 给足(memory_limiter 之外再留 50% buffer)
3. SDK 只发 Agent
OTLP endpoint 全部指向本地 Agent(localhost:4317)——Agent 挂了最坏也只是自己本节点丢数据
4. 灰度采样率
上线时采 1% → 10% → 100%(按服务重要性),观察后端压力
5. tail-based sampling
"保留错/慢 + 1% 基线"配置,trace 量可降 90%+ 而不丢关键信息
6. 资源 detector
Collector 开 k8s / docker / host 探测,自动补 pod 名/节点名
7. PII 过滤
Collector 的 attributes / transform 处理 删除邮箱/手机/身份证
8. Collector 自监控
Collector 的 telemetry metrics 要被 Prom scrape —— 送失败率、drop 率报警
9. 回退方案
后端挂了(Datadog 故障、Honeycomb 超时),Collector 是否能本地落盘或临时弃数据?配 file exporter 做失败回写
10. 关 health check 埋点
/healthz / /ready / /metrics 这类轮询路径,用 ignoreIncomingRequestHook 丢弃,否则 span 量暴涨

采样策略总纲

入口采样率   = ParentBased(TraceIdRatio(1.0))   # 应用全采,交给 Collector 决定
      ↓
Agent 层     = 透传(batch + memory_limiter)
      ↓
Gateway 层   = Tail Sampling
               - status=ERROR  → 100% 保
               - latency > 500ms → 100% 保
               - tenant 是 VIP → 100% 保
               - 其他 → 1-5%

应用不做采样的好处:业务代码不依赖采样策略,换策略只改 Collector。

高基数的血泪教训

真实事故
某团队把 user_id 放进了 http_requests_total 的 label。1000 万用户 + 10 个路由 = 1 亿条时间序列。Prometheus 内存从 10GB 涨到 200GB,crashloop 了一天。

修复:Collector 层用 attributes processor 把 user_id 从 metric 路径上删除(保留在 trace);重启 Prom,压缩旧数据,恢复。

一般规则:metric 的 attribute 基数 < 100。常见雷区:

Collector 层统一治理

processors:
  attributes/clean:
    actions:
      - key: user.id
        from_context: span       # 从 span 移除(别污染 metric)
        action: delete
      - key: password
        action: delete
      - key: email
        action: hash             # 哈希后保留

  transform/metric_safety:
    metric_statements:
      - 'keep_matching_keys(attributes, "http\\..*|rpc\\..*|service\\..*")'
      # metric 只保留协议字段,业务字段全丢

把治理规则沉淀到 Collector——运维一份配置,整公司自动合规。

LLM 可观测:GenAI SemConv

2024 年末 OTel 加了 GenAI 工作组,2025 Q3 一批字段转 Stable。核心思路:LLM 的每一次调用就是一个 span,attributes 带上模型、token、延迟、成本。

OpenAI 调用埋点

import { trace, SpanStatusCode } from "@opentelemetry/api";

const tracer = trace.getTracer("llm-app");

async function callOpenAI(messages: Message[]) {
  return tracer.startActiveSpan("chat gpt-4o", async (span) => {
    span.setAttributes({
      "gen_ai.system": "openai",
      "gen_ai.request.model": "gpt-4o",
      "gen_ai.operation.name": "chat",
      "gen_ai.request.temperature": 0.7,
      "gen_ai.request.max_tokens": 2000,
    });

    try {
      const res = await openai.chat.completions.create({
        model: "gpt-4o", messages, temperature: 0.7,
      });

      span.setAttributes({
        "gen_ai.response.model": res.model,
        "gen_ai.usage.input_tokens": res.usage!.prompt_tokens,
        "gen_ai.usage.output_tokens": res.usage!.completion_tokens,
        "gen_ai.response.finish_reasons": [res.choices[0].finish_reason],
      });
      span.setStatus({ code: SpanStatusCode.OK });
      return res;
    } catch (e) {
      span.recordException(e as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw e;
    } finally {
      span.end();
    }
  });
}

成本计算:把 token 变成 $

# Collector 里用 OTTL 算成本
processors:
  transform/cost:
    trace_statements:
      - 'set(attributes["gen_ai.cost.input_usd"],
             attributes["gen_ai.usage.input_tokens"] * 0.0025 / 1000)
             where attributes["gen_ai.request.model"] == "gpt-4o"'
      - 'set(attributes["gen_ai.cost.output_usd"],
             attributes["gen_ai.usage.output_tokens"] * 0.01 / 1000)
             where attributes["gen_ai.request.model"] == "gpt-4o"'

每次 LLM 调用都附带成本字段——Grafana 上按 tenant/user/feature 聚合 = 实时成本看板。

LangChain / LlamaIndex 追踪

主流 LLM 框架都接了 OTel(有些叫 callbacks,有些直接 OTel 原生):

# LangChain
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
LangchainInstrumentor().instrument()

# 之后任何 LangChain chain 自动产 span
chain = prompt | llm | parser
result = await chain.ainvoke({"q": "hello"})
# Jaeger 上看到:chain → prompt template → LLM call → parser 的完整链路
# LlamaIndex
from opentelemetry.instrumentation.llama_index import LlamaIndexInstrumentor
LlamaIndexInstrumentor().instrument()

OpenLLMetry:社区增强

OpenLLMetry 是 Traceloop 维护的 OTel 扩展,在标准 GenAI SemConv 之上:

from traceloop.sdk import Traceloop
Traceloop.init(app_name="my-llm-app")

# 下面所有 LLM 调用自动追踪,发到任何支持 OTLP 的后端

Agent / Tool 链路

LLM agent 的多步 tool 调用最适合 trace 可视化:

research-agent (trace_id=abc)
├─ LLM call (decide tool)          gpt-4o, 200ms, in=1200/out=45 tok
├─ tool: web_search                 Brave API, 800ms
├─ LLM call (analyze result)        gpt-4o, 600ms, in=3800/out=312 tok
├─ tool: calculator                 local, 5ms
├─ LLM call (final answer)          gpt-4o, 400ms, in=4200/out=200 tok
└─ total: 3.5s, $0.018, 5 spans

Jaeger 瀑布图里每个 tool 是一个 span,你能看到:哪步最慢、哪个模型用 token 最多、哪个 tool 失败重试——LLM 应用优化的第一手资料。

RAG 链路

RAG query span
├─ embed user query        (OpenAI text-embedding-3-small, 80ms)
├─ vector search           (Qdrant, 15ms, top_k=20)
├─ rerank                  (Cohere rerank, 120ms, top_n=5)
├─ LLM generation          (gpt-4o, 1200ms, ctx 3200 tok)
└─ total 1.4s

每一步都是 span,attributes 带 db.vector.dimensions / db.vector.top_k 等。这些是 RAG 优化(embedding 模型、top_k 调参)的依据。

LLM 专属 metric

const tokens = meter.createHistogram("gen_ai.client.token.usage", {
  unit: "token",
});
const cost = meter.createCounter("gen_ai.client.cost_usd", {
  unit: "USD",
});

// 在每次调用后
tokens.record(usage.prompt_tokens, {
  "gen_ai.request.model": model,
  "gen_ai.token.type": "input",
});
tokens.record(usage.completion_tokens, {
  "gen_ai.request.model": model,
  "gen_ai.token.type": "output",
});
cost.add(computedUsd, {
  "gen_ai.request.model": model,
  "feature": "summarize",
});

每日成本看板、按 feature 聚合 token 消耗、分位数延迟——全靠这几个 metric。

Evals 接入

评估结果也可以走 OTel:

span.setAttributes({
  "gen_ai.evaluation.name": "faithfulness",
  "gen_ai.evaluation.score": 0.87,
  "gen_ai.evaluation.passed": true,
});

在 Langfuse / Phoenix 这类 LLM 观测平台上,这些字段被渲染成专属的 eval 面板。

9 个常见 troubleshooting

1. Jaeger 只有根 span,子 span 丢了
上下游都启动了 SDK?traceparent header 被正确转发?CORS 是否放行?(见第 5 章)
2. 没有 span 上报
OTEL_LOG_LEVEL=debug 看 SDK 日志;span.end() 忘了?exporter endpoint 正确?Collector 收到没(debug exporter 打开)?
3. CPU 涨得厉害
关不必要 instrumentation(dns/fs);降采样率;换 batch processor 的 send_batch_max_size
4. 内存爆掉
Collector 的 memory_limiter 一定要配;SDK 层 BatchSpanProcessor 队列不要设太大
5. Prometheus 基数爆炸
立刻在 Collector 用 attributesprocessor 删除嫌疑字段;Prom 侧 scrape_interval 放大应急
6. trace_id 冲突
用自定义 traceId?—— 放弃,让 OTel 自动生成(128 位加密安全随机,冲突率接近 0)
7. span.start_time 和 end_time 反了
异步场景里手动 new Date() 常出错;用 SDK 默认的自动时间戳
8. 跨容器 context 丢失
HTTP header 大写敏感?nginx 的 underscores_in_headers 没开?Envoy 把 traceparent 过滤了?
9. Collector OOMkilled
tail_sampling 的 decision_wait 过长 + 突发流量 → 缓冲爆;降 decision_wait 或加 loadbalancing 分片

文档 & 告警规范

每个服务 README 写明
· 埋了哪些 span / metric / log,含义
· 用的是自动还是手动 instrumentation
· 业务 attribute(acme.*)列表
· 对应的 Grafana 面板链接
· 关键 SLO + 告警门槛(p99 / 错误率 / 成本)

这是从"埋了点"到"可运维"的关键文档。

最终回顾

结语
OTel 不是银弹,但它是把「可观测性」从厂商专有变成行业基础设施的那次关键标准化——就像 HTTP 之于 Web、Docker 之于容器、K8s 之于编排。埋一次、处处能用,这个承诺兑现了。

本教程到此结束。祝你的系统永远在绿色仪表盘上,出问题的那刻一键回滚。