AI时代的Kubernetes教程36招-第6招-kubeadm怎么搭建第一个标准Kubernetes集群?

kubeadm 只做 Bootstrap:按官方 v1.37 文档走通首个控制面

命令全绿不等于集群可用,责任边界得自己划

[读者] 要在裸机或虚拟机上自建首个 Kubernetes 控制面的工程师
[痛点] 照着文档敲完命令,节点 NotReady,不知道哪一步该由谁负责
[现在读] 官方 v1.37 的 kubeadm 路径已定型,但责任边界文档不会替你划
[读完] 能独立完成一主一从的引导,并说清每一步的代价与边界
[旧方案] 手工拼装二进制、证书与网络插件
    |
    v
[新需求] 要可复现、可交接、可升级的首个控制面
    |
    v
[冲突] kubeadm 只负责 Bootstrap,运行时和 Pod Network 由你自己选
    |
    v
[后果] 命令全绿,集群依旧不可用

我是老李,在一家做数据产品的公司带平台组。上个月算法团队提了个需求:他们要在机房自建一套 Kubernetes,跑 GPU 训练任务,还要自己挂调度器插件和 API Server 参数。托管集群的托管层不让我们动这些。

背景是:我们手上有三台机器,一台 cp1 做控制面,两台 w1、w2 做 worker,Ubuntu 22.04,8C16G。之前这套环境是同事用一份流传很广的 shell 脚本装起来的,脚本里混杂了二进制下载、自签证书、手工写 kubeconfig。装完能跑,但没人敢升级。

变化在于这次的交付要求:两周内给出一个可交接的首个控制面,并且要写清楚哪些事是 kubeadm 做的,哪些事是我们自己做的。因为下一个接手的人,必须能只看文档就把 w2 加进来。

kubeadm init 返回 success,是不是就等于集群可用了?

容器运行时和 Pod 网络,到底谁替我选?

如果我把 join 命令抄进交接文档就发出去,一周后它还能用吗?

01、故事

现场是机房的一排机柜,三台机器刚装完系统。任务是:按官方 v1.37 的 kubeadm 文档,从裸机开始引导出一个单控制面集群,再挂上两个 Worker,最后把过程写成可交接的文档。

V1 的做法很直接:照着官方 install-kubeadm 和 create-cluster-kubeadm 两页文档,把 Linux 前置、容器运行时、kubeadm/kubelet/kubectl 三件套装好,然后 cp1 上执行 kubeadm init,装一个 CNI,再 kubeadm join。

问题出在第 40 分钟。cp1 上 kubeadm init 的最后一行是成功提示,还贴心打印了一段 join 命令。我复制下来,去 w1 执行,w1 也返回成功。回 cp1 敲 kubectl get nodes,看到的是:

NAME   STATUS     ROLES           AGE   VERSION
cp1    NotReady   control-plane   3m    v1.37.x
w1     NotReady   <none>          40s   v1.37.x

两台都是 NotReady。

当时的直觉是「集群坏了」。但这里有个关键认知,后面会展开:NotReady 在引导阶段不是故障,是中间态。

真正的冲突在第二天暴露。w2 因为机房电源整改晚了一天入列,我拿着昨天那条 join 命令再执行,失败了。同一份命令,隔一天,结果完全不同。

命令会过期
中间态非故障
边界先划清
再谈可用性

02、问题

旧的 shell 脚本方案失效在三处假设上:它把 kubelet 当成一个被脚本管理的进程,把 CNI 当成一个可选插件,把 bootstrap token 当成一次性产出的永久凭证。这三个假设在「要交接、要扩容、要升级」的需求下全部不成立。

业务影响很直接:算法团队的任务队列排到下周,控制面节点不 Ready,他们连名字空间都建不了,更不用说挂自定义调度器。

技术表现是可以被观测的四条:

  1. kubectl get nodes 返回 NotReady;
  2. kubectl get pods -n kube-system 中 CoreDNS 长期停在 ContainerCreating;
  3. journalctl -u kubelet 里出现 cgroup driver 不一致或 swap 相关报错;
  4. 隔天 join 时 preflight 阶段直接失败,报 token 无法验证。

