Carry の Blog Carry の Blog
首页
关于
  • Nginx
  • Prometheus
  • Iptables
  • Systemd
  • Firewalld
  • Docker
  • Sshd
  • DBA工作笔记
  • MySQL
  • Redis
  • TiDB
  • Elasticsearch
  • OpenClaw
  • Hermes Agent
  • Claude Code
  • MySQL8-SOP手册
  • MySQL实战45讲学习笔记
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Carry の Blog

好记性不如烂键盘
首页
关于
  • Nginx
  • Prometheus
  • Iptables
  • Systemd
  • Firewalld
  • Docker
  • Sshd
  • DBA工作笔记
  • MySQL
  • Redis
  • TiDB
  • Elasticsearch
  • OpenClaw
  • Hermes Agent
  • Claude Code
  • MySQL8-SOP手册
  • MySQL实战45讲学习笔记
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • OpenClaw

  • Hermes-Agent

  • Claude-Code

  • LLM推理服务

    • vLLM 推理节点运维:容量怎么算、健康检查怎么做、故障长什么样
      • 1. 三条互相矛盾的信号,谁在说谎
      • 2. 该看的七个信号,以及它们的真实读数
        • 2.1 排队深度:num_requests_running / num_requests_waiting
        • 2.2 首 token 延迟 TTFT:用户感知的"卡不卡"
        • 2.3 单 token 生成耗时 TPOT:吐字快不快
        • 2.4 队列等待时间:区分"慢"和"挤"
        • 2.5 前缀缓存命中率:Agent 类负载的第一优化项
        • 2.6 抢占次数:容量红线的真正标志
        • 2.7 结束原因分布:质量与配置的体检
      • 3. 容量按 token 算,不按请求数算
      • 4. 健康检查:三个能让故障放大的坑
        • 坑 1:把存活探针当就绪探针
        • 坑 2:网关侧健康检查默认是关的
        • 坑 3:默认重试会把一次故障放大 5 倍
      • 5. 一套可以直接抄的告警规则
      • 6. 一个需要权衡的选择:就绪判定放在哪一层
      • 7. 可复用要点
  • AI-Agent
  • LLM推理服务
Carry の Blog
2026-08-24
目录

vLLM 推理节点运维:容量怎么算、健康检查怎么做、故障长什么样原创

# vLLM 推理节点运维:容量怎么算、健康检查怎么做、故障长什么样

一台 8×A100-80GB 的推理节点,此刻同时成立三件事: /health 在 0.8 毫秒内返回 200;每张卡的显存占用是 76.4 GB / 81.9 GB(93%); nvidia-smi 报告的 GPU 利用率是 0%。

这台机器是健康的、是过载的、还是在空烧钱? 三条信号各自都"正常",合在一起却什么都没说明。这篇讲清楚推理节点该看哪些信号、 容量按什么单位算、健康检查为什么不能只打 /health,以及三类最常见的误判。

文中的量化数据来自一台真实在跑的推理节点(vLLM 0.19.1,TP 部署于 8 卡 A100-80GB, 服务一个 30B 级 Coder 模型),观测窗口为该实例最近一次启动后的 3.3 小时、1352 次请求。 主机名、地址、密钥均已脱敏,只保留结构与量级。

# 1. 三条互相矛盾的信号,谁在说谎

先把开头那三条拆开:

/health 返回 200 —— vLLM 的 /health 是存活探针(liveness),它检查的是引擎进程还在、 事件循环没死。它不检查队列有多长、KV 缓存还剩多少、请求是不是正在被无限期排队。 一个已经排了 200 个请求、新请求要等 90 秒才轮到的实例,/health 照样 200。

显存 93% 被占 —— 这不是泄漏,是预分配。vLLM 启动时按 gpu_memory_utilization (默认 0.9)一次性把显存吃下来切成 KV cache block。实测这台机器的配置:

gpu_memory_utilization = 0.9
num_gpu_blocks         = 25730
block_size             = 1056
cache_dtype            = fp8
enable_prefix_caching  = True
1
2
3
4
5

显存曲线在启动后就是一条平线,它不随负载变化。拿显存占用率做容量告警, 你会得到一条永远在报警、或者永远不报警的曲线——取决于你把阈值定在 0.9 的哪一侧。

GPU 利用率 0% —— nvidia-smi 的 utilization.gpu 是一个采样瞬时值, 含义是"过去一个采样周期内有 kernel 在跑的时间占比"。推理服务是突发型负载: 请求到达时瞬间打满,请求之间完全空闲。在没有请求的间隙采一次,就是 0%。 把它当作"这台机器闲着,可以下线/合并"的依据,是最常见的误杀理由。

