kubeadm部署Kubernetes集群实战与NotReady排查

发布时间:2026/10/27 16:34:59
kubeadm部署Kubernetes集群实战与NotReady排查

Kubernetes集群部署这件事,第一次用kubeadm走通的时候我印象很深,不是因为顺利,而是因为从执行kubeadm init到worker节点真正Ready,整整卡了大半天。那会儿我以为装个容器运行时、敲几条命令就能成,结果在cgroup驱动、CNI网段、kubelet证书几个地方来回翻车。后来带过几批新人,发现大家踩的坑高度重合:环境检查省了、参数随手抄、报错不看日志直接搜答案。这篇就把我用kubeadm做Kubernetes集群部署的完整链路拆开讲一遍,从三台机器的环境摸底,到init之后的每一项参数含义,再到集群起来之后的补强动作。内容偏向动手实操,适合刚接触集群搭建、手里有虚拟机或物理机可以做实验的读者,也适合已经在用但想弄清楚每个参数为什么这么填的同学。全程以最小可用集群为起点,把每个"为什么"讲透,保证你能照着复现,而不是只会复制粘贴命令。

1. 为什么我建议先用kubeadm啃下集群初始化这块硬骨头

很多人一上来就想用托管服务或者一键脚本把集群拉起来,图省事。这本身没错,生产环境也确实该这么做。但如果你连kubeadm init背后干了什么都不知道,后面集群一出问题就只能干瞪眼。kubeadm的价值恰恰在于它把集群初始化这件复杂的事拆成了可见的、可解释的步骤:生成证书、写静态Pod清单、拉起控制平面组件、配置kubeconfig、输出join命令。每一步你都能看到、都能干预,出问题也知道去哪找。

1.1 kubeadm到底帮你做了什么,没做什么

先说清楚边界。kubeadm负责的是"把控制平面和节点引导起来",它不负责装容器运行时,不负责装CNI网络插件,不负责给你配负载均衡器,也不管你的机器内核参数调没调对。这几件事它默认你自己搞定,或者它只做最基本的检查然后报错退出。我见过不少人以为kubeadm init能一步到位,结果卡在kubelet起不来,本质上就是容器运行时没配好。

换句话说,kubeadm更像一个"装配工"而不是"装修队"。它把组件摆到位、接上线,但地基(操作系统)和管线(网络、运行时)得你先打好。理解这一点,你在排查问题时就不会漫无目的。比如节点一直NotReady,八成不是kubeadm的锅,而是CNI没装或运行时没通。

还有一点值得强调:kubeadm做的是"最佳实践引导",它生成的证书有效期、控制平面组件的默认参数、etcd的部署方式,都是社区公认的稳妥选择,但它不会替你做高可用拓扑,也不会帮你规划地址段。这些决策必须你自己在init之前想清楚,填错参数后面返工很麻烦。

1.2 单控制平面起步是更稳的学习路径

我不建议新手一上来就搞三控制平面高可用。原因很实际:高可用需要额外的负载均衡器(哪怕是keepalived加haproxy这种软件方案),需要--control-plane-endpoint指向一个虚拟地址,需要处理证书SAN里包含这个地址。任何一环配错,你会同时面对"集群起不来"和"不知道是哪一层的问题"两个困境。

先用一台控制平面加一到两台worker把单控制平面集群跑通,把所有基础概念吃透:节点如何注册、Pod网络如何打通、Service如何做负载均衡、证书放在哪。等你对这套流程熟了,再把控制平面扩成三个,你会发现高可用只是多了个前置负载均衡和--control-plane-endpoint参数,其余逻辑完全一致。我个人的经验是,单控制平面跑通再上高可用,整体成功率比直接硬上高可用高得多。

1.3 实验环境的最低配置与资源规划

资源这块别太抠。控制平面节点至少2核4G,worker节点至少2核2G,磁盘每台给20G以上。低于这个配置,控制平面组件(尤其是etcd和apiserver)会因为资源紧张频繁抖动,你会误以为是配置问题,实际是机器扛不住。系统盘最好用SSD,etcd对磁盘IO很敏感,机械盘在高频写入下延迟会明显拖慢整个控制平面。