可验证的完成标准也定了四条:

  1. cp1 上 kubectl get nodes,三台节点全部 Ready,且 control-plane 与 worker 角色正确;
  2. kubectl get pods -n kube-system 中 CoreDNS、kube-proxy、CNI 组件全部 Running;
  3. 交接文档里给出的入列流程,在一台全新机器上能一次通过;
  4. kubeadm certs check-expiration 能说清证书到期时间。

旧脚本管太多
新工具只管一段
标准写在前头
事后才说得清

03、原理

本篇只需要一条原理:kubeadm 是一个引导器(bootstrap tool),不是集群管理器。

它做的事情可以精确列出来:执行 preflight 检查并告知环境哪里不合格;在控制面把静态 Pod manifest 写到 /etc/kubernetes/manifests/,由 kubelet 拉起 etcd、API Server、Controller Manager、Scheduler;把证书写到 /etc/kubernetes/pki,把 admin.conf、kubelet.conf、controller-manager.conf、scheduler.conf 生成到 /etc/kubernetes/;生成 bootstrap token,让新节点能通过 TLS bootstrap 加入;把 kubelet 的工作目录约定在 /var/lib/kubelet/。

它不做的事情同样精确:不安装、不管理 kubelet 这个系统服务(kubelet 由 systemd 管,kubeadm 只写配置并让它重启);不安装 kubectl(那是客户端,装不装跟集群能不能跑没有因果关系);不安装任何 Pod Network(CNI);不做节点日常生命周期管理,打标签、排空、下线都是 kubectl 的事;不管理宿主机上的 swap、内核模块、cgroup 版本。

反直觉判断:kubeadm init 成功,只代表它把控制面组件的 manifest 写对了、把证书签好了,不代表任何节点能跑 Pod。节点 Ready 的前提是 CNI 就绪,而 CNI 是你要自己装的那个东西。所以「init 成功」和「集群可用」之间,隔着一个你从未安装的组件。

第二个反直觉判断:kubelet 是集群里唯一一个「不由集群管理」的核心组件。它由宿主机 systemd 管理,kubeadm 只是给它写了一份配置。这意味着 kubelet 的升级、cgroup 配置、日志、故障排查,全部落在宿主机运维这一层,而不是 kubectl 这一层。

第三个:bootstrap token 是有 TTL 的。kubeadm init 打印出来的 join 命令默认带 24 小时有效期的 token。它不是一个「集群加入凭证」,而是一张 24 小时有效的临时门禁卡。把它抄进长期文档,等于把一张会过期的票贴在墙上。

引导器非管家
凭证自带期限
kubelet 归系统
网络自己选

04、架构

[输入] 一台满足前置条件的 Linux 主机 + 一段 join 命令
    |
    v
[模块] kubeadm(preflight / init / join / certs)
    |
    v
[数据/状态] /etc/kubernetes/(manifests、pki、*.conf)
           /var/lib/kubelet/(kubelet 工作目录与配置)
    |
    v
[处理] kubelet 按 manifest 拉起静态 Pod,CNI 就绪后节点 Ready
    |
    v
[输出] 一个可被 kubectl 访问、可继续 join 的控制面

边界:左边是宿主机——内核模块、swap、cgroup、容器运行时,全部在 kubeadm 之外;右边是 CNI——kubeadm 到「写好 manifest」为止,Pod 网络由你安装的插件负责;上下是时间——kubeadm 只在 init、join、upgrade 这几个瞬间介入,其余时间它不在场。

收益:把「控制面组件怎么起」从手工拼装变成可重复的引导过程。证书、kubeconfig、静态 Pod manifest 的生成有固定路径和固定格式,交接时只要看两个目录就能还原大部分事实。

代价:它把一部分复杂度显式留给了你。你必须自己回答用哪个容器运行时、用哪个 CNI、pod CIDR 给多大、cgroup 是 v1 还是 v2。kubeadm 不替你决策,只在你决策完之后验证。

