侧边栏壁纸
  • 累计撰写 238 篇文章
  • 累计创建 245 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

基于 Ubuntu 24.04 搭建 AI 推理用原生 K8S 集群

zhanjie.me
2025-09-17 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

上一篇我们把大模型推理环境的最底层链路梳理清楚了:
GPU、驱动、CUDA、PyTorch、容器运行时,这几层如果没打通,后面的 vLLM、SGLang 和容器化部署都会不断返工。

但当一台机器已经能稳定跑推理服务之后,新的问题很快就会出现:

  • 模型服务能不能被团队共享
  • 多台 GPU 机器怎么统一管理
  • 服务如何对外暴露
  • 存储、调度、监控、回滚这些能力怎么补齐

这时候,Kubernetes 的价值才真正开始显现。

但 K8S 在 AI 场景里也有一个很常见的误区:
很多人一上来就想把 GPU、模型、入口、存储、监控全部一起做完,结果最基础的集群底座反而没搭稳。

所以这篇文章先不急着上 GPU 调度,也不急着部署 vLLM 或 SGLang,而是专注做一件事:

基于 Ubuntu 24.04,先搭出一套干净、可验证、适合后续承载 AI 推理服务的原生 K8S 集群底座。

为了让这篇文章更适合真正拿来操作,下面的写法会尽量遵循三个原则:

  • 先解释为什么做,再给命令
  • 关键地方补上预期输出
  • 告诉你每一步“什么样才算正常”

文中的命令输出以课程环境和实操整理为基础,实际 token、哈希值、Pod 名称、版本号可能和你的环境不同,这属于正常现象。

摘要

如果你只想先看结论,可以先记住这条搭建顺序:

  1. 先准备三台 Ubuntu 24.04 主机,固定主机名和 IP
  2. 关闭 swap,配置内核模块、转发和网桥过滤
  3. 安装并配置 containerd
  4. 安装 kubeadmkubeletkubectl
  5. kubeadm init 初始化控制平面
  6. kubeadm join 把 worker 节点加入集群
  7. 安装 Calico 网络插件
  8. 用一个最小 Nginx 应用验证集群是否真的可用

其中最关键的判断点有三个:

  • kubeadm init 成功后,kubectl get nodes 看到的是 NotReady,这很常见
  • 安装 Calico 之后,节点状态才会从 NotReady 变成 Ready
  • 只有当 Calico 正常、CoreDNS 正常、测试应用正常跑起来,这套集群才算真正具备后续承载 AI 服务的基础

系列导航

这是“大模型私有化部署实践”系列的第三篇。当前系列顺序如下:

  1. 本地、Docker、K8S:大模型私有化部署路线怎么选
  2. 大模型推理环境准备实战:GPU、驱动、CUDA、容器运行时
  3. 基于 Ubuntu 24.04 搭建 AI 推理用原生 K8S 集群
  4. 为 K8S 补齐入口与存储:MetalLB、Gateway API、NFS 动态供给
  5. 用 Ollama + Open WebUI 快速搭建本地 AI 体验环境
  6. vLLM 私有化部署实战:本地部署、Docker 部署、接口验证
  7. vLLM 上 K8S:服务部署、对外暴露、监控与验证
  8. SGLang 私有化部署实战:本地部署、Docker 部署、能力体验
  9. SGLang 上 K8S:接入 Open WebUI、服务发布与 GPU 运维
  10. 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 initjoin、证书签发、节点识别、后续 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/24192.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 加载 overlaybr_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 可选安装 ipsetipvsadm

这一步不是绝对必须,但很多环境会把它作为常规准备项:

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-worker01k8s-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

只要这里大部分还是 PendingCrashLoopBackOff,就先别急着上业务工作负载。

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 ToolkitNVIDIA Device Plugin
  • 监控、日志与 GPU 指标采集

也正因为如此,这一篇的目标从来不是“把 AI 平台全部搭完”,而是先把最容易被忽略、但又最不能跳过的集群底座搭稳。

11. 结语:先把 K8S 底座搭稳,再谈 AI 工作负载

原生 K8S 集群搭建这件事,在很多 AI 项目里经常被低估。

因为大家更容易把注意力放在模型、显卡、推理框架和效果上,但真正到了团队共享和平台化阶段,你会发现:

  • 没有稳定的集群底座,AI 服务很难规模化
  • 没有清晰的节点、网络和运行时准备,后面 GPU 插件也会反复出问题
  • 没有先验证普通工作负载,后面一上 GPU 服务就会变成黑盒

所以一个更稳的节奏通常是:

  1. 先把宿主机和推理环境打通
  2. 再把原生 K8S 集群搭稳
  3. 再继续补入口、存储、负载均衡和 GPU 能力
  4. 最后再把 vLLM、SGLang、Open WebUI 这些 AI 工作负载放进去

当你按这个顺序往前走时,后面的每一步都会更容易判断问题到底出在哪一层。

下一篇文章,我们就继续沿着这条路线往前走:
如何为这套原生 K8S 集群补齐 AI 服务真正需要的入口和存储能力,包括 MetalLBGateway APINFS 动态供给。

0

评论区