结论:这三条信号一条都不能单独用于容量或健康判断。 真正能用的信号在 /metrics 里。

# 2. 该看的七个信号,以及它们的真实读数

vLLM 暴露 Prometheus 格式的 /metrics。指标有上百条,日常运维只需要盯七个。 下面每条都给出这台节点在 3.3 小时窗口内的实测值和读法。

# 2.1 排队深度:num_requests_running / num_requests_waiting

curl -s http://<vllm-host>:8000/metrics | grep -E "num_requests_(running|waiting)"
1
vllm:num_requests_running 0.0
vllm:num_requests_waiting 0.0
1
2

这是唯一真正的过载信号。 waiting 持续 > 0 意味着请求已经排不进这一批了。 running 反映的是当前批次里的并发请求数,它的上限由 max_num_seqs 和 KV 容量共同决定。

告警应该建在 waiting 上,而不是显存或 GPU 利用率上:

# 排队请求数持续 2 分钟高于 0 —— 容量不足的第一手证据
avg_over_time(vllm:num_requests_waiting[2m]) > 0
1
2

# 2.2 首 token 延迟 TTFT:用户感知的"卡不卡"

vllm:time_to_first_token_seconds_count  1352
vllm:time_to_first_token_seconds_sum    1276.97
1
2

平均值 0.944 秒。但平均值在这里几乎没有意义,把 bucket 拉出来看分布:

分位(由 bucket 累积计数换算) TTFT
~p60 ≤ 0.5 s
~p78 ≤ 1.0 s
~p88 ≤ 2.5 s
~p97 ≤ 5.0 s
~p99 ≤ 7.5 s

平均 0.94 秒、p99 却是 7.5 秒,长尾差了 8 倍。原因是 TTFT 由 prefill 决定, 而 prefill 耗时与 prompt 长度近似线性——长上下文请求把尾巴拉长了。 只报平均 TTFT 的看板,会让你在用户已经开始抱怨时仍然看到一条平稳的绿线。

# 2.3 单 token 生成耗时 TPOT:吐字快不快

vllm:request_time_per_output_token_seconds_sum   12.23
vllm:request_time_per_output_token_seconds_count 1352
1
2

12.23 / 1352 ≈ 9.0 毫秒/token,约合 110 tokens/s 的单请求吐字速度。 TPOT 由 decode 阶段决定,它对并发敏感:并发上来后单请求的 TPOT 会线性劣化, 这正是"人多了大家都变慢"的量化形式。TPOT 是做 SLO 的合适对象,TTFT 是做告警的合适对象。

# 2.4 队列等待时间:区分"慢"和"挤"

vllm:request_queue_time_seconds_sum   79.66
vllm:request_queue_time_seconds_count 1352
1
2

平均 59 毫秒,占端到端延迟的 1%。这条指标的价值在于归因: 端到端变慢时,如果 queue_time 同步上涨,是容量问题(该加卡/加实例); 如果 queue_time 平稳而 TTFT 上涨,是请求形态变了(prompt 变长),加机器解决不了。

# 2.5 前缀缓存命中率:Agent 类负载的第一优化项

vllm:prefix_cache_queries_total 54,577,837
vllm:prefix_cache_hits_total    39,773,184
1
2

命中率 72.9%。从 token 口径看同一件事:

vllm:prompt_tokens_total          49,418,894
vllm:prompt_tokens_cached_total   35,911,392   (72.7%)
vllm:prompt_tokens_by_source_total{source="local_compute"} 13,507,502
1
2
3

也就是说,4940 万个输入 token 里只有 1350 万真正跑了 prefill 计算,其余 72.7% 直接命中缓存。 这是 Agent / 长系统提示词类负载独有的红利:每轮对话的前缀(系统提示、工具定义、 历史消息)高度重复,enable_prefix_caching 打开后,重复前缀不重算。

对应的运维动作有两条:一是任何会打乱前缀稳定性的改动都要评估(比如把时间戳、 随机 ID 塞进系统提示词的开头,会让命中率断崖式下跌);二是命中率本身要监控, 它掉下来通常意味着上游改了 prompt 模板,而不是引擎出了问题。

# 2.6 抢占次数:容量红线的真正标志

vllm:num_preemptions_total 0.0
1

