kubeadm是Kubernetes官方提供的集群引导工具,从1.24版本开始,由于dockershim的移除,所有使用kubeadm部署的集群都必须选择containerd或CRI-O作为容器运行时。这篇文章就从我实际部署Kubernetes 1.24的经验出发,完整记录kubeadm部署过程中的每一个关键步骤和踩过的坑。
我在做这个部署的时候,发现很多教程还停留在1.23甚至更早的版本,直接用旧思路装1.24就会遇到一堆问题:容器运行时不会配、kubelet起不来、节点一直NotReady。所以这篇文章我会把1.24版本特有的变化拎出来单独讲,再把完整的部署流程从零走一遍,最后把常见报错整理成速查表。
整个教程面向的是有一定Linux基础、想自己搭一套K8s环境的开发者或运维同学。如果你之前只用过minikube或者云厂商的托管集群,想搞清楚底层组件的协作关系,这篇也很适合。我用的环境是Ubuntu 22.04,但里面的思路和配置同样适用于CentOS 7.9等主流系统。
1. 为什么用kubeadm部署,以及1.24版本的关键变化
1.1 kubeadm到底解决了什么问题
Kubernetes集群最麻烦的部分其实是引导和初始化:apiserver、etcd、scheduler、controller-manager这些组件之间要互相通信,需要证书、token、kubeconfig一套完整的东西。手工部署的话,哪怕熟练工程师也要半天到一天时间,而且每一步都容易出错。
kubeadm把这些全自动化了。它会负责生成证书、启动etcd、拉起apiserver、配置kubeconfig,最后给出一份完整的节点接入方案。它不包含网络插件和存储插件,那是你拿到集群之后要自己装的东西。这个边界要明确:kubeadm管的是控制平面的“骨架”,CNI和存储属于“血肉”。
1.2 1.24版本为什么要改容器运行时
1.24这个版本在K8s历史上有一个标志性事件:dockershim正式被移除。之前K8s可以通过dockershim直接调用Docker来跑容器,但Docker封装了一层自己的逻辑,并不符合CRI标准,维护成本很高。
1.24之后,所有CRI实现都必须直接暴露给kubelet,比如containerd和CRI-O。如果你还在用Docker来跑集群容器,1.24直接不认。
这个变化带来的直接后果就是:部署方式变了。以前你装完Docker再装kubeadm就行,现在必须装containerd,而且还要保证kubelet和containerd的cgroup驱动一致。cgroup驱动不一致会导致kubelet报错,节点起不来,这是新手最容易踩的坑,后面我会专门演示怎么做。
1.3 集群拓扑规划建议
我这里部署的是一个小型测试集群,规划了1个控制平面节点加1个工作节点。如果你是个人学习,或者想跑一些轻量应用,这种规模完全够用。如果是生产环境,建议至少3个控制平面节点配合外部etcd,那是另一个话题了。
节点的操作系统我统一用了Ubuntu 22.04 LTS。这里说个经验:所有节点的内核版本和系统版本尽量一致,否则排查问题时可能会有莫名其妙的差异。
| 节点角色 | 主机名 | 配置 | 系统 |
|---|---|---|---|
| 控制平面 | k8s-master | 2核4G/40G | Ubuntu 22.04 |
| 工作节点 | k8s-node1 | 2核4G/40G | Ubuntu 22.04 |
内存2G以上是底线,再低的话etcd和kubelet都会跑得很艰难。磁盘建议留40G以上,镜像一多起来,空间消耗很快。
2. 基础环境准备:系统初始化与期待内核参数
这一节的内容非常多,但每一条都跟后面的成败直接相关。我按照自己实际操作时执行的顺序来写。
2.1 主机名、hosts解析与时间同步
第一步是改主机名。这一步很简单,但是很多人会忘,后面join节点的时候,主机名不一致会导致kubelet报错。
hostnamectl set-hostname k8s-master # 节点上执行 hostnamectl set-hostname k8s-node1然后修改每台机器的/etc/hosts,把所有节点都加进去,确保互相能解析到:
cat >> /etc/hosts << EOF 192.168.1.10 k8s-master 192.168.1.11 k8s-node1 EOF时间同步容易被忽略。K8s的证书校验基于时间,如果节点之间时间偏差太大,会导致TLS握手失败。Ubuntu 22.04默认带systemd-timesyncd,直接启用就行:
timedatectl set-ntp true timedatectl status2.2 内核模块与系统参数
K8s的网络依赖iptables和ipvs,还需要打开特定的内核模块。这里我直接一步到位执行:
cat <<EOF | tee /etc/modules-load.d/containerd.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilteroverlay是容器文件系统需要的,br_netfilter用来让iptables正确处理网桥流量。两个模块不加载的话,后面Pod网络会出各种奇怪问题。
接着配置系统参数。这一步会让iptables转发请求、并且让流量能穿过网桥:
cat <<EOF | tee /etc/sysctl.d/99-kubernetes-cri.conf net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1 net.bridge.bridge-nf-call-ip6tables = 1 EOF sysctl --system这里非常关键的是net.ipv4.ip_forward,它必须为1,否则Pod之间无法通信。每次我在新服务器上部署前都会先跑一下sysctl检查,避免后面误判。
2.3 部署containerd容器运行时
前面讲了1.24移除了dockershim,所以这里需要安装containerd。Ubuntu 22.04的官方软件源里已经有containerd,但不一定是最新版本。我更习惯直接用Docker官方的源来安装,因为它会把containerd一起装好,版本也比较稳定。
如果你不想走Docker的源,也可以去containerd的GitHub release页直接下载二进制包,但那样需要自己处理systemd service文件。用官方源最省事:
apt-get update apt-get install -y ca-certificates curl gnupg lsb-release install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 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 $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null apt-get update apt-get install -y containerd.io安装完成后,containerd的默认配置是有的,但默认配置并不适合K8s使用。核心问题在于cgroup驱动:系统默认是cgroupfs,而K8s推荐用systemd。
先重置配置:
mkdir -p /etc/containerd containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' | tee /etc/containerd/config.toml这一条命令做的事情就是生成默认配置,然后把SystemdCgroup改成true。我见过太多人直接生成默认配置就开始用,结果kubelet疯狂报failed to run kubelet的错。
改完之后重启containerd:
systemctl restart containerd systemctl enable containerd验证一下:
systemctl status containerd如果看到active (running),说明容器运行时已经就绪。
这里还有一个实操心得:如果你在中国大陆的服务器上部署,拉取镜像会非常慢或者超时,建议在containerd的配置里把sandbox_image改成国内镜像源。具体做法是在config.toml里找到pause镜像的地址,替换成你所在的镜像加速服务提供的地址。否则kubeadm init会在拉取pause镜像这步卡很久。
2.4 安装kubeadm、kubelet、kubectl
这三个组件的版本必须一致,至少主版本号和小版本号都要相同,否则可能出现kubelet和apiserver版本不匹配的问题,导致节点注册不上。我这次装的是1.24.4版本。
先添加Kubernetes的apt源。有些同学直接用Google的官方源,但国内网络访问不稳定,我一般用国内镜像源:
apt-get update apt-get install -y apt-transport-https ca-certificates curl curl -fsSL https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add - echo "deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main" | tee /etc/apt/sources.list.d/kubernetes.list apt-get update然后指定版本安装:
apt-get install -y kubelet=1.24.4-00 kubeadm=1.24.4-00 kubectl=1.24.4-00这里说个细节:很多教程把kubelet kubeadm kubectl拆开来装,建议你一次装齐。否则kubectl版本和集群版本不一致,虽然一般也能用,但API字段可能有差异,还是对齐更省心。
装完以后,先把kubelet服务设置成开机自启。记住一个重要的点:现在kubelet还没法正常启动,它需要kubeadm init之后生成的配置文件,所以现在service状态是“启动失败”也没关系:
systemctl enable kubelet不要急着看状态,kubelet在没有配置的情况下会一直重启,这是正常的。
至此,基础环境准备完毕。如果你用的是CentOS,命令上会有差异,但思路完全一致,无非就是把apt换成yum,源地址换成对应的镜像源。
3. 控制平面部署:kubeadm init全流程实战
3.1 init命令的参数逐项解析
现在到了最关键的一步,初始化控制平面节点。先说一下我用的命令,然后一个一个参数解释为什么这么写:
kubeadm init \ --apiserver-advertise-address=192.168.1.10 \ --image-repository=registry.aliyuncs.com/google_containers \ --kubernetes-version=v1.24.4 \ --pod-network-cidr=10.244.0.0/16 \ --ignore-preflight-errors=Swap--apiserver-advertise-address:指定apiserver绑定的IP。一般填控制平面节点的内网IP。如果你的机器有多块网卡,必须显式指定,否则kubeadm可能会选错网卡。
--image-repository:镜像仓库地址。默认是registry.k8s.io,国内经常拉不动,我改成阿里云的镜像仓库。这个参数在1.24版本里非常实用,省掉了很多手动拉镜像的麻烦。
--kubernetes-version:指定版本。这里必须跟kubeadm的版本一致,否则会报版本不匹配。
--pod-network-cidr:Pod网段。这里选了10.244.0.0/16,这个网段必须和后面要部署的CNI插件(我用的Flannel)的配置保持一致。如果这里填了192.168.0.0/16,那后面Flannel的默认配置就能直接用;如果填别的网段,就要改Flannel配置。
--ignore-preflight-errors=Swap:跳过Swap检查。K8s要求关闭Swap,但我觉得个人测试环境关掉Swap无所谓,就先加了这个参数跳过检查。如果是生产环境,建议还是按规范来关闭Swap。
新的1.24版本还会检查CRI,如果containerd没装好或者cgroup配置不对,preflight会直接报错。如果遇到报错,先解决报错再重启init。
输入上面的命令之后,整个初始化过程会经历这么几个阶段:生成证书、写kubeconfig、启动etcd、启动控制平面组件、安装基础服务。这个过程通常要2到5分钟,取决于镜像拉取速度。
看到输出末尾类似这样的内容就代表成功了:
Your Kubernetes control-plane has initialized successfully!并且会给出一段用于加入节点的kubeadm join命令。这个命令很重要,最好复制保存到本地,后面加入节点时要用。如果丢了也没关系,可以重新生成,后面会讲。
3.2 初始化后的kubeconfig配置与验证
初始化成功后,kubeadm会告诉你需要把kubeconfig放到$HOME/.kube目录下才能使用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会对配置文件权限敏感。顺带说一句,如果用的是root用户,执行顺序稍微有点不一样,但思路一样。
配置完成后,先跑一下最基础的健康检查:
kubectl get node你会发现NAME列有你的master节点,但STATUS是NotReady。这个完全正常,因为CNI还没装,网络插件没有就绪,节点自然处于NotReady状态。
再查看一下Pod状态:
kubectl get pods -n kube-system会看到coredns的Pod处于Pending状态。这同样是因为CNI没装,Pod无法分配到IP。下面去部署网络插件。
3.3 部署CNI网络插件(Flannel)
我选择Flannel,因为配置简单,对基础设施依赖少。但不管选哪个插件,前提都是:网络插件的网段必须和--pod-network-cidr一致。
先下载Flannel的yaml文件:
wget https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml检查这个yaml中是否包含Pod网段信息。如果我在kubeadm init时指定的pod-network-cidr=10.244.0.0/16,和Flannel默认一致,那么不需要修改,直接应用:
kubectl apply -f kube-flannel.yml如果你的网段不一样,那就要在yaml文件中找到NetConf部分,把Network改成你自己的网段。
等30秒到1分钟,然后再看节点状态:
kubectl get node正常情况下STATUS就变成Ready了。如果还是NotReady,先等一会,镜像拉取需要时间。如果等了3分钟还不行,那就要看kubelet日志了,这个问题我放在后面第五节专门讲。
3.4 控制平面功能验证
节点Ready只是第一步,我们还要验证控制平面的核心组件是否正常工作。我个人习惯按这个顺序检查:
kubectl get pods -n kube-system -o wide检查kube-system里的所有Pod是否都是Running或Completed状态。如果coredns全部Running,说明集群内部DNS服务正常,Pod之间能用域名互相访问。
再检查一下etcd:
kubectl get pods -n kube-system -o wide | grep etcdetcd的Pod应该是Running状态。etcd是K8s的数据库,它挂了整个集群就废了。
另外,我在部署完控制平面后还会做一个小测试:在其他节点还没有加入之前,先创建一下临时Pod,确认调度器、kubelet都正常工作。不过这步不是必需,可以等节点都就绪之后再测。
4. 工作节点接入:kubeadm join全流程实录
4.1 Token管理:找到并重新生成加入命令
新节点加入集群靠的是token。kubeadm init成功后打印的那一大段信息里,就包含了kubeadm join命令。如果你像我一样没保存,可以在控制平面节点上重新获取:
kubeadm token list这个命令会列出当前的token。但需要注意,token默认有效期24小时,如果超过期限还要重新创建。
完整join命令除了token,还需要--discovery-token-ca-cert-hash,这是用来验证控制平面证书的哈希值。获取方式:
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'其实还有一个更快的方式,直接用一行命令重新生成完整的join命令:
kubeadm token create --print-join-command这条命令会生成一个新的token,并直接打印出完整的kubeadm join命令,复制粘贴就能用。
4.2 节点执行join操作的全过程
把kubeadm join命令拿到工作节点上执行。我这里模拟完整的命令:
kubeadm join 192.168.1.10:6443 --token xxxxxx --discovery-token-ca-cert-hash sha256:xxxxxx如果提示缺少token,检查一下你复制的是不是完整的两部分参数。我经常看到同学只把token复制进来,忘了带后面的hash,这会导致节点加入失败。
join过程主要做这么几件事:连接apiserver、验证证书、把自己注册为节点、拉取必要镜像、启动kubelet。这过程一般1-2分钟。
看到类似这样的提示就代表加入成功:
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.4.3 加入后的状态确认与功能实测
回到控制平面节点,执行:
kubectl get nodes能看到两个节点,并且工作节点状态逐渐变为Ready。如果工作节点一直NotReady,先别急着去抓kubelet日志,先确认一下节点上有没有拉取所需要的镜像。有时候就是镜像拉得慢。
另外还要验证跨节点的通信能力。我通常会在控制平面节点上创建一个简单deployment,然后扩展到多个副本,让副本调度到工作节点上,看看Pod是否能跑起来:
kubectl create deployment nginx --image=nginx kubectl scale deployment nginx --replicas=3 kubectl get pods -o wide如果所有Pod都Running,并且分布在两个节点上,那说明集群的核心功能正常。这个测试很简单,但很能说明问题:调度器在正常工作、kubelet在正常工作、CNI网络也是通的。
如果还想进一步测试跨节点通信,可以进入一个Pod里ping另一个Pod的IP:
kubectl exec -it <pod-name> -- ping <另一个pod-ip>这里提醒一下:默认的nginx镜像里没有ping命令,需要先安装iputils。不想装也没关系,能启动、Running,一般来说网络就没问题。
5. 常见问题排查与避坑经验
这里整理了我个人在这次部署中踩过的坑,也包含了平时帮别人排查时遇到的高频问题。按从高到低的频率排序。
5.1 kubelet起不来:从日志里找答案
kubelet的日志是排查一切问题的第一入口。先看日志再猜测,这是铁律。
journalctl -u kubelet -f如果报错类似:
failed to run Kubelet: failed to create kubelet: misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"这就说明cgroup驱动不一致。解决方案有两种:
把containerd的SystemdCgroup改成和kubelet一致,或者反过来。不过推荐统一为systemd,因为内存和CPU的QoS管理在systemd下更稳定。
如果你没有改containerd配置,可以在kubelet配置文件里改,但那种做法比较绕,不推荐。
我自己处理这个问题的标准流程是:
# 1. 修改/etc/containerd/config.toml,把SystemdCgroup设为true # 2. 重启containerd systemctl restart containerd # 3. 重启kubelet systemctl restart kubelet # 4. 再看日志 journalctl -u kubelet -f5.2 节点一直NotReady的几个原因
节点NotReady,90%和网络有关。常见原因有下面几个:
第一个是CNI插件没部署或者部署失败。确认一下:
kubectl get pods -n kube-system如果网络插件Pod启动失败,拉出日志看看具体报错。我这遇到过一个情况:Flannel在启动时一直报错,原因是容器里缺少iptables。这是因为宿主机没有安装iptables。Ubuntu 22.04默认可能没装,那就执行:
apt-get install -y iptables第二个是Pod网段冲突。如果你在kubeadm init时用了10.244.0.0/16,但是Flannel里配置了别的网段,那Pod永远拿不到正确IP,节点自然不Ready。这个必须仔细复核。
第三个是容器运行时的问题。kubelet虽然起来了,但没办法和containerd通信,检查一下containerd的服务状态:
systemctl status containerd还有一种诡异情况:所有Pod都是Running,但节点就是不Ready。遇到这种,检查一下kubelet上报的条件:
kubectl describe node <node-name>在Conditions里面能看到具体是哪一项不满足,比如MemoryPressure、DiskPressure等。我之前遇到一次是磁盘空间不够,节点进入Pressure状态,导致不可调度。
5.3 重新部署:kubeadm reset的正确用法
如果部署过程中出了问题,最干脆的办法就是推倒重来。kubeadm reset可以把kubeadm初始化生成的配置清干净。
kubeadm reset -f这命令会重置,但不会清除网络插件留下的残留。我还建议手动把CNI配置清掉:
rm -rf /etc/cni/net.d如果是重新init,建议把/var/lib/kubelet也清掉:
rm -rf /var/lib/kubelet但注意,这个目录不要随便删,只有确认要完全重新部署的时候才这么操作。
之后如果用的是containerd,记得重启一下containerd,清理掉已挂载的旧容器状态:
systemctl restart containerd5.4 证书过期怎么办
K8s默认证书有效期是一年,很多测试环境集群跑着跑着证书就过期了。kubeadm的证书续期命令很简单:
kubeadm certs renew all续期后需要重启相关控制平面组件,最简单的方式是重启kubelet:
systemctl restart kubelet另外,1.24版本之后,如果你装的kubeadm支持自动续期,kubelet的证书会在每年到期前自动更新,控制平面的证书需要手动renew。如果你只是用来学习,没必要焊死在长证书上,到期renew一次就行。
5.5 镜像拉取超时怎么办
如果你用了阿里云镜像仓库还是拉不动,或者很慢,可以先手工拉取一次需要的镜像。比如:
crictl pull registry.aliyuncs.com/google_containers/pause:3.7crictl是containerd的命令行客户端,装完containerd之后会带。用crictl images能查看本机已有镜像。拉取完成后,再跑一遍kubeadm init,已经存在的镜像就不会重复拉取。
还有一个常见问题:在containerd里,通过crictl看到的镜像tag可能与docker里的不一样,这是正常现象,不用管它。
6. 集群日常维护与我的实操心得
到这里,集群已经能正常运转了。我再分享几点日常维护中积累的经验。
第一,关于节点维护。比如你要重启一个节点,或者对宿主机做内核升级,最好先执行:
kubectl drain <node-name> --ignore-daemonsets这个命令会把节点上的Pod安全驱逐到其他节点。处理完后再恢复调度:
kubectl uncordon <node-name>很多同学不知道这个操作,直接重启节点,结果Pod全部挂掉,还要手动恢复,非常被动。
第二,关于版本升级。1.24升1.25其实不算难,但要注意,升级前最好看官方changelog。比如1.25版本把PodSecurityPolicy移除了,如果你的业务依赖PSP,升级前先想好替代方案。我自己的习惯是:先在测试环境走一遍升级,确认没影响再上生产。
第三,关于Pod调度。如果你发现集群明明有两个节点,但Pod总是只调度到一个节点上,看看节点上有没有打了污点。K8s 1.24之后,如果只有控制平面一个节点参与调度,而工作节点一直NotReady,也经常被误解成“只调度到master”。检查一下:
kubectl describe node <node-name> | grep -i taintMaster节点上通常有污点node-role.kubernetes.io/control-plane:NoSchedule,这会让普通Pod不会调度到控制平面节点上。如果你希望单机环境也能跑Pod,可以把污点去掉:
kubectl taint nodes --all node-role.kubernetes.io/control-plane-不过我一般建议:即使去掉污点,也别把重量级业务放到控制平面节点,etcd和业务抢资源会影响集群稳定性。
第四,关于日志和监控。K8s集群不装监控等于盲人摸象。我建议至少在集群里装一个metrics-server,不然kubectl top没法用:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml装完后,对Pod的CPU、内存使用情况一目了然。很多新手问为什么kubectl top node没数据,绝大多数情况就是因为没装metrics-server。
最后再说一个我个人的习惯:所有通过kubectl apply部署的yaml文件,我都会找地方保留一份副本,最好是放到git仓库里管理。这样如果哪天集群出问题要重建,直接apply回来就行,不用靠记忆重新写配置。K8s的yaml文件写起来简单,但保存历史版本的价值往往被低估。
这个部署方案到这里已经完整落地了。从准备节点、配置containerd、初始化控制平面、接入工作节点,到最后的网络验证,每一个步骤我都按自己实际操作时的顺序做了说明。希望这篇内容能帮你把1.24集群稳稳跑起来。