可观察性#
LMCache 多进程模式提供三种互补的可观察性模式:**指标**(通过 OTel 的 Prometheus 计数器)、**日志**(带有可选 OTel 日志转发的 Python 日志)和 **追踪**(OTel 跨度用于每个请求的延迟)。
这三种模式都由一个内部 EventBus 驱动,该总线将生产者(L1Manager、StorageManager、MPCacheEngine)与订阅者解耦。
快速开始#
默认情况下,**指标**和**日志记录**是启用的;**追踪**是禁用的。不需要额外的标志:
lmcache server \
--l1-size-gb 100 --eviction-policy LRU
要启用追踪,请提供 OTLP 端点:
lmcache server \
--l1-size-gb 100 --eviction-policy LRU \
--enable-tracing --otlp-endpoint http://localhost:4317
配置#
参数 |
默认 |
描述 |
|---|---|---|
|
关闭 |
主开关:完全禁用 EventBus(没有注册任何指标、日志或追踪订阅者)。 |
|
关闭 |
跳过指标订阅者(Prometheus 端点未启动)。 |
|
关闭 |
跳过日志订阅者。 |
|
关闭 |
注册追踪订阅者。需要 |
|
|
事件总线队列中最大事件数,超过后将进行尾部丢弃。 |
|
(无) |
OTLP gRPC 端点(例如 |
|
|
Prometheus |
|
(未设置,默认 UUID v4) |
此 MP 服务器实例的标识符。作为 OTel 资源属性 |
|
|
跟踪生命周期直方图的块/块的比例 (0, 1.0]。计数器始终计算所有事件。默认值为 1%。 |
|
(无) |
在给定级别启用跟踪记录。目前仅支持 |
|
(无) |
写入跟踪文件的路径。如果在设置了 |
环境变量:
变量 |
默认 |
描述 |
|---|---|---|
|
|
控制所有 LMCache 日志记录器的日志级别。有效值: |
指标#
通过 OpenTelemetry 计数器收集指标,并通过进程内的 Prometheus /metrics HTTP 端点导出(默认端口 9090)。当设置 --otlp-endpoint 时,指标也会推送到 OTel 收集器。
All metrics use the lmcache_mp. prefix (multiprocess). On Prometheus,
dots are converted to underscores and counters get a _total suffix
(e.g. lmcache_mp_l1_read_chunks_total).
全局资源属性#
每个由 MP 服务器导出的指标和跨度都携带在启动时构建的资源级属性。这些属性标识生成遥测的进程,并且与每个指标的属性(例如 cache_salt)是正交的。
属性 |
命令行标志 / 配置 |
未设置时的默认值 |
|---|---|---|
|
|
在启动时生成的随机 UUID v4。 |
资源属性附加到 MeterProvider / TracerProvider 并通过 OTLP 传播到每个导出的数据点。在 Prometheus 中,SDK 资源属性出现在 target_info 系列上,而不是每个时间序列上——这是标准的 OTel 行为。
L1 指标#
指标 |
类型 |
描述 |
|---|---|---|
|
计数器 |
Number of chunks read from L1. |
|
计数器 |
Number of chunks written to L1. |
|
计数器 |
Number of chunks evicted by the EvictionController. |
|
计数器 |
L1 逐出循环迭代(每个周期,无论水位线是否被跨越)。由 |
|
计数器 |
L1 逐出循环迭代,其中 |
L1 块生命周期直方图#
通过 L1LifecycleSubscriber 进行采样的(默认 1%)块级生命周期跟踪。只有采样的块会对直方图产生贡献;上述计数器始终计算所有事件。采样是确定性的(基于哈希),因此相同的键总是会得到相同的决策,且没有内存开销。
指标 |
类型 |
描述 |
|---|---|---|
|
直方图 |
每个采样块从分配到逐出的时间。 |
|
直方图 |
每个采样块从最后访问到逐出的时间。 |
|
直方图 |
同一块的连续访问(读取或写入)之间的时间间隔。 |
|
直方图 |
逐出到下次重用的时间(上限为 300 秒)。 |
存储管理器真实重用指标#
由 SMLifecycleSubscriber 发出的工作负载级重用直方图,由面向调用者的 StorageManager 事件驱动(SM_READ_PREFETCHED_FINISHED,SM_WRITE_FINISHED)。 存储/预取控制器的内部读锁释放被排除,因此信号仅反映用户驱动的访问。
两个直方图都标记有 cache_salt 以实现每个租户的隔离。每个块的每次读取和写入(无论是否采样)都会使每个盐值的访问计数器增加,因此块间隙反映了真实的存储量;直方图本身仅记录通过(确定性、基于哈希的)采样门的块的间隙。
指标 |
类型 |
描述 |
|---|---|---|
|
直方图(标签: |
块的最后一次访问(读取或写入)与下一次读取之间的时间间隔。捕获存储成本——一个存储块在访问之间静止了多久。仅在读取事件中发出。 |
|
直方图(标签: |
每个 |
L2 指标#
指标 |
类型 |
描述 |
|---|---|---|
|
计数器 |
Number of L2 store requests submitted. |
|
计数器 |
Number of chunks submitted for L2 store. |
|
计数器 (属性: |
Number of L2 store requests completed, labeled by adapter type. |
|
计数器 |
Number of chunks successfully stored to L2. |
|
计数器 |
L2 预取查找请求的数量。 |
|
计数器 |
Number of chunks submitted for L2 prefetch lookup. |
|
计数器 |
Number of prefix chunks found in L2 lookup. |
|
计数器 |
Number of L2 prefetch load requests submitted. |
|
计数器 |
Number of chunks submitted for L2 load. |
|
计数器 |
Number of chunks successfully loaded from L2. |
|
计数器 (属性: |
Number of per-adapter L2 load requests completed, labeled by adapter type. |
The l2_name-labeled counters (l2_store_completed and
l2_load_completed) exist so dashboards can compute per-backend IOPS on
demand via rate(lmcache_mp_l2_store_completed_requests_total{l2_name="..."}[1m])
(and the equivalent for loads). No separate *_iops metric is exported;
keeping the raw counter lets dashboard users pick their own window.
失败与健康计数器#
在专用的 lmcache_mp.health OTel 计量器上发出的健康监测计数器。由 L1FailureMetricsSubscriber 和 L2FailureMetricsSubscriber 驱动,这些订阅者在启用指标时会自动注册。所有三个计数器都携带 model_name``(从每个 ``ObjectKey 中提取),以便操作员可以在 Prometheus /metrics 端点上按模型进行切片。
指标 |
类型 |
描述 |
|---|---|---|
|
计数器 |
在 |
|
计数器 |
L1 |
|
计数器 |
Chunks that L2 reported present at lookup but failed to land in L1.
Tagged by |
一旦 L2 适配器区分反序列化错误和缺失对象,将会将 reason=serde_failure 值作为附加的、非破坏性的扩展添加到 l2_prefetch_failure 中——当这项功能上线时,无需进行仪表板迁移。
有关完整的设计原理(包括哪些事件类型驱动每个计数器以及为什么推迟 lmcache_instance_id),请参阅源树中的 docs/design/v1/mp_observability/METRICS.md。
查找命中率指标#
按令牌级别计数器,其比例表示由查找请求的令牌中,从 L1 或 L2 服务的令牌的比例。L0(GPU 前缀缓存)故意被排除在外——它是 vLLM 所有的,无法从 LMCache 中观察到。
指标 |
类型 |
描述 |
|---|---|---|
|
计数器(属性: |
提交查找的总令牌数(L1+L2 令牌级命中率的分母)。仅计算与块对齐的令牌。 |
|
计数器(属性: |
在查找过程中在 L1 或 L2 中找到的总令牌数(L1+L2 令牌级命中率的分子)。仅计算连续前缀命中。 |
这两个计数器由同一事件(MP_LOOKUP_PREFETCH_END)驱动,因此它们在每次完成查找时总是一起增加。提前退出的查找对两者都贡献 0,而放弃的查找则对两者都没有贡献。
model_name 和 cache_salt 属性在查找时从 IPCCacheEngineKey 中捕获,以便仪表板可以计算每个模型或每个租户的命中率。cache_salt 可能具有高基数(每个租户或隔离域一个条目);如果存储成本重要,请在抓取时通过 metric_relabel_configs 丢弃它。
PromQL 查询命中率:
# Aggregate (all models, all salts):
rate(lmcache_mp_lookup_hit_tokens_total[5m])
/ rate(lmcache_mp_lookup_requested_tokens_total[5m])
# Per-model:
sum(rate(lmcache_mp_lookup_hit_tokens_total[5m])) by (model_name)
/ sum(rate(lmcache_mp_lookup_requested_tokens_total[5m])) by (model_name)
L0 (GPU) 块生命周期直方图#
通过 L0LifecycleSubscriber 采样(默认 1%)GPU KV Cache 块生命周期跟踪。在重新分配时(当块被分配不同的令牌时)检测逐出。采样使用随机选择,并带有一个 _skipped 集(受物理 GPU 块有限数量的限制)。
所有 L0 直方图都带有 instance_id 和 model_name OTel 属性,从而支持在 Prometheus 中按实例和按模型进行指标切片(例如 lmcache_mp_l0_block_lifetime_seconds{instance_id=\"12345\",model_name=\"llama-7b\"})。
指标 |
类型 |
描述 |
|---|---|---|
|
直方图 |
每个采样的 GPU 块从分配到逐出的时间。 |
|
直方图 |
从最后访问到逐出的时间(每个采样的 GPU 块)。 |
|
直方图 |
同一 GPU 块的连续访问之间的时间间隔。 |
L0 ↔ L1 吞吐量直方图#
每个请求的 GPU↔CPU 复制吞吐量通过 L0L1ThroughputSubscriber 进行记录。每个存储/检索请求为相应的直方图贡献一个样本:total_bytes / (end_ts - start_ts),单位为 GB/s。时间戳来自在 GPU cupy 流上发布的 MP_{STORE,RETRIEVE}_{START,END} 事件,因此它们反映了真实的 GPU 流复制时间——而不是 Python/锁的开销。
All throughput histograms are emitted with engine_id (vLLM worker
instance id), device (e.g. "cuda:3"), and model_name OTel
attributes, enabling per-worker, per-device, and per-model slicing in
Prometheus (e.g.
lmcache_mp_l0_l1_store_throughput_GB_per_second{engine_id="0",device="cuda:3",model_name="meta-llama/Llama-3.1-8B"}).
指标 |
类型 |
描述 |
|---|---|---|
|
直方图 |
每个请求的 GPU→CPU (L0→L1) 存储吞吐量(单位:GB/s)。 |
|
直方图 |
每个请求的 CPU→GPU (L1→L0) 加载吞吐量(GB/s)。 |
L1 ↔ L2 吞吐量直方图#
Per-request throughput of L1↔L2 transfers via
L2ThroughputSubscriber. The store path correlates
L2_STORE_SUBMITTED → L2_STORE_COMPLETED by
(adapter_index, task_id). The load path correlates the per-adapter
L2_LOAD_TASK_SUBMITTED → L2_LOAD_TASK_COMPLETED events by
(request_id, adapter_index); the request-level
L2_PREFETCH_LOAD_* events used by the chunk-count counters aggregate
across adapters and cannot be attributed to a specific l2_name.
时间戳跨越 提交 → 完成,因此持续时间包括适配器队列、网络和磁盘 I/O — 该值为 字节 / 端到端延迟,而不是原始传输速率。使用这些直方图来比较适配器类型并捕捉回归;当您需要纯粹的复制时间吞吐量时,请使用 L0↔L1 直方图。
All L1↔L2 throughput histograms carry a single l2_name OTel
attribute — the registered adapter type (e.g. "fs", "nixl_store",
"mooncake_store") — enabling per-backend slicing in Prometheus (e.g.
lmcache_mp_l2_store_throughput_GB_per_second{l2_name="nixl_store"}).
指标 |
类型 |
描述 |
|---|---|---|
|
直方图 |
L1→L2 store throughput in GB/s per request. |
|
直方图 |
每对(请求,适配器)的 L2→L1 加载吞吐量(单位:GB/s)。 |
引擎计数器#
与 MP 服务器通过 retrieve() 返回给每个 vLLM 工作线程相关的工作线程范围计数器。通过 ``worker_id``(vLLM 工作线程实例 ID)标记——与可能出现在其他指标上的任何调度器范围 ID 不同。
指标 |
类型 |
描述 |
|---|---|---|
|
计数器(属性: |
加载到引擎中的 LMCache 块的总数,按所有 |
可观察的仪表盘#
通过 register_gauge 注册的时间点状态快照(基于拉取的 OTel 可观察量度)。
这三个正在进行的指标携带两个属性,即使注册了多个相同后端类型的适配器,也能区分它们——与 lmcache_mp.l2_store_completed 具有相同形状:
l2_name— 注册的适配器类型(例如"fs","nixl_store","mooncake_store")。adapter_index— 控制器适配器列表中的位置。
没有正在进行的工作的适配器不会为该抓取发出数据点。
指标 |
类型 |
描述 |
|---|---|---|
|
可观察仪表 |
当前正在进行的预取作业数量。持续的高值可能表示 L2 后端缓慢或轮询延迟。 |
|
可观察仪表 |
当前在 L1 中占用的字节数。持续上升而没有达到平稳状态通常表示存在泄漏;在配置的 |
|
可观察仪表 |
L1 使用/总比率 ( |
|
ObservableGauge (attr: |
Bytes currently held in each L2 adapter, sampled at scrape time
from |
|
可观察的仪表 (属性: |
每个适配器当前正在执行的 L2 存储任务。 持续的非零值表明适配器无法跟上 L1 → L2 写入速率。 |
|
可观察的仪表 (属性: |
每个适配器当前正在执行的 L2 → L1 预取加载任务。与 |
|
可观察的仪表 (属性: |
每个适配器保留的 L1 字节,用于正在进行的 L2 → L1 预取加载。随着正在进行的字节和 |
事件总线自我监控#
EventBus 本身的健康指标,由 EventBusSelfMetricsSubscriber 在 lmcache.event_bus OTel 计量器上注册。这些指标通过 EventBus 访问器直接观察总线状态,并在每次 OTel 抓取时报告——它们不是由事件驱动的,因此丢弃或失败的订阅者无法使其静默。
使用它们来回答:EventBus 是否能够跟上发布者,是否有任何事件被丢弃,以及是否有任何订阅者回调抛出异常?非零的 dropped_events_total 或持续非零的 drain_lag_seconds 表明总线处于 --event-bus-queue-size 并且正在尾部丢弃;请提高该标志或调查慢速订阅者。
指标 |
类型 |
描述 |
|---|---|---|
|
可观察仪表 |
事件总线中当前排队的事件(在抓取时的 |
|
可观察仪表 |
自最旧的排队事件发布以来的秒数;当为空时为 |
|
可观察计数器 |
由于 EventBus 队列达到 |
|
可观察计数器 (属性: |
由 EventBus 分发期间由订阅者回调引发的累计异常,按 |
有关完整的设计原理及支持每个指标的进程内访问器,请参见源代码树中的 docs/design/v1/mp_observability/METRICS.md 和 docs/design/v1/mp_observability/event-bus.md。
Prometheus 抓取配置#
将 LMCache 服务器添加为 Prometheus 抓取目标:
scrape_configs:
- job_name: "lmcache-mp"
static_configs:
- targets: ["<lmcache-host>:9090"]
日志记录#
日志订阅者通过 Python 的标准 logging 模块为存储、检索、查找、L1 和 StorageManager 事件发出调试级别的消息。
当安装了 OpenTelemetry 时,init_logger 会自动附加一个 OTel LoggingHandler,以便将日志记录转发到任何配置的 OTel LoggerProvider。该处理程序遵循 LMCACHE_LOG_LEVEL 环境变量。
LMCACHE_LOG_LEVEL=DEBUG lmcache server ...
关键日志消息:
级别 |
消息 |
|---|---|
信息 |
|
信息 |
|
信息 |
|
调试 |
|
调试 |
|
追踪#
备注
--enable-tracing 要求 设置 --otlp-endpoint。如果在没有 OTLP 端点的情况下启用追踪,服务器将拒绝启动,因为没有本地回退用于追踪导出。
当启用追踪时(--enable-tracing --otlp-endpoint <URL>),追踪订阅者会从 START/END 事件对创建 OTel spans:
mp.store— 从MP_STORE_START到MP_STORE_ENDmp.retrieve— fromMP_RETRIEVE_STARTtoMP_RETRIEVE_ENDmp.lookup_prefetch— fromMP_LOOKUP_PREFETCH_STARTtoMP_LOOKUP_PREFETCH_END
每个跨度携带事件元数据作为跨度属性(例如 device、stored_count、found_count)。
在任何 OTel 兼容的后端中查看追踪,例如 Jaeger 或 Grafana Tempo。
# Start Jaeger all-in-one (OTLP gRPC on 4317)
docker run -d --name jaeger \
-p 16686:16686 -p 4317:4317 \
jaegertracing/all-in-one:latest
# Start LMCache with tracing
lmcache server \
--l1-size-gb 100 --eviction-policy LRU \
--enable-tracing --otlp-endpoint http://localhost:4317
每个请求的命中率属性#
每个会话都被包装在一个按请求的根跨度中——标准 MP 路径的 request 和 CacheBlend 路径的 cb.request——它将所有子跨度(mp.store、mp.retrieve、mp.lookup_prefetch)嵌套在其下。当查找阶段结束时,根跨度会用三个 OTel 属性进行注释,这些属性总结了请求级别的缓存命中率:
属性 |
OTel 类型 |
描述 |
|---|---|---|
|
|
从 L1+L2 服务的令牌(分子)。 |
|
|
用于查找的块对齐令牌(分母)。 |
|
|
|
属性在处理 MP_LOOKUP_PREFETCH_END``(标准 MP 路径)或 ``CB_LOOKUP_END``(CacheBlend 路径)时被写入——当根跨度仍然打开时。**仅存储请求**在未调用 ``lookup_prefetch_start() 时不会为查找阶段发出结束事件,因此它们的根跨度将不会携带这些属性。
示例 TraceQL 查询 (Grafana Tempo):
# Requests with less than 50% cache hit rate
{ name = "request" && span.hit_rate < 0.5 }
# Full cache hits only
{ name = "request" && span.hit_rate = 1.0 }
# Complete misses (lookup ran but nothing was cached)
{ name = "request" && span.requested_tokens > 0 && span.hit_tokens = 0 }
有关完整的事件到跨度映射以及将子跨度链接回根的注册模式,请参见源树中的 docs/design/observability/request-event-span.md。
追踪记录#
备注
跟踪记录与 --enable-tracing``(OTel spans)是 **不同的**。跟踪记录捕获每个对二进制文件的 ``StorageManager 公共 API 调用,以便可以在后续进行测试、回归查找和基准测试时 重放 相同的工作负载——无需 vLLM,并且最终无需 GPU。--enable-tracing 将实时 OTel spans 导出到 OTLP 端点以进行在线可观察性。这两个功能是独立的,可以一起使用。
当设置 --trace-level storage 时,LMCache 会将每次对 StorageManager.{reserve_write, finish_write, submit_prefetch_task, read_prefetched_results, finish_read_prefetched} 的调用记录到一个二进制文件中,以便后续重放。
记录默认是**关闭的**,关闭时几乎没有开销(每个``StorageManager``调用只需进行一次布尔检查)。开启时,记录发生在 EventBus 排出线程上,而不在请求路径上。
捕获跟踪#
带有显式输出路径:
lmcache server \
--l1-size-gb 100 --eviction-policy LRU \
--trace-level storage --trace-output /tmp/run.lct
在 $TMPDIR 下使用隐式时间戳输出路径:
lmcache server \
--l1-size-gb 100 --eviction-policy LRU \
--trace-level storage
# → INFO log: "trace recording enabled (level=storage); no
# --trace-output given, writing to
# /tmp/lmcache-trace-<pid>-<UTC>.lct"
在关闭时(SIGTERM 由 EventBus 停止路径处理),跟踪文件会被干净地关闭。
重放#
重放记录的追踪,以及用于驱动、监控和导出重放结果的完整 CLI 标志集,详见其独立页面: 追踪和调试。
捕获了什么(以及未捕获的内容)#
捕获:
每个被装饰的
StorageManager调用的完全限定名称。每个调用的输入参数(例如
keys,layout_desc,mode,extra_count,external_request_id)。每个调用的墙钟时间和单调时间戳。
一个包含跟踪模式版本、开始时间和活动的
StorageManagerConfig的 SHA-256 摘要的头部,以便重放可以检测到不匹配的配置。
未捕获:
KV 张量字节。重放练习的记账和控制逻辑;重放时的有效载荷为零。
在
MPCacheEngine、消息队列或任何 GPU 复制代码中的调用。这些层级在存储跟踪级别中是 超出范围 的。
文件格式#
一个长度前缀的 msgpack 流:
[4-byte big-endian length][msgpack Header]
[4-byte big-endian length][msgpack Record]
[4-byte big-endian length][msgpack Record]
...
Header 包含一个魔术前缀 (LMCT)、一个格式版本、跟踪级别 (今天是 storage)、一个跟踪模式版本、开始时间戳和 StorageManagerConfig 摘要。每个 Record 包含一个相对时间戳、一个墙钟时间戳、完全限定的调用位置 (qualname) 和一个参数字典。
该格式故意具有可扩展性:未来的跟踪 **级别**(mq,gpu)将共享此布局,并使用 level 头字段进行区分。额外捕获的操作会添加新的 qualname 字符串,而不会提升格式版本。
有关完整的设计原理,请参见源代码树中的 docs/design/v1/mp_observability/trace.md。