当 KV cache 不够用时,vLLM 会抢占正在运行的请求——把它踢回等待队列, 之前算好的 KV 全部作废,之后重算。这个动作对用户表现为"生成到一半卡住然后重来", 对集群表现为算力凭空蒸发。

num_preemptions_total 只要开始持续增长,就是容量已经越线的确凿证据, 比任何显存或利用率指标都直接。这台节点在窗口内是 0,说明当前负载离容量上限还很远。

# 2.7 结束原因分布:质量与配置的体检

vllm:request_success_total{finished_reason="stop"}   1311
vllm:request_success_total{finished_reason="length"} 40
vllm:request_success_total{finished_reason="error"}  0
1
2
3

length 占 3.0%——即 3% 的请求是被 max_tokens 截断的,不是自然收尾。 这个比例是上游配置的体检项:持续偏高说明客户端的 max_tokens 给小了, 用户看到的是"回答被砍掉半句",而服务端一切正常、零错误。

# 3. 容量按 token 算,不按请求数算

这台节点的 KV cache 容量可以直接算出来:

num_gpu_blocks × block_size = 25,730 × 1,056 ≈ 27,170,000 tokens
1

2717 万 token 的 KV 槽位,这才是这台机器真正的容量单位。它能同时服务多少请求, 完全取决于请求形态:

请求形态 单请求 KV 占用 理论并发
短问答(1K 上下文) ~1K token ~27,000
常规对话(8K 上下文) ~8K token ~3,400
Agent 长上下文(64K) ~64K token ~420
满上下文(128K) ~128K token ~210

同一台机器,"能扛多少并发"的答案在 210 到 27000 之间浮动 128 倍。 所以"这台机器能扛多少 QPS"是个无法回答的问题,必须先问平均上下文长度。

还有一个更反直觉的比例。这个窗口内:

prompt_tokens_total     49,418,894
generation_tokens_total    752,517
1
2

输入输出比 65.7 : 1。传统的"LLM 服务 = 生成密集型"直觉在 Agent 负载上完全不成立—— 它是极度 prefill 密集的。这个比例决定了两件事:

  1. 优化的着力点在 prefill 侧(前缀缓存、chunked prefill、上下文裁剪), 而不是常被讨论的 decode 侧吞吐。
  2. 成本结构也在输入侧。按典型的商用计价口径(输入 $0.588/M token、 输出 $2.353/M token)折算,这 3.3 小时的用量约合 $29.1 输入 + $1.8 输出 ≈ $31, 其中 94% 的成本在输入。想省钱先砍上下文,砍生成长度基本没用。

# 4. 健康检查:三个能让故障放大的坑

# 坑 1:把存活探针当就绪探针

症状:网关一直把流量往一个实例上送,用户端超时,而所有健康检查都是绿的。

原因:/health 只回答"进程还在吗"。一个 KV 打满、队列积压、抢占频发的实例, /health 依然 0.8 毫秒返回 200。

解药:存活和就绪分开。存活用 /health;就绪用 /metrics 派生的复合条件, 放在一个轻量探针里:

#!/usr/bin/env bash
# readiness probe: 队列深度 + KV 用量双门槛
m=$(curl -s -m 3 "http://${VLLM_HOST}:8000/metrics") || exit 1
waiting=$(grep -oP 'vllm:num_requests_waiting\{[^}]*\}\s+\K[0-9.]+' <<<"$m")
kv=$(grep -oP 'vllm:kv_cache_usage_perc\{[^}]*\}\s+\K[0-9.]+' <<<"$m")

awk -v w="$waiting" -v k="$kv" 'BEGIN { exit !(w < 10 && k < 0.92) }' \
  && echo "READY" || { echo "NOT_READY waiting=$waiting kv=$kv"; exit 1; }
1
2
3
4
5
6
7
8

网关拿这个脚本的出口码摘流量,而不是拿 /health 的 200。

# 坑 2:网关侧健康检查默认是关的

这是一个在网关配置里极易被忽略的默认值。以 Kong 为例,upstream 的健康检查 默认全部关闭,查询任意一个 upstream 的健康状态会看到:

curl -s http://localhost:8001/upstreams/<name>/health
1
{"data":[{"health":"HEALTHCHECKS_OFF",
          "addresses":[{"health":"HEALTHCHECKS_OFF","port":8000}]}]}
1
2

HEALTHCHECKS_OFF 的含义是:网关永远认为这个后端是好的,哪怕它已经拒绝连接。 在一次真实的配置盘点中,一套 14 个 upstream 的网关里,只有 1 个开了主动健康检查—— 其余 13 个包括推理服务在内,全部是 HEALTHCHECKS_OFF。这类配置不会报错、不会告警, 只在后端真的挂掉那天才第一次显形。

