AI时代的Kubernetes教程36招-第36招-企业数字人平台怎么在Kubernetes上完成多节点GPU生产部署?
生产级 Kubernetes 交付实战:kubeadm HA、GPU 异构调度与故障演练
三台控制面撑住了流量,却撑不住一次节点维护。
[读者] 正在把 Kubernetes 推进生产的平台架构师、SRE 与运维负责人
[痛点] 控制面单点、GPU 节点裸跑、镜像与存储各自埋着一处隐患
[现在读] 一次交付要同时满足 HA、异构算力、GitOps 与可演练
[读完] 能独立搭出并演练一套生产级集群,并说清每一步取舍
[旧方案] 单 master + kubeadm 默认 etcd + Ingress + 宿主 Docker 跑 GPU
|
v
[新需求] 三控制面 HA、GPU 池化、多租户隔离、可演练、可重建
|
v
[冲突] 控制面高可用了,GPU 调度、存储与身份仍然各自单点
|
v
[后果] 一次维护窗口就能让推理、检索、语音链路整体不可用
我是老李,做平台工程第 9 年。上一段项目经历,是把公司的推荐、检索和生成式 AI 业务,从四十多台裸机 Docker 迁到自建 Kubernetes 上。
旧集群是一台 master、五台 CPU worker、两台 GPU worker,用 kubeadm 默认参数装的,入口是 nginx Ingress,GPU 服务直接以宿主 Docker 拉起,只有少数几个 Deployment 进了 K8s。它能跑,但它更像一个带编排的虚拟机池,而不是一个可交付的生产系统。
变化来得很快:业务方要求 7×24 可用、GPU 池化共享、RAG 与 Avatar 按租户隔离;安全团队要求 RBAC 最小权限与 Pod 安全基线;运维团队要求每个季度做一次真实演练,并留下书面结论。
控制面做到三副本 HA,就等于生产可用了吗?
GPU 节点该用 taint 做隔离,还是只靠标签和亲和性挑节点?
演练做得越接近真实就越好,还是先要保证安全边界?
01、故事
周三凌晨两点,一次例行的内核补丁要重启那台唯一的 master。任务是:在维护窗口内完成控制面节点重启,并保证线上业务不中断。
结果是,kube-apiserver 停止响应之后,所有 worker 上的 kubelet 心跳开始失败,控制器管理器停摆,Gateway 的路由配置不再更新,两个正在扩容的推理 Pod 因为无法写入状态而卡在 Pending。值班同学能做的只有等。
事后复盘出三件事:控制面是单点;两台 GPU 节点上有两个服务在抢同一张卡,谁先起谁占;镜像仓库和集群在同一个机房,没有副本,维护期间新 Pod 拉不到镜像。
迁移方案定成三阶段:先把控制面变成 kubeadm HA 加 stacked etcd,用 VIP 加 L4 做前置;再把 GPU 从宿主 Docker 搬进 Device Plugin 体系,用 taint 和 label 划分算力池;最后上 GitOps,把集群期望状态变成代码,并逐条演练故障场景。
单点不喊疼
一停全露底
先立可用线
再谈性能题
02、问题
旧方案失效不是一瞬间的事,而是四条隐性依赖同时暴露。
业务影响上:控制面重启期间无法创建新 Pod,扩容失效,故障转移失效,任何一次滚动发布都要停下来等;GPU 抢卡导致同一张卡上的两个进程互相拖慢,语音链路延迟忽高忽低;仓库不可用时,节点重建等于停机。
技术表现上:kubectl 请求超时;控制器日志出现大量 connection refused 后进入退避重连;etcd 只有一个成员,不存在 quorum 概念,节点重启即集群不可写;nvidia.com/gpu 这个扩展资源根本不存在,调度器把 GPU Pod 当普通 Pod 分发。
要交付的目标,必须能被验证,而不是靠感觉。我列了六条完成标准:
- 任意一台控制面节点下线,
kubectl get --raw='/readyz?verbose'仍返回可读的服务状态; - etcd 成员数为 3,任一成员宕机后,其余成员仍能完成读写;
- 所有 GPU 类 Pod 只落在打了 GPU 标签的节点,且一张卡不会同时分配给两个 Pod;
- 业务镜像全部来自私有 Registry,节点侧具备断链时的可用路径;
- 期望状态存放在 Git 中,可用同一份声明重建一套新集群;
- 七类故障场景(控制面宕机、Worker 维护、GPU 节点掉线、Registry 失败、CSI 故障、etcd 恢复、版本升级)都有演练记录和回滚步骤。
失效看得见
影响算得清
标准可验证
才算真收工
03、原理
这一篇只需要三个原理,多一个都不讲。
第一个是 etcd 的多数派。etcd 用 Raft 达成一致,任何写入都要多数派确认。3 个成员时 quorum 是 2,能容忍 1 个节点故障;4 个成员时 quorum 是 3,还是只能容忍 1 个节点故障,却多了一份数据同步和选举成本。
反直觉判断:控制面节点从 3 台加到 4 台,可用性不会上升,反而在部分故障组合下更脆弱。etcd 成员数必须是奇数,这是容量决策,不是数量决策。
第二个是 kubeadm 的 stacked etcd。etcd 成员与控制面节点同机部署,由 kubeadm 统一管理证书与生命周期,好处是少维护一套集群,代价是故障域耦合:机器挂掉时,控制面和 etcd 成员一起消失。如果团队没有独立的 etcd 运维能力,stacked 是更务实的选择;如果有,才考虑外部 etcd。
第三个是 GPU 不是可压缩资源。nvidia.com/gpu 以整数形式声明,调度器按整卡匹配,一张卡不会分给两个 Pod(除非显式配置了时间切片或 MPS)。这意味着 GPU 的调度逻辑是「先隔离、再匹配」,而不是「先抢占、再压缩」。CPU 上那套超卖习惯,在 GPU 上会直接变成生产事故。
顺带纠正一个常见误解:HorizontalPodAutoscaler 的 spec.minReplicas 下限是 1,它本身做不到缩到 0。异步 AI 任务想 scale-to-zero,必须引入 KEDA 这类外部控制器,通过 external metrics 完成 0 到 1 的唤醒。
多数派为纲
奇数方为稳
资源不可压
零副本难成
04、架构
[输入] 用户请求 / 异步 AI 任务 / 平台侧变更
|
v
[模块] VIP+L4 -> kube-apiserver x3(stacked etcd x3)+ CPU Worker 池 + GPU Worker 池
|
v
[数据/状态] etcd 成员状态、CSI 卷、HPA/KEDA 指标、Git 中声明的期望状态
|
v
[处理] CNI 与 NetworkPolicy 放行、调度器按 label/taint/affinity 选节点、Device Plugin 分配 GPU、Gateway API 转发入口流量
|
v
[输出] 推理与检索响应、可观测信号、可回滚可重建的集群版本
边界很清楚。控制面三台只解决「控制面单点」与「etcd 多数派」这两件事,它不解决 GPU 抢卡,也不解决仓库单点。CNI 只解决 Pod 间连通与隔离策略,它不负责流量入口。Gateway API 只负责南北向入口,它不决定 Pod 落在哪台机器。
收益是:任意一台控制面节点可以随时下线维护;GPU 从「谁先起谁占」变成「可调度、可审计」;集群的期望状态有单一事实来源。
代价是:链路变长,排查面变宽;stacked etcd 让控制面机器成为更贵重的资产;GitOps 引入了一条新的交付路径,需要有人对仓库负责。
适用条件是:控制面节点数保持 3 或 5 的奇数,且分布在不同物理故障域;GPU 节点规模在几十台以内,用 label 加 taint 的静态划分足够;CNI 在 Calico 与 Cilium 之间二选一,选型依据是内核版本、团队熟悉度,以及是否需要 L7 策略和 eBPF 可观测能力,而不是简单的性能对比。
入口先收口
算力按池分
状态有归属
边界写清楚
05、实战一次
先跑通最小闭环,再谈高可用。以下步骤是按官方文档整理的预期路径,未在当前环境实测,以下为预期结果。
环境与版本:3 台控制面(4C8G,SSD,内网互通);CPU Worker 若干;GPU Worker 2 台;Linux + systemd;容器运行时 containerd。Kubernetes 版本本文按官方文档 1.3x 语义描述,https://kubernetes.io/releases/1.37/ 指向的版本在官方发布页是否已存在,需按当前官方文档核验。
依赖:一个 VIP 加 L4 负载均衡(keepalived+haproxy 或云负载均衡均可);私有 Registry;CSI 存储;Helm 3。
配置 /etc/kubernetes/kubeadm-config.yaml:
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
controlPlaneEndpoint: 10.0.0.10:6443
kubernetesVersion: v1.3x.y
networking:
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
etcd:
local:
dataDir: /var/lib/etcd
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
kubeadm.k8s.io/v1beta4 的具体字段以官方文档为准,升级前需核验各字段是否仍被支持。
核心实现与启动:
# 1) 初始化第一个控制面节点
sudo kubeadm init --config /etc/kubernetes/kubeadm-config.yaml --upload-certs
# 2) 按 init 输出加入另外两台控制面(--certificate-key 来自上一步输出)
sudo kubeadm join 10.0.0.10:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash> --control-plane --certificate-key <key>
# 3) 加入 CPU Worker
sudo kubeadm join 10.0.0.10:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>
# 4) 安装 CNI(此处以 Cilium 为例,Calico 替换为对应 helm chart 即可)
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --namespace kube-system --set kubeProxyReplacement=true --set k8sServiceHost=10.0.0.10 --set k8sServicePort=6443
# 5) 标记 GPU 节点并加污点
kubectl label node gpu-01 node-role.kubernetes.io/gpu=
kubectl taint node gpu-01 dedicated=gpu:NoSchedule
# 6) 安装 NVIDIA Device Plugin,并显式携带 tolerations
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm install nvdp nvdp/nvidia-device-plugin --namespace kube-system --set 'tolerations[0].key=dedicated' --set 'tolerations[0].operator=Equal' --set 'tolerations[0].value=gpu' --set 'tolerations[0].effect=NoSchedule'
验证集群:
kubectl get nodes -o wide
kubectl -n kube-system get pods
kubectl get --raw='/readyz?verbose'
kubectl describe node gpu-01 | grep -i nvidia
示例输出:
NAME STATUS ROLES AGE VERSION
cp-01 Ready control-plane 10m v1.3x.y
cp-02 Ready control-plane 8m v1.3x.y
cp-03 Ready control-plane 6m v1.3x.y
cpu-01 Ready <none> 4m v1.3x.y
gpu-01 Ready gpu 2m v1.3x.y
nvidia.com/gpu: 1
发一次真实请求。部署一个 RAG 服务并用临时 Pod 调用:
kubectl -n ai create deployment rag-api --image=registry.internal/ai/rag-api:1.0.0 --replicas=2
kubectl -n ai expose deployment rag-api --port=8080 --target-port=8080
kubectl -n ai run smoke --rm -i --restart=Never --image=registry.internal/tools/curl:8.0 -- curl -sS http://rag-api.ai.svc.cluster.local:8080/healthz
示例输出:
{"status":"ok","index":"rag-v1"}
到这里,一个最小可用的 V1 就跑通了:三控制面、CNI、一个 CPU 服务、一个带 GPU 标签的节点。但 V1 留下了三个问题:GPU 服务还不敢上生产;etcd 没有快照;镜像仓库仍是单点。
先把最小跑
再谈高可用
命令留证据
结论才落地
06、排查
诊断链一:第三台控制面加不进来
现象:kubectl -n kube-system get pods 里 etcd-cp-03 反复重启,etcdctl member list 里该成员始终是 unstarted。
怀疑:证书 SAN 不含新节点,或成员之间网络不通。
检查:journalctl -u kubelet -n 200,再看容器日志里最内层的那行错误。
证据:日志里出现 dial tcp 10.0.0.13:2380: i/o timeout,而同一台机器上 ss -lntp 显示 2379 与 2380 都在正常监听。
根因:控制面之间的 2379/2380 端口未在安全组与主机防火墙上放行。端口在本地听着,不代表对端能连上。
修复:放行控制面之间 2379、2380,放行客户端与 VIP 到 6443。
错误尝试:一开始判断是 /var/lib/etcd 的目录权限问题,对数据目录做了 chown 并重启 kubelet。结果毫无变化——因为 etcd 跑在静态 Pod 里,文件权限问题会给出明确的 permission denied,而不是 i/o timeout。把网络超时当成文件系统错误,是典型的「现象相似、证据方向相反」。
诊断链二:GPU Pod 一直 Pending
现象:带 nvidia.com/gpu: 1 的 Pod 长期 Pending,kubectl describe pod 提示节点资源不足。
怀疑:Device Plugin 没装好,或者调度被污点挡住了。
检查:kubectl -n kube-system get pods -l app.kubernetes.io/name=nvidia-device-plugin,再看 kubectl describe node gpu-01 | grep -i nvidia。
证据:节点上完全没有 Device Plugin 的 Pod,allocatable 里也没有 nvidia.com/gpu。
根因:给 GPU 节点打了 dedicated=gpu:NoSchedule 污点后,Device Plugin 的 DaemonSet 没有对应的 tolerations,它自己就调度不上去,于是没人向节点上报 GPU 资源。
修复:给 DaemonSet 补上 tolerations,并用 nodeAffinity 限定只跑在 GPU 节点上。
诊断链三:HPA 想缩到 0 被拒绝
现象:把 HPA 的 minReplicas 写成 0,API Server 直接拒绝,提示必须大于等于 1。
怀疑:是 RBAC 或校验 webhook 的问题。
检查:kubectl explain hpa.spec.minReplicas,以及官方文档中关于 HPA 缩容下限的说明。
证据:字段说明本身就写明了取值范围。
根因:这不是配置问题,是能力边界问题,HPA 从设计上就不支持 scale-to-zero。
修复:异步任务改由 KEDA 的 ScaledObject 驱动,用 external metrics 做 0 到 1 的唤醒。
现象不撒谎
假设要挨刀
证据链闭合
根因才现身
07、优化
V2 的每一处修改都必须对得上第六章的一条证据。
修改一:放行控制面互通端口。 根因是 2379/2380 未放行。原因是 etcd 成员需要互相通信才能达成多数派。新行为是第三个成员能正常加入并参与选举。验证方式是停掉一台控制面,观察集群是否仍可读写。
修改二:给 Device Plugin 补 tolerations 与 nodeAffinity。 根因是 DaemonSet 被自己设的污点挡住。原因是 DaemonSet 也要遵守调度约束,没有任何组件可以豁免。新行为是 GPU 节点的 allocatable 中出现 nvidia.com/gpu。验证方式是提交一个带 tolerations 的 GPU 冒烟 Pod,确认它落在 gpu-01。
修改三:把异步 AI 任务从 HPA 迁到 KEDA。 根因是 HPA 的下限为 1。原因是 0 副本需要一个能从外部把副本数拉起来的控制器。新行为是队列为空时副本数归零,任务到达时通过 external metrics 唤醒。验证方式是往队列投一条消息,观察副本从 0 变 1。
修改四:etcd 定期快照。 用 CronJob 调 etcdctl snapshot save,快照落到集群外对象存储,并保留恢复脚本。仅保存快照而不演练恢复,等于没有备份。
修改五:私有 Registry 做高可用。 至少两个副本加节点侧缓存策略,避免仓库故障直接等于节点重建失败。
修改六:GitOps 接管期望状态。 把 deploy/prod/ 目录交给 ArgoCD 同步,集群侧只允许 Git 写入,人工 kubectl apply 逐步下线。
修改七:身份与隔离。 用 RBAC 做最小权限,用 Pod Security Admission 的 baseline/restricted 标签约束命名空间,Pod 证书相关能力的具体版本状态需按当前官方文档核验。
这里不对任何优化写具体收益数字,因为当前环境没有做过对照实验。验证方式写清楚了,结论要靠跑出来。
改在根因上
有效再谈优
一次只一动
回滚有退路
08、演进
[同一输入] 一次控制面节点维护 + 一次 GPU 推理服务扩容
|
+--[V1] 单 master、宿主 Docker 跑 GPU、手工 kubectl apply
| 代价:维护即停机、GPU 抢卡、状态漂移无人知
|
+--[V2] kubeadm HA + stacked etcd、Device Plugin、GitOps
| 代价:链路更长、控制面更贵重、演练成本上升
|
[Trade-off]
得到:单节点故障可容忍、GPU 可调度、期望状态可重建
失去:部署链路上的每一次变更都更慢、更重、更需要评审
适用边界:控制面 3 或 5 台奇数、GPU 节点几十台以内、有专人维护 Git 与演练
正确性上,V2 把「靠人记住」换成了「靠声明校验」,deploy/prod/ 目录就是集群的唯一事实来源。
稳定性上,V1 的任何一次机器重启都是风险事件,V2 把风险收敛到一次可计划的演练。
复杂度上,V1 简单但不可控,V2 复杂但每一步都能被追溯。这是真实的交换,不是免费的升级。
成本上,三台控制面加 VIP 加 L4 带来的是固定开销;真正增加的是人的成本——有人要负责 Git 仓库、有人要负责季度演练。
适用范围上,V2 适合有稳定运维值班和明确 SLO 的团队;如果团队只有一两个人,先把 etcd 快照和私有 Registry 做扎实,比急着上三控制面更实际。
遗留问题有三个:stacked etcd 的故障域耦合没有解决,只是被容忍;GPU 时间切片没有引入,卡利用率仍然偏低;版本升级演练还没有跑完整流程,官方文档中的弃用项需要逐版核对。
同一份输入
两条路代价
得到即失去
边界即答案
09、洞见
9.1 副本数与可用性不是线性关系
反直觉判断:控制面从 3 台加到 4 台,可用性不会上升,反而可能下降。etcd 的容错能力由 quorum 决定,不是由成员数决定。3 成员容忍 1 台故障,4 成员仍然只容忍 1 台,却多付出一次数据同步和选举开销。数量决策之前,先问这是不是容量决策。
9.2 GPU 隔离靠污点,不靠标签
反直觉判断:只给 GPU 节点打 label 而不加 taint,最终的结果不是 GPU 不够,而是 GPU 节点上没有 GPU 可跑。标签决定「能被选」,污点决定「不被别人抢」。两者缺一,调度就失效。更隐蔽的是,加了污点之后,连你自己的 Device Plugin 都会因为缺 tolerations 而调不上去,资源上报随之消失。
9.3 scale-to-zero 不是 HPA 的能力
HPA 的下限是 1 副本,这是设计边界而非配置疏漏。异步 AI 任务真正需要的是队列深度驱动的扩缩,而不是 CPU 利用率驱动的扩缩。把 CPU 阈值用在推理任务上,会出现「队列已堆积但 CPU 还没打满,副本不扩」的空窗期。选对指标,比调对阈值更重要。
9.4 演练的价值在于暴露没人负责的那一步
每次演练真正卡住的,往往不是技术,而是流程:谁有权执行恢复、恢复到哪个时间点、恢复期间对外如何说明。把 docs/36-production-runbook.md 写成可执行的步骤清单,而不是写给审计看的概念文档,演练才有意义。
反直觉之处
恰是设计处
数据不撒谎
判断要收敛
10、系统落地
原来有什么:一台单点 master、宿主 Docker 跑 GPU、nginx Ingress、手工 kubectl apply,以及一套只在出事时才想起的备份脚本。
本篇新增了什么:kubeadm 三控制面 HA 加 stacked etcd,API Server 前置 VIP 与 L4;Calico 或 Cilium 二选一并启用 NetworkPolicy;生产 CSI 提供存储;Gateway API 承担南北向入口;GPU 节点用 label、taint、affinity 配合 Device Plugin 划分算力池,DRA 的可用状态需按当前官方文档核验;异步 AI 任务用 external metrics 尝试 scale-to-zero;应用走 Helm 与 Kustomize 交付;私有 Registry 供镜像;RBAC、Pod Security、Pod Certificates 管身份;Prometheus、Grafana、OpenTelemetry 管可观测;etcd 定期快照;GitOps 管期望状态。
现在能做什么:交付一套控制面可容忍单节点故障、GPU 可被正确调度、状态可从 Git 重建的集群,并针对控制面宕机、Worker 维护、GPU 节点掉线、Registry 失败、CSI 故障、etcd 恢复、版本升级这七类场景各跑一遍演练。
还缺什么:stacked etcd 的故障域耦合仍在;卡级利用率没有优化;跨可用区容灾还没有落地;成本分摊模型缺失;DRA、Pod Certificates 等较新能力的支持度需要逐版本核对。
下一步如何演进:把 DRA 用在一部分需要细粒度设备描述的场景上做验证;把演练放进固定节奏的变更窗口,而不是等到出事;把版本升级从一次性项目变成可重复的流水线;把每个故障域的责任人写进 runbook,而不是留在群里。
原有先盘清
新增有去处
缺口明写出
下一步可期
11、小结
Q1 → 不等于。三副本只解决控制面单点,GPU、存储、入口各自仍是独立故障域
Q2 → 两者都要。污点负责隔离不被抢占,标签与亲和性负责选对具体节点
Q3 → 越真实越有价值,但必须能在受控环境复现,并带明确的回滚步骤
状态 → 一套 kubeadm HA 生产集群加一份可执行 runbook,可交付、可回滚、可重建
问题有回音
状态可交付
集群非终点
演练即日常
12、作业
12.1 理解题:为什么 3 节点 etcd 比 2 节点更可用?
参考答案:Raft 的写入需要多数派确认。3 节点时 quorum 为 2,容忍 1 个节点故障后仍能读写;2 节点时 quorum 也是 2,任意一个节点故障就永久失去多数派,集群退化为不可写。所以 etcd 成员数应保持奇数,扩容要按「加两个」而不是「加一个」来规划。
12.2 实战题:给 GPU 节点加了污点之后,Device Plugin 的 Pod 起不来,怎么排查?
参考答案:先看 DaemonSet 是否存在且被调度,kubectl -n kube-system get pods -l app.kubernetes.io/name=nvidia-device-plugin;再 kubectl describe daemonset 看事件里是否出现 untolerated taint;确认后给 DaemonSet 补 tolerations,并用 nodeAffinity 把它限制在 GPU 节点上。修复后回到 kubectl describe node gpu-01 | grep -i nvidia,确认扩展资源已上报。
12.3 排障题:HPA 把 minReplicas 设为 0 被 API 拒绝,如何实现 scale-to-zero?
参考答案:HPA 的下限是 1,这是设计边界。需要引入 KEDA,用 ScaledObject 声明 minReplicaCount 为 0,并通过 external metrics(例如队列深度)触发从 0 到 1 的唤醒。同时要注意缩容冷却时间,避免任务刚投递就被再次缩零。
12.4 架构判断题:Calico 和 Cilium 该选哪个?
参考答案:两者都支持 NetworkPolicy,选择依据不是简单的性能对比,而是匹配条件:内核版本是否支持所需的 eBPF 特性、团队是否已有运维经验、是否需要 L7 策略与深度可观测、是否需要替换 kube-proxy。Cilium 在 eBPF 与可观测方面更强,同时对内核版本要求更高;Calico 模式多、生态成熟。选型结论应写进架构决策记录,而不是停留在讨论里。
理解定边界
实战验细节
排障靠证据
架构看取舍
13、思考
回到本篇的核心冲突:控制面做到三副本 HA,并不等于生产可用。可用性从来不是一个开关,而是多个故障域同时被覆盖之后的结果。
可以复用的工程判断是:把集群拆成控制面、网络、算力、存储、交付、身份、可观测这七个域,对每一个域都问三个问题——它挂了会怎样、谁能第一时间发现、谁能在多久内恢复。三个问题里有任何一个答不上来,那这个域就还是单点。
这套方法不依赖具体版本,也不依赖选的是 Calico 还是 Cilium。它依赖的是把「不知道」写下来,然后用演练把它变成「知道」。
一念辨边界
一证定根因
一改知取舍
一役见真章

浙公网安备 33010602011771号