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 次/分钟 不是通用答案,只是我根据这次日志回放选出的起点,后面还要继续观察是否存在误伤。
限流只是控制访问节奏,让真正来看文章的人能够顺畅打开页面。
2026-07-15 06:00:00

上一篇我们用 Kueue + NVIDIA 原生 DRA 跑通了 GPU 整卡配额:Job 先进入队列,Kueue 判断配额够不够,够了再放行给调度器。
但整卡只是第一步。真实的 GPU 集群里,一张 GPU 往往不会只给一个 Pod 用,HAMi 可以继续按显存和算力切成 vGPU。问题也跟着来了:切完之后,队列系统还能不能知道每个任务用了多少?能不能做到两个任务放行、第三个因为显存或算力配额不够继续排队?
这篇就围绕这个问题跑一遍:HAMi 把 GPU 切给 Pod,Kueue 在 Job 准入阶段先把 vGPU、显存和算力配额算清楚。重点不是“能不能切卡”,而是“切完之后还能不能管起来”。
环境基本沿用上一篇 Kueue + NVIDIA DRA 的测试环境。区别只有两点:
本文的命令和输出来自这套环境:
|
|
K8s 集群、GPU Operator、Kueue 的安装过程前几篇已经写过,这里不再重复。需要确认的一点是:后面由 HAMi 接管 GPU 切分,所以 GPU Operator 安装时要关闭 NVIDIA DevicePlugin:
|
|
节点上能看到 T4 即可:
|
|
HAMi DRA Webhook 需要 TLS 证书,因此需要提前安装 cert-manager 用于自动签发。
|
|
给节点打上 gpu=on 标签。未标记的节点不会被 HAMi 接管。
|
|
安装 HAMi,并通过 --set dra.enabled=true 开启 DRA 模式:
|
|
注意:DRA 模式与传统模式不兼容,请勿同时启用。
gpu=on 主要给 HAMi 做节点选择。后面 ResourceFlavor 用的 nvidia.com/gpu.product=Tesla-T4,则是 GPU Operator / GFD 自动打到节点上的标签。
另外,如果 GPU 驱动是主机预装,非 GPU Operator 安装,则安装时需额外指定:
|
|
HAMi 正常启动后,hami-system 下会看到三个核心组件:
|
|
底层发布出来的 GPU 资源也要确认一下:
|
|
ResourceSlice 里记录了这张 T4 的显存和算力容量:
|
|
需要注意的是 allowMultipleAllocations: true:同一张物理 GPU 可以被多个 ResourceClaim 消费,只要显存和算力容量还够,切卡才有空间。
对 HAMi 不熟悉的同学可以先看看这篇文章:Kubernetes GPU 虚拟化实战:HAMi DRA 模式完整指南
HAMi DRA 现在有两种使用方式:
| 模式 | 用户怎么写 | 谁创建底层设备申请 | 更适合什么场景 |
|---|---|---|---|
| DRA 原生模式 | 手写 ResourceClaim / ResourceClaimTemplate
|
用户 | 新业务直接接 DRA API |
| 兼容模式 | 写 nvidia.com/gpu/gpumem/gpucores
|
HAMi webhook | 存量 HAMi 业务迁移 |
我这次实测下来,Kueue 接 HAMi 原生 DRA 时,更容易先做到按 claim / 设备数量做准入;但要把 gpumem/gpucores 也纳入 Kueue 配额,兼容模式反而更顺:业务侧仍然写 nvidia.com/gpu/gpumem/gpucores,Kueue 通过 ResourceTransformation 把它们折算成队列里的总量配额资源。
所以这篇走兼容模式,业务 YAML 继续写熟悉的 HAMi 资源:
|
|
含义是:
| 资源 | 含义 |
|---|---|
nvidia.com/gpu: 1 |
申请 1 个 vGPU 设备实例 |
nvidia.com/gpumem: 4096 |
每个 vGPU 申请 4096Mi 显存 |
nvidia.com/gpucores: 50 |
每个 vGPU 申请 50% 算力 |
HAMi webhook 会拦截这个 Pod,把上面的资源申请转换成底层设备申请。调度时,kube-scheduler 看到的是 HAMi 发布出来的 GPU 设备,以及对应的显存、算力容量。
这里用一个最小例子把链路跑通。先验证 HAMi 能不能切出 4Gi / 50 cores;确认没有问题以后,再接入 Kueue,观察它会不会在准入阶段扣配额。
为了把变量降到最低,先不接 Kueue,直接提交一个 Pod:
|
|
Pod 创建后,HAMi webhook 会把原始资源申请改成 ResourceClaim:
|
|
对应的 ResourceClaim 如下:
|
|
容器里看到的显存也变成了 4096Mi:
|
|
到这里可以确认 HAMi vGPU 已经生效:业务 YAML 仍然写 nvidia.com/gpu/gpumem/gpucores,容器里实际只看到被切分后的 4096Mi 显存。
HAMi 验证通过后,就可以把 Kueue 接进来了。接入前需要先把配额口径捋清楚:HAMi 的 nvidia.com/gpumem 和 nvidia.com/gpucores 是“每个 vGPU”的资源。比如这次 Demo 里有两个相同规格的 Job,每个 Job 都申请:
|
|
那么队列整体在配额上应该按总量扣减:
|
|
Kueue 管队列配额时应该看总量,所以要在 Kueue 配置里加 ResourceTransformation。这次用的是 Kueue 0.18.1:
|
|
这几行最关键的是两个动作:
nvidia.com/gpumem
nvidia.com/total-gpumem
gpucores 同理,转成 nvidia.com/total-gpucores
multiplyBy: nvidia.com/gpu 表示先乘以 vGPU 个数,再进入 Kueue 配额修改 kueue-manager-config 后重启 Kueue:
|
|
这次只用一个队列,把资源扣减先看清楚。
场景:
|
|
Kueue 配置:
|
|
这里的显存配额写 8192,和业务侧的 nvidia.com/gpumem: 4096 保持同一个口径:单位都是 Mi。单个 Job 会被 Kueue 统计成 nvidia.com/total-gpumem: 4096,两个相同 Job 加起来就是 8192。
提交一个 4Gi / 50 cores 的 Job:
|
|
结果:
|
|
看 Workload 的准入结果:
|
|
这个结果就是 Kueue 介入后的关键变化:Pod 还没进入调度阶段,Workload 已经先按 vGPU 个数、总显存、总算力扣了一次配额。
HAMi 生成的 ResourceClaim 也可以对上:
|
|
容器里也能看到 4096Mi 显存上限:
|
|
继续提交两个相同规格的 Job:
|
|
队列总配额是:
|
|
因此第 2 个 Job 可以准入,第 3 个 Job 会继续留在队列里:
|
|
ClusterQueue 的用量已经打满:
|
|
Pending Workload 里能看到原因:
|
|
这也是我更关心的点:不是等 Pod 到调度阶段才 Pending,而是在 Job 准入阶段就把超配额任务留在队列里。
第一个坑是小数 GPU。不要写 nvidia.com/gpu: "0.5"。
Kubernetes 扩展资源必须是整数,而且 GPU 这类不可超卖资源要写在 limits 里。正确写法是:
|
|
半张卡 这种说法在 HAMi 里应该理解成:1 个 vGPU 实例 + 部分显存 + 部分算力,不是 0.5 个 nvidia.com/gpu。
只把 nvidia.com/gpu 放进 Kueue,只能限制 vGPU 个数,管不了显存和算力。
要把 HAMi 的切分资源纳入 Kueue 配额,需要做这两个转换:
|
|
ClusterQueue 里也要同时配置这三个资源:
|
|
这一篇跑完后,Kueue 系列基本就从 CPU 队列一路串到了 GPU 整卡和 HAMi vGPU。真落到 GPU 集群里,光能切卡还不够,准入和配额也一样重要: GPU 虚拟化解决的是"怎么切",Kueue 解决的是"谁先用、谁能用多少"。两者结合起来,GPU 集群才真正具备了多租户管理能力。
2026-07-02 06:00:00

