接到一个挺典型的任务机房里的银河麒麟V10服务器网络是物理隔离的完全没有外网要在这一批机器上把Kubernetes 1.32.11集群搭起来后面还有应用要往上部署。这种场景在内网交付里太常见了在线安装时一条kubeadm init就能自动完成的拉依赖、拉镜像到了离线环境全得自己动手。从前期的物料打包到系统侧的基础配置再到镜像导入、节点加入任何一环漏了都要花成倍的时间去排查。这篇文章就以k8s 1.32.11 银河麒麟高级服务器操作系统V10为例把离线部署的完整链路过一遍。内容包括打包机的准备、RPM和镜像的离线物料收集、麒麟V10系统侧的基础配置、kubeadm init的细节、worker节点加入、Calico网络插件部署以及我在实际部署中踩过的一堆坑。适合正在做内网交付、信创环境落地、或者准备在无外网机房搭k8s的运维和实施工程师参考。不同基础的人都能从中找到对应自己阶段的内容——如果纯新手按章节顺序一步步来就行如果有一定经验可以直接跳到第6章看排错部分。1. 把离线安装想清楚你就成功了一半1.1 离线装k8s到底难在哪在线安装的时候kubeadm init会替你完成几乎所有脏活从官方仓库拉取kube-apiserver、kube-controller-manager、etcd、pause等镜像从系统源里解决rpm依赖。离线环境等于把所有在线的便利全部取消你需要在另一台有网的机器上提前把整个集群需要的所有东西搬到一个U盘或者移动硬盘里再进内网。拆解下来离线部署要做的事情其实就三件rpm依赖树kubelet、kubeadm、kubectl这些组件的rpm包以及container runtime我们用的是containerd的rpm包和它们的依赖包。容器镜像k8s核心组件镜像、pause镜像、etcd镜像、coredns镜像还有网络插件Calico的镜像。这些必须预先导出成tar文件再到内网节点上导入到containerd里。系统参数就位内核模块、sysctl参数、swap关闭、时间同步、SELinux/防火墙策略。这一步在离线环境和在线环境没什么区别但在内网环境里更容易被忽略尤其内核模块缺失这种事真的能卡一整天。很多人觉得离线就是没有网只要把包拷进去就完了。实际上离线安装最考验的是完整性问题——你拷进去的包必须是完整的依赖树和镜像集合少一个都装不起来。所以先想清楚这三件事再动手后面就会顺很多。1.2 为什么选k8s 1.32.11 麒麟V10这个组合银河麒麟高级服务器操作系统V10底层兼容CentOS 8/RHEL 8的用户态和包管理生态所以大多数面向RHEL 8系的rpm包都能直接安装使用这给离线部署提供了很大的便利。k8s官方仓库提供的kubelet、kubeadm、kubectl的rpm包就分为el7和el8两套麒麟V10走el8这条线完全没有问题。k8s 1.32.x这个版本containerd早就成了唯一的CRI运行时kubeadm在初始化时已经不再为Docker单独做适配。你在配置层面只需要跟containerd打交道系统里装一个containerd然后让kubelet和kubeadm走unix:///run/containerd/containerd.sock就行。相比早期版本一堆runtime选择1.32这条线的注意力更集中。另外kernel版本方面麒麟V10的内核通常是4.19或5.10满足k8s对Linux内核的最低要求。真正要留意的是内核模块比如br_netfilter、nf_conntrack、ip_vs这些后续会专门讲到。建议不要在昂贵的CPU和内存配置上纠结先确保操作系统版本和rpm镜像架构一致这是最前面的一道坎。1.3 整体方案选型本地镜像导入 还是 内网Harbor离线环境下容器镜像的分发方式有两种常见选择。方案适用场景优点缺点每个节点本地ctr导入tar集群3~5台节点规模不大架构简单不需要额外部署镜像仓库服务节点多时每台都要导一遍操作重复内网部署Harbor集群规模较大后续频繁发布新镜像镜像集中管理新增节点不用重新导镜像接近半在线体验需要额外一台机器跑Harbor并要把所有镜像push到Harbor如果只是先把k8s集群跑起来我建议先走第一种。等集群稳定了、需要频繁发布业务镜像时再考虑架Harbor。这样能把搭集群和搭镜像仓库两件事解耦排查问题时的复杂度会低很多。本文接下来的流程也是按本地镜像导入这个方案来写的。2. 在有网机器上打包离线物料yum源、rpm、镜像三件套这一章非常关键基本决定了离线安装能不能顺利走通。打包机器的选择、rpm的下载方式、镜像的tag命名都直接关系到最后内网里能不能装成功。2.1 打包机准备架构和系统版本必须提前对齐打包机的作用是用一台能上外网的机器把rpm包和容器镜像下载整理好。这里面最容易被忽略的是架构一致性如果服务器是x86_64那打包机、所有rpm包、所有容器镜像都必须是amd64的。如果服务器是ARM鲲鹏、飞腾等常见国产CPU那打包机也必须是ARM架构rpm包要重新下载ARM版本的容器镜像要拉arm64的版本。操作系统层面建议打包机同样选一个RHEL 8系的系统CentOS 8 Stream、RockyLinux 8、或者麒麟V10都行。这样用yumdownloader --resolve解析出来的依赖树和内网麒麟V10的依赖树最贴近不容易出现这个包在打包机上能解析出来到了内网却装不上的情况。我踩过一次很深的坑在CentOS 7的机器上打包k8s相关rpm结果带出来一堆el7的依赖包到了麒麟V10里怎么都装不进去最后只能重新在RHEL 8系机器上再打一份。所以打包机的系统版本一定要模拟目标内网环境别小看这一步。2.2 下载kubeadm、kubelet、kubectl的rpm包首先在打包机上配置k8s的yum源。国内网络环境下优先使用阿里云镜像源cat /etc/yum.repos.d/kubernetes.repo EOF [kubernetes] nameKubernetes baseurlhttps://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el8-x86_64/ enabled1 gpgcheck0 repo_gpgcheck0 EOF注意baseurl里的el8和x86_64要根据你的打包机架构调整ARM机器对应的是kubernetes-el8-aarch64。然后安装yumdownloader工具下载kubeadm、kubelet、kubectl三个rpm包及其所有依赖yum install -y yum-utils mkdir -p /opt/k8s-offline/rpms cd /opt/k8s-offline/rpms yumdownloader --resolve kubeadm-1.32.11 kubelet-1.32.11 kubectl-1.32.11如果你希望之后能精确控制版本建议直接写死版本号比如kubeadm-1.32.11-0。这样打包出来的rpm目录相对干净不会因为yum源里出现更新版而拉到意外的东西。另外一同下载createrepo工具自身的rpm包后面内网建yum源时要用yumdownloader --resolve createrepo_c2.3 containerd及运行时的依赖包现在的k8s集群基本都用containerd可以从docker-ce源里拿containerd.io这个rpm包。在打包机上配置docker-ce源cat /etc/yum.repos.d/docker-ce.repo EOF [docker-ce-stable] nameDocker CE Stable baseurlhttps://mirrors.aliyun.com/docker-ce/linux/centos/8/x86_64/stable/ enabled1 gpgcheck0 EOF然后下载cd /opt/k8s-offline/rpms yumdownloader --resolve containerd.iocontainerd.io这个包一般会把runc、libseccomp这些核心依赖同时带出来--resolve会自动把它们下载到同一个目录。这样内网机器只要把这里所有rpm包都装上containerd就能跑起来了。我这里没有强制指定containerd的具体版本号建议你下载时看一下这个包当前解析出来的版本通常1.7.x或2.0.x都没问题。但要注意后续kubeadm init时containerd的CRI接口必须正常工作如果你下载的是非常老的版本有概率出现CRI兼容问题。2.4 用国内镜像仓库准备离线镜像首先在一台能访问外网的机器上根据k8s版本号生成镜像清单kubeadm config images list --kubernetes-versionv1.32.111.32.11这个版本对应的核心镜像大致包括镜像说明registry.k8s.io/kube-apiserver:v1.32.11API Serverregistry.k8s.io/kube-controller-manager:v1.32.11Controller Managerregistry.k8s.io/kube-scheduler:v1.32.11Schedulerregistry.k8s.io/kube-proxy:v1.32.11Proxy每个节点都需要registry.k8s.io/pause:3.10每个Pod的基础容器registry.k8s.io/etcd:3.5.16etcd存储registry.k8s.io/coredns/coredns:v1.12.1集群DNS国内网络直接拉registry.k8s.io的镜像通常很慢实际工作中我用的是阿里云的镜像仓库做中转。可以把镜像拉下来后重新打tag再导出成tar包docker pull registry.aliyuncs.com/google_containers/kube-apiserver:v1.32.11 docker tag registry.aliyuncs.com/google_containers/kube-apiserver:v1.32.11 registry.k8s.io/kube-apiserver:v1.32.11核心组件以外的pause、etcd、coredns同样在registry.aliyuncs.com/google_containers下能找到路径后缀与官方完全一致只是前缀不一样。把所有镜像pull完、tag完再统一导出docker save -o /opt/k8s-offline/images/k8s-images.tar \ registry.k8s.io/kube-apiserver:v1.32.11 \ registry.k8s.io/kube-controller-manager:v1.32.11 \ ...这里有一个非常容易踩的坑docker save保存的是你在本地看到的镜像名。如果你没有先把tag改成registry.k8s.io/...就直接save导入到内网后镜像名会带着registry.aliyuncs.com/google_containers前缀。之后kubeadm init去找registry.k8s.io/kube-apiserver:v1.32.11时会直接报镜像找不到。所以一定要在打包机上把tag改好或者在内网导入后再用ctr images tag补一次。这个细节后面第6章还会展开讲。2.5 内网yum源的最小可用方案rpm包下载好之后到了内网有两种装法。第一种机器少就可以纯rpm -Uvh一把梭cd /opt/k8s-offline/rpms rpm -Uvh *.rpm这种方式的缺点是不能解决依赖顺序问题如果rpm包之间有依赖关系比如先装libseccomp再装containerd直接*.rpm批量装有可能失败。好在yumdownloader --resolve已经把依赖包都下全了rpm -Uvh *.rpm通常能一次成功。万一碰到xxx is needed by xxx的报错就手动先装被依赖的包。第二种生产环境我更推荐在某个内网节点上建一个本地yum源。把整个rpms目录拷到内网服务器上用createrepo生成元数据createrepo /opt/k8s-offline/rpms然后用python一句话起个http服务cd /opt/k8s-offline python3 -m http.server 8080其他节点只需要配置一个repo文件指向这台机器就能像在线环境一样yum install了。后续扩容worker节点时不用再拷贝一堆rpm包直接yum源装包体验好很多。3. 麒麟V10系统侧准备先让containerd健康再谈kubelet系统侧准备是离线安装中最容易被跳过的部分。很多人在内网里清了包、导了镜像然后kubeadm init结果kubelet起不来或者一直NotReady最后排查一圈发现是内核模块或者swap没关。这部分步骤比较繁琐但绝对省不掉。3.1 主机规划、时间同步、关闭swap先把每台机器的主机名和IP规划好写入/etc/hosts。kubeadm在初始化时会对hostname做解析如果解析不到会直接宕掉预检。hostnamectl set-hostname k8s-master01 cat /etc/hosts EOF 192.168.10.11 k8s-master01 192.168.10.12 k8s-node01 192.168.10.13 k8s-node02 EOF时间同步方面离线环境访问不了外网NTP服务器所以集群内至少要有一台机器能作为内网时间源或者所有节点在部署时确保手动校正时间一致。k8s的证书机制对时间比较敏感节点间时间差太大会出现各种诡异的认证失败。关闭swap是kubeadm预检的硬性要求swapoff -a sed -i / swap / s/^/#/ /etc/fstab第2行的注释是为了防止重启后swap又自动挂载回来。如果机器内存确实紧张kubeadm也允许加--ignore-preflight-errorsSwap跳过检查但生产环境不建议长期带swap跑关掉是最稳的。3.2 内核模块和sysctl参数离线下最容易卡住的地方k8s集群正常工作依赖几个内核模块典型的是br_netfilter、nf_conntrack以及用了IPVS模式时需要的ip_vs系列模块。IPVS这块尤其容易出事因为默认内核并没有都自动加载。创建模块加载配置cat /etc/modules-load.d/k8s.conf EOF br_netfilter nf_conntrack ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh EOF modprobe br_netfilter modprobe nf_conntrack modprobe ip_vs然后配置sysctl参数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 vm.swappiness 0 EOF sysctl --system注意br_netfilter如果没加载即使你写了net.bridge.bridge-nf-call-iptables1也会在sysctl --system时报错或者参数根本不生效。可以先检查一下lsmod | grep br_netfilter如果模块加载失败先看内核有没有这个模块文件。麒麟V10上我遇到过内核模块存在但modprobe失败的情况通常是模块依赖没装上检查/lib/modules/$(uname -r)下相关文件是否存在必要时重启机器让模块配置重新加载。3.3 安装containerd并校准Cgroup配置进入内网后安装之前打包好的containerd相关rpmcd /opt/k8s-offline/rpms rpm -Uvh *.rpm然后启动并设置开机自启systemctl enable containerd --nowcontainerd包的默认配置不一定会自动生成建议手动生成一份完整配置mkdir -p /etc/containerd containerd config default /etc/containerd/config.toml这里必须改一个关键参数SystemdCgroup。找到plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options这段把SystemdCgroup改为true。为什么要改这个因为kubelet默认使用systemd作为cgroup driver如果containerd这边还是cgroupfs两边会不一致最终kubelet启动后直接报cgroup相关的错误Pod无法正常运行。这个参数不做对齐后面必然要返工。配置完成后重启containerdsystemctl restart containerd顺便配置crictl让它能和containerd通信cat /etc/crictl.yaml EOF runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF验证containerd已经健康crictl version crictl info如果crictl info正常输出说明容器运行时这块就绪了。在这个阶段花十分钟检查会避免后面kubeadm init卡半小时。3.4 SELinux和防火墙内网部署别让安全策略变成拦路虎麒麟V10默认的SELinux可能是enforcing也可能是permissive。kubelet和容器运行时在文件访问上经常会跟SELinux策略冲突最典型的症状是Pod拉起失败、挂载权限被拒。内网部署时我一般直接设为permissivesetenforce 0 sed -i s/^SELINUXenforcing/SELINUXpermissive/ /etc/selinux/config防火墙方面如果是标准的等保内网我建议放行以下端口而不是直接关firewalld。这样做安全策略也能交代过去端口用途6443kube-apiserver2379-2380etcd客户端通信10250kubelet10259kube-scheduler10257kube-controller-manager30000-32767NodePort服务端口段179Calico BGP端口执行放行后最好用nc -vz或telnet验证一下节点间端口是否真的通了。离线环境里没有外网干扰但节点与节点之间的网络却是最容易出问题的环节。4. kubeadm init镜像导入和初始化参数的一次性对齐系统侧准备完毕接下来的重点是把镜像导入到每台机器然后用kubeadm init把控制平面初始化出来。这一步的核心是镜像名称必须和kubeadm期望的名称完全一致。4.1 把离线镜像包导入到k8s.io命名空间kubeadm通过containerd的CRI接口检查镜像是否存在而containerd里和k8s相关的镜像都放在k8s.io这个命名空间下。所以导入时必须指定-nk8s.ioctr -nk8s.io images import /opt/k8s-offline/images/k8s-images.tar导入完成后检查一下镜像列表ctr -nk8s.io images list | grep -E v1.32.11|pause|etcd|coredns如果发现镜像名的前缀不对比如还是registry.aliyuncs.com/google_containers/...用ctr images tag修正ctr -nk8s.io images tag registry.aliyuncs.com/google_containers/kube-apiserver:v1.32.11 registry.k8s.io/kube-apiserver:v1.32.11pause镜像必须存在于容器运行时的镜像列表里因为在每个Pod创建时kubelet都会去拉pause镜像。如果你的containerd配置里sandbox_image指向的还是registry.k8s.io/pause:3.10那导入的pause也必须恰好是这个tag一个字符都不能差。这里也可以顺手验证crictl视角下的镜像列表crictl imagescrictl默认连的是containerd的k8s.io命名空间所以它看到的就是kubelet能看到的内容。4.2 编写kubeadm配置文件直接在命令行里挂一大堆参数虽然也能init但可维护性太差。更推荐的方式是写一份配置文件然后kubeadm init --config。k8s 1.32.x用的API版本是kubeadm.k8s.io/v1beta4如果你拿到的kubeadm版本比较旧只认v1beta3把版本号改回去即可apiVersion: kubeadm.k8s.io/v1beta4 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.11 bindPort: 6443 nodeRegistration: criSocket: unix:///run/containerd/containerd.sock --- apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration kubernetesVersion: v1.32.11 imageRepository: registry.k8s.io networking: podSubnet: 172.16.0.0/16 serviceSubnet: 10.96.0.0/12这里几个关键点说明一下advertiseAddress填的是本机内网IPk8s组件将在这个IP上监听后续worker节点也要通过这个IP访问apiserver。imageRepository默认是registry.k8s.io如果你的镜像都已经是这个前缀保持默认即可如果内网镜像tag用了别的仓库名前缀这里就要改成对应的值。podSubnet建议不要用192.168.0.0/16除非你确定内网网段不冲突。很多内网环境本身就用192.168网段Calico的默认网段撞车之后Pod之间怎么都不通。这里用172.16.0.0/16避开主网段是个比较稳妥的选择。4.3 执行init并按里程碑排查配置写好后执行kubeadm init --configkubeadm-config.yaml正常的情况下你会看到kubeadm依次做这些事情检查系统预检条件。从containerd中检查镜像是否存在。启动kubelet。初始化控制平面组件启动apiserver、etcd、controller-manager、scheduler。安装coredns和kube-proxy。最后输出的kubeadm join命令要保存下来后面worker节点加入集群时要用。如果没保存也没关系控制平面节点上随时可以用kubeadm token create --print-join-command重新生成。如果init卡在某个环节优先做两件事journalctl -u kubelet -f另一件事是查看kubelet是否真的连接上了containerdcrictl ps -a最常见的失败原因就两个镜像缺失kubeadm init报image not found或者kubelet和containerd之间的cgroup driver不匹配。排查完修复后用kubeadm reset清理现场kubeadm reset -f rm -rf /etc/kubernetes/manifests /var/lib/kubelet再重新执行init就可以。kubeadm reset只处理k8s自己的配置不会动容器镜像和rpm包所以离线环境里反复init的成本并不高。5. join节点和网络插件离线集群最后的两步棋控制平面起来了接下来就是让worker节点加入集群然后装好网络插件让Pod之间真正能通信。5.1 worker节点加入离线环境下怎么join最稳worker节点不需要init只需要把系统侧准备第3章完整做一遍包括containerd、内核模块、sysctl、rpm包。然后把镜像tar包同样导入到worker节点的k8s.io命名空间因为kube-proxy、pause、calico-node这些组件也会在worker节点上跑。如果之前保存过kubeadm join命令直接在worker节点执行即可kubeadm join 192.168.10.11:6443 --token xxxx --discovery-token-ca-cert-hash sha256:xxxx如果token过期或者ca-cert-hash字符串找不到了这是离线项目里特别容易发生的事因为你可能隔了几天甚至几周才去扩容节点在master节点上执行kubeadm token create --print-join-command这会打印一条全新的带token和hash的join命令直接复制到worker上跑就行。离线环境还有一种更不容易出错的join方式使用--discovery-file直接指定ca.crt文件。先把master节点上的/etc/kubernetes/pki/ca.crt拷贝到worker节点然后kubeadm join 192.168.10.11:6443 --token xxxx --discovery-file /etc/kubernetes/ca.crt这种方式绕过了Hash计算和校验对离线环境更友好。不过要注意token仍然会过期文件方式只解决了ca证书发现的问题。批量加入大量worker时建议提前把token的TTL设长一点或者接受每次扩容都重新生成一次join命令。5.2 控制平面高可用如果不止一台master单master的集群跑测试没问题但生产环境通常要三台控制平面节点。离线环境下配置HA稍微多几步但整体也不复杂。主master init时配置里加上controlPlaneEndpoint比如controlPlaneEndpoint: 192.168.10.100:6443这个地址可以是内网负载均衡比如keepalived VIP也可以是域名。后面第二台、第三台master加入时用kubeadm join 192.168.10.100:6443 --token xxxx --discovery-token-ca-cert-hash sha256:xxxx --control-plane注意--control-plane参数它表示这个节点要作为控制平面节点加入kubeadm会自动把etcd、apiserver等control plane组件在这个节点上拉起。控制平面节点之间的证书分发kubeadm通过--certificate-key来协商如果这个参数不方便管理也可以直接把master1上的/etc/kubernetes/pki整个目录拷贝到其他master节点更直接。5.3 安装Calico网络插件镜像tag、网段、IPIP模式网络插件我以Calico为例它在离线环境里的部署足够经典。先在有网机器上准备Calico的镜像。以Calico v3.29为例通常需要三个镜像docker pull docker.io/calico/cni:v3.29.0 docker pull docker.io/calico/node:v3.29.0 docker pull docker.io/calico/kube-controllers:v3.29.0 docker save -o calico-images.tar docker.io/calico/cni:v3.29.0 docker.io/calico/node:v3.29.0 docker.io/calico/kube-controllers:v3.29.0这里保持docker.io/calico/...前缀即可不需要刻意改成registry.k8s.io前缀。导入到每个节点ctr -nk8s.io images import calico-images.tar然后下载Calico的部署清单有网机器上可以curl拿官方给的calico.yaml内网机器也能在打包时一起带入。部署前改两个地方修改Pod网段把CALICO_IPV4POOL_CIDR改成和kubeadm init里的podSubnet一致例如172.16.0.0/16。确认网络模式内网环境通常用IPIP或VXLAN模式取决于你的网络架构。如果所有节点在同一个二层网络IPIP模式就够了如果跨VPC、跨SDN网络用VXLAN更稳。Calico默认的IPIP模式在大多数内网环境都能跑通。然后应用清单kubectl apply -f calico.yaml等待Calico全部Runningkubectl get pods -n calico-system -w当calico-node都变成Runningcoredns也不再是Pending之后整个离线集群的网络就算通了。6. 离线安装排错实录这些坑我都替你踩过了写完流程惯例要聊聊排错。离线环境的排错和在线环境有个最大的不同你没有去网上搜一条现成的解决方法的便利很多问题必须自己根据日志和现象倒推。以下几个坑是按实际踩坑频率排序的可以说每一个都价值半天工时。6.1 坑一kubelet起不来cgroup driver不一致症状kubeadm init卡在[kubelet-start] Waiting for the kubelet to startjournalctl -u kubelet里刷错常见内容包含failed to run Kubelet: failed to get cgroup stats或者cgroup driver: cgroupfs is different from docker之类的提示。原因containerd的SystemdCgroup没有改成true。kubelet这边用的是systemdcontainerd那边还是cgroupfs两边管理cgroup的机制对不上。处理把/etc/containerd/config.toml里SystemdCgroup false改成true然后systemctl restart containerd再kubeadm reset重来。这个坑在麒麟V10上特别容易出现因为很多人改配置的时候只改了前半段没有找到runc相关的SystemdCgroup项。6.2 坑二镜像导入后tag不对kubeadm报镜像找不到症状kubeadm init执行到一半报某个镜像image not found比如registry.k8s.io/kube-apiserver:v1.32.11 not found。原因这是离线安装最高频的问题。打包时如果直接从阿里云镜像仓库拉取并保存没有把tag改成registry.k8s.io前缀导入后containerd里存的镜像名是registry.aliyuncs.com/google_containers/kube-apiserver:v1.32.11而kubeadm拿着registry.k8s.io/kube-apiserver:v1.32.11去找当然找不到。处理ctr -nk8s.io images list # 确认实际的镜像名前缀 ctr -nk8s.io images tag registry.aliyuncs.com/google_containers/kube-apiserver:v1.32.11 registry.k8s.io/kube-apiserver:v1.32.11如果镜像很多写个循环批量处理更快。我习惯在打包机上save之前就统一tag好这是最省事、最不会忘的做法。6.3 坑三节点NotReady大量Pod Pending症状kubectl get nodes显示master状态NotReadykubectl get pods -A里coredns一直是Pending。原因网络插件没装或者Calico尚未就绪。NotReady的节点通常意味着CNI没有成功初始化节点上的Pod因为网络不通无法调度到Ready状态。处理先看Calico节点Pod状态kubectl logs -n calico-system daemonset/calico-node常见原因包括Calico镜像没有导入到当前节点Pod卡在ImagePullBackOff。内核没有加载ipip模块Calico的IPIP模式起不来。firewalld挡住了179端口BGP peer建立失败。内网环境最常见的是镜像导入不全其次是ipip模块缺失。检查一下lsmod | grep ipip如果没有加载模块modprobe ipip6.4 坑四token过期join时提示过期或找不到症状过了一段时间再扩容节点跑之前保存的kubeadm join命令报token过期或者token not found。原因kubeadm的bootstrap token默认有效期24小时过期后之前的命令就失效了。处理这条真的不用慌重新生成就行kubeadm token create --print-join-command如果你希望token长期有效可以加--ttl0表示永不过期但建议不要在正式环境这么干安全风险不值得。token过期问题是我在离线项目里遇到频率最高的管理性小问题每次去现场扩容第一件事就是先刷新token。6.5 坑五SELinux导致的挂载或日志问题症状Pod创建失败日志里报Permission denied或者节点上的kubelet日志目录无法写入kubelet直接退出。原因麒麟V10默认SELinux可能是enforcing而k8s组件和容器运行时不总在SELinux策略覆盖的范围内。处理内网环境我建议直接setenforce 0并修改/etc/selinux/config为permissive。虽然关SELinux不是最优解但在离线环境里排查SELinux策略的成本太高permissive模式已经能保证功能正常同时还能保留SELinux的日志审计能力。6.6 坑六系统中残留的docker或旧版本containerd症状crictl info报错或者kubelet连接不上containerdsock文件路径对不上。原因有些镜像系统或者前期试验环境里装过dockerdockerd自身的containerd使用了/run/dockerd/containerd.sock或者别的路径导致你新装的containerd的/run/containerd/containerd.sock没有创建或者被旧进程占用。处理先确认当前生效的containerd进程systemctl status containerd ss -xl | grep containerd如果发现sock文件属于dockerd建议把dockerd停掉或禁用让k8s专属的containerd接管/run/containerd/containerd.sock。在交付环境里我倾向于让一台机器上只有一套容器运行时避免dockerd和containerd抢sock、抢资源、抢cgroup这种破事。最后再分享一点个人经验离线安装最磨人的地方其实不是某个命令敲错而是各种环境假设不一致。比如打包机架构跟内网不一致、镜像tag前缀没有统一、containerd的SystemdCgroup忘了改、内网网段和Calico默认网段冲突这些问题的根源都可以追溯到准备阶段。所以我现在做离线交付时会先用一晚时间把物料清单列成一张表rpm包列表、镜像列表、镜像tag、版本号、校验和全部记录下来再进内网。另一个小技巧是每次导入完镜像先跑一遍crictl images | grep v1.32.11快速核对镜像名称和tag是否齐全再去执行kubeadm init。这一步虽然看起来多余但它能帮你把第4章和第6章大部分问题直接挡在爆发之前。离线环境没有百度可查最可靠的依赖就是自己的checklist和充分的准备。这套流程跑过两三遍之后你会觉得离线装k8s其实也就那么回事真正宝贵的是那份耐心和次序感。