2026-08-27 04:00:00

模型服务运行后,固定副本数很难同时兼顾突发请求和资源利用率。请求增加时需要扩容,流量回落后又希望及时缩容。
本文通过一个简单 Demo,为 KServe 模型服务接入 KEDA,根据 vLLM 的请求指标自动调整副本数,并验证服务从 1 -> 2 -> 1 的扩缩容过程。
需要在额外安装 KEDA 和 Prometheus,工作流程如下:
/metrics 暴露运行指标。vllm:num_requests_running 和 vllm:num_requests_waiting。
本文实测 KEDA 2.17.2:
|
|
安装后确认 Operator、Admission Webhook 和 Metrics API Server 都已启动:
|
|
|
|
只有 CRD 不够。external.metrics.k8s.io 不可用时,ScaledObject 可以创建,但 HPA 读不到 KEDA 提供的指标。
本文使用 kube-prometheus-stack 部署 Prometheus 和 Prometheus Operator,Grafana、Alertmanager、Exporter 和默认告警规则都关闭,只保留指标采集需要的组件。
|
|
先确认 Prometheus Service 已经创建。下面的 InferenceService 和后续查询都使用这个 Service;如果命令没有输出,应先检查 Helm Release 和 Prometheus Pod,不要继续创建 InferenceService。
|
|
当前配置不创建 PrometheusRule,因此一并关闭了只用于校验规则的 Admission Webhook。安装完成后,集群中应保留 Prometheus Operator 和 Prometheus 两个 Pod:
|
|
当前测试节点只有一张 GPU,而 Demo 需要把服务扩到两个副本,因此继续使用 HAMi DRA 共享 GPU。
创建 ResourceClaimTemplate 和完整的 InferenceService,同时配置每个副本的 GPU 配额、模型和 KEDA 自动扩缩容:
|
|
每个 Predictor 副本通过 qwen-hami-gpu 申请 3Gi 显存和 20% GPU 核心,因此单 GPU 节点可以同时运行两个副本。
KServe 创建 Predictor Deployment 和 ScaledObject,KEDA 再创建 HPA 管理副本数。
这份 YAML 中扩缩容配置如下:
|
|
各参数含义:
autoscalerClass: keda:使用 KEDA 进行扩缩容minReplicas、maxReplicas:限制最小、最大副本数serverAddress:Prometheus 地址query:使用 running + waiting,同时统计正在执行和已经进入 vLLM 队列的请求target.value:每个副本期望承载的目标值Prometheus 查询得到的是所有 Predictor Pod 的请求总数,可以理解为:
|
|
target.value: 1 表示希望每个副本平均处理 1 个请求。当前副本范围是 1~2,扩缩容过程可以直接这样看:
Query = 0 或 1,保持 1 个副本。Query >= 2,说明请求超过单个副本的目标值,扩到 2 个副本。maxReplicas: 2。Query 回到 0 或 1,等待 300 秒缩容稳定窗口结束,再缩回 1 个副本。Prometheus Stack 不会自动采集 qwen-llm。创建 ServiceMonitor,通过 InferenceService Label 选择 KServe 生成的 Predictor Service:
|
|
ServiceMonitor 不会通过 Service 的 ClusterIP 随机抓取一个后端。Prometheus Operator 会读取 Service 对应的 EndpointSlice,并把每个 Predictor Pod 都作为独立 Target。
通过 Kubernetes Service Proxy 检查 Target。当前只有一个 Predictor,因此应看到一行 qwen-llm-predictor-*,状态为 up:
|
|
再检查空闲时的指标:
|
|
实测输出是:
|
|
KServe 创建 Deployment 和 ScaledObject,KEDA 再创建并管理 HPA:
|
|
空闲状态下,各条命令输出中的关键字段是下面这些。这里是整理后的字段,不是上一段单条命令的原始输出:
|
|
直接读取 HPA,确认缩容稳定窗口:
|
|
当前结果是:
|
|
先查询 Envoy Gateway 的地址,发送一次推理请求,确认服务正常:
|
|
使用以下命令产生负载:
|
|
这条命令会一直占用当前终端,并将并发数保持为 12 持续发送请求。
另开两个终端,同时观察 Prometheus 指标以及扩缩容资源:
|
|
|
|
本次实测时间线是:
|
|

指标值是整个 Deployment 的 running + waiting 总数。两个副本、总值 12 时,平均每个副本为 6,因此 HPA 的目标列显示约 6/1。
扩容期间再调用一次 Chat Completions,仍然返回正确结果。至此可以确认 KEDA 已经根据 vLLM 指标把服务从 1 个副本扩到 2 个。
回到运行压测的终端,按 Ctrl+C 停止全部请求。
先重复前面的 Prometheus 查询,确认结果回到 0,再检查 ScaledObject 变成 ACTIVE=False。Deployment 不会立即缩容,因为当前生成的 HPA 带有 300 秒缩容稳定窗口:
|
|
实际缩容时间还会受到 KEDA 轮询、HPA 同步周期和 Pod 终止时间影响。用下面的命令等待 Deployment 回到一个 Ready 副本:
|
|
本次停止负载后,Prometheus 查询值回到 0,ScaledObject 变成 ACTIVE=False;约 299 秒后,HPA 将 Deployment 从 2 缩回 1。最后重新执行前面的 Chat Completions 请求,仍然返回 5。
本文验证了模型服务从 1 -> 2 -> 1 的完整链路,最后总结一下整个流程:
/metrics 暴露运行指标。vllm:num_requests_running 和 vllm:num_requests_waiting。
2026-08-17 04:00:00