网络规划要提前定好三件事:节点之间能互相通信(建议同一网段或至少路由可达)、Pod网段不能和节点网段重叠、Service网段也不能和节点网段重叠。这三条如果冲突,后面会出现"Pod能起但访问不了"这类诡异问题。我一般的习惯是节点用192.168.10.0/24,Pod用10.244.0.0/16,Service用10.96.0.0/12,这套组合几乎不会和常见内网冲突。

角色建议配置数量说明
控制平面2核4G,20G磁盘1(实验)/3(高可用)跑apiserver、etcd、controller-manager、scheduler
worker2核2G,20G磁盘1-3承载业务Pod
网络节点同网段,Pod/Service网段不冲突-三网段必须互不重叠

2. 三台机器的环境摸底:从内核参数到容器运行时的逐项排查

环境准备这一步最容易被跳过,也最容易埋雷。我总结下来,80%的"init失败"或"节点NotReady"都能在这一步提前掐掉。下面几项是我每次都必查的,按顺序做完基本不会有环境级问题。

2.1 主机名、hosts与时间同步这些不起眼但会咬人的细节

主机名必须唯一,且符合DNS命名规范(小写字母、数字、短横线,别用下划线和大写)。我见过有人把三台机器都叫localhost,结果节点注册时互相覆盖,排查了半天。改完主机名记得同步更新/etc/hosts,把每台机器的IP和主机名对应上,控制平面和worker都要能互相解析。

时间同步更要命。Kubernetes的证书校验、token有效期、etcd的raft选举都对时间敏感,节点间时间差超过几分钟就可能出现证书校验失败或节点无法加入。实验环境里手动用chronyc或timedatectl配一个可靠的时间源即可,至少保证所有节点时间误差在几十秒以内。

# 设置主机名(每台机器分别执行) hostnamectl set-hostname k8s-master01 # 校验解析 cat /etc/hosts ping -c 2 k8s-master01 # 检查时间同步状态 timedatectl status

这三条命令我每次开局必跑。ping那一步看着傻,但能立刻暴露hosts没写对的问题。时间同步用timedatectl status看System clock synchronized是否为yes,不是的话先解决这个再往下走。

2.2 关闭swap与内核模块加载的实际影响

kubelet默认不允许swap开启,这不是洁癖,而是调度器的内存计算模型假设节点没有swap,开了swap会导致Pod内存超限时的行为不可预测。所以要swapoff -a并把/etc/fstab里的swap行注释掉,保证重启后也不生效。

内核模块方面,需要加载br_netfilter和overlay。前者让iptables能处理桥接流量(Pod跨节点通信依赖它),后者是overlayfs文件系统的支撑,容器镜像分层存储要用。同时要打开net.bridge.bridge-nf-call-iptables和net.ipv4.ip_forward这两个内核参数,否则Pod的流量出不了节点。

# 关闭swap swapoff -a sed -ri 's/.*swap.*/#&/' /etc/fstab # 加载模块并持久化 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-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sysctl --system

sysctl --system执行后,可以用sysctl net.ipv4.ip_forward确认值是不是1。这一步如果漏了,最典型的症状是Pod能拿到IP但ping不通别的节点上的Pod,很多新手会误以为是CNI的问题,其实是ip_forward没开。

2.3 容器运行时选型:containerd与Docker的关系梳理

从Kubernetes 1.24开始,内置的dockershim被移除,kubelet不再直接对接Docker。现在主流选择是直接用containerd,它更轻、启动更快、和kubelet的CRI接口对接更顺。如果你以前习惯用Docker命令,用containerd之后可以用crictl替代,功能上完全够用。

