TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

用 OpenTelemetry 观测 LLM 服务:vLLM 的实际输出信号、与标准的差距、用 Collector 弥补

状态:草案(2026-10-07,2026-10-09 补充了 Collector 开销的实测数据)。 本文中的所有数值均为作者亲自测量。运行环境为:单张 8GB 级 GPU、vLLM 0.31.0、Qwen3-1.7B、OpenTelemetry Collector contrib 0.162.0、单台服务器。由于 GenAI 语义约定仍处于 开发(Development)阶段 ,因此其名称仍在变动(就在写这篇文章的当天,有一个指标名称就变了),且 SGLang 与 Dynamo 的追踪、追踪存储(Tempo、Jaeger 等)以及指标存储均为 未实测 。 编写规则:以实测数据为基础,未能测出的内容标注为「未实测」。

目录

当 LLM 服务器变慢时,我们通常想知道三件事:请求在队列中等待了多久、计算花费了多少时间、这个请求是从哪个服务的哪次调用开始发起的。OpenTelemetry (OTel) 是一种将这三者以相同方式记录下来的标准,而考试(OTCA)考察的 Collector、采样、语义约定和上下文传播都在这里得到了应用。本文将其真实接入到单个 vLLM 实例中,并解答了五个问题:

  1. vLLM 会为单个请求留下哪些 Span 和属性,指标的名称是什么。
  2. 从客户端发起的追踪是否能够延续到 vLLM 内部。
  3. 开启的开销是多少。
  4. 它与标准(GenAI 语义约定)有多少差距,能否通过 Collector 弥补。
  5. 仅保留慢请求的采样是如何运作的,它与概率采样有什么不同。

简短回答。 每个请求都在 llm_request 后面留下了一个 Span,仅凭该属性就可以将首字延迟拆解为等待时间和计算时间。开启追踪的开销在误差范围内。属性名称虽然与标准不同(如 traceparent 对比prompt_tokens ),但可以通过 Collector 的转换处理器进行修改。仅保留慢请求的尾部采样成功留下了全部 19 个慢请求,而 10% 的概率采样在 22 个慢请求中只保留了 1 个。 input_tokens1. 架构

vLLM 0.31.0

클라이언트(계측, traceparent 전송) ──HTTP──▶ vLLM(--otlp-traces-endpoint) ──OTLP/gRPC──▶ OTel Collector ─▶ 파일(JSON)
                                              └─ /metrics(Prometheus) ◀──스크레이프── Collector(prometheus 수신기)

2.1 追踪:每个请求对应一个

实体 llm_request 客户端创建了父 Span,并携带 W3C

标头发送了三个请求。到达 Collector 的三个 vLLM Span 均为 traceparent (类型为 SERVER),其 llm_request父 Span ID 与客户端 Span 的 ID 相同 ,Trace ID 也相同。也就是说,从客户端发起的追踪一直延续到了 vLLM 内部。 单个 Span 具有十二个属性,无论如何更改配置,这十二个属性都保持不变。

属性

值示例 含义 0.0195(秒)
gen_ai.latency.time_to_first_token 从请求进入到生成第一个 Token 为止 0.00002(秒)
gen_ai.latency.time_in_queue 在队列中等待的时间 预填充(输入处理)
gen_ai.latency.time_in_model_prefill 0.0176 解码(Token 生成)
gen_ai.latency.time_in_model_decode 0.4504 模型工作的总体情况
gen_ai.latency.time_in_model_inference 0.4680 整个请求
gen_ai.latency.e2e 0.4697 输入和输出 Token 数量
gen_ai.usage.prompt_tokens, gen_ai.usage.completion_tokens 10, 32 请求信息
gen_ai.request.id, max_tokens, n, top_p cmpl-…, 32, 1, 1 缺失的内容同样重要。模型名称(

)和gen_ai.request.model在此 Span 中不存在,且资源的 gen_ai.operation.name 为 service.name 。如果不指定服务名称,来自多个服务器的 Span 就会混在同一个名称下。可以通过环境变量 unknown_service 来设置,或者使用 Collector 的 OTEL_SERVICE_NAME 处理器来填充 resource (我们在第 4 节中使用了后者)。 service.name 服务器启动的过程也会作为追踪记录下来。