适用条件:你需要一个贴近上游、参数可自己掌控的控制面;你能接受宿主机层要自己维护;你的网络里 API Server 的地址是稳定的。

左边是宿主
右边是容器网
中间只一刀
时间之外无它

05、实战一次

环境与版本:Ubuntu 22.04,三台机器 cp1、w1、w2,kubeadm/kubelet/kubectl 从 pkgs.k8s.io 的 v1.37 stable 仓库安装。具体包版本号、cgroup v1/v2 支持情况与镜像列表,需按当前官方文档核验。

依赖一:内核模块与转发,三台都做。

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ipv6tables = 1
net.ipv4.ip_forward                 = 1
EOF

sudo sysctl --system

依赖二:关闭 swap 并持久化。

sudo swapoff -a
sudo sed -i.bak '/\sswap\s/ s/^/#/' /etc/fstab

依赖三:containerd。

sudo apt-get update
sudo apt-get install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl enable --now containerd
sudo systemctl restart containerd

这里必须与 kubelet 的 cgroup driver 保持一致。SystemdCgroup 这一行位于 runc 运行时选项下,不同 containerd 大版本的配置文件结构有差异,需按当前官方文档核验。

依赖四:kubeadm、kubelet、kubectl。

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.37/deb/Release.key \
  | sudo 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.37/deb/ /' \
  | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

apt-mark hold 是刻意的:这三个包的版本必须一起走,不能因为一次 apt upgrade 让 kubelet 单独跳版本。

核心实现,只在 cp1 执行:

sudo kubeadm init \
  --control-plane-endpoint "cp1:6443" \
  --apiserver-advertise-address 10.0.0.11 \
  --pod-network-cidr "10.244.0.0/16" \
  --cri-socket unix:///run/containerd/containerd.sock

启动后配置 kubeconfig:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

第一次验证,预期看到 NotReady:

$ kubectl get nodes
NAME   STATUS     ROLES           AGE   VERSION
cp1    NotReady   control-plane   72s   v1.37.x

这不是失败,是 CNI 还没装。安装 CNI,示例用 Flannel,其默认 pod CIDR 是 10.244.0.0/16,与上面的 --pod-network-cidr 对齐;若选 Calico 或 Cilium,须按各自官方文档设置与之一致的 CIDR:

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

再次验证:

$ kubectl get nodes
NAME   STATUS   ROLES           AGE   VERSION
cp1    Ready    control-plane   5m    v1.37.x

加入第一个 Worker,命令现取现用:

sudo kubeadm token create --print-join-command

在 w1 上执行输出的那条 kubeadm join 命令,回 cp1 查看:

$ kubectl get nodes
NAME   STATUS   ROLES           AGE   VERSION
cp1    Ready    control-plane   18m   v1.37.x
w1     Ready    <none>          40s   v1.37.x

V1 到这里跑通了,但它留下三个未验证的假设:w2 上的 swap 已经关干净、w2 上的 containerd 和 w1 配置完全一致、第二天那条 join 命令还有效。这三个假设,第二天全被打脸。

以上命令未在当前环境逐条实测,v1.37 的具体包名、镜像列表与 cgroup 支持需按当前官方文档核验;上面的命令输出均为示例输出。

先跑通最小环
再留三个假设
假设不写下来
次日全部爆炸

06、排查

诊断链一:w2 在 join 之前 kubelet 就起不来。

现象:在 w2 上执行 systemctl status kubelet,服务反复重启。怀疑:kubelet 配置写错了。检查:journalctl -u kubelet -n 50,看到:

failed to run Kubelet: running with swap on is not supported,
please disable swap or set failSwapOn to false

证据:free -h 显示 swap 仍有数值,/etc/fstab 里 swap 那一行还在。根因:前一天在 cp1 和 w1 上是用 swapoff -a 临时关的,w2 这台新机器只跟着装包,swapoff 被漏掉了,而 kubelet 的要求是 failSwapOn 默认为 true。修复:执行 sudo swapoff -a,并注释 /etc/fstab 中的 swap 行,重启 kubelet。

