1. 线上推理故障排查:从监控曲线到根因的十分钟路径
大模型推理上线之后,真正让人睡不着的不是模型效果,而是凌晨两点监控突然报警:TTFT 从 300ms 飙到 3s、TBT 长尾拖到 200ms、显存 OOM 把进程干掉、吞吐量直接跌到接近零。这三类问题几乎覆盖了所有推理线上事故,也是 27 届大模型岗位面试里高频拷问的"故障排查肌肉记忆"。
这篇聚焦一个具体场景:你手上有一套推理服务,通过 TaoToken 统一 Key 通道调用模型,现在线上出现 TTFT/TBT 抖动、OOM 或吞吐跌零,怎么在十分钟内定位根因并止血。我会给出可复制的 settings.json 与 config.toml 配置骨架、验证请求命令、日志核对动作,以及三类故障的排查树。适合正在准备大模型面试的同学,也适合刚接手推理服务的工程同学。
核心检索词先对齐:TTFT 是首 token 延迟,TBT 是 token 间延迟,OOM 是显存溢出,吞吐跌零是 QPS 或生成 token 数突然掉到接近 0。这四个指标是排查的锚点,后面所有动作都围绕它们展开。
2. TaoToken 统一 Key 通道前置配置
排查线上问题最怕的是"请求根本没到模型"和"请求到了但模型侧异常"分不清。用 TaoToken 统一 Key 通道的好处是:所有推理请求走同一个入口,日志、trace、计费、限流都在一层,排查时能快速区分是通道问题还是模型侧问题。
官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数。你需要先在控制台创建 API Key,控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
拿到 Key 之后,不要急着写业务代码,先把通道连通性验证一遍。这一步能排除掉 80% 的"看起来是模型慢其实是通道超时"的误判。
2.1 settings.json 配置片段
如果你用的是 Claude Code 或类似支持 settings.json 的客户端,配置骨架如下。注意 base_url 指向 TaoToken 的 API 地址,model 字段按你实际要排查的模型填。
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "sk-你的TaoToken密钥", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514", "API_TIMEOUT_MS": "60000" }, "permissions": { "allow": ["Bash", "Read", "Write"] } }这里 API_TIMEOUT_MS 设 60 秒是有讲究的:排查 TBT 长尾时,如果超时设太短,请求会被客户端提前掐断,你看到的是客户端超时而不是模型侧 TBT 异常,根因就找偏了。
2.2 config.toml 配置片段
如果你用的是 Codex 或支持 config.toml 的工具,配置骨架如下:
model_provider = "taotoken" model = "gpt-4o" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat" [request] timeout_ms = 60000 max_retries = 2max_retries 设 2 而不是默认的 5,是因为排查吞吐跌零时,重试会掩盖真实的失败率。你希望看到的是第一次请求就失败,而不是重试三次后"看起来成功"。
2.3 环境变量方式
不想写配置文件的话,直接导出环境变量也能跑:
export TAOTOKEN_API_KEY="sk-你的TaoToken密钥" export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="$TAOTOKEN_API_KEY"配置完成后,先别跑业务,用下面的验证请求确认通道是通的。
3. 可复制的验证请求与指标采集
排查的前提是能观测。先把 TTFT 和 TBT 的打点埋进去,否则你只能看到"慢"但不知道慢在哪一段。
3.1 最小验证请求
用 curl 发一个流式请求,观察首 token 到达时间和 token 间隔:
curl -N -X POST "https://taotoken.net/api/v1/messages" \ -H "Content-Type: application/json" \ -H "x-api-key: $TAOTOKEN_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-sonnet-4-20250514", "max_tokens": 128, "stream": true, "messages": [{"role": "user", "content": "用一句话解释什么是 KV Cache"}] }'-N关闭 curl 缓冲,你能实时看到每个 SSE 事件到达的时间。如果第一个content_block_delta事件迟迟不来,问题在 Prefill 或排队;如果首 token 很快但后续 token 间隔忽大忽小,问题在 Decode 侧。
3.2 Python 打点采集
生产环境用 Prometheus histogram 采集分位数,重点看 p99 而不是均值:
import time import prometheus_client as pc HIST_TTFT = pc.Histogram( "llm_ttft_ms", "Time to first token", buckets=[100, 300, 500, 1000, 2000, 5000] ) HIST_TBT = pc.Histogram( "llm_tbt_ms", "Inter-token latency", buckets=[10, 30, 50, 100, 200, 500] ) class ReqTracer: def __init__(self, req_id): self.req_id = req_id self.t_enq = time.time() self.t_first = None self.t_last = None def on_first_token(self): self.t_first = time.time() HIST_TTFT.observe((self.t_first - self.t_enq) * 1000) def on_token(self): now = time.time() if self.t_last is not None: HIST_TBT.observe((now - self.t_last) * 1000) self.t_last = now均值会掩盖长尾。一个 8k token 的长请求 Prefill 可能几百毫秒,它会把同窗口所有请求的 TTFT 拉高,但均值只涨一点点,p99 才会暴露真相。
3.3 日志核对动作
每次请求在日志里至少留四个时间戳:enqueue_time、first_token_time、last_token_time、finish_time。排查时按 request_id 捞一条慢请求的完整时间线,对照监控曲线看它落在哪个异常窗口。如果日志里 first_token_time 缺失,说明请求根本没进 Prefill,问题在网关或排队层。
4. 三类故障的排查树与根因定位
4.1 TTFT 抖动排查
TTFT 高通常卡在 Prefill 阶段。Prefill 是计算密集且不可抢占的,一个长 prompt 会阻塞同窗口所有请求。排查顺序:
先看调度队列深度和排队时长直方图,如果排队时间占 TTFT 的大头,说明并发超过容量,需要限流或扩容。再看 prompt token 数分布,如果 p99 prompt 长度远高于均值,长请求就是元凶。然后确认 chunked prefill 是否开启,没开的话长 Prefill 会独占计算资源。
# vLLM 开启 chunked prefill 的启动参数示意 --enable-chunked-prefill \ --max-num-batched-tokens 8192 \ --scheduler-policy fcfschunked prefill 把 Prefill 切成小块与 Decode 交织,长请求不再独占,TTFT 长尾会明显改善。如果条件允许,PD 分离把 Prefill 和 Decode 放到不同实例池,效果更彻底。
4.2 TBT 长尾排查
TBT 高卡在 Decode 阶段。Decode 每步只算一个新 token,计算量小但受 KV Cache 带宽和批大小支配。常见根因和对策:
批内混入超长序列,KV 挤占带宽,表现为同批 max_len 远高于均值,解法是按长度分组调度。KV Cache 碎片化,显存够但申请失败,换 paged KV 或 prefix cache。PD 分离后 Decode 实例等 KV 传输,优化 KV 传输或本地化。某卡降频掉速,单卡 TBT 明显偏高。
import pynvml pynvml.nvmlInit() for i in range(pynvml.nvmlDeviceGetCount()): h = pynvml.nvmlDeviceGetHandleByIndex(i) util = pynvml.nvmlDeviceGetUtilizationRates(h) clk = pynvml.nvmlDeviceGetClockInfo(h, pynvml.NVML_CLOCK_SM) pwr = pynvml.nvmlDeviceGetPowerUsage(h) / 1000.0 print(f"gpu{i} util={util.gpu}% sm_clk={clk}MHz pwr={pwr}W")如果某张卡的 sm_clk 远低于其他卡,基本可以锁定是降频或掉速,这是 TBT 长尾的隐形元凶。我试过在压测时逐卡巡检,抓到过一张卡因为散热问题降频到 800MHz,其他卡都在 1800MHz,TBT 长尾全来自它。
4.3 OOM 显存溢出排查
OOM 是上线最常见的硬故障,长上下文和多模态尤其容易炸。先算 KV Cache 的理论占用:
def est_kv_bytes(n_layers, n_kv_heads, head_dim, seq, batch, dtype_bytes=2): per_tok = 2 * n_layers * n_kv_heads * head_dim * dtype_bytes return per_tok * seq * batch # 7B 模型 32 层 32 头 dim128, seq=4096, batch=64 print(est_kv_bytes(32, 32, 128, 4096, 64) / 1e9, "GB")据此倒推 max_num_seqs,避免预留超显存。排查树:静态 KV 预留过大就下调 max_num_seqs 和 max_model_len;单请求超长就设 max_input_len 上限并拒绝或截断;激活峰值高就降 max_num_batched_tokens;碎片化导致有总显存但无连续块,用 paged KV 和 prefix cache 复用;多模型共卡就隔离部署或限制并发。
4.4 吞吐跌零排查
吞吐突然掉到接近 0,通常是软死锁而非硬件坏。现象和对策对照:
请求全部卡住,多半是调度死锁或某请求异常占批,超时踢出加重启 worker。QPS 为 0 但进程还在,查上游断流和健康检查误杀。GPU 利用率 0 但显存满,基本是 KV 泄漏,请求未释放,修请求生命周期加 watchdog。吞吐阶梯下跌,扩缩容抖动或权重加载阻塞,预热加灰度发布。
import asyncio async def guarded_generate(engine, req, max_tokens=2048, timeout=60): try: return await asyncio.wait_for( engine.generate(req, max_tokens=max_tokens), timeout=timeout ) except asyncio.TimeoutError: engine.abort(req.req_id) # 必须显式 abort 释放 KV raise TimeoutError(f"req {req.req_id} aborted after {timeout}s")关键工程实践:给每个请求设硬超时和最大生成长度,请求结束时强制释放 KV 块,用 watchdog 监控"显存占用 vs 活跃请求数"的偏离度,偏离即告警。
5. 本篇常见错排查
配置和排查过程中,有几个坑反复出现,单独列出来。
第一个坑是 base_url 写错。TaoToken 的 API 地址是 https://taotoken.net/api ,不要带 UTM 参数,也不要写成官网首页地址。写成首页会导致请求 404,但报错信息可能被客户端包装成"模型不可用",让你误判成模型侧问题。
第二个坑是超时设太短。排查 TBT 长尾时,客户端超时如果设成 10 秒,长请求会被提前掐断,你看到的是客户端超时而非模型侧 TBT 异常。建议排查阶段把超时设到 60 秒以上。
第三个坑是只看均值不看 p99。TTFT 均值 400ms 看起来健康,但 p99 可能 3s,长尾用户已经在投诉了。所有指标都要看分位数。
第四个坑是重试掩盖失败率。排查吞吐跌零时,客户端默认重试 5 次会让你看到"成功率 100%",但实际第一次请求全失败。排查阶段把 max_retries 降到 1 或 2。
第五个坑是忘了 abort 释放 KV。请求超时后如果不显式 abort,KV 块不会释放,显存慢慢泄漏,最后 OOM。上面 guarded_generate 里的 engine.abort 不能省。
第六个坑是日志缺时间戳。没有 enqueue_time 和 first_token_time,你无法区分是排队慢还是 Prefill 慢。埋点要埋全。
6. 面试速答与后续动作
面试里被问到 TTFT 高查什么,答:先看是不是 Prefill 阶段排队或长 prompt 未切分,再确认 chunked prefill 或 PD 分离是否开启,最后看实例冷启动。核心是 Prefill 计算密集且不可抢占,长请求会阻塞同窗口。
被问到 TBT 长尾但 GPU 利用率不低,答:多半是批内混入超长序列挤占 KV 带宽,或某卡降频掉速,用 pynvml 逐卡看 SM 时钟和功耗往往能抓到单卡异常。
被问到 OOM 但显存总量够,答:KV Cache 碎片化,没有连续块可分配,用 paged KV 和 prefix cache 复用解决,并下调 max_num_seqs。
被问到吞吐跌零怎么止血,答:先确认是否请求级死锁或 KV 泄漏,给请求加硬超时和 abort 释放,显存满但利用率 0 基本是 KV 泄漏,重启 worker 并修生命周期。
后续动作上,如果你要长期跑编码类 Agent 做推理压测和故障复现,可以看 Coding Plan 页面 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ;如果只是想快速验证某个模型在异常流量下的表现,用模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 更直接;接入细节和参数说明在接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。把这篇的排查树和配置骨架存下来,下次线上报警时按顺序走一遍,十分钟定位根因不是口号。