之前我写过一篇文章 OpenSpec + Superpowers,SDD+TDD 双驱动 AI 编程工作流,记录了我当时把 OpenSpec 和 Superpowers 组合成一套工作流的尝试。
那时候我觉得这两个东西搭在一起还挺顺,OpenSpec 负责需求和 Spec 对齐,Superpowers 负责计划、TDD、子 Agent 实现和代码审查。
后来,我发现社区里很多人也在用这套工作流,先写需求和设计,拆任务,再实现和验证,大家都在试图给 Agent 加一套更稳定的工作方法。
这件事本来挺美好的,直到 GPT-5.6 出现。
我用 GPT-5.6 Sol 做一个很简单的需求,本来以为读一下代码、改完、跑个测试就结束了,结果任务跑了很久。
看执行过程发现 Codex 并不是卡住了,而是 Superpowers 把头脑风暴、计划、子 Agent 实现、TDD 和代码审查一层层串了起来,一个简单需求就被升级成了一套完整的软件工程流程。
而在社区里,我也看到了很多类似的使用反馈,大致可以分成两类:
当然,这里也不能把变慢全部归到 Superpowers 身上。
从使用体感看,GPT-5.6 Sol 本身就比 GPT-5.5 慢。再叠加头脑风暴、计划和 Review 等完整流程,等待时间和额度消耗又被进一步放大。
所有讨论最后都落到了同一个问题上:GPT-5.6 Sol 模型能力已经这么强了,还要不要继续使用 Superpowers 这类 Skill?
只靠使用体感,很难回答前面的问题,或者说可信度不够。
恰好 Comet 最近做了一次很有参考价值的调整,新增了 Native 模式。
0.4.0-beta.7 新增了面向强模型的 Native 模式。
原来的 Classic 模式会串联 OpenSpec 和 Superpowers,依次经过 Open、Design、Build、Verify、Archive。
Native 不再依赖这两个外部 Skill,只保留 Shape、Build、Verify、Archive 四个阶段:
它的思路很直接:过程放松,结果收紧。
Native 没有去掉验证,只是不再强制模型按照固定的方法完成任务。外层工作流负责锁住验收规则、验证证据和风险边界,具体怎么实现则交给模型决定。
具体如何完成任务交给模型自由发挥,我们要做的就是完成后验证任务是否真的完成。

Comet 还做了一轮 Native 和 Classic 的官方对齐实验。两种模式处理相同的 16 个业务任务,每个任务各跑 3 次,每种模式 48 次,总共 96 次运行。
pass@1 单次运行的成功率,反映模型不依赖重试、一次完成任务的能力。pass@3 是三次里至少成功一次,代表这个任务能不能做。pass^3 更严格,要求三次全部成功,看的则是能不能稳定地做。两种模式的 pass@3 都是 100%,说明去掉 OpenSpec 和 Superpowers 之后,Native 没有丢掉这 16 个任务的覆盖能力。反而在单次严格通过率和三次连续成功率上更高。
为了避免「任务提前失败,所以看起来更省」这种误差,官方报告这里只统计 Native 和 Classic 都严格通过的 41 组配对样本。
效率图将 Classic 归一化为 100%;数据只统计两种模式都通过的 41 组配对样本,表中均为单个成功样本的平均值。
最明显的是,总 Token 下降了 76.8%,模型成本下降了 75.1%,Agent 轮次和工具调用也减少了一半以上。
在这组实验里,流程变轻之后,任务覆盖没有下降,执行成本却明显降低了。
当然,这只是 Comet 的第一方实验,不能直接外推到所有模型和任务。
但至少在这 16 个任务里,去掉固定方法论后,完成质量没有下降,执行成本却明显降低了。
但数据只能告诉我们发生了什么,解释不了为什么。
我倒不觉得是 Superpowers 突然没用了。
上一篇文章 OpenSpec + Superpowers,SDD+TDD 双驱动 AI 编程工作流 里,我说用上 Superpowers 之后,代码质量比让模型直接动手好不少。这个判断放在当时依然成立。模型容易跳过设计、忘记测试,Superpowers 就用一套完整的需求讨论、计划、TDD 和 Review 流程把这些缺口补上。
但现在,大人,时代变了。😏
Superpowers 没变,变的是模型。
对强模型来说,能力瓶颈已经不在「会不会写代码」,而在「能不能被信任地完成任务」。
GPT-5.6 已经会主动理解需求、调查代码、形成方案,再根据改动风险选择测试和验证方式。很多过去需要 Superpowers 反复提醒的工作习惯,模型自己已经学会了。
这时再套上 Superpowers,一个简单需求就可能重新经历 brainstorming、计划、TDD 和独立 Review。两套流程未必会得出相反结论,但每多一个 Agent、每多一次 Review,都要重新读取上下文并调用模型。如果没有发现新的问题,增加的就只有 Token 和时间。
问题不是模型和 Skill 在打架,而是通用 Skill 的边际收益正在下降。
为什么会这样?我觉得有两层变化。
一部分能力,被模型学会了。
规划、调试、测试和自我检查都是通用方法,还能通过代码、测试与评测反复验证。使用轨迹积累得足够多,后续模型自然有机会把这些习惯吸收进去。
另一部分能力,被 Agent Runtime 接管了。
任务状态、子 Agent 调度、工具调用、上下文压缩和结果验证,已经逐渐成为 Codex、Claude Code 这类产品的原生能力。过去需要 Skill 串起来的执行循环,现在 Runtime 自己就能跑。
所以 Superpowers 不是变差了。它原本负责解决的问题,一部分进入了模型,另一部分进入了 Runtime。
我的答案很直接,垂直 Skill。
这里说的垂直,不只是某个行业专用的 Skill,而是那些带着具体团队、具体项目和真实业务环境的 Skill。
越接近通用方法论,Skill 的生命周期反而越短。
因为它可以在大量任务里反复运行和评测,有效的方法最终可能进入模型训练,或者直接成为 Codex、Claude Code 这类 Agent Runtime 的原生能力。
就像一个特别好用的第三方插件,如果它解决的是所有用户都会遇到的问题,最后很可能被官方直接集成。能力还在,只是大家不再需要单独安装它。
Skill 也是一样。今天还要额外挂载的规划、测试和审查流程,明天可能就会成为默认能力。
但有些东西,通用模型不会预先知道。
真正会被长期保留下来的 Skill,往往包含三类东西:

这些信息敏感、零散,还会随着业务不断变化,很难进入通用模型的训练数据,却决定了 Agent 能不能在真实环境里完成工作。
真正会留下来的 Skill,解决的是模型不知道、拿不到,也不能擅自决定的问题。
这类 Skill 往往更简单,也更轻。它不负责教 Agent 怎么思考,只负责告诉它,在这里工作需要知道什么、可以调用什么,以及做到什么才算完成。
回到开头的问题,GPT-5.6 Sol 还需要 Superpowers 吗?
答案很明确:对于 Fable 5、GPT-5.6 这类已经能自主规划和验证的强模型,我更倾向于先移除 Superpowers,直接运行一段时间,再根据真实缺口补回必要的约束。
Comet Native 的实验也提供了一个参考:在这 16 个任务里,去掉固定方法论之后,完成质量没有下降,Token、耗时和成本却明显减少。
现在的趋势是,通用 Skill 会逐渐被模型和 Runtime 吸收。真正长期留下来的,是模型不知道的私有知识、拿不到的执行能力,以及不能擅自决定的业务边界。
Skill 不会消失,它只是会回到自己原本该在的位置。
2026-08-10 04:00:00

前面已经用 KServe 跑起了 Qwen,但一个小模型独占一张 GPU 有些浪费。这篇则是在此基础上引入 HAMi,通过 HAMi 实现 GPU 共享,让多个小模型共用一张 GPU。
本次测试环境如下:
|
|
本文从 HAMi 安装和原生 DRA 资源声明开始,完整走通 KServe 集成 HAMi GPU 共享部分。
由 GPU Operator 安装 NVIDIA Driver、Container Toolkit 和监控组件,但关闭原生 NVIDIA Device Plugin,后续由 HAMi DRA Driver 管理 GPU:
|
|
如果节点已经预装 NVIDIA Driver,可以把 driver.enabled 改为 false。无论驱动由谁安装,devicePlugin.enabled=false 都不能省略,否则原生 Device Plugin 和 HAMi-DRA 会同时管理同一设备。
HAMi-DRA Webhook 需要 TLS 证书,测试环境使用 cert-manager 签发:
|
|
为需要接管的 GPU 节点添加 gpu=on 标签,再安装本文实测的 HAMi-DRA 0.2.1:
|
|
上面的命令适用于 GPU Operator 安装 Driver 的场景。如果 NVIDIA Driver 由宿主机预装,则增加:
|
|
安装完成后,HAMi 创建了一个 DeviceClass:
|
|
同时节点插件通过 ResourceSlice 发布 GPU 信息:
|
|
输出中只保留本文关心的字段:
|
|
allowMultipleAllocations: true 表示同一个设备可以接受多份分配。这里的 memory 和 cores 是 HAMi 用于调度和限制的可消耗容量,不是 Node 上的传统扩展资源。
KServe 0.18 版本已经支持原生 DRA,可以在 Predictor 级引用 ResourceClaimTemplate,再由容器级 resources.claims 使用对应的 Claim。
我们只需要提前创建一个 ResourceClaimTemplate,然后在 InferenceService 中引用即可,完整的 YAML 如下:
|
|
minReplicas: 2 保证 Demo 期间至少存在两个 Predictor,用于验证它们能否同时获得共享 GPU 配额,不展开副本自动调整行为。
这里没有再声明 nvidia.com/gpu、nvidia.com/gpumem 或 nvidia.com/gpucores。CPU 和内存仍使用普通 requests/limits,GPU 完全使用 DRA Claim 形式声明。