错误尝试: 我先试的是在 join 时加 --ignore-preflight-errors=all,把 preflight 检查整个跳过。这在当时看起来「很有效」,因为 preflight 确实过了。为什么错:swap 不是 preflight 的洁癖,它是 kubelet 的硬约束。跳过 preflight 只是把失败点从 join 的检查阶段推迟到 kubelet 的启动阶段,报错位置更靠后、日志更分散,而且会顺带把其他真实的 preflight 问题(端口占用、镜像拉取失败、cgroup 版本不符)一起屏蔽掉。

诊断链二:w2 的 join 隔天执行报 token 错误。

现象:

error execution phase preflight: couldn't validate the identity of the API Server:
could not find a JWS signature in the cluster-info ConfigMap for token ID "abcdef"

怀疑:网络不通,或者 CA 证书 hash 抄错。检查:先 curl -k https://cp1:6443/healthz 返回 ok,说明网络与证书都没问题;再 kubectl get secrets -n kube-system,昨天的 bootstrap token 已经不在列表里。证据:bootstrap token 默认 TTL 为 24 小时,过期即被清理。根因:把 join 命令当成长期凭证抄进了交接文档。修复:在 cp1 上重新执行 kubeadm token create --print-join-command,用新命令在 w2 上执行;交接文档里写「获取 join 命令的方法」,而不是命令本身。

诊断链三:w1 上 Pod 一直起不来,但节点显示 Ready。

现象:kubectl get pods -n kube-system 里 CoreDNS 长期停在 ContainerCreating 或 CrashLoopBackOff。怀疑:镜像有问题。检查:kubectl describe pod -n kube-system 看到 sandbox 创建失败;journalctl -u kubelet | grep -i cgroup 看到:

misconfiguration: kubelet cgroup driver: "systemd" is different from
runtime cgroup driver: "cgroupfs"

证据:w1 上的 /etc/containerd/config.toml 里 SystemdCgroup 仍是 false,因为它是 apt 装完后没改配置就直接启动的。根因:containerd 与 kubelet 的 cgroup driver 不一致,Pod 的 cgroup 无法被正确创建。修复:把 w1 上 config.toml 的 SystemdCgroup 改为 true,重启 containerd,再重启 kubelet。

三条链有一个共同点:kubeadm 早就退场了,问题全在它交还给我们的那一侧。

报错在 kubelet
根因在宿主机
跳过检查最贵
日志只说一半

07、优化

严格基于 06 的证据形成 V2。V2 不引入新组件,只把「宿主机前置」和「凭证时效」这两件事,从口头约定变成可执行、可验证的步骤。

V2-1:把宿主机前置写成脚本并加自检。

根因来自诊断链一和诊断链三:swap 与 containerd cgroup 配置属于宿主机状态,靠人一条条敲必然漏。修改思路是把模块加载、sysctl、swapoff 加 fstab、containerd 配置这四步写成一个脚本,末尾追加一段自检。核心实现:

#!/usr/bin/env bash
set -euo pipefail

# 1. 内核模块与转发
cat >/etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter

cat >/etc/sysctl.d/k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ipv6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sysctl --system >/dev/null

# 2. 关闭 swap 并持久化
swapoff -a
sed -i.bak '/[[:space:]]swap[[:space:]]/ s/^/#/' /etc/fstab

# 3. containerd 配置
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
systemctl enable containerd
systemctl restart containerd

# 4. 自检:任何一条不通过就退出
fail=0
grep -q '^overlay' /proc/modules          || { echo "FAIL: overlay not loaded"; fail=1; }
grep -q '^br_netfilter' /proc/modules     || { echo "FAIL: br_netfilter not loaded"; fail=1; }
[ "$(cat /proc/sys/net/ipv4/ip_forward)" = "1" ] || { echo "FAIL: ip_forward=0"; fail=1; }
[ -z "$(swapon --show --noheadings)" ]    || { echo "FAIL: swap still on"; fail=1; }
grep -q 'SystemdCgroup = true' /etc/containerd/config.toml || { echo "FAIL: SystemdCgroup not true"; fail=1; }
[ -S /run/containerd/containerd.sock ]    || { echo "FAIL: containerd socket missing"; fail=1; }

