上一篇我们把大模型推理环境的最底层链路梳理清楚了:
GPU、驱动、CUDA、PyTorch、容器运行时,这几层如果没打通,后面的 vLLM、SGLang 和容器化部署都会不断返工。
但当一台机器已经能稳定跑推理服务之后,新的问题很快就会出现:
- 模型服务能不能被团队共享
- 多台 GPU 机器怎么统一管理
- 服务如何对外暴露
- 存储、调度、监控、回滚这些能力怎么补齐
这时候,Kubernetes 的价值才真正开始显现。
但 K8S 在 AI 场景里也有一个很常见的误区:
很多人一上来就想把 GPU、模型、入口、存储、监控全部一起做完,结果最基础的集群底座反而没搭稳。
所以这篇文章先不急着上 GPU 调度,也不急着部署 vLLM 或 SGLang,而是专注做一件事:
基于 Ubuntu 24.04,先搭出一套干净、可验证、适合后续承载 AI 推理服务的原生 K8S 集群底座。
为了让这篇文章更适合真正拿来操作,下面的写法会尽量遵循三个原则:
- 先解释为什么做,再给命令
- 关键地方补上预期输出
- 告诉你每一步“什么样才算正常”
文中的命令输出以课程环境和实操整理为基础,实际 token、哈希值、Pod 名称、版本号可能和你的环境不同,这属于正常现象。
摘要
如果你只想先看结论,可以先记住这条搭建顺序:
- 先准备三台 Ubuntu 24.04 主机,固定主机名和 IP
- 关闭 swap,配置内核模块、转发和网桥过滤
- 安装并配置
containerd - 安装
kubeadm、kubelet、kubectl - 用
kubeadm init初始化控制平面 - 用
kubeadm join把 worker 节点加入集群 - 安装
Calico网络插件 - 用一个最小
Nginx应用验证集群是否真的可用
其中最关键的判断点有三个:
kubeadm init成功后,kubectl get nodes看到的是NotReady,这很常见- 安装
Calico之后,节点状态才会从NotReady变成Ready - 只有当
Calico正常、CoreDNS正常、测试应用正常跑起来,这套集群才算真正具备后续承载 AI 服务的基础
系列导航
这是“大模型私有化部署实践”系列的第三篇。当前系列顺序如下:
- 本地、Docker、K8S:大模型私有化部署路线怎么选
- 大模型推理环境准备实战:GPU、驱动、CUDA、容器运行时
- 基于 Ubuntu 24.04 搭建 AI 推理用原生 K8S 集群
- 为 K8S 补齐入口与存储:MetalLB、Gateway API、NFS 动态供给
- 用 Ollama + Open WebUI 快速搭建本地 AI 体验环境
- vLLM 私有化部署实战:本地部署、Docker 部署、接口验证
- vLLM 上 K8S:服务部署、对外暴露、监控与验证
- SGLang 私有化部署实战:本地部署、Docker 部署、能力体验
- SGLang 上 K8S:接入 Open WebUI、服务发布与 GPU 运维
- vLLM 和 SGLang 到底怎么选
如果说上一篇解决的是“推理环境怎么准备”,这一篇解决的就是:
当宿主机已经能稳定使用 GPU 之后,如何进一步把多台机器组织成一个可承载 AI 推理服务的 Kubernetes 集群。
1. 集群规划:先把主机和网络边界定清楚
真正开始装 K8S 之前,先别急着敲 kubeadm init。
AI 场景里的集群底座,最怕的不是命令不会写,而是主机规划从一开始就含糊不清。
1.1 一个最小可用的三节点规划
沿用课程里的最小实践规模,我们先用 3 台主机构成一个基础集群:
| 角色 | 主机名 | IP | 建议配置 |
|---|---|---|---|
| control-plane | k8s-master01 |
192.168.10.140 |
8C / 8G / 100G+ |
| worker | k8s-worker01 |
192.168.10.141 |
8C / 16G / 100G+ |
| worker | k8s-worker02 |
192.168.10.142 |
8C / 16G / 100G+ |
如果你后面要承载 GPU 推理服务,通常还需要进一步考虑:
- 哪些节点具备 GPU
- 模型文件将来放在哪里
- 是否需要单独的管理节点和算力节点
但在本篇里,我们先把“原生集群本身能不能稳定工作”解决掉。
1.2 为什么建议固定主机名和静态 IP
因为后面的 kubeadm init、join、证书签发、节点识别、后续 GPU 节点管理,都会依赖稳定的主机名和网络身份。
如果你一开始还在 DHCP 漂移状态,后面很多问题都会变得很难排查。
2. 主机初始化:别跳过这些看起来“基础”的步骤
K8S 安装阶段最容易被省略的,恰恰是最基础的系统初始化。
而这些步骤在后面一旦出问题,定位成本通常非常高。
下面这些操作需要在所有节点上执行。
2.1 设置主机名
控制平面节点:
hostnamectl set-hostname k8s-master01
hostnamectl
预期输出里应该能看到类似内容:
Static hostname: k8s-master01
Operating System: Ubuntu 24.04 LTS
两个工作节点分别执行:
hostnamectl set-hostname k8s-worker01
hostnamectl set-hostname k8s-worker02
这一步很简单,但非常值得做,因为后面你看到的 kubectl get nodes 结果会直接反映这些名字。
2.2 配置静态 IP
在 Ubuntu 24.04 上,常见方式是使用 netplan。
例如,控制平面节点可以写成:
network:
version: 2
renderer: networkd
ethernets:
ens33:
dhcp4: no
addresses:
- 192.168.10.140/24
routes:
- to: default
via: 192.168.10.2
nameservers:
addresses: [119.29.29.29,114.114.114.114,8.8.8.8]
应用配置:
netplan apply
ip addr show ens33
预期输出中应该能看到:
inet 192.168.10.140/24 brd 192.168.10.255 scope global ens33
两个 worker 节点分别换成 192.168.10.141/24 和 192.168.10.142/24 即可。
2.3 配置 hosts 解析
所有节点都执行:
cat >> /etc/hosts <<'EOF'
192.168.10.140 k8s-master01
192.168.10.141 k8s-worker01
192.168.10.142 k8s-worker02
EOF
验证:
tail -n 5 /etc/hosts
ping -c 1 k8s-master01
如果 ping 能正确解析主机名,说明这一步已经生效。
2.4 时间同步不能省
在集群环境里,时间漂移会给证书、日志、排障都带来额外噪音。
先看当前时间:
date
timedatectl
如果时区不对,可以先统一时区,例如:
timedatectl set-timezone Asia/Shanghai
timedatectl
使用 ntpdate 做一次同步:
apt update
apt install -y ntpdate
ntpdate time1.aliyun.com
典型输出类似这样:
adjust time server 203.107.6.88 offset -0.012345 sec
再用计划任务保持同步:
(crontab -l 2>/dev/null; echo '0 */1 * * * ntpdate time1.aliyun.com') | crontab -
crontab -l
预期输出中应该包含:
0 */1 * * * ntpdate time1.aliyun.com
3. K8S 前置内核与网络准备:这些是 kubelet 依赖的地基
这部分仍然要在所有节点执行。
3.1 加载 overlay 和 br_netfilter
创建模块加载文件:
cat <<'EOF' | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter
lsmod | egrep 'overlay|br_netfilter'
正常输出通常像这样:
overlay 151552 0
br_netfilter 32768 0
bridge 307200 1 br_netfilter
只要这两个模块没加载好,后面的网络转发和容器网络经常会出问题。
3.2 配置内核转发与网桥过滤
写入 sysctl 参数:
cat <<'EOF' | tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system
然后验证:
sysctl net.bridge.bridge-nf-call-iptables
sysctl net.ipv4.ip_forward
预期输出类似:
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
3.3 可选安装 ipset 和 ipvsadm
这一步不是绝对必须,但很多环境会把它作为常规准备项:
apt install -y ipset ipvsadm
如果你后面需要更完整的网络调试和 IPVS 相关排查,这一步会比较方便。
3.4 关闭 swap
这是 kubeadm 安装阶段的经典前置条件。
先临时关闭:
swapoff -a
free -h
预期输出中 Swap 应该为 0B:
total used free shared buff/cache available
Mem: 15Gi 1.2Gi 11Gi 20Mi 2.9Gi 13Gi
Swap: 0B 0B 0B
然后永久关闭,编辑 /etc/fstab,把 swap 那一行注释掉,例如:
#/swap.img none swap sw 0 0
验证:
grep -n swap /etc/fstab
4. 安装 containerd:为后面的 Pod 运行打基础
在当前这套文章路线里,我推荐直接使用 containerd 作为容器运行时。
理由很简单:
- 它已经是 Kubernetes 主流标准路径
- 后续接 NVIDIA Container Toolkit 更顺
- 比额外引入 Docker + cri-dockerd 更干净
4.1 安装 containerd
所有节点执行:
apt update
apt install -y containerd
containerd --version
预期输出类似:
containerd github.com/containerd/containerd 1.7.x
4.2 生成默认配置并启用 SystemdCgroup
这一点非常关键。
很多 kubelet 和运行时不协调的问题,最后都落在这里。
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
grep -n 'SystemdCgroup' /etc/containerd/config.toml
预期输出:
114: SystemdCgroup = true
4.3 启动并验证 containerd
systemctl enable --now containerd
systemctl status containerd --no-pager
crictl --version || true
在 systemctl status 输出中,至少应该能看到:
Active: active (running)
如果 containerd 这里都没起来,后面 kubelet 基本不会稳定。
5. 安装 kubeadm、kubelet、kubectl
仍然在所有节点执行。
5.1 安装依赖和 Kubernetes 仓库
apt update
apt install -y apt-transport-https ca-certificates curl gpg
mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | \
gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | \
tee /etc/apt/sources.list.d/kubernetes.list
apt update
这里的版本仓库你可以根据自己的目标版本调整。
本篇重点不是锁定某个版本号,而是把安装链路理顺。
5.2 安装组件并锁定版本
apt install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl
kubeadm version
kubectl version --client
预期输出类似:
kubeadm version: &version.Info{Major:"1", Minor:"30", ...}
Client Version: v1.30.x
5.3 启动 kubelet
systemctl enable --now kubelet
systemctl status kubelet --no-pager
这里即使看到 kubelet 暂时有报错,也不一定代表安装失败。
在集群初始化前,kubelet 没有拿到完整配置,处于等待状态是正常的。
6. 初始化控制平面:kubeadm init
这一部分只在 k8s-master01 上执行。
6.1 先写一份 kubeadm 配置
为了让初始化参数更清晰,也更便于后续复用,我建议用配置文件方式。
例如:
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.30.0
controlPlaneEndpoint: "192.168.10.140:6443"
networking:
podSubnet: "192.168.0.0/16"
serviceSubnet: "10.96.0.0/12"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
保存成:
vim kubeadm-config.yaml
6.2 执行初始化
kubeadm init --config kubeadm-config.yaml --upload-certs -v=9
初始化成功时,输出里会出现非常关键的一段:
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join 192.168.10.140:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
如果你没有看到 initialized successfully,就不要继续往下做,先把这里的错误处理完。
6.3 准备 kubectl 配置
还是在控制平面节点执行:
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get nodes
这时你很可能会看到类似结果:
NAME STATUS ROLES AGE VERSION
k8s-master01 NotReady control-plane 2m10s v1.30.x
先别紧张,这个 NotReady 很常见。
原因通常不是初始化失败,而是你还没有安装网络插件。
7. 加入工作节点:kubeadm join
这一部分在 k8s-worker01 和 k8s-worker02 上执行。
7.1 使用 kubeadm join
直接使用控制平面初始化输出里的 join 命令,例如:
kubeadm join 192.168.10.140:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
如果执行成功,输出最后通常会包含类似结果:
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
7.2 在控制平面验证节点状态
回到 k8s-master01:
kubectl get nodes
此时常见结果如下:
NAME STATUS ROLES AGE VERSION
k8s-master01 NotReady control-plane 7m28s v1.30.x
k8s-worker01 NotReady <none> 29s v1.30.x
k8s-worker02 NotReady <none> 24s v1.30.x
这一步的重点不是必须立刻看到 Ready,而是:
- 三个节点都成功注册进来了
ROLES和主机身份对应正确- 没有节点完全缺失
NotReady 仍然是预期内现象,因为 Pod 网络还没安装。
8. 安装 Calico:把节点从 NotReady 拉到 Ready
K8S 集群初始化完成后,最容易让新手困惑的一幕就是:
kubectl get nodes能看到节点- 但全都是
NotReady CoreDNS处于Pending
这在 Calico 还没装的时候非常典型。
8.1 安装前先看一次状态
在控制平面节点执行:
kubectl get nodes
kubectl get pods -n kube-system
你通常会看到:
NAME STATUS ROLES AGE VERSION
k8s-master01 NotReady control-plane 7m28s v1.30.x
k8s-worker01 NotReady <none> 29s v1.30.x
k8s-worker02 NotReady <none> 24s v1.30.x
以及类似这样的 kube-system 状态:
NAME READY STATUS RESTARTS AGE
coredns-xxxxxxxxxx-xxxxx 0/1 Pending 0 7m54s
coredns-xxxxxxxxxx-yyyyy 0/1 Pending 0 7m54s
etcd-k8s-master01 1/1 Running 0 8m8s
kube-apiserver-k8s-master01 1/1 Running 0 8m8s
kube-controller-manager-k8s-master01 1/1 Running 0 8m8s
kube-scheduler-k8s-master01 1/1 Running 0 8m9s
看到 CoreDNS Pending 不要慌,这正是“网络插件还没装”的典型信号。
8.2 安装 Calico
可以直接应用官方清单:
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/custom-resources.yaml
如果你是在内网或离线环境,建议提前准备镜像和清单文件。
8.3 验证 Calico 是否真正起来了
kubectl get pods -n calico-system
正常情况下,你会看到类似输出:
NAME READY STATUS RESTARTS AGE
calico-kube-controllers-76bbb9b96b-rvdrd 1/1 Running 0 15m
calico-node-cp5xf 1/1 Running 0 15m
calico-node-tv27t 1/1 Running 0 15m
calico-node-x2c4p 1/1 Running 0 15m
calico-typha-65c8d59447-ldfbp 1/1 Running 0 14m
calico-typha-65c8d59447-zlcjt 1/1 Running 0 15m
只要这里大部分还是 Pending 或 CrashLoopBackOff,就先别急着上业务工作负载。
8.4 再看一次节点状态
kubectl get nodes
kubectl get pods -n kube-system
期望状态应该从之前的 NotReady 逐步变成:
NAME STATUS ROLES AGE VERSION
k8s-master01 Ready control-plane 18m v1.30.x
k8s-worker01 Ready <none> 11m v1.30.x
k8s-worker02 Ready <none> 10m v1.30.x
如果到了这一步节点已经全部 Ready,说明你的原生集群底座基本已经站稳了。
9. 用一个最小 Nginx 工作负载做最终验证
AI 场景里很多人会在集群刚装完时,直接去上 GPU 插件或模型服务。
但更稳的做法,是先用一个最小的普通工作负载确认:
- Pod 调度是否正常
- Service 是否工作正常
- 集群网络是否真的打通
9.1 准备一个最小 Deployment
创建 nginx.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginxweb
spec:
replicas: 2
selector:
matchLabels:
app: nginxweb1
template:
metadata:
labels:
app: nginxweb1
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginxweb-svc
spec:
selector:
app: nginxweb1
ports:
- port: 80
targetPort: 80
type: ClusterIP
应用资源:
kubectl apply -f nginx.yaml
kubectl get deploy
kubectl get pods -o wide
kubectl get svc
9.2 预期输出应该长什么样
kubectl get deploy:
NAME READY UP-TO-DATE AVAILABLE AGE
nginxweb 2/2 2 2 35s
kubectl get pods -o wide:
NAME READY STATUS RESTARTS AGE IP NODE
nginxweb-6d8f9d4f7c-abcde 1/1 Running 0 35s 192.168.36.10 k8s-worker01
nginxweb-6d8f9d4f7c-fghij 1/1 Running 0 35s 192.168.52.11 k8s-worker02
kubectl get svc:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 25m
nginxweb-svc ClusterIP 10.101.88.210 <none> 80/TCP 35s
9.3 用临时 Pod 做一次集群内访问验证
kubectl run curl-test --rm -it --image=curlimages/curl -- sh
进入临时容器后执行:
curl http://nginxweb-svc
如果一切正常,你会看到经典的 Nginx 欢迎页 HTML 片段,例如:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
只要你能在集群内打通这个访问链路,说明:
- Pod 调度正常
- Service 正常
- DNS 和网络插件基本正常
这时候再进入后面的 MetalLB、Gateway API、存储和 GPU 插件,会稳得多。
10. AI 场景下,这套集群为什么只是开始,不是结束
到这里为止,我们搭好的其实还只是一个“干净的原生 K8S 集群底座”。
它已经具备:
- 多节点工作负载调度能力
- 基础服务发现能力
- Pod 网络能力
- 继续承载后续组件安装的基础条件
但对 AI 推理来说,这还远远不是全部。
后面还需要继续补的能力至少包括:
MetalLB或等价负载均衡能力Gateway API或 Ingress 入口能力- NFS 或其他持久存储能力
NVIDIA Container Toolkit与NVIDIA Device Plugin- 监控、日志与 GPU 指标采集
也正因为如此,这一篇的目标从来不是“把 AI 平台全部搭完”,而是先把最容易被忽略、但又最不能跳过的集群底座搭稳。
11. 结语:先把 K8S 底座搭稳,再谈 AI 工作负载
原生 K8S 集群搭建这件事,在很多 AI 项目里经常被低估。
因为大家更容易把注意力放在模型、显卡、推理框架和效果上,但真正到了团队共享和平台化阶段,你会发现:
- 没有稳定的集群底座,AI 服务很难规模化
- 没有清晰的节点、网络和运行时准备,后面 GPU 插件也会反复出问题
- 没有先验证普通工作负载,后面一上 GPU 服务就会变成黑盒
所以一个更稳的节奏通常是:
- 先把宿主机和推理环境打通
- 再把原生 K8S 集群搭稳
- 再继续补入口、存储、负载均衡和 GPU 能力
- 最后再把 vLLM、SGLang、Open WebUI 这些 AI 工作负载放进去
当你按这个顺序往前走时,后面的每一步都会更容易判断问题到底出在哪一层。
下一篇文章,我们就继续沿着这条路线往前走:
如何为这套原生 K8S 集群补齐 AI 服务真正需要的入口和存储能力,包括 MetalLB、Gateway API 和 NFS 动态供给。
评论区