前面两篇:Kubernetes 官方出品:一个 Controller 搞定 Job 排队和资源配额 和 终于搞懂 Kueue:5 个核心对象一次讲透 把 Kueue 的基本玩法和核心对象走了一遍,不过 Demo 都跑在 CPU 上。
真到 GPU 集群里,大家更关心的是另一个问题:
Kueue 能不能管理 DRA 模式下的 GPU?
这一篇就把它跑通。NVIDIA DRA Driver 负责把整卡 GPU 发布成 DeviceClass / ResourceSlice,Kueue 在 Job 准入阶段读取 DRA 设备申请,判断这个 Job 能不能进入队列。
对 DRA 不熟悉的同学,可以先看这篇:DRA P1:DRA 能解决什么问题?从部署到使用的完整体验
环境准备分几段走:先创建 K8s 集群,再用 GPU Operator 把 GPU Driver / Container Runtime 准备好,最后装 Kueue 和 NVIDIA DRA Driver。
集群使用 KubeClipper 创建。KubeClipper 1.6.0 默认支持 Kubernetes 1.36.1、containerd 2.2.4,和本文环境一致,详细步骤可以参考:KubeClipper 1.6.0 发布:kcctl 优化与 K8s 1.36 支持。
快速创建单节点集群的命令如下:
|
|
集群起来后确认版本:
|
|
GPU Driver、NVIDIA Container Toolkit 等基础组件使用 GPU Operator 安装。完整说明可以参考之前这篇:GPU 环境搭建指南:使用 GPU Operator 加速 Kubernetes GPU 环境搭建。
本文后面会安装 NVIDIA DRA Driver,所以安装 GPU Operator 时需要关闭 DevicePlugin:
|
|
--set devicePlugin.enabled=false:关闭 DevicePlugin,避免与后续安装的 DRA Driver 冲突。
安装完成后,确认 GPU Operator 组件正常运行:
|
|
再确认节点能看到 GPU:
|
|
Kueue 使用 0.18.1,安装方式和前两篇一样:
|
|
如果测试环境访问 registry.k8s.io 不稳定,也可以用 GitHub Release 里的 chart 包:
|
|
最后安装 NVIDIA DRA Driver 25.12.0:
|
|
安装完成后,先看 DRA Driver 组件:
|
|
再看 DeviceClass:
|
|
整卡调度用的是 gpu.nvidia.com。对应的 ResourceSlice 里能看到节点上的 T4:
|
|
后面的 Job 会直接引用 gpu.nvidia.com 这个 DeviceClass,实际可分配设备则来自这些 ResourceSlice。
DRA 和 Kueue 使用的资源名称并不是同一个。ResourceClaimTemplate 里写的是 deviceClassName: gpu.nvidia.com,而 ClusterQueue 里扣配额用的是资源名,所以这里需要做一次映射:
|
|
这段配置的意思是:只要 Workload 通过 ResourceClaimTemplate 申请 gpu.nvidia.com 这个 DeviceClass,Kueue 就把它折算成 nvidia.com/gpu 这个逻辑资源来扣配额。