if [ "$fail" -eq 0 ]; then echo 'node-prepare: OK'; else exit 1; fi

原因:把「人记得做」换成「脚本验证过」。自检的每一行都对应 06 章里出现过的真实症状。新行为:新机器入列前先跑这个脚本,输出 node-prepare: OK 才允许执行 join。验证方式:在 w2 上先故意保持 swap 打开跑一次,脚本应以 FAIL: swap still on 退出;swapoff 后重跑输出 OK。未在当前环境实测,以上为预期结果。

V2-2:join 流程改为现取现用加显式参数。

根因来自诊断链二:join 命令被当成了静态文档。修改:交接文档里只写步骤,不写具体 token;join 时显式指定 token TTL 与 discovery 参数。在 cp1 上执行:

sudo kubeadm token create --ttl 1h --print-join-command

在新节点上显式指定运行时 socket,避免自动探测带来的歧义:

sudo kubeadm join cp1:6443 \
  --token <TOKEN> \
  --discovery-token-ca-cert-hash sha256:<HASH> \
  --cri-socket unix:///run/containerd/containerd.sock

原因:token 有 TTL,--cri-socket 在机器上装了多个运行时的时候能避免歧义。新行为:交接文档里写的是「在控制面执行 token create,然后在新节点执行输出的命令」,任何人任何时间执行都能得到一条有效命令。验证方式:kubeadm token list 能看到新 token 的 TTL 列;一小时后同一 token 消失。

V2-3:把证书期限纳入交付检查。

sudo kubeadm certs check-expiration

原因:引导完成不代表一年后还能用,证书期限是交付清单的一部分,不是运维事故的一部分。新行为:交付文档里附上这条命令的输出,并写明续期方式。验证方式:命令输出的到期时间与交付日期一致。

把记得变验证
把凭证变动作
把期限写进表
把假设逐个删

08、演进

[同一输入] 一台全新 Linux 主机 + 一个待加入的 Worker 角色
    |
    +--[V1] 手工逐条前置 + 抄昨天打印的 join 命令
    |       代价:swap/cgroup 靠记忆,token 靠运气,失败点后移
    |
    +--[V2] node-prepare.sh 自检 + 现取现用的 join 流程 + 证书期限检查
    |       代价:多维护一个脚本,多一个必须跑的前置动作
    |
[Trade-off]
得到可复现的节点引导与可交接的凭证流程
失去「随手一条命令就加节点」的随意性
适用边界:机器规模在几十台以内、镜像来源可控时收益最大

正确性:V1 依赖执行者记得住多少条前置;V2 把前置变成脚本自检,漏一项会在 join 之前就失败,而不是等 kubelet 启动之后才失败。

稳定性:V1 的失败模式是分散的——swap 在 kubelet 日志里、cgroup 在 containerd 配置里、token 在 preflight 里,三处三样;V2 把它们收敛到一处前置检查,和一个有效期明确的 token。

复杂度:V2 增加了一个脚本和一条证书检查命令,但减少的是「每次加机器都要重新推理一遍」的隐性成本。

成本:几乎没有新增资源开销,新增的是维护成本——脚本本身要跟着官方文档更新,尤其是 containerd 配置结构变化时,需按当前官方文档核验。

适用范围:单控制面、机器规模可控。如果要做高可用控制面(多控制面加负载均衡 endpoint),引导流程会更复杂,--control-plane-endpoint 与证书分发的部分需要单独设计。

遗留问题:V2 仍然没有解决 kubelet 的版本升级路径(apt-mark hold 挡住了自动升级,手动升级要按顺序先控制面后节点),也没有解决 Pod 数量的规模问题(每节点默认 110 个 Pod 的上限和 pod CIDR 的容量规划)。这两件事留到后续。

同一台新机器
两条不同的路
一条靠记忆
一条靠验证

09、洞见

9.1 引导工具的边界,就是你的运维边界