安装containerd之后,最关键的一步是改配置文件里的cgroup驱动。新版containerd默认用systemd作为cgroup驱动,而kubelet这边也必须配成systemd,两边必须一致。不一致的典型症状是kubelet反复重启,日志里报failed to run Kubelet: misconfiguration: kubelet cgroup driver: "cgroupfs" is different from docker cgroup driver: "systemd"。这个坑我踩过不止一次。

# 生成默认配置 containerd config default > /etc/containerd/config.toml # 修改关键项:cgroup驱动改为systemd,sandbox镜像改为可用地址 sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml systemctl restart containerd systemctl enable containerd # 验证 systemctl status containerd

改完记得重启并设开机自启。另外config.toml里的sandbox镜像(pause镜像)地址如果拉取困难,建议换成企业内部仓库地址或提前在每台机器上预拉取。这是环境准备里比较繁琐但很值得做的一步。

2.4 kubelet、kubeadm、kubectl的版本对齐原则

这三个组件的版本关系有个硬规则:kubeadm和kubelet的版本必须一致或非常接近,kubectl可以比集群版本低一个小版本,但不能高。比如集群是1.28,kubectl用1.28或1.27都行,用1.29就可能出现兼容问题。

安装时用包管理器固定版本,避免装到最新版导致和预期不符。以Debian系为例,添加仓库后执行apt-get install -y kubelet=1.28.x-* kubeadm=1.28.x-* kubectl=1.28.x-*,把版本号钉死。装完用kubeadm version和kubelet --version确认一致。

还有个小细节:apt-mark hold把这三个包锁住版本,防止后续apt upgrade无意中升级。我见过一次因为系统自动更新把kubelet升了半代,结果和kubeadm版本对不上,init直接报错。锁版本这个动作花不了十秒,能省掉很多麻烦。

3. kubeadm init之前必须想清楚的几个参数

kubeadm init这一条命令里,参数填错比环境没配好更难排查,因为错误往往是"看起来像集群问题"的形态出现。下面这几个参数是我认为最需要提前想清楚的,每个都说清楚取值逻辑。

3.1 --pod-network-cidr与CNI插件的地址段配合

这个参数不是随便填的,它必须和你选的CNI插件默认网段一致,或者你改CNI配置去适配它。Calico默认用192.168.0.0/16,Flannel默认用10.244.0.0/16。如果你填了10.244.0.0/16却装Calico,Calico会按自己的192.168.0.0/16去分配,两边对不上,Pod虽然能起来但跨节点通信会出问题。

我的习惯是统一用10.244.0.0/16,然后装Flannel时不用改配置,装Calico时把CALICO_IPV4POOL_CIDR改成一致。这样无论后面换哪个插件,地址段都是明确的。填之前务必确认这个网段和节点网段不重叠,重叠的后果是Pod IP和节点IP撞车,路由会乱。

另外这个参数一旦定了,后面改起来很麻烦,因为已经分配的Pod IP都在这个池子里。所以建议在集群创建前就规划好,不要抱着"先随便填后面再改"的心态。

3.2 --apiserver-advertise-address在单机与多网卡环境下的取值

这个参数告诉集群"apiserver对外暴露的地址是哪个"。单网卡环境直接填控制平面节点的IP就行。多网卡环境就要小心了,如果你机器上有内网管理网卡和业务网卡,填错了会导致worker节点join时连不上apiserver。

判断方法很简单:想清楚worker节点会通过哪个网段访问控制平面,就填那个网段对应的IP。不确定的话,ip addr看一下有几块网卡、各自在什么网段,然后选worker能路由到的那个。我一般会在init之后立刻从worker上telnet或curl一下6443端口,确认网络通再继续,避免join到一半失败还要回滚。

3.3 --control-plane-endpoint与高可用雏形的关系

单控制平面实验用不上这个参数,但如果你打算后面扩成高可用,我强烈建议一开始就加上。这个参数的值是一个稳定的DNS名或虚拟IP,指向控制平面的负载均衡入口。即使现在只有一个控制平面,把它设成控制平面节点的IP或一个临时域名,后面加节点时就不用改worker的kubeconfig。