除了 llm_request 之外,启动时的数十个 Span 也作为一个追踪输出。对于已经存在模型文件的四台服务器的平均数据,其 Overall Loading 为 18.7 秒, EngineCoreProc init 为 14.1 秒, Prepare model 为 6.7 秒, Executor init 为 6.1 秒, Load model 为 3.4 秒, Warmup (GPU) 为 2.7 秒, Load weights 为 1.8 秒(由于 Span 之间存在父子包含关系,因此不能直接相加)。首次启动的服务器下载权重花费了 298 秒,其 Overall Loading 为 341 秒。我们无需翻看日志,通过 Span 的耗时就能了解「服务器为什么启动得这么慢」。

2.2 指标:不是 OTLP,而是 Prometheus /metrics

在此版本中,指标仅能通过 /metrics(Prometheus 格式)进行确认。通过 Collector 的 prometheus 接收器每 5 秒抓取一次,共输出了 93 个名称,其中有用的如下(全部带有 vllm: 前缀)。

指标 类型 解读方法
time_to_first_token_seconds 直方图 直到首个 Token
inter_token_latency_seconds 直方图 Token 间间隔
e2e_request_latency_seconds 直方图 整个请求
request_queue_time_seconds, request_prefill_time_seconds, request_decode_time_seconds 直方图 分阶段耗时
num_requests_running, num_requests_waiting 仪表盘 当前排队与等待情况
kv_cache_usage_perc 仪表盘 KV 缓存利用率
prefix_cache_queries_total, prefix_cache_hits_total 总计 前缀缓存命中
prompt_tokens_total, generation_tokens_total 总计 Token 吞吐量
num_preemptions_total 总计 抢占 (KV 不足)

我们检查了这三个方面。

3. 开启的开销

在三种配置下,通过重启服务器两次对相同的负载(输入 512,输出 128,并发 1、8、32)进行了测量。由于每次重启服务器时 KV 缓存的位置可能会有所不同(第 14 篇),我们检查了服务器日志,所有六台服务器均为 26,560 个 Token。

配置 并发 1 吞吐量 / TTFT 并发 8 并发 32
关闭 66 tok/s / 73ms, 73ms 384, 384 tok/s / 349, 353ms 833, 831 tok/s / 670, 669ms
--otlp-traces-endpoint 66, 66 / 73, 73ms 385, 384 / 350, 348ms 832, 832 / 668, 668ms
上文 + --collect-detailed-traces all 66, 66 / 74, 73ms 385, 384 / 349, 351ms 832, 834 / 667, 667ms

4. 与标准的差距及通过 Collector 进行弥补

OpenTelemetry 的 GenAI 语义约定已移至独立仓库(semantic-conventions-genai),其状态为 开发中 。将确认时(2026-10-06 的最新提交)的内容与 vLLM 的输出进行对比,结果如下。

项目 GenAI 约定 vLLM 0.31.0
推理 Span 名称 gen_ai.client.inference 类型,Span 名称 {operation} {model},必需属性 gen_ai.operation.name, gen_ai.provider.name llm_request,服务端 Span,无上述必需属性
Token 数量 gen_ai.usage.input_tokens, gen_ai.usage.output_tokens gen_ai.usage.prompt_tokens, gen_ai.usage.completion_tokens(旧名称)
模型名称 gen_ai.request.model Span 中无,指标中有 model_name 标签
首 Token 时间 服务端指标 gen_ai.server.time_to_first_token(直方图,秒) Span 属性 gen_ai.latency.time_to_first_token(自定义名称),指标 vllm:time_to_first_token_seconds
Token 间隔,请求耗时 gen_ai.server.time_per_output_token, gen_ai.server.request.duration vllm:inter_token_latency_seconds, vllm:e2e_request_latency_seconds

由于标准仍处于开发阶段,与其说是哪一方“错了”,不如说是 vLLM 在标准确定之前使用了旧名称。对于使用者而言,通过 Collector 进行转换则无需修改服务器。我们通过实际运行以下配置进行了验证。

