经常有人问我:“想自己搭一套 Kubernetes 集群,到底从哪下手?”我一般都会回复一句话:先把 kubeadm 用熟,再谈别的。kubeadm 是 Kubernetes 官方维护的集群引导工具,它的目标就是让你别在节点初始化、证书签发、控制面组件部署这些琐碎事情上反复折腾。这篇文章就围绕“kubeadm 安装 k8s集群”这条主线,从环境准备、组件安装、控制面初始化、工作节点加入、网络插件部署,一直到常见故障排查,完整走一遍。适合有一定 Linux 基础、想亲手搭建一套可用集群的读者,也适合那些已经被各种安装教程绕晕、想搞清楚每一步到底在干什么的人。1. 为什么要用 kubeadm 装集群1.1 自建集群的几种主流方案很多人第一次搭集群,脑子里冒出来的方案可能有这几种:二进制手动部署、kubeadm、minikube、kind、k3s。它们不能说谁绝对好,只能说适用场景完全不同。方案上手难度适合场景说明二进制手动部署很高生产环境、高度定制化需求所有组件都要手动下载、编排、生成证书,链路长且容易出错kubeadm中等生产环境、标准化部署、学习 K8s 内部机制官方工具,组件以静态 Pod 方式托管,升级维护相对简单minikube / kind低本地开发、快速验证单节点为主,和真实多节点集群的行为有差异k3s低边缘计算、资源受限环境架构做了裁剪,很多组件被合并,和有完整功能的集群不完全一致我的建议是:如果你想在生产环境或者测试环境里用一套“正经的”多节点集群,又不想从零开始写 systemd 单元文件去手动拉起 kube-apiserver,那就老老实实走 kubeadm。它把 K8s 决定要做好的事情——证书、token、kubeconfig、控制面组件、节点 bootstrap——全部自动化了,而你仍然保留了对集群关键配置的掌控权。1.2 kubeadm 的设计思路kubeadm 的核心思路用一个词概括就是“引导”。它不会替你管理集群的日常运维,但它负责在你执行kubeadm init或者kubeadm join的那一刻,把节点从“一个普通的主机”变成“集群的一个成员”。它具体做的事包括:自动生成 CA 证书、apiserver 证书、kubelet 证书,并把证书分发到对应位置;把 kube-apiserver、kube-controller-manager、kube-scheduler 以静态 Pod 的形式写到/etc/kubernetes/manifests目录下,由 kubelet 负责拉起;为控制面节点生成 kubeconfig 文件,方便你使用kubectl管理集群;生成用于工作节点加入的 token 和 discovery-token-ca-cert-hash。理解了这个设计思路,你后面遇到很多问题都不会慌。比如控制面组件起不来,你第一反应应该是去看kubectl get pods -n kube-system;比如 kubelet 状态异常,你要知道 kubelet 是集群的“底座”,控制面组件是它拉起来的,底座不稳,上面全崩。2. 环境准备:这一步做不好后面全是坑2.1 硬件与系统规划先说硬件。K8s 官方要求控制面节点至少 2 核 CPU、2GB 内存,工作节点至少 2 核、2GB 内存。这个只是“能跑”的门槛,实际用起来建议控制面节点 4 核 4GB 起步,工作节点看负载情况再定。我这里演示的是一套三个节点的集群:一台控制面节点,两台工作节点。如果你只有一台机器,也可以把控制面和工作节点复用,但只适合学习和功能验证,别拿单节点去扛生产流量。操作系统方面,Debian、Ubuntu、CentOS 等主流发行版都能装,但要注意内核版本别太老,官方比较推荐内核 4.15 以上。我习惯用 Ubuntu 22.04 LTS 系列做演示,包管理走 apt,命令逻辑也清晰。无论用哪个发行版,请记住一个原则:集群内所有节点的主机名必须唯一,且不要带下划线,IP 能互通、hostname 能解析是硬前提。2.2 初始化系统的四项硬性操作在敲 kubeadm 命令之前,有几项系统层面的准备必须先做。很多新手集群装不上,九成问题出在这一步。第一,配置主机名和 hosts 解析。建议把控制面节点主机名设为master01,工作节点设为node01、node02,然后在每台机器的/etc/hosts里把三台机器的 IP 和主机名对应写上。# 每个节点分别执行,替换为自己的主机名 hostnamectl set-hostname master01# /etc/hosts 追加内容,IP 替换为实际地址 192.168.1.10 master01 192.168.1.11 node01 192.168.1.12 node02第二,时间同步。K8s 集群里的证书校验、日志时间戳、调度逻辑都依赖节点间时间一致,偏差大了会出现各种诡异问题。用 chrony 或者 systemd-timesyncd 都行,确保状态为 synchronized。第三,加载内核模块并设置 sysctl 参数。K8s 网络通信依赖 iptables 和 IP 转发,需要提前把模块挂上。cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfiltercat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sudo sysctl --system解释一下这几条参数的作用。bridge-nf-call-iptables的作用是让桥接的流量也经过 iptables 规则,这对 K8s 里 Service 的 DNAT 和负载均衡至关重要;ip_forward则是让数据包能在不同网络命名空间之间转发,Pod 之间通信和 Pod 访问外网都依赖它。第四,关闭 swap。kubelet在默认情况下不允许节点启用 swap,因为 K8s 的 CPU 和内存资源模型假设调度是基于节点可分配内存的,而 swap 的存在会让内存压力判断失真。关闭方式如下,并且要保证重启后依然生效。sudo swapoff -a # 然后注释掉 /etc/fstab 中的 swap 相关行还有一个小细节:防火墙和 SELinux 的问题。不同发行版情况不同,Ubuntu 默认没启用 SELinux,CentOS 则需要设置成 permissive。防火墙方面,如果是在内网环境搭建,我建议先把默认防火墙放行必要端口或者直接临时关闭,重点是确保节点间 6443、10250、2379、30000-32767 等端口互通。生产环境再根据安全策略精细化配置,学习阶段被防火墙卡住实在不值得。2.3 容器运行时的前置条件:iptables 模式K8s 在 1.27 版本之后,已经不再推荐使用 Docker 作为直接运行时,而是通过 containerd 来承载容器。不管最终选哪个运行时,底层依赖都一样:iptables 或者 nftables。Ubuntu 22.04 默认使用的是 nftables 后端,而 K8s 的 kube-proxy 默认用 iptables,为了避免模式冲突,需要显式切换。sudo update-alternatives --set iptables /usr/sbin/iptables-legacy这一步不处理,后面 kube-proxy 可能起不来,或者 Service 的转发规则根本不生效。虽然现在 K8s 对 nftables 的支持也在逐步完善,但为了少踩坑,传统方式最稳。3. 安装容器运行时和 kubeadm 工具链3.1 为什么首选 containerdK8s 从 1.24 版本起彻底移除了对 Docker 作为运行时的一部分直接支持,但这不代表不能用了,只是需要你额外装一个 cri-dockerd 适配层。我的建议是:直接上 containerd。containerd 本身是 CNCF 的项目,和 K8s 的 CRI(Container Runtime Interface)是原生的语言适配关系,不需要中间转换层,稳定性更高,资源占用也更少。而且 kubeadm 在初始化集群时拉取的 pause 等系统镜像,走 containerd 是最顺的。3.2 安装 containerd 并做两处关键修改以 Ubuntu 为例,安装 containerd 可以直接从 Docker 官方 apt 源获取。sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y containerd.io装好之后,先初始化 containerd 的默认配置:sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml然后必须修改两处地方,否则 kubelet 和 containerd 的 cgroup 驱动不一致,节点会一直显示 NotReady。第一处,SystemdCgroup = false要改成true。找到[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]这一段,确认里面有SystemdCgroup = true。这是因为现在绝大多数 Linux 发行版都用 systemd 作为 init 系统,让 containerd 和 kubelet 都走 systemd cgroup 驱动,两者才不会有冲突。第二处,sandbox_image要设为和你的 K8s 版本匹配的 pause 镜像地址。比如:[plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.k8s.io/pause:3.9"修改完成之后重启 containerd:sudo systemctl daemon-reload sudo systemctl enable --now containerd sudo systemctl status containerd3.3 安装 kubeadm、kubelet、kubectl:版本匹配是底线这三个工具必须安装相同版本,千万不要一个 1.27 一个 1.28 混着来。我用的是 1.28 系列作为示例。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.28/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.28/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 kubectlapt-mark hold这步很关键,防止哪天不小心执行apt upgrade把版本给带偏了。装完之后,kubelet 默认是不会自动启动的,这很正常,要等kubeadm init或者kubeadm join时才会拿到配置并被引导起来。用命令验证一下版本:kubeadm version kubelet --version kubectl version --client到这里,所有节点上的基础软件都准备好了。接下来就在控制面节点上执行集群初始化。4. 控制平面初始化与工作节点加入4.1 kubeadm init 命令和参数别乱填在控制面节点执行初始化。我用一个包含两个核心参数的示例:sudo kubeadm init \ --apiserver-advertise-address=192.168.1.10 \ --pod-network-cidr=10.244.0.0/16 \ --kubernetes-version=v1.28.0解释一下这些参数:--apiserver-advertise-address:指定 API Server 对外宣告的 IP,也就是工作节点要连接的地址。如果不填,kubeadm 默认取节点默认路由对应的 IP。在多网卡机器上,这一步必须显式指定,否则集群搭起来之后,工作节点可能连不上 API Server。--pod-network-cidr:Pod 网络的 CIDR 网段。这个参数和后面要装的网络插件强相关,比如我准备用 Flannel,所以填了10.244.0.0/16;如果你用 Calico,可能得填192.168.0.0/16。这个网段规划错了,网络插件和 kube-proxy 之间会打架,Pod 之间通信直接炸掉。--kubernetes-version:指定 K8s 版本,避免 kubeadm 从网上拉取最新版本信息时和本地的 kubelet 版本不一致。如果你的控制面节点有两个网卡,或者你想为将来高可用做铺垫,还可以加--control-plane-endpoint. 单节点学习阶段可以不填,但建议从一开始就养成填这个参数的习惯,因为它指定的是集群的统一入口,后续想扩展成高可用控制面,这个参数不用回改。init 过程会拉取一批镜像,包括 kube-apiserver、kube-controller-manager、kube-scheduler、etcd、pause 等,耗时取决于网络状况。如果网络环境一般,可以先手动预拉,看看到底是哪些镜像卡住了:sudo kubeadm config images list sudo kubeadm config images pull我当时第一次装的时候,就是先看到了registry.k8s.io下的几个镜像列表,才意识到预拉这步有多省心,不至于让 init 卡在一半干瞪眼。4.2 init 输出的信息一定要看懂init 成功之后,终端会输出一大段提示,这段信息含金量很高,最容易让人忽略的就是最后面的几行:Your Kubernetes control-plane has been initialized successfully! You can now join any number of worker nodes by running the following on each as root: kubeadm join 192.168.1.10:6443 --token xxxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxx这里有两个关键信息:--token和--discovery-token-ca-cert-hash。前者是工作节点加入集群的临时凭据,后者是校验控制面 CA 证书的哈希,两个必须同时带上,join 才能成功。还有一段是关于 kubeconfig 的。root 用户或普通用户需要把生成的 kubeconfig 放到~/.kube/config下,kubectl 才知道该连哪个集群。mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config这一步不做,你执行kubectl get nodes的时候会报本地没有集群配置的错误。顺便可以给 kubectl 配个自动补全,用起来效率高一些:echo "source <(kubectl completion bash)" >> ~/.bashrc source ~/.bashrc4.3 工作节点 join 进来接下来在所有工作节点上执行上面输出的 join 命令。注意,这条命令必须在 root 权限下执行,因为 kubelet 需要写系统级配置文件。sudo kubeadm join 192.168.1.10:6443 --token xxxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxx如果遇到 token 过期,或者一开始忘记保存这段输出,也不用慌,在控制面节点上重新生成一条即可:sudo kubeadm token create --print-join-command这个命令会自动生成新的 token 并拼好 join 命令,直接复制到工作节点跑就行。等所有节点执行完 join,回到控制面节点执行:kubectl get nodes正常情况下能看到类似下面的输出:NAME STATUS ROLES AGE VERSION master01 Ready control-plane 6m v1.28.0 node01 Ready <none> 3m v1.28.0 node02 Ready <none> 3m v1.28.04.4 集群状态验证的正确姿势这里多说一句:kubectl get nodes看到的 STATUS 是 NotReady 的话,先别急着怀疑自己哪步做错了。控制面节点初始化完成之后,集群内部还没有安装网络插件,所有节点都会处于 NotReady 状态,这是正常的。用一句大白话说:机器之间能注册成功,但 Pod 还不会互相通信,所以集群认为自己“还没准备好”。要查 kubelet 的实际状态,可以登录对应节点执行:systemctl status kubelet journalctl -u kubelet -fkubelet 是一个高频写入日志的组件,如果它在反复启动,日志里多半会直接告诉你为什么。下一节要装的网络插件,就能解决 NotReady 这个问题。5. 网络插件:让 Pod 真正互通的关键一步5.1 CNI 插件到底干了件什么事K8s 的网络模型要求每个 Pod 都拥有独立的 IP,Pod 之间可以直接通过 IP 互通,不需要做 NAT。这个要求不是 K8s 自身实现的,而是通过 CNI(Container Network Interface)插件完成的。CNI 插件在 kubelet 启动 Pod 的时候会被调用,它的任务包括:给 Pod 分配 IP、创建 veth 对、设置路由规则、把流量从 Pod 的网络命名空间导出来。不同插件的底层实现思路差异很大:Flannel 走的是 VXLAN 叠加网络,简单、易排查;Calico 走的是纯三层 BGP 路由模式,性能好、策略能力强,生产环境用得最多。这里有个不能偷懒的认知:Pod 网段必须和 kubeadm init 时的--pod-network-cidr保持一致,否则插件分配的 IP 和控制面规划的网段对不上,SDN 直接瘫痪。5.2 以 Calico 为例完成部署我习惯用 Calico,因为它在性能和功能之间平衡得比较好,还能顺带提供网络策略能力。部署前要先下载对应版本的清单文件。从 Calico 官方文档获取对应版本的calico.yaml,然后执行:kubectl apply -f calico.yaml如果你选择的 Pod 网段不是 Calico 默认的网段,需要先修改清单文件里的CALICO_IPV4POOL_CIDR这个环境变量的值,改成和--pod-network-cidr一致。这一步忘了改,Calico 会按照默认网段分配 IP,集群内部路由就乱了。等待资源创建完成,可以通过下面的命令确认 Calico 组件是否正常运行:kubectl get pods -n kube-system | grep calico看到 STATUS 为 Running,节点状态会在几十秒内慢慢变成 Ready。如果一直没变化,看 Calico 的日志排查:kubectl logs -n kube-system -l k8s-app=calico-node5.3 网络验证的两种操作集群 Ready 之后,建议大家做一次真实的网络验证,而不是只看 Node 状态。最简单的方式是创建一个临时的 busybox Pod,然后测试 Pod 之间和 Pod 访问外网的能力:kubectl run test-net --image=busybox --restart=Never -- sleep 3600 kubectl exec -it test-net -- ping 8.8.8.8再测一下集群的 DNS 解析。K8s 集群里的核心 DNS 组件是 CoreDNS,它部署在 kube-system 命名空间。如果 DNS 出问题,Service 之间互相调用会全部失败,那是很隐蔽的故障。kubectl exec -it test-net -- nslookup kubernetes.default.svc.cluster.local这一步能通,说明集群内部 DNS 和 Service 网络都正常。到这一步,一套多节点的 K8s 集群就已经具备了基本的可用性。6. 常见问题与排查实录6.1 kubelet 报 cgroup 驱动不一致这是新手最容易碰到的经典报错。症状是 kubelet 服务起不来,journalctl -u kubelet里出现类似这样一段:failed to run kubelet: "failed to create kubelet" ... misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"原因就是我前面强调的:kubelet 和容器运行时必须是同一个 cgroup 驱动。Ubuntu 上 kubelet 默认用 systemd,如果 containerd 还保持默认的 cgroupfs,两边就对不上。解决办法:修改/etc/containerd/config.toml,把SystemdCgroup置为true,然后重启 containerd。这是个高概率翻车点,也是我几乎每次排查节点 NotReady 时都会确认的第一个地方。6.2 初始化时镜像拉取失败kubeadm init 输出一直卡在[cri] pulling images,或者直接报failed to pull image,多见于镜像仓库无法连接或网络策略受限的环境。我的处理办法分三步走。先执行kubeadm config images list把需要的镜像清单打印出来,再逐一手动拉取确认到底是哪个失败;接着给所有节点提前拉好镜像包或者通过内网分发,最后再执行 kubeadm init。这里提醒一句:不要在 join 完成之前换节点名或者改 IP,否则证书和 kubelet 配置里的地址信息会全部对不上。6.3 node 一直 NotReady节点 join 成功但 STATUS 一直是 NotReady,先别急着重置节点,按这个顺序排查。先看 CNI 插件有没有正常跑起来:kubectl get pods -n kube-system发现 flannel 或 calico 的 Pod 一直 Pending,大概率是节点资源不足或者镜像没拉下来。再看节点上的 kubelet 状态:systemctl status kubelet如果 kubelet 在反复重启,用journalctl -u kubelet -f看实时日志,里面有具体的报错原因。还有一个容易忽略的原因:节点的主机名和 MAC 地址冲突。K8s 集群要求每个节点的 hostname 必须唯一,如果你用虚拟机模板克隆出来的机器,hostname 会重复,节点注册时会互相顶掉,表现就是节点状态疯狂在 Ready 和 NotReady 之间横跳。解决办法就是把 hostname 改掉。6.4 token 过期与重新生成join 报错提示 token 无效或者过期,不必重新初始化集群。kubeadm 生成的 token 默认有效期是 24 小时,重新生成即可:sudo kubeadm token create --print-join-command如果控制面上也找不到原来的 CA 哈希,可以用下面的命令重新获取:openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'然后把 hash 拼到 join 命令里。6.5 集群重置与清理打个比方,kubeadm 的好处就是“有后悔药”,kubeadm reset可以把节点上的配置打回原形。sudo kubeadm reset -f sudo rm -rf /etc/kubernetes /var/lib/kubelet /var/lib/etcd还有一类残留容易被人遗忘:iptables 规则和路由表。如果你之前装过 Calico,节点上会有大量路由和 IPVS 规则,重置之后不清干净,下次重新初始化集群很可能留下一堆脏路由。清理方式:sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F sudo ip link delete cni0重置之后再重新执行kubeadm init或者kubeadm join,基本能回到一个干净的初始状态。这也是我喜欢 kubeadm 的原因之一,它把集群的“可重入性”做得很好,出错成本低。最后再分享一点个人习惯:每次初始化集群前,我习惯把所有节点的时间同步先确认一遍,然后一定在控制面节点执行kubeadm config images pull预拉镜像。这两件事做完,下面几乎都是一路顺畅。K8s 集群的安装本身并不神秘,很多问题的根子都在于对每个组件职责不熟,加上版本、驱动、网段这些细节没对齐。把这些基础打牢,后面做应用的滚动发布、弹性伸缩,你才有底气。
经常有人问我:“想自己搭一套 Kubernetes 集群,到底从哪下手?”我一般都会回复一句话:先把 kubeadm 用熟,再谈别的。kubeadm 是 Kubernetes 官方维护的集群引导工具,它的目标就是让你别在节点初始化、证书签发、控制面组件部署这些琐碎事情上反复折腾。这篇文章就围绕“kubeadm 安装 k8s集群”这条主线,从环境准备、组件安装、控制面初始化、工作节点加入、网络插件部署,一直到常见故障排查,完整走一遍。适合有一定 Linux 基础、想亲手搭建一套可用集群的读者,也适合那些已经被各种安装教程绕晕、想搞清楚每一步到底在干什么的人。1. 为什么要用 kubeadm 装集群1.1 自建集群的几种主流方案很多人第一次搭集群,脑子里冒出来的方案可能有这几种:二进制手动部署、kubeadm、minikube、kind、k3s。它们不能说谁绝对好,只能说适用场景完全不同。方案上手难度适合场景说明二进制手动部署很高生产环境、高度定制化需求所有组件都要手动下载、编排、生成证书,链路长且容易出错kubeadm中等生产环境、标准化部署、学习 K8s 内部机制官方工具,组件以静态 Pod 方式托管,升级维护相对简单minikube / kind低本地开发、快速验证单节点为主,和真实多节点集群的行为有差异k3s低边缘计算、资源受限环境架构做了裁剪,很多组件被合并,和有完整功能的集群不完全一致我的建议是:如果你想在生产环境或者测试环境里用一套“正经的”多节点集群,又不想从零开始写 systemd 单元文件去手动拉起 kube-apiserver,那就老老实实走 kubeadm。它把 K8s 决定要做好的事情——证书、token、kubeconfig、控制面组件、节点 bootstrap——全部自动化了,而你仍然保留了对集群关键配置的掌控权。1.2 kubeadm 的设计思路kubeadm 的核心思路用一个词概括就是“引导”。它不会替你管理集群的日常运维,但它负责在你执行kubeadm init或者kubeadm join的那一刻,把节点从“一个普通的主机”变成“集群的一个成员”。它具体做的事包括:自动生成 CA 证书、apiserver 证书、kubelet 证书,并把证书分发到对应位置;把 kube-apiserver、kube-controller-manager、kube-scheduler 以静态 Pod 的形式写到/etc/kubernetes/manifests目录下,由 kubelet 负责拉起;为控制面节点生成 kubeconfig 文件,方便你使用kubectl管理集群;生成用于工作节点加入的 token 和 discovery-token-ca-cert-hash。理解了这个设计思路,你后面遇到很多问题都不会慌。比如控制面组件起不来,你第一反应应该是去看kubectl get pods -n kube-system;比如 kubelet 状态异常,你要知道 kubelet 是集群的“底座”,控制面组件是它拉起来的,底座不稳,上面全崩。2. 环境准备:这一步做不好后面全是坑2.1 硬件与系统规划先说硬件。K8s 官方要求控制面节点至少 2 核 CPU、2GB 内存,工作节点至少 2 核、2GB 内存。这个只是“能跑”的门槛,实际用起来建议控制面节点 4 核 4GB 起步,工作节点看负载情况再定。我这里演示的是一套三个节点的集群:一台控制面节点,两台工作节点。如果你只有一台机器,也可以把控制面和工作节点复用,但只适合学习和功能验证,别拿单节点去扛生产流量。操作系统方面,Debian、Ubuntu、CentOS 等主流发行版都能装,但要注意内核版本别太老,官方比较推荐内核 4.15 以上。我习惯用 Ubuntu 22.04 LTS 系列做演示,包管理走 apt,命令逻辑也清晰。无论用哪个发行版,请记住一个原则:集群内所有节点的主机名必须唯一,且不要带下划线,IP 能互通、hostname 能解析是硬前提。2.2 初始化系统的四项硬性操作在敲 kubeadm 命令之前,有几项系统层面的准备必须先做。很多新手集群装不上,九成问题出在这一步。第一,配置主机名和 hosts 解析。建议把控制面节点主机名设为master01,工作节点设为node01、node02,然后在每台机器的/etc/hosts里把三台机器的 IP 和主机名对应写上。# 每个节点分别执行,替换为自己的主机名 hostnamectl set-hostname master01# /etc/hosts 追加内容,IP 替换为实际地址 192.168.1.10 master01 192.168.1.11 node01 192.168.1.12 node02第二,时间同步。K8s 集群里的证书校验、日志时间戳、调度逻辑都依赖节点间时间一致,偏差大了会出现各种诡异问题。用 chrony 或者 systemd-timesyncd 都行,确保状态为 synchronized。第三,加载内核模块并设置 sysctl 参数。K8s 网络通信依赖 iptables 和 IP 转发,需要提前把模块挂上。cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfiltercat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sudo sysctl --system解释一下这几条参数的作用。bridge-nf-call-iptables的作用是让桥接的流量也经过 iptables 规则,这对 K8s 里 Service 的 DNAT 和负载均衡至关重要;ip_forward则是让数据包能在不同网络命名空间之间转发,Pod 之间通信和 Pod 访问外网都依赖它。第四,关闭 swap。kubelet在默认情况下不允许节点启用 swap,因为 K8s 的 CPU 和内存资源模型假设调度是基于节点可分配内存的,而 swap 的存在会让内存压力判断失真。关闭方式如下,并且要保证重启后依然生效。sudo swapoff -a # 然后注释掉 /etc/fstab 中的 swap 相关行还有一个小细节:防火墙和 SELinux 的问题。不同发行版情况不同,Ubuntu 默认没启用 SELinux,CentOS 则需要设置成 permissive。防火墙方面,如果是在内网环境搭建,我建议先把默认防火墙放行必要端口或者直接临时关闭,重点是确保节点间 6443、10250、2379、30000-32767 等端口互通。生产环境再根据安全策略精细化配置,学习阶段被防火墙卡住实在不值得。2.3 容器运行时的前置条件:iptables 模式K8s 在 1.27 版本之后,已经不再推荐使用 Docker 作为直接运行时,而是通过 containerd 来承载容器。不管最终选哪个运行时,底层依赖都一样:iptables 或者 nftables。Ubuntu 22.04 默认使用的是 nftables 后端,而 K8s 的 kube-proxy 默认用 iptables,为了避免模式冲突,需要显式切换。sudo update-alternatives --set iptables /usr/sbin/iptables-legacy这一步不处理,后面 kube-proxy 可能起不来,或者 Service 的转发规则根本不生效。虽然现在 K8s 对 nftables 的支持也在逐步完善,但为了少踩坑,传统方式最稳。3. 安装容器运行时和 kubeadm 工具链3.1 为什么首选 containerdK8s 从 1.24 版本起彻底移除了对 Docker 作为运行时的一部分直接支持,但这不代表不能用了,只是需要你额外装一个 cri-dockerd 适配层。我的建议是:直接上 containerd。containerd 本身是 CNCF 的项目,和 K8s 的 CRI(Container Runtime Interface)是原生的语言适配关系,不需要中间转换层,稳定性更高,资源占用也更少。而且 kubeadm 在初始化集群时拉取的 pause 等系统镜像,走 containerd 是最顺的。3.2 安装 containerd 并做两处关键修改以 Ubuntu 为例,安装 containerd 可以直接从 Docker 官方 apt 源获取。sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y containerd.io装好之后,先初始化 containerd 的默认配置:sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml然后必须修改两处地方,否则 kubelet 和 containerd 的 cgroup 驱动不一致,节点会一直显示 NotReady。第一处,SystemdCgroup = false要改成true。找到[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]这一段,确认里面有SystemdCgroup = true。这是因为现在绝大多数 Linux 发行版都用 systemd 作为 init 系统,让 containerd 和 kubelet 都走 systemd cgroup 驱动,两者才不会有冲突。第二处,sandbox_image要设为和你的 K8s 版本匹配的 pause 镜像地址。比如:[plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.k8s.io/pause:3.9"修改完成之后重启 containerd:sudo systemctl daemon-reload sudo systemctl enable --now containerd sudo systemctl status containerd3.3 安装 kubeadm、kubelet、kubectl:版本匹配是底线这三个工具必须安装相同版本,千万不要一个 1.27 一个 1.28 混着来。我用的是 1.28 系列作为示例。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.28/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.28/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 kubectlapt-mark hold这步很关键,防止哪天不小心执行apt upgrade把版本给带偏了。装完之后,kubelet 默认是不会自动启动的,这很正常,要等kubeadm init或者kubeadm join时才会拿到配置并被引导起来。用命令验证一下版本:kubeadm version kubelet --version kubectl version --client到这里,所有节点上的基础软件都准备好了。接下来就在控制面节点上执行集群初始化。4. 控制平面初始化与工作节点加入4.1 kubeadm init 命令和参数别乱填在控制面节点执行初始化。我用一个包含两个核心参数的示例:sudo kubeadm init \ --apiserver-advertise-address=192.168.1.10 \ --pod-network-cidr=10.244.0.0/16 \ --kubernetes-version=v1.28.0解释一下这些参数:--apiserver-advertise-address:指定 API Server 对外宣告的 IP,也就是工作节点要连接的地址。如果不填,kubeadm 默认取节点默认路由对应的 IP。在多网卡机器上,这一步必须显式指定,否则集群搭起来之后,工作节点可能连不上 API Server。--pod-network-cidr:Pod 网络的 CIDR 网段。这个参数和后面要装的网络插件强相关,比如我准备用 Flannel,所以填了10.244.0.0/16;如果你用 Calico,可能得填192.168.0.0/16。这个网段规划错了,网络插件和 kube-proxy 之间会打架,Pod 之间通信直接炸掉。--kubernetes-version:指定 K8s 版本,避免 kubeadm 从网上拉取最新版本信息时和本地的 kubelet 版本不一致。如果你的控制面节点有两个网卡,或者你想为将来高可用做铺垫,还可以加--control-plane-endpoint. 单节点学习阶段可以不填,但建议从一开始就养成填这个参数的习惯,因为它指定的是集群的统一入口,后续想扩展成高可用控制面,这个参数不用回改。init 过程会拉取一批镜像,包括 kube-apiserver、kube-controller-manager、kube-scheduler、etcd、pause 等,耗时取决于网络状况。如果网络环境一般,可以先手动预拉,看看到底是哪些镜像卡住了:sudo kubeadm config images list sudo kubeadm config images pull我当时第一次装的时候,就是先看到了registry.k8s.io下的几个镜像列表,才意识到预拉这步有多省心,不至于让 init 卡在一半干瞪眼。4.2 init 输出的信息一定要看懂init 成功之后,终端会输出一大段提示,这段信息含金量很高,最容易让人忽略的就是最后面的几行:Your Kubernetes control-plane has been initialized successfully! You can now join any number of worker nodes by running the following on each as root: kubeadm join 192.168.1.10:6443 --token xxxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxx这里有两个关键信息:--token和--discovery-token-ca-cert-hash。前者是工作节点加入集群的临时凭据,后者是校验控制面 CA 证书的哈希,两个必须同时带上,join 才能成功。还有一段是关于 kubeconfig 的。root 用户或普通用户需要把生成的 kubeconfig 放到~/.kube/config下,kubectl 才知道该连哪个集群。mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config这一步不做,你执行kubectl get nodes的时候会报本地没有集群配置的错误。顺便可以给 kubectl 配个自动补全,用起来效率高一些:echo "source <(kubectl completion bash)" >> ~/.bashrc source ~/.bashrc4.3 工作节点 join 进来接下来在所有工作节点上执行上面输出的 join 命令。注意,这条命令必须在 root 权限下执行,因为 kubelet 需要写系统级配置文件。sudo kubeadm join 192.168.1.10:6443 --token xxxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxx如果遇到 token 过期,或者一开始忘记保存这段输出,也不用慌,在控制面节点上重新生成一条即可:sudo kubeadm token create --print-join-command这个命令会自动生成新的 token 并拼好 join 命令,直接复制到工作节点跑就行。等所有节点执行完 join,回到控制面节点执行:kubectl get nodes正常情况下能看到类似下面的输出:NAME STATUS ROLES AGE VERSION master01 Ready control-plane 6m v1.28.0 node01 Ready <none> 3m v1.28.0 node02 Ready <none> 3m v1.28.04.4 集群状态验证的正确姿势这里多说一句:kubectl get nodes看到的 STATUS 是 NotReady 的话,先别急着怀疑自己哪步做错了。控制面节点初始化完成之后,集群内部还没有安装网络插件,所有节点都会处于 NotReady 状态,这是正常的。用一句大白话说:机器之间能注册成功,但 Pod 还不会互相通信,所以集群认为自己“还没准备好”。要查 kubelet 的实际状态,可以登录对应节点执行:systemctl status kubelet journalctl -u kubelet -fkubelet 是一个高频写入日志的组件,如果它在反复启动,日志里多半会直接告诉你为什么。下一节要装的网络插件,就能解决 NotReady 这个问题。5. 网络插件:让 Pod 真正互通的关键一步5.1 CNI 插件到底干了件什么事K8s 的网络模型要求每个 Pod 都拥有独立的 IP,Pod 之间可以直接通过 IP 互通,不需要做 NAT。这个要求不是 K8s 自身实现的,而是通过 CNI(Container Network Interface)插件完成的。CNI 插件在 kubelet 启动 Pod 的时候会被调用,它的任务包括:给 Pod 分配 IP、创建 veth 对、设置路由规则、把流量从 Pod 的网络命名空间导出来。不同插件的底层实现思路差异很大:Flannel 走的是 VXLAN 叠加网络,简单、易排查;Calico 走的是纯三层 BGP 路由模式,性能好、策略能力强,生产环境用得最多。这里有个不能偷懒的认知:Pod 网段必须和 kubeadm init 时的--pod-network-cidr保持一致,否则插件分配的 IP 和控制面规划的网段对不上,SDN 直接瘫痪。5.2 以 Calico 为例完成部署我习惯用 Calico,因为它在性能和功能之间平衡得比较好,还能顺带提供网络策略能力。部署前要先下载对应版本的清单文件。从 Calico 官方文档获取对应版本的calico.yaml,然后执行:kubectl apply -f calico.yaml如果你选择的 Pod 网段不是 Calico 默认的网段,需要先修改清单文件里的CALICO_IPV4POOL_CIDR这个环境变量的值,改成和--pod-network-cidr一致。这一步忘了改,Calico 会按照默认网段分配 IP,集群内部路由就乱了。等待资源创建完成,可以通过下面的命令确认 Calico 组件是否正常运行:kubectl get pods -n kube-system | grep calico看到 STATUS 为 Running,节点状态会在几十秒内慢慢变成 Ready。如果一直没变化,看 Calico 的日志排查:kubectl logs -n kube-system -l k8s-app=calico-node5.3 网络验证的两种操作集群 Ready 之后,建议大家做一次真实的网络验证,而不是只看 Node 状态。最简单的方式是创建一个临时的 busybox Pod,然后测试 Pod 之间和 Pod 访问外网的能力:kubectl run test-net --image=busybox --restart=Never -- sleep 3600 kubectl exec -it test-net -- ping 8.8.8.8再测一下集群的 DNS 解析。K8s 集群里的核心 DNS 组件是 CoreDNS,它部署在 kube-system 命名空间。如果 DNS 出问题,Service 之间互相调用会全部失败,那是很隐蔽的故障。kubectl exec -it test-net -- nslookup kubernetes.default.svc.cluster.local这一步能通,说明集群内部 DNS 和 Service 网络都正常。到这一步,一套多节点的 K8s 集群就已经具备了基本的可用性。6. 常见问题与排查实录6.1 kubelet 报 cgroup 驱动不一致这是新手最容易碰到的经典报错。症状是 kubelet 服务起不来,journalctl -u kubelet里出现类似这样一段:failed to run kubelet: "failed to create kubelet" ... misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"原因就是我前面强调的:kubelet 和容器运行时必须是同一个 cgroup 驱动。Ubuntu 上 kubelet 默认用 systemd,如果 containerd 还保持默认的 cgroupfs,两边就对不上。解决办法:修改/etc/containerd/config.toml,把SystemdCgroup置为true,然后重启 containerd。这是个高概率翻车点,也是我几乎每次排查节点 NotReady 时都会确认的第一个地方。6.2 初始化时镜像拉取失败kubeadm init 输出一直卡在[cri] pulling images,或者直接报failed to pull image,多见于镜像仓库无法连接或网络策略受限的环境。我的处理办法分三步走。先执行kubeadm config images list把需要的镜像清单打印出来,再逐一手动拉取确认到底是哪个失败;接着给所有节点提前拉好镜像包或者通过内网分发,最后再执行 kubeadm init。这里提醒一句:不要在 join 完成之前换节点名或者改 IP,否则证书和 kubelet 配置里的地址信息会全部对不上。6.3 node 一直 NotReady节点 join 成功但 STATUS 一直是 NotReady,先别急着重置节点,按这个顺序排查。先看 CNI 插件有没有正常跑起来:kubectl get pods -n kube-system发现 flannel 或 calico 的 Pod 一直 Pending,大概率是节点资源不足或者镜像没拉下来。再看节点上的 kubelet 状态:systemctl status kubelet如果 kubelet 在反复重启,用journalctl -u kubelet -f看实时日志,里面有具体的报错原因。还有一个容易忽略的原因:节点的主机名和 MAC 地址冲突。K8s 集群要求每个节点的 hostname 必须唯一,如果你用虚拟机模板克隆出来的机器,hostname 会重复,节点注册时会互相顶掉,表现就是节点状态疯狂在 Ready 和 NotReady 之间横跳。解决办法就是把 hostname 改掉。6.4 token 过期与重新生成join 报错提示 token 无效或者过期,不必重新初始化集群。kubeadm 生成的 token 默认有效期是 24 小时,重新生成即可:sudo kubeadm token create --print-join-command如果控制面上也找不到原来的 CA 哈希,可以用下面的命令重新获取:openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'然后把 hash 拼到 join 命令里。6.5 集群重置与清理打个比方,kubeadm 的好处就是“有后悔药”,kubeadm reset可以把节点上的配置打回原形。sudo kubeadm reset -f sudo rm -rf /etc/kubernetes /var/lib/kubelet /var/lib/etcd还有一类残留容易被人遗忘:iptables 规则和路由表。如果你之前装过 Calico,节点上会有大量路由和 IPVS 规则,重置之后不清干净,下次重新初始化集群很可能留下一堆脏路由。清理方式:sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F sudo ip link delete cni0重置之后再重新执行kubeadm init或者kubeadm join,基本能回到一个干净的初始状态。这也是我喜欢 kubeadm 的原因之一,它把集群的“可重入性”做得很好,出错成本低。最后再分享一点个人习惯:每次初始化集群前,我习惯把所有节点的时间同步先确认一遍,然后一定在控制面节点执行kubeadm config images pull预拉镜像。这两件事做完,下面几乎都是一路顺畅。K8s 集群的安装本身并不神秘,很多问题的根子都在于对每个组件职责不熟,加上版本、驱动、网段这些细节没对齐。把这些基础打牢,后面做应用的滚动发布、弹性伸缩,你才有底气。