不加这个参数的后果是:worker的kubeconfig里直接写死了控制平面IP,等你扩成三个控制平面,worker还是只连原来那台,高可用形同虚设。这个坑很隐蔽,因为集群表面上能跑,只有那台挂了你才发现问题。

# 高可用场景的init示例(单控制平面也可先这么写) kubeadm init \ --control-plane-endpoint "k8s-api.example.internal:6443" \ --apiserver-advertise-address 192.168.10.11 \ --pod-network-cidr 10.244.0.0/16 \ --image-repository registry.example.internal/k8s \ --kubernetes-version v1.28.5

这里k8s-api.example.internal是个示例域名,实际用你环境里能解析的名字。域名要提前在每台机器的hosts或内部DNS里配好,否则init时会因为解析失败卡住。

3.4 镜像仓库与超时问题的处理思路

kubeadm默认从公共仓库拉取控制平面组件镜像,网络条件不好的环境里这一步经常超时。解决办法有两个:一是用--image-repository指定一个可达的镜像仓库,二是提前kubeadm config images pull把镜像拉到本地。

我一般推荐第二种,因为可控。先kubeadm config images list看看需要哪些镜像,然后用--image-repository配合pull一次性拉全。拉完之后init时就不会再去网络上折腾,速度快很多,也避免中途超时导致集群半初始化状态。

如果拉取过程中某几个镜像反复失败,可以用crictl pull单独拉,或者从其他机器crictl save导出再load进来。这个手法在完全隔离的内网环境里几乎是必备技能,值得提前练一遍。

4. 执行kubeadm init之后的完整链路拆解

init成功那一刻的终端输出信息量很大,很多人看一眼就往下走,其实那几段输出把后面所有关键信息都给了。这一段我按实际执行顺序拆开讲,把每条输出的用途说清楚。

4.1 init输出里的三件事:kubeconfig、join命令、token

init成功后会打印三类东西。第一类是kubeconfig配置命令,三条mkdir、cp、chown,作用是让当前用户能用kubectl操作集群。这三条必须执行,否则kubectl get nodes会报连不上。执行完敲kubectl get nodes,应该能看到控制平面节点,状态是NotReady,这正常,因为还没装CNI。

第二类是worker节点的join命令,形如kubeadm join <ip>:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>。这条命令24小时内有效(token默认TTL),过期了可以用kubeadm token create --print-join-command重新生成。注意join命令里的hash是证书的指纹,用来验证控制平面身份,别随手改。

第三类是高可用场景下其他控制平面节点的join命令,带--control-plane参数,单控制平面实验用不上。如果你一开始就规划了高可用,这条命令要保存好,后面加控制平面节点要用。

4.2 安装CNI插件:Calico与Flannel的取舍与实测

CNI不装,节点永远是NotReady,这是新手最常卡住的地方。Flannel和Calico是最常见的两个选择。Flannel配置简单,一条kubectl apply就完事,适合实验和小规模集群;Calico功能更强,支持网络策略,生产环境更常见,但配置项多一些,对内核和路由的要求也略高。

实验环境我一般先用Flannel把流程跑通,因为它几乎不会有额外依赖,装完几分钟节点就Ready。等流程熟了再换Calico体验网络策略。需要注意Flannel的yaml里默认网段要和--pod-network-cidr一致,不一致就改yaml再apply。

# Flannel部署(网段需与init时一致) kubectl apply -f https://example.internal/manifests/kube-flannel.yml # 观察Pod状态 kubectl get pods -n kube-flannel -o wide kubectl get nodes

kubectl get pods -n kube-flannel要看到所有Pod是Running,kubectl get nodes状态从NotReady变Ready。如果Flannel Pod一直ContainerCreating或CrashLoopBackOff,八成是网段冲突或br_netfilter没加载,回到第2章那几项复查。Calico的话还要关注calico-node这个DaemonSet是不是在每个节点都跑起来了。

4.3 节点NotReady的排查顺序