KServe 的 HuggingFace Runtime 原本根据 GPU limit 选择 -gpu 镜像。原生 DRA 配置里没有这个 limit,因此本文显式指定已经验证过的 GPU 镜像。docker.m.daocloud.io 是测试环境使用的镜像代理;如果环境可以直接访问 Docker Hub,可以改为 kserve/huggingfaceserver:v0.18.0-gpu。
KServe 最终生成的 Deployment 保留了两级引用:
|
|
|
|
Deployment 创建两个 Pod 后,Kubernetes 会根据同一个 ResourceClaimTemplate 为每个 Pod 生成独立 Claim。不能让多个副本直接引用一份固定 ResourceClaim,否则它们不会获得各自独立的 3Gi/20 配额。
Kubernetes 为每个 Pod 生成一份 Claim。下面只保留其中一份 Claim 的申请字段;它由 Pod 持有,Pod 删除后会一起清理:
|
|
|
|
调度完成后,Claim 状态中的关键字段记录了实际分配:
|
|
ResourceClaimTemplate 只定义申请规格,Kubernetes 为每个 Pod 生成 Claim,并完成设备选择和容量扣减。HAMi DRA Driver 随后响应 kubelet 的 NodePrepareResources,生成 CDI 配置并返回设备信息,最终由 containerd 把对应 GPU 和 HAMi-Core 运行环境应用到容器。
进入其中一个 Predictor 容器执行完整的 nvidia-smi:
|
|
可以看到 HAMi 把同一张 GPU 的可见显存限制为 3072 MiB。下面是本次实测的完整状态表;命令前后的 HAMI 初始化和退出日志不属于 nvidia-smi 输出,这里没有混入:
|
|
把 Predictor 的最小副本数设为 2,两份 Pod 使用相同的 3Gi、cores=20 配置。

先查看 Pod 和 Claim:
|
|
本次新建的两份 Claim 都分配成功:
|
|
两个 Pod 都调度到 lixd-test-gpu,并分别看到 3072 MiB 显存:
|
|
逐个进入容器检查可见 GPU:
|
|
两个容器都返回:
|
|
通过 Gateway 调用接口:
|
|
API 可以正常返回,说明在使用 GPU 共享之后,服务依旧可以正常运行。
验证完成后删除 InferenceService 和 ResourceClaimTemplate:
|
|
两份由 Pod 生成的 ResourceClaim 会随 Pod 一起删除。
KServe 可以直接通过 ResourceClaimTemplate 和 resources.claims 使用 HAMi DRA 模式,不需要额外适配。每个 Predictor Pod 都会生成一份独立 ResourceClaim,再由 HAMi 分配显存和算力配额。
通过 HAMi 共享后,一张 GPU 可以同时承载多个推理副本或服务工作负载,避免小模型整卡独占,让空闲的显存和算力得到更充分的利用。
2026-08-02 04:00:00

上一篇完成了 KServe 的安装,并通过 InferenceService 跑通了一个 Qwen 模型服务。
当时提交的 InferenceService 只包含模型格式、存储地址、GPU 资源和启动参数,KServe 却创建出了 Deployment、Service 和 HTTPRoute。这份 YAML 中没有容器镜像,那么 KServe 如何选中 HuggingFaceServer,Pod 中又为什么会运行 vLLM?
这一篇先看 KServe 的整体架构和核心 CRD,再结合上一篇的 qwen-llm,梳理从提交 InferenceService 到请求进入 vLLM 的完整流程。
以下内容基于 KServe 0.18,部署模式为 Standard。
KServe 是一个构建在 Kubernetes 之上的 AI 推理平台:

整体分为控制面和数据面:

控制面负责创建和维护推理服务。整个过程从用户提交 InferenceService 开始:
kubectl apply 将 InferenceService 提交到 Kubernetes API Server。modelFormat 匹配 ServingRuntime,从 Runtime 中取得模型服务器镜像和默认启动参数,再与 InferenceService 中的模型地址、运行参数和资源需求合并。PredictorReady、IngressReady 和最终的 Ready 状态。所以,控制面管理的是推理服务的生命周期:创建、更新、状态同步,以及配置变更后的持续调谐。
数据面由实际运行模型和处理请求的资源组成。上一篇的环境使用 Envoy Gateway 作为 Gateway API 的实现。模型服务 Ready 后,客户端就可以通过数据面访问推理服务。
客户端访问模型域名时,请求先到达 Envoy Gateway 管理的 Envoy Proxy。Envoy Proxy 应用 HTTPRoute/qwen-llm 的规则,并根据其中指向 Service/qwen-llm-predictor 的 backendRef,将请求发送到 Ready 的 Predictor Pod。这里的 Service 用于标识和发现后端,并不是一个独立的代理进程。Pod 中运行的是 HuggingFaceServer,当前配置使用 vLLM 作为推理后端。vLLM 完成推理后,响应沿原链路返回客户端。
|
|
控制面决定服务应该如何部署,并保证它处于期望状态;数据面负责服务创建后的请求转发和模型推理。在线请求不会经过 KServe Controller。
KServe 通过 CRD 描述模型服务、运行时、推理图和模型存储初始化方式。对于上一篇的 Qwen Demo,最核心的是 InferenceService 和 ClusterServingRuntime。
二者之间的关系如下:
|
|
InferenceService 用于描述一个模型服务需要什么,包括模型格式、模型地址、运行参数、副本数和资源需求。
一个最小的 InferenceService 如下:
|
|
InferenceService 的 Spec 可以包含以下三个逻辑部分:
| 组成部分 | 是否必需 | 作用 |
|---|---|---|
| Predictor | 是 | 加载模型并执行预测或生成 |
| Transformer | 否 | 请求预处理和响应后处理 |
| Explainer | 否 | 生成模型解释 |
对于大模型推理,通常只需要 Predictor。上一篇的 Qwen 服务也只配置了 Predictor。
这里的 Predictor 是一个逻辑角色。到了 Standard 模式下,它最终会变成名为 qwen-llm-predictor 的 Deployment 和 Service。
InferenceService 描述要运行什么模型,ServingRuntime 描述使用什么模型服务器运行。
Runtime API 按作用范围分为两种:
一个简化后的 HuggingFace Runtime 如下:
|
|
containers 定义模型服务器的镜像和启动方式,supportedModelFormats 声明它支持哪些模型格式。
当 InferenceService 没有显式指定 Runtime 时,KServe 会根据 modelFormat、版本、协议以及 Runtime 的 autoSelect 和 priority 自动匹配。
因此:
|
|
表示模型格式为 huggingface,KServe 会选择支持该格式的 Runtime。它并不表示 Pod 中只能使用 Hugging Face Transformers 进行推理。
除了 InferenceService 和 Runtime,KServe 还提供了一些面向特定场景的 CRD:
上一篇的 Demo 没有使用这些资源,后续遇到对应场景时再单独分析。
CRD 只描述期望状态,真正读取这些对象并创建模型服务的是 KServe 控制面。
Webhook 位于资源写入 Kubernetes API 的入口。InferenceService Admission Webhook 负责补充默认值,并拒绝不符合 API 约束的配置;Pod Mutating Webhook 则会在 Pod 创建时注入模型存储相关配置。
在当前环境中,Webhook 和 Controller 运行在同一个 kserve-controller-manager Pod 中,但承担不同职责。
KServe Controller 持续 Watch InferenceService,并通过 Reconcile 循环让实际状态与期望状态保持一致。
它主要负责:
Controller 只负责资源编排。Deployment 创建以后,Pod 由 Kubernetes Scheduler 调度,GPU 由 NVIDIA Device Plugin 分配;Gateway Controller 负责让 HTTPRoute 生效,实际流量由 Envoy Proxy 转发。
以上一篇的 Qwen 服务为例,Predictor Pod 内部的层次如下:
|
|
Predictor Pod 是模型服务的运行载体,容器主进程是 HuggingFaceServer。它负责启动模型服务并提供 OpenAI 兼容接口。
当前模型使用 vLLM 作为 HuggingFaceServer 的推理后端。HuggingFaceServer 会在同一个容器中创建 vLLM 引擎,由 vLLM 使用 GPU 执行推理。
前面介绍的 CRD 和组件,最终会通过 Controller 的调谐过程串起来。
上一篇的 qwen-llm 使用 Standard 模式,下面只分析该模式下的资源创建和请求流程。
上一篇使用的核心配置如下:
|
|
qwen-model PVC 是上一篇提前创建的,InferenceService 只负责引用它。
从提交 InferenceService 到服务 Ready,完整流程如下:
|
|
可以从 InferenceService Status 查看最终选择结果:
|
|
|
|
Controller 选择 kserve-huggingfaceserver,再把 InferenceService 中的参数和 GPU 资源合并到 Runtime 提供的容器模板中。最终 Deployment 使用 GPU 版本的 HuggingFaceServer 镜像:
|
|
Standard 模式下创建的资源关系如下:
|
|
Deployment 可用、HTTPRoute 就绪后,Controller 会把底层状态汇总到 InferenceService:
|
|
storageUri 描述模型的来源,不是容器内的最终路径,也不是 HuggingFaceServer 的启动参数。Controller 会将 pvc://qwen-model 写入 Pod 模板注解;ReplicaSet 创建 Pod 时,KServe Pod Mutating Webhook 读取该注解,将 qwen-model 解析为 PVC 名称,并向模型容器注入 PVC volume 和挂载到 /mnt/models 的 volumeMount。
如果这里使用 hf:// 地址,Webhook 则会注入 Storage Initializer init container,先从 Hugging Face Hub 下载模型,再通过共享的 EmptyDir volume 将模型提供给模型容器。
HuggingFaceServer 启动后,backend 默认为 auto。GPU 镜像中包含 vLLM;当 vLLM 可用且模型架构在 vLLM 的支持列表中时,HuggingFaceServer 会创建 vLLM 后端并从 /mnt/models 加载模型。
实际日志如下:
|
|
kserve-huggingfaceserver 是本次部署选择的 Runtime,vLLM 是 HuggingFaceServer 容器内自动选择的推理后端。因此,这次部署不需要单独指定 vLLM Runtime;集群中是否还存在其他自定义 vLLM Runtime,与这条执行链路无关。
服务 Ready 后,上一篇的 OpenAI Chat Completions 请求沿着下面的路径进入模型:
|
|
这条请求链路属于数据面。KServe Controller 会继续维护 Deployment 和 InferenceService 状态,但不会参与请求转发。
结合上一篇的 Qwen Demo,KServe 的整个工作过程可以概括为:

