1. 项目背景与核心问题
1.1 为什么K8s安装如此容易翻车
K8s安装这件事,说实话,我一直觉得它被严重低估了。很多人以为K8s难在后面的调度、服务发现、持久化存储这些上层概念,结果真上手装的时候才发现,光是把一个集群跑起来,就能让人怀疑人生。
我自己在不止一个环境里装过K8s,有生产用的多节点集群,也有拿来练手学习的单节点环境。过程不能说一帆风顺,准确点说是一路踩坑一路填。有次装生产环境的控制平面节点,光一个初始化失败就折腾了大半天,最后发现是个非常低级的配置问题——cgroup驱动没对齐。这种问题不遇到一次,光看文档根本想不到。
所以这篇笔记,我不打算讲PPT式的架构原理,而是把我实际安装K8s过程中遇到过的、解决过的问题,连同排查思路和命令一起整理出来。内容包括:环境准备阶段的检查项、kubeadm init失败的典型错误、CNI网络插件接入的坑、工作节点加入时的异常排查、以及一张可以直接当手册用的问题速查表。
如果你正准备搭一套K8s环境,或者已经装到一半卡住了,这篇文章应该能帮你省下不少时间。即使你用的是托管型K8s服务,了解底层安装的这些坑,对排查业务侧的问题也有帮助。
1.2 我使用的环境与版本
先说清楚我的安装环境,方便你比对。集群是3节点的,操作系统是CentOS 7.9,内核版本没刻意升级,就用的系统默认内核。K8s版本用的是1.28.x,容器运行时用的containerd 1.7.x,安装工具kubeadm、kubelet、kubectl三个二进制版本保持一致。
之所以新装环境不再用Docker作为CRI,是因为从K8s 1.24开始,Dockershim已经被正式移除。如果你还要用Docker,就得靠cri-dockerd这个适配器来桥接。个人建议新环境直接上containerd,省心不少。containerd的cgroup驱动配置如果和kubelet对不上,会出现节点一直NotReady的诡异问题,后面我会专门展开讲。
2. 环境准备阶段的关键检查
2.1 内核参数、模块与系统资源的核对清单
很多人装K8s失败,第一道坎就卡在环境检查上。kubeadm init在正式干活之前,会自检一大堆前置条件,任何一项不满足都会直接报错退出。所以第一步,先把系统层面该配的配齐。
先说内核模块。K8s依赖iptables对流量做转发和过滤,而iptables要正确处理桥接流量,需要加载br_netfilter模块。检查方法:
lsmod | grep br_netfilter如果没有输出,就用modprobe加载,同时写进开机自动加载的配置:
modprobe br_netfilter echo "br_netfilter" >> /etc/modules-load.d/k8s.conf接下来是内核参数。需要在/etc/sysctl.d/目录下放一个配置文件,里面至少包含这几项:
cat <<EOF > /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 sysctl --systemnet.ipv4.ip_forward这个参数尤其容易漏。K8s集群里Pod与Pod之间、Pod与外部网络之间的通信都需要经过节点转发,ip_forward不开,后面网络插件再正常也跑不通。所谓"内核参数没配好,后面全都白干",这句话真不是吓唬人。
swap也必须关掉。kubeadm默认要求禁用swap,因为K8s的QoS机制和内存管理依赖cgroup,开启swap会影响内存限制的语义。注意一点:光执行swapoff -a只对当前生效,重启之后swap又回来了。正确做法是把/etc/fstab里swap那一行注释掉,或者直接删掉。
主机名和/etc/hosts也值得提前处理。每个节点的hostname要全局唯一,不要出现两个节点都叫localhost的情况。主节点要能通过主机名解析到其他节点,建议把集群内所有节点的IP和主机名映射都写进/etc/hosts。否则后面kubelet注册节点、etcd组集群的时候,可能因为解析不出节点名而各种报错。
资源方面,我踩过内存不足的坑。当初在一个只有2GB内存的机器上跑kubeadm init,控制平面组件全部PENDING,查看日志发现kube-apiserver因为OOM反复重启。后来官方文档也确认了,控制平面节点建议至少2核CPU、2GB内存。单节点实验环境最少给到4GB内存,否则跑起来也是卡到怀疑人生。
2.2 containerd配置:cgroup驱动与pause镜像
容器运行时这边我用的是containerd。直接yum装好之后,不要急着跳过配置,有两处必须改。
第一处是cgroup驱动。kubelet默认使用systemd作为cgroup驱动,而containerd默认配置的cgroup驱动是cgroupfs。如果两边驱动不一致,kubelet启动后会报类似"failed to run kubelet"的错误,或者节点状态一直是NotReady,Kubelet日志里会明确提示"cgroup driver: cgroupfs"与预期不符。
修改方法,编辑/etc/containerd/config.toml,找到SystemdCgroup参数,默认是false,改成true。如果你手里没有这份配置文件,先通过containerd config default > /etc/containerd/config.toml生成一份默认配置再改。
第二处是pause镜像地址。containerd默认的sandbox_image指向的是k8s.gcr.io/pause:3.x,这个地址在国内网络环境下基本拉不动,更别说内网隔离环境了。我之前就卡在这,kubelet一直在反复创建sandbox容器,但pause镜像始终拉不下来,节点看着是Ready,Pod全被卡在ContainerCreating。
解决思路是把sandbox_image换成可以访问的镜像源。改完之后记得重启containerd。我一般是这么操作:
systemctl restart containerd然后验证一下配置是否生效:
crictl info | grep sandbox总之,containerd的配置要像检查系统参数一样,装完就改,别等出了问题才回头找原因。这里还有一个容易被忽略的点,containerd的registry.mirrors配置,也就是镜像加速配置,虽然平时看起来不影响集群初始化,但后面部署业务镜像时会深刻影响体验,建议提前配好。
3. kubeadm init失败的典型错误与解决
3.1 预先校验:kubeadm init之前的自检项
kubeadm init的输出信息本质上是很友好的,它会先跑一遍preflight checks,把所有不满足的条件一次性列出来。有些人看到一堆带[ERROR]前缀的输出就直接截图问人,其实它们每条都写得很直白。
我常用的做法是先在每个节点上跑一次:
kubeadm init --dry-run或者直接执行kubeadm init,等它自己报错,然后把[ERROR]开头的行收集起来逐条处理。这些错误常见的有几类。
比如[ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables],说明2.1节的内核参数没有设置;[ERROR Swap]说明swap没关。这种错误,按照提示改配置就行。真正麻烦的是那种通过自检但初始化仍然失败的场景,比如apiserver容器起不来。
3.2 初始化失败的几种典型场景
场景一是apiserver容器反复重启。查看容器状态时发现kube-apiserver一直在重启,日志里报listen tcp的问题。这个多半是--apiserver-advertise-address参数配置不对。如果你机器上有多个IP,kubeadm会默认使用第一个内网IP,但有时候那个IP不可达,导致apiserver监听的地址不对。解决办法很直接,init的时候显式指定:
kubeadm init --apiserver-advertise-address=192.168.1.10 ...场景二是etcd集群建立失败。这在多节点控制平面场景比较常见。etcd之间会通过节点hostname互相发现,如果/etc/hosts里没有各节点的主机名解析,或者hostname重复,etcd日志里会出现一堆peer连接失败的记录。解决路径是把集群控制平面节点的IP、主机名解析关系老老实实写进hosts。
场景三是containerd socket找不到。kubeadm init报[ERROR CRI]: container runtime is not running。这种情况先排查:
systemctl status containerd crictl info如果containerd确实在跑,但kubeadm还是找不到,检查一下kubelet的配置,看看--container-runtime-endpoint是不是被指向了错误的socket路径。新版本默认是unix:///run/containerd/containerd.sock,如果之前机器上装过Docker,可能残留配置指向了docker.sock,就容易出现这种错位。
3.3 镜像拉取失败的解决方案
kubeadm init过程中要拉取kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、pause、etcd、coredns这7类镜像。任何一个拉不到,初始化就直接失败。
解决的思路无非两种:一是让kubeadm从能访问的镜像仓库拉取镜像,二是提前把镜像拉好,再用kubeadm init --image-repository指定本地或私有仓库。
先看当前需要的镜像列表:
kubeadm config images list如果网络不通,就换镜像源。比如指定某云厂商提供的K8s镜像仓库作为镜像仓库地址:
kubeadm init --image-repository=registry.xxx.io/k8s这种写法相当于把kubeadm默认的registry.k8s.io整体替换掉。如果你有内网私有仓库,只需要把镜像手动推到私有仓库里,再用同样的参数指定即可。
还有一种方式是手动拉镜像再导入。适合完全离线的环境,方法是在一台能联网的机器上,把kubeadm需要的镜像push到私有仓库,然后在离线节点上用crictl pull或者直接让kubeadm从私有仓库拉取。
这里请注意一点:如果你只是需要在测试环境快速跑起来,优先用防火墙与加速器方案而不是手动装载。防抖的原则是在网络层面解决镜像源不可达的问题,少跟镜像耦合死磕。
4. CNI网络插件的接入与故障定位
4.1 选Flannel还是Calico
集群初始化完成之后,还有个关键步骤就是装CNI网络插件。这个不装,节点状态永远都是NotReady。CNI插件负责给每个Pod分配IP、维护节点间的网络连通。
很多新手觉得CNI只是一个可选组件,其实它和K8s集群的可用性强相关。我最初装Flannel用了几年,后来切到Calico又踩了不少坑,这两个插件我都实际部署过,说下选型感受。
Flannel的优点是简单。它用VXLAN或者host-gw模式在节点间建立隧道,部署起来就是一条kubectl apply命令的事,资源占用也低。缺点是它不带网络策略功能,也不要指望它对流量做太多控制。适合小规模集群、学习环境、对网络策略要求不高的场景。
Calico的功能更丰富,支持BGP路由模式、NetworkPolicy网络策略、甚至可以把外部网络和Pod网段直接打通。它适合中大规模集群和生产环境,但配置项多,排障门槛也高。BGP模式下,对节点的网络环境有要求,跨数据中心互通时要额外配置。
我的建议是:先想清楚你的集群要解决什么问题,再选插件。不要一上来就追求Calico的功能全开,那只会给自己加难度。
4.2 Flannel安装后的Pod不通问题
Flannel配置里最关键的参数是--pod-network-cidr。如果你在kubeadm init时指定的是--pod-network-cidr=10.244.0.0/16,那Flannel的部署文件里也必须是这个网段。
我第一次装Flannel就是栽在这。init时随手写了个10.0.0.0/16作为Pod网段,然后直接拿官方默认的kube-flannel.yml去apply,结果Pod之间ping不通。Flannel分配IP时,会默认使用10.244.0.0/16这个网段去规划子网,和kube-controller-manager分配给节点的PodCIDR完全对不上,网络自然就废了。
如果遇到了,先检查节点分配的PodCIDR:
kubectl get node -o jsonpath='{.items[*].spec.podCIDR}'再对照一下Flannel daemonset的环境变量,看网段是否一致。不一致就改,改完重新apply。
另一个Flannel常见问题是多网卡环境下选错网卡。Flannel默认会用默认路由对应的网卡来建立VXLAN隧道,但如果你机器上有多个网卡,比如内网网卡和外网网卡并存,Flannel可能选到外网网卡,导致隧道建立不起来。解决办法是在kube-flannel.yml的args里增加指定网卡的参数,比如--iface=eth1。
4.3 Calico部署后的地址冲突
Calico安装本身通常很顺利,但有个问题容易掉坑——默认的Pod网段。Calico默认使用192.168.0.0/16作为Pod网段,这个网段和企业内网的地址冲突率相当高。
如果你的局域网恰好把192.168.x.x段占用了,Calico会给Pod分配一个跟物理网络冲突的IP,导致Pod访问外部IP时路由错乱,表现为Pod能启动但网络不通。
解决办法是在安装Calico之前,先想清楚你的Pod网段规划。Calico部署文件里有一个CALICO_IPV4POOL_CIDR环境变量,改成你自己的规划网段,比如10.20.0.0/16,然后再安装。同时保证kubeadm init时的--pod-network-cidr和这个网段保持一致。
Calico还有个selector特性,只对匹配了特定标签的Pod生效。默认的IPPool配置通常覆盖全部Pod。如果你做了自定义的IPPool,注意每个节点分配的PodCIDR要落在池子里,否则Pod创建后IP分配不出,状态会一直Pending。
5. 工作节点的加入与控制平面异常
5.1 token过期与join命令失效
kubeadm init结束之后,会输出一段kubeadm join命令,里面有token和证书hash。很多人喜欢先把这条命令存下来,结果隔了一天再去加节点,发现token已经过期了。
token默认有效期是24小时。加节点时报错ERROR TokenExpired,解决办法是重新生成token:
kubeadm token create --print-join-command这条命令会输出一个新的join命令,直接复制到工作节点上执行即可。如果加了--ttl=0,token永远不会过期,适合懒人,但不建议在生产这么干。
还有种情况是join命令报证书hash不匹配。仔细看,kubeadm init输出的是--discovery-token-ca-cert-hash sha256:xxx,如果你把命令复制错了,或者中间证书轮转过,就会报错。再次执行:
kubeadm token create --print-join-command把整条命令完整复制下来,包括后面的hash参数,基本能解决。
5.2 节点加入后一直NotReady
节点执行完join命令后,在控制平面节点上看,新节点的状态很可能长时间是NotReady。这种情况先别急着骂kubelet,按照从近到远的思路排查。
先在控制平面节点上查看:
kubectl get nodes kubectl describe node <节点名>describe的输出里有Conditions列表,会说明具体卡在哪。最常见的是Ready Unknown,一般意味着kubelet上报心跳有问题。
下一步进到工作节点上看kubelet状态:
systemctl status kubelet journalctl -u kubelet -f日志是最直接的证据。我发现几个高频原因:
一是镜像拉不动。kubelet启动后需要拉pause容器,如果你的containerd没配置好镜像源,kubelet日志里会一直刷Failed to pull image。解决方式参照2.2节。
二是节点证书没有被批准。Kubelet启动时会生成一个证书签名请求,如果没有在控制平面节点上批准,节点永远无法注册成功。查看方法:
kubectl get csr看到Pending状态的证书请求,就用:
kubectl certificate approve <csr名称>实际操作时,如果节点多,可以用一条命令批量批准所有Pending的请求。
三是PLEG问题。kubelet日志里出现PLEG is not healthy字样,多半是containerd响应卡顿,或者容器列表同步不过来了。一般先重启containerd再重启kubelet能暂时缓解。如果反复出现,需要检查节点负载和系统资源。
5.3 控制平面组件的守护与恢复
控制平面的几个Pod(kube-apiserver、kube-controller-manager、kube-scheduler)都是跑在static pod里的,也就是说它们本质上是kubelet直接管理的容器。这些组件起不来的话,整个集群都处于亚健康状态。
排查apiserver异常,我是这么做的:进入控制平面节点,先看容器状态:
crictl ps -a | grep apiserver没有输出说明kubelet没把它拉起来。看kubelet日志。如果容器处于Exited状态,就把容器日志抓出来看:
crictl logs <容器ID>常见问题是证书路径不对、etcd连接失败、或者apiserver的advertise地址不通。apiserver起不来时,集群所有API请求都会失败,kubectl get nodes会直接报连接被拒绝。
有一种情况容易忽略:控制平面组件占用的端口被占用。比如主机上本来跑了别的服务占用了6443端口,apiserver就会起不来。排查的时候顺手看一下:
ss -lntp | grep 6443如果确认被占用,要么停掉占用的服务,要么修改apiserver的监听端口。
6. 安装问题速查表与排障心得
6.1 高频问题速查表
日常排障多了,我习惯把问题整理成一张对照表,用的时候直接查,效率高很多。下面这表是我在多次安装中总结的,覆盖了大部分安装阶段的常见问题。
| 现象 | 排查命令 | 常见根因 | 解决方案 |
|---|---|---|---|
| kubeadm init自检报swap错误 | free -h | 未关闭swap | swapoff -a,注释fstab条目 |
| init自检报iptables错误 | sysctl net.bridge.bridge-nf-call-iptables | 内核模块未加载 | modprobe br_netfilter,设置内核参数 |
| 初始化卡在拉镜像 | kubeadm config images list | 镜像源不可达 | 配置镜像仓库或私有仓库 |
| 节点加入后NotReady | kubectl describe node | 网络插件未安装 | 安装CNI插件 |
| 节点NotReady且日志有PLEG | journalctl -u kubelet | containerd异常或负载高 | 重启containerd、kubelet,检查资源 |
| Pod一直ContainerCreating | kubectl get pods -A | pause镜像拉取失败 | 修改sandbox_image,重启containerd |
| join报token过期 | kubeadm token create --print-join-command | token超过24h | 重新生成token |
| apiserver反复重启 | crictl logs apiserver容器 | advertise地址不对或端口占用 | 显式指定IP,释放端口 |
| etcd起不来 | crictl logs etcd容器 | hostname无法解析 | 修复/etc/hosts解析 |
| kubelet报cgroup驱动不一致 | journalctl -u kubelet | containerd和kubelet驱动不一致 | 修改SystemdCgroup=true |
这张表不完整,但覆盖面已经足够广。遇到新问题时,基本上都是"容器起没起来,日志怎么说,配置对不对"这三个维度去查。
6.2 几条拿得出手的排障习惯与避坑心得
踩了这么多坑之后,我也沉淀出了几套固定的排查习惯,现在每次装K8s都会下意识执行,确实帮我省了很多事。
第一,安装前先做"三查三试"。查内核模块加载情况、查系统参数、查swap状态。试一下containerd能否正常使用、试一下crictl能否连接、试一下镜像能否拉下来。这六件事做完,后面80%的初始化报错都不会再出现。
第二,版本绝对不要混装。kubeadm、kubelet、kubectl三个二进制文件版本必须一致,K8s版本和容器运行时版本也要兼容。有一次我kubelet和kubeadm差一个minor版本,结果init一切正常,但节点加入时kubelet直接拒绝启动,日志里报版本不匹配,查了好久才意识到问题。
第三,kubeadm reset是一个非常好的工具。每次环境搞乱了,与其反复猜测哪里没配置好,不如直接:
kubeadm reset -f rm -rf /etc/cni/net.d iptables -F && iptables -t nat -F && iptables -t mangle -F把残留配置清理干净,再重新init。干净的初始状态比任何修复技巧都可靠。注意reset之后,cni配置文件和iptables规则可能会残留,不清理干净,重装后CNI插件反而会闹脾气。这步我每次都会做。
第四,遇到问题先看日志再提问,不要盲改配置。很多K8s问题的表象是一样的,但根因千差万别。比如NotReady,有可能是网络插件没装、镜像拉不动、证书没批准、cgroup驱动不一致、资源不足五类原因。不打开journalctl看kubelet的日志,光靠猜是做无用功。
第五,初始化的参数一定要留档。--pod-network-cidr、--service-cidr、--apiserver-advertise-address这些参数,我每次都会写进部署文档里。因为以后加节点、换网络插件、排查路由问题时,都要回来参考这些参数。别等到出问题时才发现自己忘了当初填的是什么网段。
最后再分享一个小技巧:多节点环境里,如果控制平面节点和工作节点都装了防火墙,尽量先关掉或者把K8s需要的端口加到白名单。我以前在一台防火墙默认拒绝的环境里装集群,节点状态反反复复,最后才发现是防火墙把kubelet和apiserver通信的端口拦了。像6443、2379-2380、10250、10251、10252、30000-32767这些端口,都得确保放通。与其在排障时一层层剥洋葱,不如一开始就把网络层面的通路准备好。
K8s安装这件事,本质上不复杂,但涉及的系统层面细节很多,很容易被一个不起眼的参数绊住。希望这篇笔记能让你少走一些我走过的弯路。如果你正卡在某个具体的报错上,照着上面的思路一路查下来,大部分问题都能定位到根因。