给推理后端补上主动健康检查:

curl -X PATCH http://localhost:8001/upstreams/<name> \
  --data 'healthchecks.active.type=http' \
  --data 'healthchecks.active.http_path=/health' \
  --data 'healthchecks.active.healthy.interval=5' \
  --data 'healthchecks.active.healthy.successes=2' \
  --data 'healthchecks.active.unhealthy.interval=5' \
  --data 'healthchecks.active.unhealthy.http_failures=3' \
  --data 'healthchecks.active.unhealthy.timeouts=3'
1
2
3
4
5
6
7
8

# 坑 3:默认重试会把一次故障放大 5 倍

症状:后端刚开始不稳定,压力反而瞬间翻数倍,故障从"部分失败"迅速变成"全线雪崩"。

原因:Kong 的 service.retries 默认值是 5。一个失败请求会被重投 5 次, 后端瞬时压力放大到 5 倍。对普通的毫秒级 HTTP 服务这不算什么, 但推理请求单次要占住 GPU 几秒钟,重试成本高得多,而且重试的请求会重新排队, 把队列越推越长。

解药:推理类 service 的 retries 调到 1,并把超时按真实延迟分布设置—— 读超时至少要覆盖 p99 的端到端延迟(这台节点 p95 ≈ 10 秒,60 秒的默认读超时是合理的, 但如果你把它按普通 API 的习惯调到 5 秒,会把 15% 的正常长请求判成失败):

curl -X PATCH http://localhost:8001/services/<vllm-service> \
  --data 'retries=1' --data 'read_timeout=120000'
1
2

# 5. 一套可以直接抄的告警规则

把上面的信号落成规则,只留真信号:

groups:
- name: vllm
  rules:
  # 真过载:请求排不进批次
  - alert: VLLMQueueBacklog
    expr: avg_over_time(vllm:num_requests_waiting[2m]) > 0
    for: 2m

  # 容量越线:开始抢占,算力在浪费
  - alert: VLLMPreemption
    expr: increase(vllm:num_preemptions_total[5m]) > 0
    for: 0m

  # 用户感知劣化:TTFT 尾部
  - alert: VLLMSlowTTFT
    expr: histogram_quantile(0.95,
            rate(vllm:time_to_first_token_seconds_bucket[5m])) > 5
    for: 5m

  # 上游 prompt 模板被改坏:前缀缓存命中率塌陷
  - alert: VLLMPrefixCacheDrop
    expr: rate(vllm:prefix_cache_hits_total[15m])
          / rate(vllm:prefix_cache_queries_total[15m]) < 0.4
    for: 15m

  # 引擎失联(存活层,仍然要保留)
  - alert: VLLMDown
    expr: up{job="vllm"} == 0
    for: 1m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

刻意没有建的告警:GPU 显存使用率、GPU 利用率。前者是启动即固定的常数, 后者是会在空闲瞬间读到 0% 的采样值——两条都只会产生噪声。

# 6. 一个需要权衡的选择:就绪判定放在哪一层

补全健康检查有两条路,我选了第二条:

方案 A:网关主动健康检查打 /health。 改动最小,一条 PATCH 就完事, 零新增组件。代价是它只能发现进程级死亡——引擎卡死、队列积压、KV 打满这些 "活着但不可用"的状态,它一概看不见,而这些恰恰是推理服务最常见的劣化形态。

方案 B:网关健康检查指向一个基于 /metrics 的复合就绪端点。 探针按第 4 节的 双门槛(num_requests_waiting < 10 且 kv_cache_usage_perc < 0.92)判定, 网关据此摘流量。代价是多了一个必须自己维护的探针进程,而且门槛值需要按负载形态调—— 上下文长的场景 KV 阈值要更早触发。

选 B,理由是故障形态决定的:推理服务真正的故障模式不是"进程没了", 而是"进程还在但已经不该再接流量了"。方案 A 覆盖不到这一类,等于把最常见的故障漏掉。 承担一个探针进程的维护成本,换来的是摘流量的时机从"已经挂了"提前到"快撑不住了"。

保留的回退路径:探针进程自身异常时,网关退回方案 A 的 /health 判定, 即探针不可用不能导致后端全被摘掉——探针故障必须 fail-open。