修改 Kueue manager config 后,需要重启 kueue-controller-manager 让新配置生效。
这次只用一个队列,GPU 配额只给 1 张 T4。这样后面再提交第二个 Job 时,Pending 状态会看得很清楚。
|
|
注意这里的 coveredResources 里包含 nvidia.com/gpu。这是映射后的逻辑资源名,不是 Pod 里直接写的扩展资源。
使用 DRA 之后,Job 不再写 resources.limits.nvidia.com/gpu: 1,而是引用一个独立的 ResourceClaimTemplate。
这里 Kueue 做的事情很直接:读取 Workload 引用的 ResourceClaimTemplate,识别里面的 deviceClassName 和 count,再通过 deviceClassMappings 折算成 ClusterQueue 里的配额资源。
本文不展开 extended resource 路径,避免把两种 DRA 接入方式混在一起。
先创建 ResourceClaimTemplate:
|
|
这里有两个细节容易混:
deviceClassName 是 gpu.nvidia.com,不是 nvidia.com/gpu
ExactCount + count: 1 表示申请 1 张整卡再提交 Kueue 管理的 Job:
|
|
Job 被 Kueue 准入后,会从 Suspended 变成 Running:
|
|
ResourceClaim 已经分配:
|
|
容器里能看到 T4:
|
|
直接看 Workload 的准入结果:
|
|
这说明 deviceClassMappings 已经生效:用户写的是 ResourceClaimTemplate,Kueue 扣的是 nvidia.com/gpu 这个逻辑配额。
再看看 ClusterQueue,可以发现 GPU 配额已经被扣掉了:
|
|
队列里只有 1 张 GPU 配额。如果再提交一个同样申请 single-gpu 的 Job:
|
|
|
|
再看 Workload 状态,就能发现为什么第二个 Job 一直起不来:
|
|
ResourceClaimTemplate 里写的是:
|
|
ClusterQueue 里写的是:
|
|
两者靠 Kueue 配置里的 deviceClassMappings 关联起来。少了这段映射,Workload 会被标成 Inadmissible,原因类似:
|
|
这两个名字最容易看混:
single-gpu:前面单独创建的 ResourceClaimTemplate,定义“我要 1 张 gpu.nvidia.com 整卡”。gpu-claim:Pod 里的本地 claim 名字,后面容器通过它来使用 GPU。Pod 里这段不是重新定义一个模板,而是引用已经存在的 single-gpu 模板:
|
|
容器里再通过同一个 gpu-claim 关联到这次申请到的设备:
|
|
所以完整关系是:ResourceClaimTemplate(single-gpu) -> Pod resourceClaims(gpu-claim) -> container resources.claims(gpu-claim)。
这里要注意一点:Kueue 只是负责“准入”,真正把 GPU 分配给 Pod 的还是 kube-scheduler 和 DRA Driver。
所以生产环境里建议同时关注 ResourceClaim 状态和 Pod 状态。如果希望 Workload 在 Pod 长时间起不来时释放 Kueue 配额,可以结合 waitForPodsReady 做保护。
到这里可以看到,Kueue 并没有直接参与 GPU 分配,而是站在 Job 准入这一层,通过 deviceClassMappings 把 DRA 的设备申请转换成队列里的配额资源。这样既保留了 DRA 的设备模型,也让 GPU 可以继续纳入 Kueue 的统一配额管理。
| 组件 | 负责什么 |
|---|---|
| NVIDIA DRA Driver | 把 GPU 作为 gpu.nvidia.com DeviceClass / ResourceSlice 发布出来 |
| Kueue | 通过 deviceClassMappings 把 DRA 设备折算成 nvidia.com/gpu 配额 |
| kube-scheduler | 在 Pod 调度阶段完成 ResourceClaim 的实际设备分配 |
下一篇继续往前走一步,把 HAMi 引入进来:一张 GPU 被切成多份 vGPU 之后,Kueue 是否还能继续管理显存和算力配额。