modelFormat 声明模型格式,供 KServe 匹配 RuntimestorageUri 指定模型存储地址args 设置模型服务器和推理引擎的启动参数resources 声明 CPU、内存和 GPU 等资源需求ClusterServingRuntime/kserve-huggingfaceserver 提供模型服务器镜像和启动方式2026-07-29 04:00:00

在 Kubernetes 上启动一个推理服务并不难。如果只有一个模型,vLLM + Deployment 确实够了。但服务多起来以后,模型从哪里加载、使用哪个 Runtime、GPU 怎么分配、服务怎么暴露,每个服务都要重复处理一遍,配置很快就会散落在一堆 YAML 里。KServe 解决的不是怎么启动 vLLM,而是怎么用统一的方式管理这些模型服务。
本文从零安装 KServe Standard 模式和 Envoy Gateway,通过本地 PVC 部署 Qwen2.5-0.5B-Instruct 模型。
Standardized Distributed Generative and Predictive AI Inference Platform for Scalable, Multi-Framework Deployment on Kubernetes.
KServe 是一个面向 Kubernetes 的可扩展、多框架部署的标准化分布式生成式和预测式 AI 推理平台
即:KServe 是一个 AI 推理平台。

KServe 不直接执行模型计算,它负责管理模型服务。我们创建一个 InferenceService,写清楚模型地址、模型格式和资源需求,KServe Controller 就会选择对应的 Runtime,并创建底层的 Deployment、Service、HTTPRoute 等资源。
KServe 最初叫 KFServing,由 Google、IBM、Bloomberg、NVIDIA 和 Seldon 等团队在 2019 年共同发起。2021 年,项目从 Kubeflow 组织迁移到独立的 KServe GitHub 组织,并正式更名。现在 KServe 是独立的 CNCF 孵化项目,同时仍是 Kubeflow 生态中的重要组件,既可以随 Kubeflow 使用,也可以独立安装。
本文使用 Kubernetes 1.32+。假设节点已经安装 NVIDIA 驱动、Container Toolkit 和 Device Plugin,开始前先确认节点能够看到 GPU:
|
|
输出中的 GPU 应该大于 0。本文使用标准的 nvidia.com/gpu 资源;如果这里是 <none>,后面的推理 Pod 会一直处于 Pending。
这里需要分成两层理解。
InferenceService 是通用模型服务 API,它有 Standard 和 Knative 两种部署模式:
LLMInferenceService 是另一套独立 API,面向大语言模型的分布式工作负载、Prefill/Decode 分离和智能路由等场景。它不是 InferenceService 的第三种部署模式;如果只是部署单 GPU vLLM,使用 InferenceService Standard 模式通常更简单。
本文后面的安装流程只覆盖 InferenceService。LLMInferenceService 需要额外安装 Addon 和相关依赖,系列最后一篇再单独展开。
可以根据需求选择:
|
|
这里我们选择比较简单的 InferenceService(Standard) 模式进行演示。
KServe 依赖 cert-manager,需要提前安装,支持的最低版本是 1.15.0。
|
|
查看 Pod:
|
|
|
|
KServe 可以通过 Gateway API 或 Ingress 暴露服务。本文使用 Gateway API,并选择 Envoy Gateway 作为具体实现。
使用 Helm 安装 Envoy Gateway,包括 Gateway API CRD 和 Controller:
|
|
先创建 GatewayClass:
|
|
然后创建 Gateway。本文的测试集群没有 LoadBalancer,因此使用 EnvoyProxy 将 Envoy Service 配置为 NodePort;如果集群已经有可用的 LoadBalancer,可以删掉 EnvoyProxy 以及 Gateway 中的 parametersRef,直接使用默认配置。
|
|
|
|
|
|
|
|
查看 HuggingFaceServer Runtime 使用的基础镜像:
|
|
这里配置的是 v0.18.0 基础标签;InferenceService 申请 NVIDIA GPU 后,KServe 会自动在标签后追加 -gpu,最终 Pod 使用的是 v0.18.0-gpu。
查看 KServe Controller:
|
|
|
|
KServe 支持从 Hugging Face、PVC、S3 等位置加载模型。由于当前环境不能稳定访问 Hugging Face,本文提前从 ModelScope 下载模型,再通过 PVC 挂载。
|
|
检查模型文件:
|
|
本文只有一个 GPU 节点,因此使用 hostPath 静态 PV。生产环境更适合使用 CephFS、NFS、JuiceFS 等共享存储。
|
|
|
|
然后部署 InferenceService:
|
|
核心参数是这部分:
|
|
model.modelFormat:声明模型格式,会用来匹配 Runtime。当前指定的 huggingface 会匹配到 ClusterServingRuntime/kserve-huggingfaceserver。storageUri:模型来源。当前 pvc://qwen-model 说明模型在 qwen-model 这个 PVC 里面。args:传给 HuggingFaceServer 的启动参数。--model_name 设置 API 中的模型名;其余三个参数用于 vLLM,分别限制最大上下文长度、单次调度最多处理的序列数和 GPU 显存使用比例。
|
|
服务就绪后,READY 会变成 True:
首次启动需要读取模型、初始化 CUDA 并完成 CUDA Graph 预热。普通环境通常只需要几分钟,但磁盘较慢的单节点环境可能需要十几分钟,因此这里将等待上限设置为 30 分钟。
|
|
Standard 模式下,KServe 会为这个 InferenceService 创建 Deployment、Service 和 HTTPRoute:
|
|