# 7. 可复用要点

  1. 推理节点的容量单位是 token,不是 QPS。 num_gpu_blocks × block_size 就是 KV 总容量;能扛多少并发完全由平均上下文长度决定, 同一台机器在 1K 和 128K 上下文下的并发能力可以差两个数量级。谈 QPS 之前先问上下文长度。

  2. 显存和 GPU 利用率都不是健康信号。 显存是启动即固定的预分配,利用率是突发负载下 会读到 0% 的采样值。真信号是 num_requests_waiting(过载)和 num_preemptions_total (越线),两条都在 /metrics 里。

  3. /health 是存活不是就绪。 一个队列积压、KV 打满的实例照样毫秒级返回 200。 就绪判定必须建在 /metrics 的复合条件上,并且探针本身要 fail-open。

  4. 检查网关侧的默认值,它们是为毫秒级 API 设计的。 健康检查默认关闭 (HEALTHCHECKS_OFF 不报错、不告警,只在后端真挂那天显形)、重试默认 5 次 (把一次故障放大 5 倍并让请求重新排队)。推理后端要显式改成"开检查、少重试、长超时"。

  5. Agent 类负载是 prefill 密集的,实测输入输出比可达 65:1。 优化和成本都在输入侧: 前缀缓存命中率(实测 72.9%)是第一优化项,且它掉下来通常是上游改了 prompt 模板, 不是引擎故障——所以它值得单独一条告警。

🤖 Agent 可直接解析的元数据块(点击展开)
{
  "_meta": {
    "doc_version": "2026-08-24",
    "topic": "vllm-inference-node-operations",
    "audience": "SRE / platform engineer running self-hosted LLM inference"
  },
  "environment": {
    "gpu": "8x A100-80GB (SXM4)",
    "engine": "vLLM 0.19.1",
    "python": "3.10.18",
    "model_class": "30B-class coder model",
    "gateway": "Kong 3.9.1"
  },
  "engine_config": {
    "gpu_memory_utilization": 0.9,
    "num_gpu_blocks": 25730,
    "block_size": 1056,
    "cache_dtype": "fp8",
    "enable_prefix_caching": true,
    "kv_capacity_tokens": 27170880
  },
  "observed_window": {
    "duration_hours": 3.3,
    "requests": 1352,
    "ttft_avg_s": 0.944,
    "ttft_p95_s_approx": 5.0,
    "ttft_p99_s_approx": 7.5,
    "e2e_avg_s": 5.5,
    "tpot_avg_ms": 9.0,
    "queue_time_avg_ms": 59,
    "prompt_tokens": 49418894,
    "generation_tokens": 752517,
    "prefill_to_decode_ratio": 65.7,
    "prefix_cache_hit_rate": 0.729,
    "preemptions": 0,
    "finished_reason_length_pct": 3.0
  },
  "key_metrics_to_watch": [
    "vllm:num_requests_waiting",
    "vllm:num_preemptions_total",
    "vllm:time_to_first_token_seconds_bucket",
    "vllm:request_time_per_output_token_seconds",
    "vllm:request_queue_time_seconds",
    "vllm:prefix_cache_hits_total / vllm:prefix_cache_queries_total",
    "vllm:request_success_total{finished_reason}"
  ],
  "anti_signals": [
    "gpu memory utilization (pre-allocated, constant)",
    "nvidia-smi utilization.gpu (instantaneous sample, 0% while idle between bursts)",
    "/health status code (liveness only, not readiness)"
  ],
  "gateway_defaults_to_override": {
    "kong_upstream_healthchecks": "OFF by default -> enable active http check on /health",
    "kong_service_retries": "5 by default -> set to 1 for inference backends",
    "kong_read_timeout_ms": "must cover p99 e2e latency"
  },
  "q9_decision": {
    "choice": "方案 B:基于 /metrics 的复合就绪探针",
    "reason": "推理服务的主流故障形态是『活着但不可用』,/health 覆盖不到",
    "trade_off": "多一个自维护探针进程,门槛值需按上下文长度调",
    "safety_rule": "探针故障必须 fail-open,回退到 /health 判定"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#vLLM#SRE#GPU#容量规划#可观测性#Kong
上次更新: 8/24/2026

← Claude Code 实战 12|盘点跨交易所资产:CLI 包装、私钥安全与金融防错规范

最近更新
01
Hermes Agent 实战 16|给 AI Agent 平台做可观测:多 profile 服务的指标、日志与告警设计 原创
08-24
02
关于这个博客
08-24
03
Browserless 使用笔记:把 Chrome 变成一个可以被并发调用的网络服务
08-07
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式