kubeadm 的介入时间只有三个瞬间:init、join、upgrade。除此之外它不在场。这意味着它生成的东西——/etc/kubernetes/ 下的 manifest、pki、kubeconfig,以及 /var/lib/kubelet/ 下的 kubelet 配置——是被「写下来」的,而不是被「持续管理」的。

反直觉判断:一个工具帮你做的事情越标准化,它留给你的责任就越显式。kubeadm 把控制面组件的启动方式标准化了,代价是把宿主机前置、运行时选择、网络选择这三块全部显式交还给你。这三块不是「它忘了做」,而是「它明确不做」。

9.2 引导阶段的 NotReady 是中间态,不是故障

节点在 CNI 安装之前必然 NotReady。这条状态位的语义是「容器网络未就绪」,不是「节点坏了」。把它当故障去查,会浪费大量时间翻 kubelet 日志;把它当预期状态,下一步动作就只有一个:装 CNI。

反直觉判断:在一个健康集群里,第一次 kubectl get nodes 看到 NotReady 的概率,比你看到 Ready 的概率更高,因为控制面自举完成和 Pod 网络就绪之间天然有时间差。

9.3 跳过检查,是把故障推迟到更贵的位置

--ignore-preflight-errors 这个参数存在,但它的语义是「我知道这条检查为什么失败」。用它绕过 swap 检查,swap 不会消失,它只是从 join 阶段移到了 kubelet 阶段。判断一条 preflight 能不能跳过,标准是:这条检查的对象是否会被下游组件再次强制。swap、cgroup 版本、端口占用,都属于会被再次强制的类别。

9.4 凭证有 TTL,文档里不要抄凭证

bootstrap token 默认 24 小时有效。一条 join 命令里同时包含 token 和 CA 证书 hash,前者会过期,后者对应集群身份。把整条命令写进交接文档,等于把一张会过期的门票贴在墙上。正确的交付物是「获取命令的方法」,不是命令本身。

9.5 版本一致性靠 hold,不靠自觉

kubelet、kubeadm、kubectl 三个包的版本必须同步推进,而宿主机的日常 apt upgrade 不会替你区分这件事。apt-mark hold 是最低成本的一致性手段:它把「不要动这三个包」的决策从人的记忆,写进包管理器的状态里。升级时按官方文档的顺序解除 hold、逐台升级,比事后排查版本错配便宜得多。

边界即责任
中间态非病
凭证别上墙
版本须同行

10、系统落地

原来有什么:一台 cp1 和三台待入列机器的裸机环境,一份别人写的历史安装脚本,一个口头流传的加节点流程。

本篇新增了什么:一套可执行的节点前置脚本 node-prepare.sh,含六项自检;一条确定的引导路径,从 pkgs.k8s.io 仓库到 containerd(SystemdCgroup=true)、kubeadm init、CNI、kubeadm join;两个目录的明确认知,/etc/kubernetes/ 是控制面的声明与身份,/var/lib/kubelet/ 是 kubelet 的工作区;一条现取现用的 join 流程,以及证书期限检查命令。

现在能做什么:任意一台通过自检的 Linux 主机,可以在十分钟内被加入集群;交接文档不包含任何会过期的东西;集群证书的到期时间是随时可查的。

还缺什么:控制面仍是单点,cp1 挂了 API Server 就没了;没有节点标签与污点策略,GPU 机器和普通机器还没区分开;没有监控与告警,节点 NotReady 只能靠人发现;没有 etcd 备份流程。

下一步如何演进:把 --control-plane-endpoint 换成负载均衡地址并加入第二、第三个控制面节点,同时把 etcd 快照纳入定时任务;之后再为 GPU 节点打标签、配污点。每一步都仍然遵守同一条原则:kubeadm 负责引导,宿主机与网络的选择由我们负责并写进自检。

引导只是开始
高可用在后面
每步都要验证
边界别忘记

11、小结