查看模型服务日志:
|
|
日志中出现下面的内容,说明 HuggingFaceServer 已经使用 vLLM 加载 /mnt/models:
|
|
KServe 创建的 HTTPRoute 使用域名匹配:
|
|
如果没有配置 DNS,可以直接访问 Envoy 的 NodePort,同时手动设置 Host Header。
查看 Envoy Service:
|
|
NodePort 是动态分配的,先读取实际访问地址:
|
|
|
|
返回结果:
|
|
|
|
返回 chat.completion,说明下面这条链路已经打通。这里的 HTTPRoute 是 Envoy Proxy 使用的路由配置,Envoy Gateway Controller 不在在线请求链路中:
|
|
Qwen2.5-0.5B-Instruct 主要用于验证部署链路,模型规模很小,输出质量不能代表生产模型。
到这里,我们已经用 KServe 跑通了一条完整的 LLM 推理链路,从本地 PVC 加载模型,再通过 Envoy Gateway 管理的 Envoy Proxy 调用 OpenAI 兼容接口。
KServe 的价值不只是启动一个模型进程,而是把模型地址、Runtime、GPU 资源和访问入口收敛到 InferenceService 中,让推理服务也能沿用 Kubernetes 的声明式方式统一管理。
这也是理解 KServe 的起点。下一篇继续往下拆,看看一份 InferenceService YAML 提交之后,KServe 在集群里到底创建了什么。
2026-07-19 04:00:00

事情是这样的。
前几天有读者跟我反馈,说博客变慢了。
不是偶尔卡一下,是点开文章以后,文字出来了,图片还要在原地转上好几秒。
我自己试了几次,确实不对劲。更奇怪的是,前不久我才压缩过图片。
一个纯静态博客,为什么会突然慢成这样?
我第一反应是服务器扛不住了。
毕竟这就是一台很普通的小服务器,2C2G。真遇上请求高峰,被打得喘不过气也不奇怪。
结果我打开监控一看,CPU 使用率还不到 10%。
机器闲得很。
真正出问题的,是带宽。
我这台服务器的公网出口只有 3 Mbps。正常情况下,机器的流出带宽很低,只有 70.793 Kbit/s。

但被大量请求刷起来以后,曲线完全变了。把粒度缩到 1 分钟,公网流出带宽直接冲到 3.289 Mbit/s,贴着 3M 上限跑。