节点NotReady是最常见的状态,排查要有固定顺序,否则容易东一榔头西一棒子。我的顺序是:先看kubectl describe node <name>的Conditions,看是NetworkUnavailable还是KubeletNotReady;再看journalctl -u kubelet -n 100看kubelet报什么;最后看CNI Pod是否正常。

NetworkUnavailable基本都是CNI没装或没装好。KubeletNotReady则可能是cgroup驱动不一致、运行时没起来、证书问题。kubelet日志里如果出现failed to get sandbox image,说明pause镜像拉不到;出现cgroup driver字样,回到2.3节改containerd配置。这个排查链路走熟了,大部分NotReady都能在十分钟内定位。

4.4 worker节点加入与集群状态确认

worker节点执行join命令前,确认它已经完成了第2章的全部环境配置,尤其是swap关闭、内核参数、容器运行时。join成功后,在控制平面执行kubectl get nodes -o wide,应该看到worker节点,初始状态NotReady,等CNI在它上面也跑起来后会变Ready。

确认集群健康我一般跑三条:kubectl get nodes看节点状态、kubectl get pods -A看所有系统Pod是否Running、kubectl run一个测试Pod看能不能被调度到worker上并正常访问。第三条最能说明问题,因为节点Ready不代表Pod真的能跑起来,网络、调度、镜像拉取任何一个环节有问题都会在这一步暴露。

5. 首次集群跑起来后最常见的几个坑与排查链路

集群"能起来"和"能稳定用"是两回事。这一章我把实际遇到频率最高的几类问题按排查链路的顺序写出来,方便你对照现象定位,而不是只给结论。

5.1 cgroup驱动不一致导致的kubelet反复重启

现象是systemctl status kubelet显示activating (auto-restart),journalctl -u kubelet里反复出现misconfiguration: kubelet cgroup driver "cgroupfs" is different from ... "systemd"。根因是containerd用systemd而kubelet用了cgroupfs,或者反过来。

修复方法是两边统一。containerd这边改/etc/containerd/config.toml里的SystemdCgroup = true并重启;kubelet这边在/var/lib/kubelet/config.yaml里确认cgroupDriver: systemd,或者在kubeadm init时通过--cri-socket和配置确保一致。改完systemctl restart kubelet,观察日志不再报错即可。这个坑的特点是日志写得很明确,照着改就行,难的是第一时间想到去看kubelet日志而不是去搜报错。

5.2 防火墙与iptables链路对Pod通信的影响

节点间Pod通信依赖iptables规则和转发,如果系统防火墙(firewalld或ufw)没放行,或者FORWARD链默认策略是DROP,跨节点Pod通信就会断。现象是同一节点内的Pod能通,跨节点的Pod ping不通。

排查方法是先在源Pod里ping目标Pod IP,再在节点上traceroute或tcpdump看包到哪一层被丢。确认是防火墙问题后,实验环境可以直接关掉防火墙或放行相关网段;生产环境则要精细放行,至少放行节点网段、Pod网段、Service网段以及apiserver的6443、etcd的2379/2380、kubelet的10250等端口。

5.3 证书过期与集群恢复的应急处理

kubeadm生成的证书默认一年有效,到期后apiserver起不来,整个集群失联。这个问题的排查链路是:kubectl报x509: certificate has expired,journalctl -u kubelet里也会看到证书过期相关信息。确认后执行kubeadm certs check-expiration看各证书到期时间。

恢复用kubeadm certs renew all续期,然后重启控制平面组件(静态Pod会自动重建,或手动重启kubelet)。续期后要重新生成kubeconfig并分发,worker节点也要更新/etc/kubernetes/kubelet.conf里的证书信息。这个过程有点繁琐,建议在证书到期前一个月就检查一次,别等挂了才处理。

5.4 时区、DNS与CoreDNS的连锁故障

系统Pod频繁重启、日志里出现DNS解析失败、Service名访问不了,这类问题往往和CoreDNS有关。CoreDNS本身依赖Service网络和CNI,如果CNI没装好,CoreDNS会一直Pending或CrashLoopBackOff,进而导致所有依赖Service名的Pod异常。