Q1 → init 成功不等于集群可用,节点 Ready 还差一个由你选择的 CNI
Q2 → 运行时由你选(containerd + SystemdCgroup),Pod 网络由你选,CNI 的 pod CIDR 必须与 --pod-network-cidr 对齐
Q3 → 不能。join 命令里的 bootstrap token 默认 24 小时过期,文档要写「怎么获取」,不要写「具体命令」
状态 → 一主一从的引导流程已可复现、可交接,宿主机前置有自检,证书期限可查

问题有答案
边界有归属
流程可交接
集群可继续

12、作业

12.1 理解题:kubeadm init 成功之后,为什么 kubectl get nodes 里的节点是 NotReady?

参考答案:节点的 Ready 条件依赖容器网络就绪,而 kubeadm 不安装 CNI。init 阶段完成的是控制面组件 manifest 与证书、kubeconfig 的生成,以及静态 Pod 的启动;节点上的 Pod 网络要等你自己安装 CNI 插件之后才能就绪。所以 NotReady 在引导阶段是预期中间态,不是故障。

12.2 实战题:写一条命令,在控制面上生成一条一小时后过期的 join 命令。

参考答案:

sudo kubeadm token create --ttl 1h --print-join-command

输出内容包含 --token 与 --discovery-token-ca-cert-hash 两部分,可直接在新节点执行;如果新节点上存在多个容器运行时,再补一个 --cri-socket unix:///run/containerd/containerd.sock 消除歧义。

12.3 排障题:一个新节点加入后长期 NotReady,systemctl status kubelet 显示服务反复重启,你会怎么查?

参考答案:先看 journalctl -u kubelet -n 100,按三类关键词分流:swap 相关(running with swap on is not supported)指向 swapon --show 与 /etc/fstab;cgroup 相关(kubelet cgroup driver 与 runtime cgroup driver 不一致)指向 /etc/containerd/config.toml 里的 SystemdCgroup;运行时相关(containerd socket 不存在或 CRI 连接失败)指向 systemctl status containerd。这三类检查全部落在宿主机层,而不是 kubeadm 层。

12.4 架构判断题:团队提出「为了省事,把所有节点的 swap 都留着,在 kubelet 里设置 failSwapOn: false 就行」,这个方案可以接受吗?

参考答案:这是把宿主机层的约束改成了运行时配置项,需要谨慎评估。kubelet 对 swap 的支持与具体版本、feature gate 以及 cgroup v1/v2 有关,须按当前官方文档核验。即便技术上可用,也要评估两点:一是内存压力下 Pod 的延迟表现会变得不可预测;二是这条设置会掩盖「新机器忘了关 swap」这类信号,而这个信号在本篇里恰恰是最便宜就能发现的。如果确实需要,应当作为显式决策写进节点配置基线并配套监控,而不是当成一个省事开关。

题题有证据
答答有边界
会查才会改
会改才叫懂

13、思考

回到开头那个冲突:kubeadm init 返回 success,是不是就等于集群可用了?

这篇给出的答案是:不等于,而且这个不等号不是 kubeadm 的缺陷,是它的设计。它的定位是引导器,把控制面组件的启动方式标准化、把证书与 kubeconfig 的生成标准化、把新节点加入的握手过程标准化;它刻意不做三件事——不管理 kubelet 的生命周期、不替你安装 kubectl、不替你选择 Pod Network。

这个边界可以推广成一条可复用的判断:当你引入一个把某个环节标准化的工具时,先问它把哪一段做成了一次性动作,再问这段之外还剩下谁。引导类工具的典型特征就是介入时间短、责任范围清晰、剩下的部分全部显式留给你。读得懂这个边界的人,拿到的是可复现的流程;读不出这个边界的人,拿到的是命令全绿但集群不可用。

再往上一层:本篇真正的交付物不是三台 Ready 的机器,而是两条可交接的东西——一条含自检的宿主机前置流程,一条现取现用的入列流程。前者把「我记得关过 swap」变成「脚本不许我漏」,后者让「这条命令还能用吗」变成一个不再需要问的问题。

工具只管一段
其余全是自己
边界读得清楚
流程交得出去

posted @ 2026-10-12 10:27  李福春  阅读(2)  评论(0)    收藏  举报