背景:隔离怎么就"软"了
我们的 K8s 集群跑在阿里云 ACK 上,用 cGPU 做显存隔离——每个 pod 申请一定量的 GPU 显存配额,多个 pod 共享一张物理卡。理论上,pod 用超配额就会被 CUDA OOM 拦住,不会影响同卡邻居。
直到有一天,我们发现一个只申请了 4GB 配额的模型服务,实际跑着 23GB 显存——没有 OOM,安然无恙。
不是一个 pod 的个例。全集群所有 pod 都能 burst 超出申请配额,显存隔离形同虚设。
提了阿里云工单排查,确认是 cGPU 组件老版本存在 bug,显存隔离没有正常生效。需要升级 cGPU 组件到新版本恢复硬隔离。
但升级不能直接做——先升级 = 定时炸弹。一旦硬隔离生效,所有当前超配的 pod 会瞬间撞配额 OOM,大面积模型不可用。正确顺序是:先把配额修对,再升级。
为了验证升级流程和硬隔离效果,我们先在测试集群做了一轮完整的升级演练。4 台 GPU 节点逐台升级,结果踩了五个坑——每个都是生产环境升级前必须知道的。
坑一:组件版本 bump 不够,必须重置系统盘
cGPU 的核心是一个内核模块(.ko 文件)。升级 cGPU 组件(DaemonSet)后,installer 检测到节点上已有旧 .ko,日志打印:
CGPU has installed. Skip precheck and install.
直接跳过安装。新组件看到旧模块就不干活了。
这意味着存量节点必须走完整的移除-重置流程才能真正升级:
节点 drain → 从集群移除(不释放 ECS)→ 重置系统盘 → 重新加回集群
节点重置后,磁盘上没有旧 .ko,installer 才会真正编译安装新版本的内核模块。详细的升级操作可参考阿里云官方文档。
WARNING
"加回集群不重置系统盘"= 旧 .ko 还在 = 又 Skip = 白折腾。这是升级和普通应用更新最大的不同——内核模块不走镜像层,走的是宿主机磁盘。
坑二:假 Ready 真报错——GPU OOM 的隐蔽形态
升级后硬隔离生效,一个 4GB 配额的模型服务在加载 TensorRT 引擎时报错:
[TRT] [E] Error Code 2: OutOfMemory (Requested size was 3630956544 bytes)
引擎加载失败,变量为 None。但 pod 的状态是 Running 1/1 Ready。
这就是 "假 Ready":
- CUDA OOM ≠ 容器 cgroup OOMKill——CUDA
malloc失败只返回错误码,不杀进程 - 应用框架把它当 Warning 吞了,继续跑
- gRPC 端口照常监听,readiness 探针通过
- 但每个推理请求都报错(
'NoneType' object has no attribute 'set_input_shape')
从监控看 pod 一切正常;从业务看请求 100% 失败。这种故障极难发现——你的告警系统大概率抓不住。
修法:readiness 探针必须检查引擎/模型是否真正加载成功,而不只是检查端口是否在监听。
坑三:drain 被 PDB 挡住
升级一台节点时,drain 任务失败:
error.code: DrainNodeViolatePDB
那台节点上跑着 APISIX 的 etcd 集群成员(3 副本,PDB 设了 minAvailable: 51%)。恰好另一个 etcd 成员因为前面的节点重置 churn 进入 CrashLoop(member not found),只剩 2/3 健康——PDB 不允许再驱逐一个,drain 被卡死。
而且不止一台——APISIX etcd 的另一个成员在另一台待升级节点上,两台节点都被 PDB 挡住,直到运维修好 CrashLoop 的 etcd 成员才能继续。
NOTE
绝不能对 etcd 强制忽略 PDB 驱逐——会掉到 1/3 丢 quorum,APISIX 网关全部瘫痪,AI 推理服务的 gRPC 流量全断。PDB 拦得对。
教训:升级/重置节点前,先扫节点上有没有带 PDB 的有状态 quorum 成员(etcd / ZooKeeper / MongoDB),确保集群全部健康。kubectl get pdb -A 看当前允许中断数。
坑四:节点重置暴露死镜像 tag
节点重置后,本地镜像缓存被清空。几个 Deployment 立刻 ImagePullBackOff——它们钉着 9 个月前的镜像 tag,Harbor 早就清理掉了。之前靠节点上的本地缓存续命,一重置就裸了。
教训:升级前扫一遍所有 Deployment 的镜像 tag,确认 Registry 里还存在。否则重置到哪台,哪台上的旧服务集体拉不到镜像。
坑五:修 etcd 时误删数据 PVC
修复 CrashLoop 的 APISIX etcd 成员时,运维删了 PVC 让它以新成员重加入。但 APISIX 的路由配置存在 etcd 里,没有自动 reconcile 机制、没有外部备份——PVC 一删,300+ 条网关路由全部丢失。
手动补回了所有路由,之后补了 etcdctl snapshot 定期备份。
教训:对有状态组件操作前,先确认数据有没有外部备份 / reconcile 机制。"删 PVC 重建"在无状态场景是常规操作,在有状态场景是灾难。
升级铁律:先修配额,再升级
五个坑里,坑二(假 Ready)是最本质的。它告诉你:
升级恢复硬隔离之前,必须把所有 pod 的配额按真实用量修对。顺序反了 = 全集群超配 pod 同时 GPU OOM = 大面积假 Ready = 模型集体不可用。
升级前的 Checklist:
- 全量配额按 7 天峰值扫一遍,超配的先调大
- 加超配率告警(用量 ÷ 配额 ≥ 90% 告 Warning,≥ 100% 告 Critical)作为安全闸
- 扫节点上的有状态 quorum 成员,确保全部健康
- 扫所有 Deployment 的镜像 tag,确认 Registry 可拉取
- 有状态组件备份(etcd snapshot 等)
附:GPU 调度三旋钮模型
除了隔离,升级过程中我们还梳理了 GPU 共享集群的调度全景。一个 GPU pod 的"落哪"是两步决策 + 一个运行时行为:
① 落哪个节点? ← K8s 调度器
② 节点上哪张卡? ← 阿里云 GPU 共享调度器
③ 同卡多 pod 怎么分算力? ← cGPU 隔离驱动
对应三个独立的调控旋钮:
| 旋钮 | 控制什么 | 配置方式 | 爆炸半径 |
|---|---|---|---|
| topologySpread | pod 分散到不同节点 | Deployment topologySpreadConstraints | 单服务 |
| placement | 节点内分散到不同卡 | 节点 label(binpack / spread) | 该节点池所有 GPU 服务 |
| POLICY | 同卡多 pod 的算力分配 | cGPU 隔离驱动参数 | 全集群 |
比喻:楼层 = 节点,房间 = 卡,桌子空间 = 算力。topologySpread 把人分散到不同楼层;placement 在同一层分散到不同房间;POLICY 决定挤在一个房间时桌子怎么分。三者接力,不重叠。
POLICY 档位
| 值 | 模式 | 隔离强度 |
|---|---|---|
| 0 | 平均时间片(固定 1/N) | 强,有保底 |
| 3 | 固定算力百分比 | 强,有保底 |
| 5 | 原生(驱动默认,裸抢) | 无 |
我们的集群是 POLICY=5(无隔离),所以同卡 pod 越多越慢——纯靠驱动层裸抢 SM。改 0 或 3 可以让每个 pod 有保底算力,但代价是独占卡的 pod 也被封顶,降峰值吞吐。
渐进落地建议
- 先 topologySpread——纯 K8s 原生,对单服务生效,完全可逆,风险最小
- 再 placement=spread——节点级别,影响该节点池所有 GPU 服务,但不需重启
- 最后 POLICY——全集群生效,需要重启节点,在没有延迟数据之前不要盲调
Checklist:GPU 共享集群运维
- 确认 GPU 隔离组件版本和 policy 是否真正生效(
nvidia-smi对比实际用量与配额) - 升级内核模块类组件,存量节点必须移除再加回并重置系统盘(不能只 bump 镜像版本)
- readiness 探针要检查模型/引擎加载状态,不只是端口
- 升级前扫配额超配、PDB quorum、死镜像 tag、有状态备份
- GPU 调度三旋钮按 topologySpread → placement → POLICY 顺序渐进,不要一步到位
参考链接
- ack-ai-installer 组件介绍与变更说明 — cGPU 组件版本历史与功能概览
- 管理共享 GPU 调度组件 — 安装、升级 cGPU 组件及存量节点升级流程
- 配置共享 GPU 调度节点选卡策略 — binpack / spread 选卡配置(对应三旋钮之 placement)
- 通过共享 GPU 调度实现算力分配 — 算力隔离与分配配置(对应三旋钮之 POLICY)
- Kubernetes PodDisruptionBudget — PDB 配置与 drain 行为
- APISIX 高可用部署 — APISIX + etcd 的部署模式与备份建议