从 70.793 Kbit/s 到 3.289 Mbit/s,相差超过 46 倍。
Caddy 返回静态页面几乎不费 CPU,所以没有收到报警信息。但出口一旦被占满,正常读者再打开一张图片,就只能在后面排队。
顺着带宽曲线往下查,我发现 Caddy Access Log 最近轮转得比平时快了不少。
我一开始还没太当回事。博客嘛,被搜索引擎和 RSS 阅读器抓一抓很正常。
直到我把 27 天的轮转日志全部合到一起,才发现里面有一批访问频率异常的请求。
这份日志快照实际覆盖 27.46 天,统计结果如下:
| 指标 | 数值 |
|---|---|
| 全站请求 | 1,132,456 |
| 异常请求 | 477,314 |
| 异常请求占比 | 42.15% |
| 异常响应流量 | 9.00 GB |
| 异常 IP | 75 个 |
| 返回 200 | 475,806 次 |
47 万是累计总量,短时间内的请求频率更值得看。
| 行为 | 观测结果 |
|---|---|
| 最高一天 | 25,936 次 |
| 最高一小时 | 6,864 次,占该小时全站请求 85.98% |
| 最高一分钟 | 627 次,全部来自同一个 IP |
| 最快 100 次 | 6.281 秒 |
| 最快 500 次 | 43.412 秒 |
| 最长连续突发 | 7 分 28 秒内请求 3,539 次 |
那段最长突发里,相邻请求没有一次停顿超过 1 秒。这段时间共切换了 45 种 User-Agent,成功请求文章页面 3,308 次。
从文章覆盖范围看,这些请求也不是偶尔访问几篇。
当前 sitemap.xml 中有 278 篇文章。这批 IP 访问了全部 278 篇,仅对这些文章就请求了 394,112 次。按篇数粗略折算,相当于把整个文章库读取了 1,418 遍。
这就离谱😂
单篇文章被读取的中位数是 1,163 次,152 篇文章超过 1,000 次,最高一篇达到 5,551 次。
不过,请求多并不能直接说明这些访问不正常。
搜索引擎、RSS 阅读器,甚至某个读者连续打开很多文章,都可能制造高峰。还需要继续看这些请求的具体特征。
先看 IP。
27 天里一共出现了 75 个相关地址,单个 IP 通常只活跃 1~6 天,随后换成同一地址空间里的另一个。
再看 User-Agent。同一个 IP 在不到一秒内留下过这样的记录:
|
|
Chrome 100~144 一共 45 个版本全部出现,每个版本的请求量都在 9,441~10,045 次之间。排除每个 IP 的第一条记录后,97.77% 的相邻请求都会更换 User-Agent。
正常浏览器不会请求一次就换一个 Chrome 版本。
再看请求目标。这批地址主要请求 RSS 和文章 HTML,几乎不加载文章图片。
我第一反应就是封 IP。
但对方 27 天换了 75 个 IP,逐个封禁只能短期止血,批量封禁又容易误伤正常用户。
继续判断请求来自谁没有太大意义,我只需要限制访问频率。
问题变成了,每分钟允许多少次,既能拦住抓取,又不影响正常读者?
为了避免阈值设得太紧,误伤正常访问,我先拿最近 24 小时的日志做了一次固定自然分钟回放:
| 阈值 | 回放结果 |
|---|---|
| 全部请求 60 次/分钟 | 命中 3 个主要异常 IP 和 1 个额外 IP |
| 全部请求 120 次/分钟 | 只命中 3 个主要异常 IP |
| 页面请求 30 次/分钟 | 命中 3 个主要异常 IP 和 1 个额外 IP |
| 页面请求 60 次/分钟 | 只命中 3 个主要异常 IP |
结合回放结果,我设置了两个独立限流区:
|
|
页面请求同时计入两个区。HTML、分类、标签和 RSS 每分钟最多 60 次,CSS、JavaScript 等其他资源只受每分钟 120 次的兜底限制。
日志回放按固定自然分钟统计,后面使用的模块是滑动窗口,两者不会完全一致。这组阈值只对应当时的访问情况,后续还要根据 429 和误伤情况调整。
Caddy v2.11.4 的官方标准二进制不包含 rate_limit,需要通过 xcaddy 构建自定义版本。本文使用的是第三方模块 mholt/caddy-ratelimit。
为了以后换服务器还能复现,我固定了 Caddy、xcaddy 和插件提交:
|
|
构建完成后确认模块已经包含在二进制中:
|
|
限流配置如下:
|
|
将配置写入实际使用的 Caddyfile 后,先用新二进制校验配置,再替换当前版本并重启服务:
|
|
只替换磁盘上的二进制还不够,正在运行的旧进程不会自动加载新模块,因此这里需要重启 Caddy。
配置生效后,我从同一出口 IP 并发请求页面:
|
|
超过窗口额度后,客户端开始收到 429:
|
|
看到 429 后,可以确认限流配置已经生效。
限流在 2026-07-16 10:55:50 随新 Caddy 进程正式生效。
前面的云监控截图中,带宽打满发生在 2026-07-15 20:29~21:28。我把这 1 小时作为上线前窗口,再取限流上线后相同钟点的 2026-07-16 20:29~21:28 做对比。
上线后的同一小时内,异常 IP 成功获取的 HTML 减少了 4,478 次,Caddy 少返回了 89.46 MB 响应数据,还有 1,287 次请求被直接挡在 429。
| 指标 | 上线前 | 上线后 | 实际变化 |
|---|---|---|---|
| 异常地址成功返回 HTML | 5,297 | 819 | 减少 4,478(-84.5%) |
| 异常地址响应流量 | 112.03 MB | 22.57 MB | 减少 89.46 MB(-79.9%) |
| 异常地址返回 200 | 5,664 | 932 | 减少 4,732(-83.5%) |
| 异常地址返回 429 | 0 | 1,287 | 新增 1,287 次拦截 |
| 异常地址请求 | 5,684 | 2,223 | 减少 3,461(-60.9%) |
| 全站请求 | 6,637 | 3,071 | 减少 3,566(-53.7%) |
成功返回的 HTML 和异常响应流量分别下降 84.5% 和 79.9%,限流生效后有 1,287 次请求返回 429。全站请求量也下降了 53.7%,不过里面混着正常访问,只能作为参考。
单个小时还不能代表长期效果,后续仍要观察 429 来源以及搜索引擎是否被误伤。
文章发出来,本来就是给大家看的。有人搜索、订阅,甚至批量抓取文章,我其实都无所谓。我介意的是,持续的高频请求已经影响到正常读者打开文章和图片。
120 次/分钟 和 60 次/分钟 不是通用答案,只是我根据这次日志回放选出的起点,后面还要继续观察是否存在误伤。
限流只是控制访问节奏,让真正来看文章的人能够顺畅打开页面。