排查顺序是:kubectl get pods -n kube-system看CoreDNS状态,kubectl logs -n kube-system <coredns-pod>看报错,如果报no route to host或connection refused,回到CNI和kube-proxy那边查。另外节点时间不同步也会导致证书校验失败,进而让CoreDNS起不来,这就是为什么第2章要强调时间同步。

现象最可能原因排查入口
节点一直NotReadyCNI未安装/网段冲突kubectl describe node、CNI Pod日志
kubelet反复重启cgroup驱动不一致journalctl -u kubelet
跨节点Pod不通防火墙或转发未开节点iptables、ip_forward
kubectl报证书过期证书超过有效期kubeadm certs check-expiration
CoreDNS起不来CNI异常或时间不同步CoreDNS日志、timedatectl

6. 把集群从"能跑"推到"敢用"的几个补强动作

集群Ready只是起点。真要用起来,还有几件事得补,不然你会在业务上线后发现各种"能用但不顺手"的问题。

6.1 移除控制平面污点与节点标签规划

kubeadm默认给控制平面节点打上node-role.kubernetes.io/control-plane:NoSchedule污点,意思是普通业务Pod不会被调度上去。实验环境只有一台机器的话,你得把这个污点去掉才能跑业务Pod:kubectl taint nodes <name> node-role.kubernetes.io/control-plane-。生产环境则保留污点,让控制平面专心跑管理组件。

节点标签也要提前规划。给worker打上node-role.kubernetes.io/worker之类的标签,后面做亲和性调度、污点容忍时才有依据。我一般还会按硬件规格给节点打标签(比如disk=ssd、cpu=high),方便后续用nodeSelector做定向调度。

6.2 持久化存储与Ingress的接入时机

业务一旦涉及有状态服务,就需要持久化存储。实验环境可以用local-path-provisioner这类简单方案快速验证PVC流程,生产环境则要上分布式存储或对接云盘。接入时机建议在业务部署前,而不是等业务跑了才发现没存储。

Ingress同理。Service的NodePort或LoadBalancer能满足基本访问,但要做域名路由、TLS终止就得Ingress。我一般在集群稳定后第一件事就是装Ingress Controller,配合一个示例应用验证从外部域名到Pod的完整链路,这条链路通了,后面的业务部署会顺很多。

6.3 备份etcd与证书轮换的日常操作

etcd存着整个集群的状态,丢了就得重建。备份用etcdctl snapshot save,建议做成定时任务,把快照存到集群外的位置。恢复时用etcdctl snapshot restore,注意恢复后apiserver和kubelet的证书路径要对应上,不然起不来。

证书轮换除了前面说的到期续期,还可以在kubeadm init时通过配置缩短证书有效期做演练,熟悉renew流程。日常巡检我一般看两样:kubeadm certs check-expiration和etcd快照的最近时间,这两项正常,集群的"底盘"就是稳的。

6.4 版本升级路径的选择

kubeadm升级要逐个小版本走,不能跨版本。比如1.26升1.28,要先升到1.27再升1.28。升级顺序是先升控制平面(kubeadm upgrade plan看可升级版本,然后kubeadm upgrade apply),再逐台升级kubelet和kubectl,最后升worker节点。每一步之间要确认节点状态正常再继续。

升级前务必做etcd快照和配置备份,并且在一个非生产集群上先把整个流程演练一遍。我见过跳过演练直接升生产、结果卡在某个版本差异上回滚的案例,代价很大。升级这件事,慢就是快。

最后分享一个我个人一直在用的小习惯:每次集群搭完,把kubeadm init的完整命令、join命令、证书到期时间、CNI版本、各节点角色整理成一页文档存起来。后面无论是扩容、排错还是交接,这一页纸能省掉大量回忆成本。集群运维的很多麻烦,本质上不是技术难,而是信息散落各处没人整理。