processors:
  resource/service:
    attributes:
      - {key: service.name, value: vllm, action: upsert}
  transform/genai:
    trace_statements:
      - context: span
        statements:
          - set(attributes["gen_ai.usage.input_tokens"], attributes["gen_ai.usage.prompt_tokens"]) where attributes["gen_ai.usage.prompt_tokens"] != nil
          - set(attributes["gen_ai.usage.output_tokens"], attributes["gen_ai.usage.completion_tokens"]) where attributes["gen_ai.usage.completion_tokens"] != nil
          - delete_key(attributes, "gen_ai.usage.prompt_tokens")
          - delete_key(attributes, "gen_ai.usage.completion_tokens")
  metrics_transform/genai:
    transforms:
      - {include: "vllm:time_to_first_token_seconds", action: update, new_name: gen_ai.server.time_to_first_token}
      - {include: "vllm:inter_token_latency_seconds", action: update, new_name: gen_ai.server.time_per_output_token}
      - {include: "vllm:e2e_request_latency_seconds", action: update, new_name: gen_ai.server.request.duration}

结果是,Span 的服务名称变成了 vllm ,Token 数量属性更改为了 input_tokens, output_tokens (旧名称已消失),并且三个指标名称更改为了 gen_ai.server.* 。不过,仅凭名称无法得知值的 含义 是否与标准一致。例如,标准中的首 Token 时间是指“成功响应的第一个 Token 为止”,我们并未确认 vLLM 的值是否为同一区间(未测量)。并且正如前面所看到的,单位为空,需要单独补充。

5. 如何查找慢请求

5.1 仅通过 Span 属性区分首 Token 时间

我们使用从四台服务器(两台开启追踪,两台开启详细)收集的 500 个请求 Span,对首 Token 耗时超过 1 秒的 69 个请求与少于 0.2 秒的 38 个请求进行了对比。

请求 首 Token 平均值 等待 预填充 (Prefill)
耗时超过 1 秒的 69 个 1,494ms 886ms 491ms
少于 0.2 秒的 38 个 74ms 0ms 65ms

在慢请求的首 Token 时间中,等待所占比例的中位数为 59%。慢请求变慢不是因为计算慢,而是因为排队导致的,这得出的结论不是“更换更快的 GPU”,而是“提高并发处理上限、增加副本数或限制请求”。首 Token 时间并不完全等于等待时间和预填充时间的总和,差值的中位数为 78ms,最大为 221ms(推测是由 Token 化或首次解码阶段等引起的,但未作细分)。请求的总耗时与首 Token 时间和解码时间的总和在 1.1ms 的误差范围内一致。

5.2 尾部采样与概率采样

发送了 120 个请求,带有混合负载(输入长度为 64、512、1,500 个词元,并发数为 16)。客户端测得的请求耗时中位数为 2.08 秒,p90 为 2.73 秒,最大为 3.02 秒;从服务器端的 Span 耗时来看,有 19 个请求的耗时超过了 2.5 秒。

策略 保留的 Trace 慢速请求(≥ 2.5秒)
全部保留(仅转换) 120 19
尾部采样(Tail Sampling), latency 策略 ≥ 2,500ms 19(15.8%) 19(全部)
概率采样 10%(300 个请求负载) 39(13%) 22 个中的 1 个

6. 遇到的问题

7. OTCA 范围与延伸阅读

总结本实验直接展示了测试范围中的哪些项目。

测试范围 本文中确认的内容
上下文传播 traceparent 通过
将客户端 Span 与 vLLM Span 连接为父子关系 service.name 资源与语义约定 unknown_service如果没有
收集器管道 接收器(otlp、prometheus)、处理器(resource、transform、metrics_transform、tail_sampling、batch)、导出器(file)以及处理器的顺序
采样 概率采样和尾部采样在保留慢速请求方面的程度差异
指标 累积直方图、缺失单位、聚合与求和(Gauge and Sum)

LabHub 的 OTCA 课程增加了一个模块(otca-llm-serving,实作第 7 阶段),用于使用实际的 Span 和直方图亲自计算这些内容。在部署完成的环境中可见。

8. 局限性与未测量项

参考资料

登录后即可点赞

评论

还没有评论。

登录后即可发表评论