ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大模型推理可观测性实战:追踪Token消耗与延迟

大模型推理可观测性实战:追踪Token消耗与延迟 大模型推理服务的账单和性能问题往往不是模型不行而是看不见。我见过太多团队在本地跑得好好的模型一上生产就出现响应忽快忽慢、Token 消耗远超预算、GPU 利用率忽高忽低的情况排查起来全靠猜。问题的根源在于传统 APM 工具是为 HTTP 请求设计的它能看到这个接口花了 3 秒但看不到这 3 秒里prefill 占了多少、decode 占了多少、KV Cache 命中率如何、输入输出各消耗了多少 Token。大模型推理的可观测性需要一套完全不同的埋点思路和指标体系。这篇文章围绕追踪每一次推理的 Token 消耗与延迟这个核心目标展开把我在实际项目中搭建推理可观测性体系的完整思路拆开来讲。从指标怎么设计、埋点埋在哪里、数据怎么采集和存储到怎么用这些数据定位性能瓶颈、控制成本、做容量规划都会给出可落地的方案和代码示例。不管你是用 vLLM、TGI 还是自己写的推理服务这套方法论都能直接套用。适合正在做大模型部署、推理优化、成本控制的工程师和运维同学参考。1. 为什么传统监控在大模型推理场景下会失效1.1 从一次诡异的延迟抖动说起之前接手过一个线上推理服务用户反馈有时候快有时候慢但监控面板上 P99 延迟看起来还行。打开传统的接口监控确实只能看到一个总的请求耗时曲线平均值 1.2 秒P99 在 4 秒左右看起来没有明显异常。但用户的实际体感是同样的问题有时候 0.5 秒就回来了有时候要等七八秒。后来我们在推理引擎内部加了细粒度埋点才发现问题这个服务的请求输入长度差异极大短的几十个 Token长的能到 8000 多 Token。传统监控把这两种请求混在一起统计平均值自然被拉平了。真正的问题在于长输入请求触发了 prefill 阶段的排队而排队时间没有被单独度量全部被算进了总延迟里。更关键的是长输入请求的 Token 消耗是短请求的上百倍但监控里完全看不出来——因为传统 APM 根本不认识 Token 这个维度。这就是大模型推理监控的第一个核心矛盾请求的重量差异巨大但传统监控把所有请求当成等价的。一个 50 Token 的请求和一个 8000 Token 的请求在 HTTP 层面看起来都是一次请求但在 GPU 层面它们的计算量、显存占用、耗时可能差两个数量级。1.2 大模型推理的三个阶段各有各的瓶颈要做好可观测性首先得理解推理过程到底分几个阶段。一个典型的大模型推理请求在引擎内部会经历三个阶段排队阶段Queueing请求到达后如果 GPU 正在处理其他请求就需要排队等待。这个阶段的耗时取决于当前并发量、批处理策略continuous batching 还是 static batching、以及调度器的实现。vLLM 用的是 continuous batching新请求可以在当前 batch 的某个序列生成结束后立即插入所以排队时间通常比 static batching 短但在高并发下依然会显著增长。预填充阶段Prefill把输入的 prompt 一次性喂给模型计算出所有输入 Token 的 KV Cache。这个阶段的耗时和输入长度基本成正比而且计算密集GPU 利用率会拉满。对于长输入请求prefill 可能是整个推理过程中最耗时的部分。解码阶段Decode逐个生成输出 Token每生成一个 Token 都要做一次前向计算。这个阶段是显存带宽密集型的因为每步都要读取整个 KV Cache。输出越长decode 阶段耗时越长。这三个阶段的瓶颈完全不同排队看调度策略prefill 看算力decode 看显存带宽。如果不分开度量你根本不知道延迟到底卡在哪一环。我见过一个案例团队一直以为是 GPU 算力不够疯狂加卡结果加了卡之后延迟没降多少——因为真正的瓶颈是调度器的排队策略不合理长请求把短请求堵死了。1.3 Token 消耗被忽视的成本黑洞延迟问题看得见Token 消耗问题往往是账单来了才发现。大模型推理的成本直接和 Token 数量挂钩但很多团队在服务上线初期根本没有按 Token 维度做统计。我遇到过最夸张的一个案例某个内部工具接了大模型 API做文档摘要。上线一个月后账单暴涨排查发现是前端有个 bug在某些情况下会把同一份文档重复提交十几次。因为没有任何 Token 维度的监控这个问题拖了整整一个月才被发现。即使是用自建推理服务Token 消耗也直接关系到 GPU 资源的消耗。输入 Token 决定 prefill 的计算量输出 Token 决定 decode 的步数两者共同决定了这个请求占用了多少 GPU 时间。如果你能精确追踪每个请求、每个用户、每个业务线的 Token 消耗就能做精细化的成本分摊和容量规划。提示Token 统计一定要区分 input_tokens 和 output_tokens因为它们的成本模型完全不同。输入 Token 影响 prefill 耗时输出 Token 影响 decode 耗时在计费和优化时不能混为一谈。2. 推理可观测性的指标体系该怎么设计2.1 四层指标体系从请求到 GPU设计指标体系的核心原则是每一层都要能回答一个具体的排查问题。我把推理可观测性分成四层每层解决不同的问题。层级核心指标回答的问题请求层请求总数、成功率、端到端延迟、TTFT、TPOT用户体验怎么样有没有报错Token 层input_tokens、output_tokens、总 Token 数、Token 吞吐率成本花在哪效率高不高引擎层排队时长、prefill 时长、decode 时长、batch size、KV Cache 命中率延迟卡在哪个阶段资源层GPU 利用率、显存占用、显存带宽、功耗硬件是不是瓶颈这四层不是孤立的而是可以下钻关联的。比如请求层发现 P99 延迟升高下钻到引擎层发现是排队时长增加再下钻到资源层发现 GPU 显存快满了导致 batch size 上不去。这种逐层下钻的能力才是可观测性的真正价值。2.2 TTFT 和 TPOT大模型推理的两个黄金指标在请求层有两个指标比端到端延迟更有诊断价值TTFTTime To First Token首 Token 延迟和TPOTTime Per Output Token每输出 Token 耗时。TTFT 衡量的是从请求发出到收到第一个 Token 的时间它主要反映了排队时长加 prefill 时长。对于流式输出的场景用户感知到的响应快不快很大程度上取决于 TTFT。TTFT 高说明要么在排队要么 prefill 太慢输入太长或算力不足。TPOT 衡量的是生成阶段每个 Token 的平均耗时它主要反映 decode 阶段的效率。TPOT 高说明显存带宽可能是瓶颈或者 batch size 太大导致单步计算变慢。对于长文本生成场景TPOT 直接决定了用户要等多久才能看到完整回答。这两个指标分开看能快速定位问题方向。我一般会这样判断TTFT 高但 TPOT 正常问题在排队或 prefill检查并发量和输入长度分布。TTFT 正常但 TPOT 高问题在 decode检查显存带宽和 batch 配置。两个都高可能是整体资源不足或者请求分布发生了剧烈变化。2.3 分位数比平均值重要一百倍大模型推理的延迟分布是典型的长尾分布平均值几乎没有参考价值。一个服务可能平均延迟 1 秒但 P99 是 15 秒——那 1% 的用户体验极差而平均值完全掩盖了这个问题。所以指标采集必须记录分位数至少要有 P50、P90、P95、P99。在 Prometheus 里用 Histogram 类型来记录延迟分布查询时用 histogram_quantile 函数计算分位数。分桶的边界要根据实际延迟范围来设置我一般会用这样的桶0.05、0.1、0.25、0.5、1、2.5、5、10、30、60 秒覆盖从快到慢的完整范围。注意分位数是有时间窗口的P99 在低流量时段可能波动很大。建议同时看多个时间窗口如 5 分钟和 1 小时避免被瞬时抖动误导。2.4 按维度切分让指标能回答是谁的问题光有全局指标还不够必须能按维度切分。至少要有这几个维度模型维度不同模型的延迟和 Token 消耗差异巨大必须分开统计。用户/租户维度多租户场景下要能定位是哪个用户在消耗资源。请求类型维度对话、摘要、翻译等不同任务的输入输出长度分布完全不同。输入长度分桶把请求按输入长度分成几档如 0-128、128-512、512-2048、2048分别统计延迟。这些维度组合起来才能回答是哪个模型的哪类请求在什么输入长度下出现了延迟升高这种具体问题。没有维度切分的监控就像没有分类的账本只能看到总数看不到结构。3. 埋点实操在推理引擎的哪些位置打点3.1 请求入口记录原始请求信息埋点的第一站是请求入口也就是请求刚到达服务、还没进入推理引擎的时候。这里要记录的信息包括请求 ID、时间戳、模型名称、输入 Token 数如果能在入口就算出来的话、用户标识、请求类型等。输入 Token 数的计算需要注意不同模型的 tokenizer 不一样必须在入口处用对应模型的 tokenizer 来算。如果入口处拿不到 tokenizer也可以先记录原始文本长度后续在引擎内部再补算精确的 Token 数。但精确的 Token 数对成本统计很重要建议尽量在入口就算准。import time import uuid from prometheus_client import Histogram, Counter # 定义指标 REQUEST_LATENCY Histogram( llm_request_latency_seconds, End-to-end request latency, [model, request_type], buckets[0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60] ) TTFT Histogram( llm_ttft_seconds, Time to first token, [model], buckets[0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] ) TOKEN_COUNTER Counter( llm_tokens_total, Total tokens processed, [model, token_type, user_id] # token_type: input/output ) def handle_request(request): request_id str(uuid.uuid4()) start_time time.time() # 计算输入 Token 数 input_tokens count_tokens(request.prompt, request.model) TOKEN_COUNTER.labels( modelrequest.model, token_typeinput, user_idrequest.user_id ).inc(input_tokens) # 记录请求元信息用于后续关联 request_context { request_id: request_id, start_time: start_time, input_tokens: input_tokens, model: request.model, user_id: request.user_id } return request_context这段代码的关键点是请求 ID 要贯穿整个推理过程后续所有埋点都带上这个 ID这样才能把一次请求在各个阶段的耗时和 Token 数关联起来。3.2 引擎内部拆解排队、prefill、decode引擎内部的埋点是整个体系的核心也是最难做的部分因为需要修改或扩展推理引擎的代码。以 vLLM 为例它的调度器和执行器内部有明确的阶段划分可以在这些位置插入埋点。如果不想改引擎源码一个折中方案是利用引擎暴露的 metrics 接口。vLLM 本身就暴露了 Prometheus 格式的指标包括vllm:time_to_first_token_seconds、vllm:time_per_output_token_seconds、vllm:num_requests_running、vllm:num_requests_waiting等。这些指标虽然粒度不如自己埋点细但胜在开箱即用适合快速搭建基础监控。如果要自己埋点关键是在这几个位置打时间戳# 伪代码展示埋点位置 class InstrumentedScheduler: def schedule(self, request): # 埋点 1请求进入调度队列 request.queue_start time.time() metrics.record_queue_start(request.id) def execute_prefill(self, batch): # 埋点 2prefill 开始 prefill_start time.time() result self.model.prefill(batch) # 埋点 3prefill 结束 prefill_duration time.time() - prefill_start for req in batch.requests: metrics.record_prefill(req.id, prefill_duration) return result def execute_decode(self, batch): # 埋点 4每个 decode step step_start time.time() output self.model.decode(batch) step_duration time.time() - step_start for req in batch.requests: metrics.record_decode_step(req.id, step_duration) return output这里有个实操中的坑prefill 和 decode 在 continuous batching 下是混在一起的。一个 batch 里可能既有正在 prefill 的新请求又有正在 decode 的老请求。所以不能简单地按 batch 来记录 prefill 和 decode 时长而要按请求来记录。vLLM 内部会把每个请求的状态标记清楚埋点时要跟着请求的状态走而不是跟着 batch 走。3.3 输出阶段统计输出 Token 和完成时间输出阶段的埋点相对简单主要是统计输出 Token 数和请求完成时间。但有一个细节要注意流式输出场景下输出 Token 是逐个产生的要在每个 Token 产生时记录时间戳这样才能算出 TPOT。def stream_output(request_context, output_stream): output_tokens 0 first_token_time None token_times [] for token in output_stream: now time.time() if first_token_time is None: first_token_time now # 记录 TTFT TTFT.labels(modelrequest_context[model]).observe( now - request_context[start_time] ) token_times.append(now) output_tokens 1 yield token # 请求完成 end_time time.time() REQUEST_LATENCY.labels( modelrequest_context[model], request_typerequest_context.get(request_type, unknown) ).observe(end_time - request_context[start_time]) # 统计输出 Token TOKEN_COUNTER.labels( modelrequest_context[model], token_typeoutput, user_idrequest_context[user_id] ).inc(output_tokens) # 计算 TPOT if output_tokens 1: tpot (token_times[-1] - first_token_time) / (output_tokens - 1) TPOT.labels(modelrequest_context[model]).observe(tpot)TPOT 的计算要注意分母是output_tokens - 1因为第一个 Token 的时间已经算在 TTFT 里了从第二个 Token 开始才是真正的 decode 间隔。3.4 埋点的性能开销别让监控拖慢推理埋点本身是有开销的尤其是高频的 decode step 埋点。如果每个 Token 都做一次 Prometheus 的 observe 操作在高并发下可能成为瓶颈。我实测过在 QPS 500、平均输出 200 Token 的场景下如果每个 Token 都同步写 Prometheus会额外增加 3%-5% 的 CPU 开销。优化方案有几个批量聚合在内存里先聚合每隔 100ms 或每 100 个 Token 批量 flush 一次。异步写入把指标写入放到单独的线程或协程里不阻塞推理主流程。采样对高频指标做采样比如每 10 个请求采样 1 个做细粒度埋点其余只记录粗粒度指标。提示埋点开销一定要实测。我的经验是埋点带来的额外延迟不应超过总延迟的 2%否则就得不偿失。如果超过优先考虑采样或异步方案。4. 数据采集、存储与可视化链路搭建4.1 Prometheus Grafana最省事的起步方案如果不想自己造轮子Prometheus Grafana 是最成熟的方案。推理服务暴露/metrics接口Prometheus 定时抓取Grafana 做可视化。这套方案的优势是生态成熟、查询语言强大、告警配置方便。配置上关键是设计好指标的 label 维度。label 太多会导致时间序列爆炸label 太少又没法下钻。我的经验是模型名、请求类型、用户 ID 这三个 label 是必须的但用户 ID 如果量很大比如上万用户建议做哈希分桶或者只保留 top N 用户避免时间序列数量失控。# prometheus.yml 抓取配置 scrape_configs: - job_name: llm-inference scrape_interval: 15s static_configs: - targets: [inference-service:8000] metrics_path: /metricsGrafana 面板建议至少包含这几个图请求量和成功率、TTFT 和 TPOT 的分位数曲线、Token 消耗速率按输入输出分开、排队请求数、GPU 利用率和显存占用。这几个图放在一起基本能覆盖 80% 的日常排查场景。4.2 高基数问题的处理当用户 ID 成为负担Prometheus 最大的坑就是高基数high cardinality。如果把用户 ID 作为 label每个用户都会产生一组独立的时间序列用户一多Prometheus 的内存和存储就会爆炸。我见过一个案例因为把 request_id 当成了 label导致 Prometheus 内存直接打满。处理高基数有几个策略用户 ID 分桶把用户按哈希分成 100 个桶label 用桶编号而不是原始 ID。分离存储高频指标如全局延迟放 Prometheus低频但需要精确归因的数据如每个用户的 Token 消耗放 ClickHouse 或 TDengine 这类列式数据库。预聚合在服务端先按维度聚合只把聚合结果暴露给 Prometheus。对于 Token 消耗这种需要精确归因到用户的数据我强烈建议用 ClickHouse 单独存。每次请求完成后把 request_id、user_id、model、input_tokens、output_tokens、各阶段耗时等字段写一条记录进去。ClickHouse 的写入吞吐和聚合查询性能都很强适合这种场景。-- ClickHouse 建表 CREATE TABLE llm_inference_log ( request_id String, user_id String, model String, request_type String, input_tokens UInt32, output_tokens UInt32, queue_duration_ms Float32, prefill_duration_ms Float32, decode_duration_ms Float32, ttft_ms Float32, tpot_ms Float32, total_duration_ms Float32, created_at DateTime DEFAULT now() ) ENGINE MergeTree() ORDER BY (created_at, model, user_id);有了这张表就能做各种灵活的聚合查询比如查某个用户过去 7 天的 Token 消耗趋势、查某个模型在不同输入长度下的平均 TTFT这些在 Prometheus 里做起来很别扭的查询在 ClickHouse 里就是一句 SQL。4.3 链路追踪把一次请求的完整生命周期串起来指标能告诉你哪里慢了但有时候你需要知道这一次具体的请求到底经历了什么。这时候就需要链路追踪Tracing。用 OpenTelemetry 给每个请求生成一个 trace把排队、prefill、decode 各个阶段作为 span 记录下来就能在 Jaeger 或 Tempo 里看到完整的调用链。from opentelemetry import trace tracer trace.get_tracer(llm-inference) def process_request(request): with tracer.start_as_current_span(llm_request) as span: span.set_attribute(model, request.model) span.set_attribute(input_tokens, request.input_tokens) with tracer.start_as_current_span(queue) as queue_span: queue_span.set_attribute(queue_position, get_queue_position()) wait_in_queue() with tracer.start_as_current_span(prefill) as prefill_span: prefill_span.set_attribute(prefill_tokens, request.input_tokens) do_prefill() with tracer.start_as_current_span(decode) as decode_span: output do_decode() decode_span.set_attribute(output_tokens, len(output)) span.set_attribute(total_tokens, request.input_tokens len(output))链路追踪的采样率要控制好全量采集在高 QPS 下开销很大。一般用尾部采样tail sampling只保留慢请求和错误请求的完整 trace正常请求只保留指标。4.4 告警规则什么时候该叫人起来监控的最终目的是告警。但告警规则设计不好要么天天误报让人麻木要么真出事了不响。我的经验是告警要分级别并且基于用户可感知的问题而不是内部指标异常。告警级别触发条件响应方式P0成功率低于 95% 持续 2 分钟立即电话P1P99 TTFT 超过 5 秒持续 5 分钟即时消息P2Token 消耗速率超过预算 150%工单P3GPU 利用率持续低于 20%日报注意 P0 和 P1 的区别成功率是硬指标掉了就是用户用不了TTFT 升高是体验问题可能还能忍。分级的好处是避免所有问题都触发最高级别告警导致真正的紧急问题被淹没。5. 用可观测性数据定位真实性能瓶颈5.1 案例一长输入请求拖垮短请求回到开头那个延迟忽快忽慢的案例。加了细粒度埋点后我们把请求按输入长度分桶画出了不同桶的 TTFT 曲线。结果非常清晰输入长度小于 512 的请求TTFT 稳定在 200ms 左右输入长度大于 2048 的请求TTFT 在 3-8 秒之间剧烈波动。进一步看排队时长发现长请求的排队时长占了 TTFT 的 80% 以上。原因是 vLLM 的调度器在 batch 快满时会优先让已经在跑的请求继续 decode新来的长请求要等更久才能插入。而长请求的 prefill 又特别占时间一旦开始 prefill又会阻塞其他请求。解决方案有两个方向一是给长请求单独开一个推理实例做请求隔离二是调整调度参数比如限制单个 batch 的最大 prefill Token 数避免一个长请求独占整个 batch。我们最后选了方案一因为隔离最彻底虽然成本高一点但短请求的 TTFT 直接降到了 150ms 以内用户体验提升明显。5.2 案例二Token 消耗异常增长的排查过程另一个案例是 Token 消耗突然增长。某天早上Token 消耗速率曲线突然翘头比平时高了 3 倍。因为有了按用户维度的 Token 统计我们很快定位到是某个内部服务在疯狂调用。下钻到请求类型发现全是同一个 prompt 模板的请求输入长度都在 3000 Token 左右。再查这个服务的日志发现它在一个循环里调用推理接口而且没有做结果缓存。原来是一个定时任务出了 bug本该每天跑一次的任务变成了每分钟跑一次。这个问题的排查过程只花了 15 分钟完全归功于 Token 维度的监控。如果没有这套体系可能要等到月底账单出来才发现损失就大了。5.3 案例三GPU 利用率高但吞吐上不去还有一个反直觉的案例GPU 利用率显示 95%看起来已经跑满了但吞吐量就是上不去。按常理GPU 利用率高说明算力用满了吞吐应该也高才对。细看指标才发现问题GPU 利用率高但显存带宽利用率只有 40%而且 decode 阶段的 TPOT 明显偏高。这说明 GPU 大部分时间在等显存而不是在算。进一步查 batch size发现 batch 被限制得很小因为显存不够——KV Cache 占用了大量显存留给 batch 的空间不多。解决方案是启用 KV Cache 量化把 KV Cache 从 FP16 降到 INT8显存占用直接减半batch size 翻倍吞吐量提升了 60%。这个案例说明GPU 利用率是个会骗人的指标必须结合显存带宽、batch size、TPOT 一起看才能判断真正的瓶颈。5.4 从指标到行动建立排查决策树积累了一定经验后可以把排查思路固化成决策树让团队里任何人都能快速定位问题先看成功率掉了就是服务问题查错误日志。成功率正常看 TTFT高了查排队和 prefill看并发量和输入长度分布。TTFT 正常看 TPOT高了查 decode看显存带宽和 batch 配置。都正常看 Token 消耗异常增长查调用方异常下降查是否有请求被丢弃。都正常但用户仍反馈慢查网络和客户端可能是端到端链路上其他环节的问题。这棵决策树不是万能的但能覆盖大部分常见问题大大缩短排查时间。6. 成本归因与容量规划让可观测性产生业务价值6.1 按业务线分摊 Token 成本可观测性数据最有价值的应用之一是把 Token 成本精确分摊到各个业务线。做法很简单在请求入口记录业务线标识然后按业务线聚合 Token 消耗乘以单价就是每个业务线的推理成本。这件事的意义在于它把技术成本变成了业务成本让业务方有动力去优化自己的调用方式。我见过一个团队做了成本分摊之后各个业务线主动来问怎么减少 Token 消耗因为他们要为自己的成本负责。优化手段包括精简 prompt、做结果缓存、调整输出长度限制等整体成本降了 30% 多。6.2 用历史数据做容量规划容量规划的核心问题是当前资源能支撑多少 QPS什么时候需要扩容。有了历史指标数据这个问题就能量化回答。具体做法是统计不同负载下的 TTFT 和 TPOT 变化曲线找到延迟开始明显上升的拐点这个拐点对应的 QPS 就是当前配置的容量上限。然后根据业务增长趋势预测什么时候会达到这个上限提前扩容。# 简化的容量预测逻辑 def predict_capacity(history_data, target_growth_rate): # 找到延迟拐点 qps_points sorted(history_data, keylambda x: x[qps]) capacity_limit None for point in qps_points: if point[p99_ttft] 2.0: # TTFT 超过 2 秒视为拐点 capacity_limit point[qps] break # 预测达到上限的时间 current_qps qps_points[-1][qps] days_to_limit 0 while current_qps capacity_limit: current_qps * (1 target_growth_rate / 365) days_to_limit 1 return capacity_limit, days_to_limit这个逻辑虽然简化但思路是对的用延迟拐点定义容量用增长趋势预测扩容时间。比拍脑袋定再加两张卡科学得多。6.3 识别浪费那些被忽视的低效调用可观测性数据还能帮你发现浪费。常见的浪费模式有几种超长输出有些请求的输出长度远超实际需要比如用户只要一句话摘要模型却生成了 500 字。通过统计输出长度分布可以设置合理的 max_tokens 限制。重复请求相同或相似的 prompt 被反复调用可以通过缓存来避免。统计 prompt 的哈希分布能发现重复率。低效 prompt有些 prompt 的输入 Token 很多但实际有效信息很少比如塞了大量无关的上下文。通过分析输入 Token 和输出质量的关系可以优化 prompt 设计。这些优化不需要改模型只需要改调用方式但效果往往立竿见影。我做过一个统计在一个典型业务里通过识别和消除这三类浪费Token 消耗能降低 20%-40%。6.4 建立成本预算和告警机制最后一步是把成本控制变成常态化的机制。给每个业务线设置 Token 预算当消耗速率超过预算时触发告警。预算可以按月设置也可以按日设置看业务特点。告警的阈值设置有讲究太松了没意义太紧了天天误报。我的经验是用过去 30 天的日均消耗作为基线预算设为基线的 120%告警阈值设为预算的 90%。这样既能提前预警又不会因为正常的日常波动而误报。提示成本告警一定要区分增长和异常增长。业务正常增长导致的消耗上升是好事不应该告警只有突发的、无对应业务增长的消耗上升才需要告警。可以在告警规则里加入业务指标的关联判断比如Token 消耗增长 50% 且订单量没有增长才触发告警。7. 落地这套体系时踩过的坑7.1 埋点位置选错数据全废最开始做埋点时我把 prefill 和 decode 的计时放在了 batch 层面而不是请求层面。结果数据出来后发现所有请求的 prefill 时长都一样——因为它们是同一个 batch共享同一个 prefill 时间。但实际每个请求的 prefill 计算量是不同的长请求的 prefill 应该更久。这个坑的教训是埋点必须跟着请求走而不是跟着 batch 走。在 continuous batching 下一个 batch 里的请求状态各不相同必须按请求维度记录。后来改成在每个请求的状态转换时打时间戳数据才准确。7.2 时间戳的精度和时钟问题另一个坑是时间戳精度。Python 的time.time()精度在毫秒级对于 TTFT 这种可能只有几十毫秒的指标来说精度不够。后来改用time.perf_counter()它是单调时钟精度到微秒级而且不受系统时间调整的影响。还有一个分布式场景下的时钟同步问题。如果推理服务和埋点采集服务在不同机器上机器之间的时钟偏差会导致数据错乱。解决方案是用请求 ID 关联而不是依赖时间戳排序或者用 NTP 做时钟同步把偏差控制在毫秒级以内。7.3 指标爆炸导致 Prometheus 崩溃前面提过高基数问题这里再强调一次。我踩过的具体坑是把request_id作为 label 加到了指标里。结果每个请求都产生一组独立的时间序列Prometheus 内存几小时内就涨到了几十 GB最后直接 OOM。修复方案是把request_id从 label 里去掉只保留在日志和 ClickHouse 里。Prometheus 的 label 只保留低基数的维度模型名、请求类型、状态码。高基数的归因数据全部走 ClickHouse。7.4 采样策略不当关键数据丢失为了降低开销我们一度对所有指标做了 10% 采样。结果出问题的时候发现慢请求的 trace 全都没被采样到——因为慢请求本来就是少数10% 采样后几乎全丢了。后来改成尾部采样正常请求采样 1%慢请求P99 以上和错误请求 100% 采集。这样既控制了开销又保证了关键数据不丢。尾部采样的实现需要在请求完成后才能决定是否采集所以要在内存里先缓存 trace 数据请求结束后再决定是否上报。7.5 监控面板太多没人看最后一个坑是监控面板泛滥。一开始大家很兴奋给每个指标都做了面板结果几十个面板没人看得过来。真正出问题的时候反而不知道该看哪个。后来我们做了精简只保留三个核心面板总览面板成功率、QPS、TTFT/TPOT 分位数、Token 消耗速率、性能排查面板排队/prefill/decode 分解、按输入长度分桶的延迟、成本面板按业务线和用户的 Token 消耗。每个面板都有明确的用途出问题时按决策树依次查看效率高很多。监控这件事少即是多。与其做一百个没人看的面板不如做三个真正会被用起来的面板。这套推理可观测性体系从零搭建到稳定运行大概花了我们两个月时间中间踩了不少坑但收益是实打实的延迟问题的平均排查时间从几小时缩短到十几分钟Token 成本降低了 30% 多容量规划从拍脑袋变成了数据驱动。如果你也在做推理服务建议尽早把这套体系建起来越早建省下的排查时间和成本越多。
返回列表