LMCache 匿名使用遥测(Usage Telemetry)完整解析:消息契约、上报路径与故障隔离机制
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
匿名使用遥测(Anonymous Usage Telemetry)是 LMCache 内置的一套"phone-home"统计系统,用于回答"LMCache 部署长什么样、缓存命中率如何"这类问题,帮助项目团队了解真实负载并据此优化。本文以 lmcache/usage_telemetry/README.md 为骨架,结合包内 14 个模块的源码、引擎与 MP 服务器的调用点以及完整测试用例,讲解它的消息契约、六条上报路径、无抛异常(no-throw)保障、匿名身份体系与退出机制,读完你可以掌握这套遥测系统的数据流、环境变量配置,并具备按扩展指南新增消息类型的能力。
遥测的定位:给维护者看的使用统计,而非运维可观测性
先厘清一个容易混淆的边界:匿名使用遥测不是面向部署运维人员的可观测性指标。两者在 LMCache 中是两套并行的系统:
| 维度 | 匿名使用遥测 | 运维可观测性(Operator Observability) |
|---|---|---|
| 消费者 | LMCache 维护团队(用于了解真实世界负载) | 部署运维人员(用于监控自己的服务) |
| 单进程模式实现 | lmcache/usage_telemetry/ | lmcache/observability.py |
| MP 模式实现 | lmcache/usage_telemetry/(MP 上报器) | lmcache/v1/mp_observability/(Prometheus/OTel) |
| 传输通道 | HTTP POST 到 LMCache 统计服务器 | Prometheus / OTel 导出 |
| 退出开关 | 独立(LMCACHE_TRACK_USAGE/DO_NOT_TRACK) | 独立 |
两个系统在部分事件源上有共享(例如 MP 模式的持续遥测复用 EventBus 事件),但消费者、传输层和退出开关完全独立。遥测只回答部署形态与缓存使用量的问题,从不包含请求内容。
包结构:14 个模块各司其职
lmcache/usage_telemetry/包的模块划分如下(README 原始表格 + 源码印证):
| 模块 | 职责 |
|---|---|
| messages.py | 线上契约(wire schema)的唯一权威来源:全部消息 dataclass、USAGE_SCHEMA_VERSION、每条消息的ENDPOINT |
| guard.py | swallow_telemetry_errors装饰器 —— 无抛异常保障的载体 |
| identity.py | 退出开关is_usage_tracking_enabled、UsageIdentity(session/machine 两个匿名 ID) |
| transport.py | UsageMessageSender(HTTP 发送器)、build_usage_payload(身份+schema 盖章、扁平化) |
| env_probe.py | 硬件/平台/云厂商探测,供给EnvMessage |
| flush.py | start_usage_flush_thread—— 间隔解析 + 各间隔上报器共享的PeriodicThread刷盘调度 |
| context.py | UsageContextBase、单进程UsageContext、InitializeUsageContext——/context快照上报器 |
| continuous.py | ContinuousUsageContext间隔计数器与缓存生命周期直方图 |
| metric_specs.py | MetricSpec(map-reduce 指标契约)与默认指标注册表(不从包根导出) |
| mp_continuous.py | MPContinuousUsageReporter—— MP 服务器端缓冲/归约/发送MetricSpec指标(不从包根导出) |
| l1_usage.py | L1UsageReporter—— MP 服务器 L1 池占用率(不从包根导出) |
| l2_usage.py | L2ConnectorUsageReporter—— 按 L2 适配器类型统计流量与占用(不从包根导出) |
| mp.py | MP 服务器MPUsageContext、InitializeMPUsageContext |
需要特别注意metric_specs.py、mp_continuous.py、l1_usage.py、l2_usage.py这四个模块不从包根__init__.py导出:它们依赖lmcache.v1.mp_observability,而单进程引擎路径导入lmcache.usage_telemetry时绝不能被牵连加载该模块(见 mp_continuous.py 的模块注释)。
此外,旧路径 lmcache/usage_context.py 保留为向后兼容 shim,仅重新导出包化之前的公开名字(InitializeUsageContext、UsageContext、ContinuousUsageContext、各消息类等),实现已全部迁入新包。
消息契约:类名即线上 message_type
messages.py 的模块 docstring 开门见山:它是"protobuf 风格"的注册表 —— LMCache 能 phone-home 的每一条消息都是一个 dataclass,且只存在于该文件中。契约要点:
- 类名是线上
message_type判别符:build_usage_payload通过type(message).__name__派生,发送方无法与 schema 产生分歧; - 每条消息以类属性声明
ENDPOINT(要 POST 到的统计服务器端点),发送方据此路由; - 字段扁平且 JSON 可序列化:后端以扁平 key-value 存储;列表会被拼成带分隔符的字符串(如
EnvMessage.architecture的二元组、l2_adapter_types的逗号拼接); session_id、machine_id、schema_version、deployment_mode由build_usage_payload统一盖章,不在单条消息里声明。
公共字段与 schema 版本
USAGE_SCHEMA_VERSION = 1会盖到每条 payload 上。版本号的提升规则很严格:只有已有字段的"含义"发生变化时才 bump(例如某个字段从字节改为 token);新增消息类型或新增字段不算 schema 变更。这样后端分析可以按 schema 版本分区。
DeploymentMode枚举定义了deployment_mode的取值:
single_process:LMCache 运行在服务引擎内部(vLLM/SGLang/TRT-LLM 集成);mp_server:LMCache 作为独立多进程缓存服务器运行。
build_usage_payload的实现(transport.py)把消息实例vars(message)展开进 payload,唯一特例是torch.dtype值会被str()化为字符串(例如测试中断言kv_dtype == "torch.bfloat16",见 tests/test_usage_telemetry.py)。
消息目录
| 消息类型 | ENDPOINT | 发送时机 | 关键字段 |
|---|---|---|---|
EnvMessage | context | 每次启动(两种模式都有) | provider、num_cpu、cpu_type、total_memory、architecture、gpu_count、gpu_type、gpu_memory_per_device、source |
EngineMessage | context | 单进程引擎启动 | chunksize、local_device、max_local_cache_size、remote_url、remote_serde、enable_blending、blend_recompute_ratio、model_name、world_size、kv_dtype、kv_shape等 |
MetadataMessage | context | 单进程引擎启动 | start_time、duration |
MPServerMessage | context | MP 服务器启动 | lmcache_version、chunk_size、hash_algorithm、max_gpu_workers、max_cpu_workers、p2p_enabled、l1_size_bytes、l1_medium、eviction_policy、l2_adapter_types、l2_serde_types等 |
ContinuousContextMessage | cache-usage | 周期性(两种模式) | interval_num_stored_tokens、interval_num_hit_tokens、interval_stored_kv_size、sequence_number、uptime_seconds |
CacheLifespanMessage | cache-lifespan | 周期性(仅单进程) | cache_lifespan_histogram(分钟下界 → 样本数) |
L2ConnectorUsageMessage | l2-usage | 周期性(仅 MP,每活跃适配器类型一条) | l2_name、active_seconds、interval_stored_bytes、interval_store_succeeded_keys、interval_load_submitted_keys、bytes_used、capacity_bytes、unbounded_adapters |
L1UsageMessage | l1-usage | 周期性(仅 MP,每间隔一条) | active_seconds、bytes_used、capacity_bytes |
EnvMessage的整数字段用0表示"未知"(如 CPU 数不可得时)。L2ConnectorUsageMessage的bytes_used在占用探测不可用时为-1;capacity_bytes为0表示该类型所有适配器均无容量上界/未知 —— 此时bytes_used / capacity_bytes不构成有意义的占用率。ContinuousContextMessage.interval_stored_kv_size在 MP 模式是精确字节数,在单进程模式则是"每 token KV 字节数 × 存储 token 数"的估计值。
EngineMessage.from_config与MPServerMessage.from_configs是两个值得细看的构造器:前者从LMCacheEngineConfig+LMCacheMetadata快照引擎配置(blend_recompute_ratio当前硬编码为0.15);后者从MPServerConfig+StorageManagerConfig派生l1_size_bytes(与集群内存视图共享get_configured_capacity_bytes推导,保证一致)、l1_medium("dram"/"gds"/"dram+devdax")以及逗号拼接的l2_adapter_types/l2_serde_types。其中l1_medium的字符串是线上格式,属于不可随意改动的 wire 契约。
六条上报路径:谁在何时发什么
README 的 Reporting Paths 表格完整描述了六条路径,下面结合调用点源码逐一展开:
| 路径 | 触发点 | 消息 |
|---|---|---|
| 单进程一次性 | LMCacheEngine.__init__→InitializeUsageContext | EnvMessage、EngineMessage、MetadataMessage |
| MP 服务器一次性 | run_cache_server→InitializeMPUsageContext | EnvMessage、MPServerMessage |
| 持续上报(单进程) | LMCacheStatsLogger.log_worker,每LMCACHE_USAGE_TRACK_INTERVAL秒(默认 600) | ContinuousContextMessage、CacheLifespanMessage |
| 持续上报(MP 服务器) | EventBus(MP_RETRIEVE_END、MP_STORE_END;仅 lmcache 驱动的传输),每LMCACHE_USAGE_TRACK_INTERVAL秒刷盘 | ContinuousContextMessage;空间隔兼作心跳 |
| L2 连接器用量(MP 服务器) | EventBus 流量(L2_STORE_COMPLETED、L2_LOAD_TASK_SUBMITTED)+ 刷盘时StorageManager占用探测,同一间隔 | 每个活跃 L2 适配器类型一条L2ConnectorUsageMessage |
| L1 用量(MP 服务器) | 刷盘时StorageManager占用探测,同一间隔 | 每间隔一条L1UsageMessage |
调用点证据
单进程路径的入口在 lmcache/v1/cache_engine.py:引擎__init__中调用InitializeUsageContext(config, metadata);MP 服务器路径在 lmcache/v1/multiprocess/server.py 的服务器启动流程中连续注册四个上报器:
InitializeMPUsageContext(mp_config, storage_manager_config) InitializeMPContinuousUsage(event_bus, mp_config.chunk_size) InitializeL2ConnectorUsage(event_bus, ctx.storage_manager) InitializeL1Usage(event_bus, ctx.storage_manager)单进程持续上报:计数器与生命周期直方图
ContinuousUsageContext(continuous.py)由LMCacheStatsLogger.log_worker周期调用incr_or_send_stats(stats)驱动:
- 累计
interval_num_hit_tokens(命中 token,即从 LMCache 取回并服务成功的 token)与interval_num_stored_tokens; - 通过
np.prod(metadata.kv_shape) * kv_dtype.itemsize / kv_shape[2]推导"每 token KV 字节数",从而把存储 token 折算成interval_stored_kv_size估计值;非标准 kv shape 时该值退化为 0(不可用标记); - 收集
stats.interval_request_cache_lifespan样本,按cache_lifespan_buckets(0/1/5/10/20/40/60/80/100/250/500/750/1000/2500/5000 分钟)分桶成直方图; - 距上次发送超过
min_logging_interval就执行send_caching_message():递增sequence_number、发送两条消息(cache-usage+cache-lifespan)、清零计数器。
值得一提的细节:直方图通过np.histogram实现,桶键为"桶下界分钟数",在 JSON 序列化时数字键自动变为字符串(测试test_end_to_end断言 1.0 秒样本落入[1, 5)桶、键为"5")。
MP 持续上报:map-reduce 式 MetricSpec
MP 模式没有 stats-logger,而是把MetricSpec定义成"一个 EventBus 事件 → 一个数值样本 → 归约到一个ContinuousContextMessage字段"的声明式单元(metric_specs.py):
MetricSpec( event_type=EventType.MP_RETRIEVE_END, field="interval_num_hit_tokens", extract=lambda e: int(e.metadata["retrieved_count"]) * chunk_size, reduce=sum, )默认注册表default_metric_specs(chunk_size)给出三个指标,与单进程上报器对齐(parity):
MP_RETRIEVE_END→interval_num_hit_tokens(检索 chunk 数 × chunk_size);MP_STORE_END→interval_num_stored_tokens(存储 chunk 数 × chunk_size);MP_STORE_END→interval_stored_kv_size(total_bytes精确字节)。
MPContinuousUsageReporter(mp_continuous.py)在 EventBus 的 drain 线程上只做缓冲(回调只buffer.append(sample),不发送),由独立刷盘线程归约并发送;缓冲满(默认max_buffered_samples=65536)会wake()刷盘线程提前发送。构造时校验 spec 字段必须恰好覆盖一次ContinuousContextMessage的全部指标字段(sequence_number/uptime_seconds除外),否则抛ValueError。EventBusstop()时执行最终一次刷盘(shutdown→ 停线程 →flush())。
局限(README 明确注明):默认指标只统计MP_RETRIEVE_END/MP_STORE_END,即lmcache 驱动的传输路径;引擎驱动的传输不计入。另外,MP 服务器只有等服务引擎注册 KV 缓存后才知道模型与 KV 布局信息,注册时的一次性报告(MPInstanceMessage)在 README 撰写时列为后续 PR 计划。
L2 连接器用量:按类型聚合流量与占用
L2ConnectorUsageReporter(l2_usage.py)回答"哪些 L2 适配器类型被使用、用了多久、搬了多少数据":
- 流量侧订阅
L2_STORE_COMPLETED(累加bytes_transferred、succeeded_count、failed_count)与L2_LOAD_TASK_SUBMITTED(累加key_count、total_bytes;注意 load 完成事件不带字节数,所以上报的是请求字节数); - 占用侧来自刷盘时对
StorageManager.get_l2_usages_by_type()的探测(L2TypeUsage:bytes_used、capacity_bytes、unbounded_adapters),探测失败时该刷盘报告占用哨兵值; - 每间隔对"当前活跃 ∪ 本间隔有流量"的类型各发一条消息;空闲但已配置的类型仍会上报(使
active_seconds能衡量连接器使用时长),完全无 L2 且无流量的间隔不发任何消息。
L1 用量:刷盘时探针
L1UsageReporter(l1_usage.py)不订阅任何事件(get_subscriptions返回空),仅注册在 EventBus 上以便EventBus.stop()驱动关闭与最终刷盘。每间隔探测StorageManager.get_l1_usage(),发一条L1UsageMessage;探针失败时上报bytes_used=-1/capacity_bytes=0哨兵而不是跳过该间隔。
空间隔 = 心跳
持续上报器在空闲间隔依然发送(reduce必须能接受空序列,sum是常见实现),因此空间隔天然充当存活心跳,让后端能区分"进程死了"与"进程闲着"。
故障隔离:无抛异常(No-Throw)保证
遥测任何环节失败都绝不能影响缓存与推理服务。README 给出从外到内的四层机制,全部有源码佐证:
- 入口装饰器:凡是从服务代码调用的入口 ——
InitializeUsageContext、InitializeMPUsageContext、InitializeMPContinuousUsage、InitializeL2ConnectorUsage、ContinuousUsageContext.incr_or_send_stats、report_once、MP 事件回调、MP 上报器flush—— 都套了guard.swallow_telemetry_errors(guard.py):异常以 debug 级别记录并吞掉,函数返回None。 - 后台线程:一次性上报在 daemon 线程上运行(
threading.Thread(..., daemon=True),线程名lmcache-usage-report),启动流程从不阻塞等待统计服务器。 - 传输层兜底:
UsageMessageSender.send自带 5 秒超时,并吞掉所有传输异常(transport.py 的 try/except Exception)。 - 构造器抗错:可从服务代码触达的构造器在坏输入下不抛异常 —— 非标准 kv shape 退化为 kv-bytes-per-token=0;格式错误的
LMCACHE_USAGE_TRACK_INTERVAL回退默认值 600。这是必须的:ContinuousUsageContext.GetOrCreate在LMCacheStatsLogger.__init__内是无保护调用的(引擎启动路径上),构造器若抛异常会直接炸掉引擎启动。usage_flush_interval_seconds同样用max(interval, 1.0)把间隔钳制到 ≥1 秒。
TestFailureIsolation(tests/test_usage_telemetry.py)完整验证了这套契约:RaisingSender(每次 send 都抛RuntimeError)不能破坏启动报告与 stats-logger 刷盘;不可写的 local_log(传入一个目录)不能破坏报告;UsageMessageSender().send("http://127.0.0.1:1/context", ...)(端口 1 必无监听)不抛异常;非标准 kv shape 退化为 0 字节。
身份与关联契约:匿名 ID 如何串起一条会话
每条 payload(无论一次性还是持续)都被盖章四个公共字段(README 原文 + 源码印证):
session_id:每次进程铸造一次的随机 UUID。关联键:按session_id键控的/context行描述了产生该session_id下所有其他行的部署环境。machine_id:持久化在~/.config/lmcache/machine_id的随机 UUID(identity.py 的_read_or_create_machine_id)。跨重启把同一台机器的多个 session 归组(部署级去重、重启节奏分析)。文件创建失败时为空字符串。schema_version:字段含义变化时提升USAGE_SCHEMA_VERSION,后端按 schema 分区分析。deployment_mode:single_process或mp_server。因为EnvMessage与持续消息在两种模式间共享,不加这个字段就会产生歧义。
持续消息额外携带sequence_number(每个 session 内单调递增)。发送失败直接丢弃该间隔数据而非重试—— 遥测绝不允许累积无界状态 —— 因此序列号空洞是后端判断"间隔丢失"而非"空闲"的信号。
隐私规则
- ID 都是随机 UUID,绝不从硬件派生(无 MAC 地址、主机名或
/etc/machine-id); - MP 服务器的
instance_id被刻意排除在 payload 之外:它是运维可设置的(--instance-id),可能包含可识别的字符串;它只留在面向运维的 OTelservice.instance.id中; - 绝不发送 prompt、密钥或 KV 缓存内容;模型名会上报(模型名标识的是公开的模型架构,而非具体部署)。
退出机制(Opt-out)
is_usage_tracking_enabled()是唯一闸门(identity.py),所有路径统一检查:
LMCACHE_TRACK_USAGE=false(LMCache 专属开关);DO_NOT_TRACK为1/true/yes(跨工具的行业惯例,不区分大小写,"TRUE"同样生效)。
禁用后:工厂函数返回None、持续上报器 no-op、不创建任何状态文件(machine_id不会被写)。注意get_usage_identity的文档特别强调:调用方必须先检查is_usage_tracking_enabled()再取身份,否则被退出的用户也会被写入状态文件。
环境变量完整参考(整理自 docs/source/developer_guide/usage/usage_stats_collection.rst):
| 环境变量 / 文件 | 默认值 | 作用 |
|---|---|---|
LMCACHE_TRACK_USAGE | 未设置 | 设为false关闭全部使用统计采集 |
DO_NOT_TRACK | 未设置 | 设为1/true/yes关闭采集(跨工具惯例) |
LMCACHE_USAGE_TRACK_URL | http://stats.lmcache.ai:8080 | 覆盖统计服务器端点(例如指向私有 sink) |
LMCACHE_USAGE_TRACK_INTERVAL | 600 | 持续消息刷盘间隔(秒) |
~/.config/lmcache/machine_id | 首次发送时创建 | 存放随机匿名机器 UUID,删除文件即可轮换标识 |
usage_server_url(endpoint)对 URL 的处理有一个易踩的坑:urljoin会丢弃不带尾斜杠 base 的最后一段路径,因此实现中先给 base 补/,保证http://host/api/v1+context得到http://host/api/v1/context(测试TestUsageServerUrl用参数化用例验证了三种 base 形态)。
本地日志与数据预览
除了发给统计服务器,遥测还支持本地日志:初始化时传入local_log路径,每条已发送的 payload 会以key: value形式追加写入该文件(UsageContextBase._write_local)。参考 docs/source/developer_guide/usage/usage_stats_collection.rst:
from lmcache.usage_telemetry import InitializeUsageContext usage_ctx = InitializeUsageContext( config=engine_config, metadata=engine_metadata, local_log="~/.config/lmcache/usage.log", )省略local_log(或传None)即禁用本地文件日志。启用后可用tail ~/.config/lmcache/usage.log查看最近发送的条目。官方文档还给出了EnvMessage的示例 JSON payload(字段含云厂商、CPU、内存、GPU、安装来源source等)。
传输边界与测试策略
UsageMessageSender是可注入的传输边界。测试通过注入记录型桩(RecordingSender,记录(url, payload)列表)捕获 payload,任何测试都不得真正访问网络;tests/conftest.py用 autouse fixture 全局设置LMCACHE_TRACK_USAGE=false,确保其他测试套件不会意外 phone-home。TestEndToEnd则反其道:在进程内起一个真实的ThreadingHTTPServersink(UsageSink),用LMCACHE_USAGE_TRACK_URL指过去,验证真实 HTTP 传输下的完整链路(单进程 3 条/context消息、MP 2 条、持续 2 条分别打到/cache-usage与/cache-lifespan)。
扩展指南:如何新增一种消息类型
README 给出的四步流程,结合源码可以给出精确的操作指引:
- 在 messages.py 定义 dataclass:继承
UsageMessage,字段扁平且 JSON 可序列化,声明ENDPOINT(类名自动成为线上message_type)。参考L2ConnectorUsageMessage的实现:列表字段(如l2_adapter_types)在构造时拼成逗号分隔字符串。 - 通过
build_usage_payload发送:让身份与 schema 盖章保持一致;任何新的服务代码入口都要包上swallow_telemetry_errors。若消息属于 MP 服务器路径,还要遵循"MP 独有消息是新消息类型,而不是给共享类加字段"的约定(见 continuous_metrics.md)。 - 与统计服务器 owner 协调:未知
message_type在后端会被静默丢弃。 USAGE_SCHEMA_VERSION只在已有字段含义变化时 bump;新增消息类型或字段不算 schema 变更。
若新增的是 MP 持续指标,还需在 metric_specs.py 增加MetricSpec、在messages.py增加对应字段,spec 字段集合必须恰好覆盖消息的指标字段(构造器会校验,测试test_specs_must_cover_message_fields验证了不完整 spec 抛ValueError);若想改默认的sum归约,测试test_custom_reduce_function展示了自定义 reduce(如max_or_zero)的完整用法。
指标路线图:从计数器到归因分析
持续指标的未来方向记录在 docs/design/usage_telemetry/continuous_metrics.md:
- 已落地(
mp-continuous-counters,PR #4098):map-reduce 上报器,实现与单进程的 parity 计数器 +uptime_seconds; - 规划中:L1 驱逐计数(
L1_KEYS_EVICTED)、注意力架构归因(attn_arch:由AttnWindowDesc.num_chunks_in_sw推导full/full+swa/full+linear,use_mla独立成位)、chunk 复用追踪器(确定性哈希采样hash % R == 0,理想命中率与生命周期分析,哈希绝不出进程)。
后端落库规则(InfluxDB)同样值得注意:session_id是field 而非 tag(防止无界序列增长);发送间隔增量而非累计计数器(重启安全);直方图保持 dict 字段、入库时按le=<bound>打散;发送分子/分母/采样率,绝不发送预计算比值。
结语
LMCache 的匿名使用遥测是一个设计克制的工程范本:messages.py作为线上契约唯一权威、build_usage_payload统一盖章、四层故障隔离保证"遥测永不伤及服务"、随机 UUID + 明确隐私边界守住匿名底线、双环境变量退出开关(含跨工具惯例)。无论你是想深入理解 LMCache 的部署形态统计、在私有环境中搭建自己的上报 sink,还是打算按扩展指南贡献新的消息类型,本文涉及的 lmcache/usage_telemetry/ 全部模块、调用点 与 测试套件 都是可以直接对照阅读的活文档。
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考