前阵子被拉去给一批新到的麒麟V10服务器装Kubernetes机房在隔离网里没法访问外网版本还要卡在1.32.11。网上在线安装的教程一抓一大把可真到了离线环境几乎所有教程都会卡死在第一步下载命令根本没法定外网跑。这篇就把我在麒麟V10上离线安装k8s 1.32.11的完整过程写下来从离线介质准备、系统适配、部署实操到踩坑复盘适合正在隔离网或离线环境里做K8s集群部署的同学参考。环境先交代一下我这批服务器是麒麟V10服务器版x86_64架构控制平面一台工作节点两台共享一套内网yum源但内容很旧只有基础系统包。整个过程最麻烦的不是装K8s本身而是把所有该带的依赖、二进制、镜像都想全然后一次性搬进内网。1. 离线部署的总体设计版本、架构与介质清单离线安装最忌讳上来就装装到一半发现缺东西。别问我怎么知道的我第一遍就是因为少了一个conntrack-tools的RPM包卡在kubeadm init前面整整半天。所以第一步先把版本组合、节点规划、介质清单全部定下来。1.1 先聊版本组合为什么是 1.32.11 containerd 1.7.xKubernetes从1.24之后就彻底移除了dockershim容器运行时基本只有containerd和CRI-O这类选择。生产环境我一般直接用containerd理由很简单RPM包和二进制都方便离线准备kubelet对接稳定社区默认就是它。K8s 1.32.11在1.32.x这个补丁系列里处于比较靠后的位置稳定性已经经过多轮验证。containerd我选的是1.7.x系列这个版本对SystemdCgroup支持成熟和1.32的kubelet配合没什么坑。CNI选Flannel没选Calico原因后面会说。这里有一个关键点K8s的版本选择不是越新越好而是要和操作系统生态匹配。麒麟V10的内核一般不会太新而K8s 1.32要求Linux内核足够支持overlayfs、iptables、ipvs这些基础能力4.19基本都能满足。版本太高反而可能出现内核模块不匹配的问题。1.2 节点规划控制平面、工作节点与资源建议规划节点时先确认架构和角色分配。我这边是3台节点角色数量最低建议配置主要负载控制平面14核8G系统盘100Gkube-apiserver、etcd、controller-manager、scheduler工作节点24核8G系统盘100G数据盘100Gkubelet、containerd、业务Pod介质制作机1有外网2核4G即可下载RPM、二进制、镜像并打包这里要特别提醒控制平面的磁盘不要抠门。etcd默认数据目录在/var/lib/etcd虽然一开始占用不大但集群运行半年后日志和事件数据会慢慢膨胀。工作节点的数据盘建议单独挂给containerd别跟系统盘混在一起这个我后面会展开讲。1.3 离线介质的内容清单一个文件都不能少离线介质包不是简单地拷几个RPM就完事里面至少包含四类东西K8s核心二进制kubeadm、kubelet、kubectl我直接下载官方RPM包省去手动解压二进制再写systemd服务文件的麻烦。容器运行时containerd本体加cri-containerd可能涉及的文件还有CNI插件cni-plugins。系统依赖RPMkubelet、containerd运行时依赖的库和工具比如libseccomp、conntrack-tools、socat、ipvsadm等内网yum源里不一定全必须提前准备。容器镜像K8s控制面组件镜像kube-apiserver、etcd、coredns、pause等、网络组件镜像flannel、业务测试镜像nginx。CNI部署manifest比如flannel的yaml文件离线环境没法从GitHub直接拉必须提前下好。我把这些写成了一张Excel清单每项都标注了版本号和来源后面所有机器都按这张表干活。这个习惯建议养成尤其多人协作时清单能避免“我以为你拷了”这种悲剧。2. 麒麟V10系统侧准备装之前必须改掉的四个地方麒麟V10虽然整体跟RHEL系的CentOS很像但细节上有自己的脾气。系统侧我踩过的坑多半是“看起来没问题装到一半就报错”。2.1 内核与架构摸底麒麟V10的服务器版内核通常基于4.19或5.xx86_64架构。动手前先跑一下这几个命令uname -a cat /etc/os-release arch根据输出确认发行版本、内核和架构再去选对应版本的RPM和二进制包。比如arch是x86_64下载K8s二进制时就要选linux-amd64不要搞成arm64这个低级错误真有人犯过。另外看看/var/lib/dpkg或/var/lib/rpm是否存在确定包管理器是dpkg还是rpm。麒麟V10不同子版本可能一个基于Debian系一个基于RPM系这个直接影响离线RPM的兼容性。我这边全是RPM系下面所有命令也都按RPM系来写。2.2 加载 overlay 与 br_netfilter配好内核参数containerd需要overlayfs支持镜像分层K8s的网络转发需要br_netfilter。这俩内核模块不加载后面会出现各种玄学问题比如Pod网络不通、service不能访问。写一个加载模块的配置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注意net.ipv4.ip_forward一定要设成1。默认情况下很多Linux发行版ipv4转发是关的K8s的Pod跨节点通信依赖这个开关不然后面node之间互相访问Pod会超时。2.3 swap、防火墙与 SELinux该关的关该放行的放行Kubelet默认不允许swap开着至少1.32版本上还是这样。所以先关掉swapswapoff -a sed -i / swap / s/^/#/ /etc/fstab别只执行swapoff -a就完事重启后fstab还会把它挂回来。记得注释fstab里的swap行。防火墙和SELinux是另一对常客。安全要求高的环境不建议直接firewalld stop可以把K8s需要的端口放行控制平面6443apiserver2379-2380etcd10250kubelet10259scheduler10257controller-manager工作节点1025030000-32767NodePort如果只想快速验证直接关掉firewalldsystemctl stop firewalld systemctl disable firewalldSELinux也一样要么放行要么setenforce 0并修改/etc/selinux/config。我通常在内网测试环境选择直接关闭但在生产环境还是建议保留SELinux并用策略放行只是麒麟V10的SELinux策略对K8s的支持有时不太完善需要额外调。2.4 磁盘规划别让系统盘被容器镜像塞满工作节点上containerd默认数据目录是/var/lib/containerd系统盘空间如果只有100G跑几个镜像和数据卷就可能撑爆。我习惯单独挂一块数据盘给containerd挂载参数同时考虑限额mkfs.xfs /dev/sdb mkdir -p /var/lib/containerd mount /dev/sdb /var/lib/containerd echo /dev/sdb /var/lib/containerd xfs defaults 0 0 /etc/fstab为什么不建议用ext4xfs在处理大文件和高并发写入时更稳尤其容器镜像分层多overlayfs在xfs下表现更好。当然ext4也能用只是个人经验里xfs的坑少一些。控制平面上的etcd数据目录/var/lib/etcd也可以单独挂盘但测试环境不强制生产环境一定挂因为etcd是集群的命根子磁盘满了整个集群直接不可写。3. 制作离线介质包在有网机器上一次准备到位这一步需要一台能访问外网的机器通常是运维的笔记本或者一台临时跳板机。介质包做得好不好直接决定内网部署顺不顺利。3.1 K8s 二进制包与 RPM 依赖下载先建目录结构mkdir -p k8s-1.32.11-offline/{rpm,bin,images,manifests} cd k8s-1.32.11-offlineK8s官方RPM仓库在特定镜像站有同步可以在介质机上配置临时yum源然后下载。先用yumdownloader把依赖一并拉下来yum install -y yum-utils yumdownloader --resolve --destdir./rpm \ kubeadm \ kubelet \ kubectl如果直接下载官方二进制可以用wget https://dl.k8s.io/v1.32.11/kubernetes-server-linux-amd64.tar.gz这个Server包比较大但里面包含完整的kube-apiserver、kube-controller-manager、kube-scheduler等二进制用来部署控制平面非常方便。如果你倾向于RPM方式那RPM仓库里的kubeadm、kubelet、kubectl三个包就够了注意把它们的依赖RPM一起--resolve下来。containerd和CNI插件也是关键wget https://github.com/containerd/containerd/releases/download/v1.7.x/containerd-1.7.x-linux-amd64.tar.gz wget https://github.com/containernetworking/plugins/releases/download/v1.x/cni-plugins-linux-amd64-v1.x.tgz版本号按你实际选择去release页面核对。顺便把runc的二进制也装了containerd的tar包里通常不会自带runc单独下载一份wget https://github.com/opencontainers/runc/releases/download/v1.1.x/runc.amd64最后把依赖系统工具装齐yum install -y yum-utils tar socat conntrack-tools ipvsadm ipset这些工具的内网系统不一定自带先在外网机器上下载RPM包放进rpm目录。3.2 K8s 系统镜像导出用 kubeadm 的清单做事K8s控制面镜像版本多且杂手敲很容易错。最稳妥的办法是用kubeadm自身列出的镜像清单kubeadm config images list --kubernetes-versionv1.32.11输出大概是下面这些registry.k8s.io/kube-apiserver:v1.32.11registry.k8s.io/kube-controller-manager:v1.32.11registry.k8s.io/kube-scheduler:v1.32.11registry.k8s.io/kube-proxy:v1.32.11registry.k8s.io/etcd:3.5.xregistry.k8s.io/coredns/coredns:v1.11.xregistry.k8s.io/pause:3.10注意kubeadm config images list输出的版本号会随K8s依赖组件的更新变化你要以实际输出为准。然后在介质机上用containerd或docker把这些镜像拉下来再导出ctr -n k8s.io images pull registry.k8s.io/kube-apiserver:v1.32.11 # 逐个pull后导出成tar ctr -n k8s.io images export ./images/k8s-images.tar \ registry.k8s.io/kube-apiserver:v1.32.11 \ registry.k8s.io/kube-controller-manager:v1.32.11 \ ...如果介质机上有docker也可以用docker pull加docker save效果一样。导出成一个总tar包的好处是内网导入时只需执行一次命令。Flannel镜像同样提前拉下来docker pull flannel/flannel:v0.25.7 docker pull flannel/flannel-cni-plugin:v1.5.1-flannel2 docker save flannel/flannel:v0.25.7 flannel/flannel-cni-plugin:v1.5.1-flannel2 ./images/flannel-images.tar业务测试镜像也顺手放一个docker pull nginx:alpine docker save nginx:alpine ./images/nginx-test.tar3.3 校验、打包与内网落地介质包准备好了不能急着拷进去先做两件事第一校验sha256。把每个tar包、RPM包都算一遍哈希内网导入后可以通过sha256sum -c验证文件完整性。离线环境没有外网下载二次校验的可能介质机的哈希表就是唯一凭证。第二写README。把介质包内容、版本、导入顺序全写进一个txt文件。别嫌啰嗦一个月后再回来维护你看到README里的步骤能省很多时间。最后打包上传到内网存储或直接U盘拷贝tar czf k8s-1.32.11-offline.tar.gz k8s-1.32.11-offline/内网任意一台机器解压后共享给其他节点即可。4. 内网部署实操从 containerd 到集群可用介质包到了内网部署开始。4.1 安装 containerd 并改对三处配置首先装依赖RPM。麒麟V10内网的yum源不完整直接用本地目录安装rpm -ivh ./rpm/*.rpm --nodeps--nodeps是双刃剑依赖缺失时它会跳过检查但运行时会报错。我更推荐用yum localinstallyum localinstall -y ./rpm/*.rpm它会尝试从内网yum源补全缺失依赖实在找不到再人工介入。我这边有几台机器缺socat和libseccomp都是从介质包补上的。然后解压containerd和CNI插件tar Cxzf /usr/local -C /tmp containerd-1.7.x-linux-amd64.tar.gz mkdir -p /opt/cni/bin tar Cxzf /opt/cni/bin -C /tmp cni-plugins-linux-amd64-v1.x.tgz生成containerd默认配置mkdir -p /etc/containerd containerd config default /etc/containerd/config.toml这个默认配置要改三处。第一处是SystemdCgroup从false改成true否则kubelet和containerd的cgroup管理方式不一致Pod创建会报错。[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc] systemd_cgroup true第二处是sandbox_image换成我们导入的pause镜像地址。离线网络环境下如果保持默认值kubelet一旦需要创建sandbox就会试图去联网拉镜像。[plugins.io.containerd.grpc.v1.cri] sandbox_image registry.k8s.io/pause:3.10第三处是registry.mirrors配置为了内网如果有私有镜像仓库时能优先走内网地址避免每次拉镜像都超时。如果没有私有仓库可以留空或指定镜像站。改完启动containerdsystemctl daemon-reload systemctl enable --now containerd4.2 导入镜像namespace 错了等于白导containerd的镜像导入必须注意namespace。默认ctr命令操作的namespace是default但kubelet通过CRI调用containerd时使用的是k8s.io这个namespace。用ctr images import导入到defaultkubelet照样看不到。正确的导入姿势ctr -n k8s.io images import ./images/k8s-images.tar ctr -n k8s.io images import ./images/flannel-images.tar ctr -n k8s.io images import ./images/nginx-test.tar导入后用以下命令确认ctr -n k8s.io images list | grep registry.k8s.io这个坑我第一遍就踩了当时导完半天结果Pod一直ImagePullBackOff查到最后发现是namespace不对。记住CRI用的namespace是k8s.io。4.3 kubeadm init 与配置文件不推荐直接写一长串命令行参数建议把配置写进yaml文件方便留存和审计。先创建kubeadm.ymlapiVersion: kubeadm.k8s.io/v1beta4 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.11 bindPort: 6443 nodeRegistration: name: master01 criSocket: unix:///var/run/containerd/containerd.sock --- apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration kubernetesVersion: v1.32.11 controlPlaneEndpoint: 192.168.10.11:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12podSubnet用的是10.244.0.0/16这个网段要和后面Flannel的默认网段一致不然装完网络组件后Pod拿不到IP。controlPlaneEndpoint填高可用虚拟IP或第一个控制平面节点IP即使单控制平面也建议写明白。执行初始化kubeadm init --configkubeadm.yml如果一切顺利最后会输出一段kubeadm join命令。记得保存或者用kubeadm token create --print-join-command重新生成。初始化完成后按提示配置kubeconfigmkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config4.4 工作节点 join 与 CNI 网络一次搞定工作节点上先装好同样的依赖RPM、containerd并且导入介质包里的镜像。然后执行控制平面生成的join命令kubeadm join 192.168.10.11:6443 --token xxx \ --discovery-token-ca-cert-hash sha256:xxxjoin成功后再装CNI。我选Flannel的原因就是简单就一个yaml文件加几个镜像离线部署压力小kubectl apply -f ./manifests/kube-flannel.yml注意kube-flannel.yml要在介质准备阶段下载好或者在控制平面节点能联网时下载否则离线环境手抖都抖不出来。Flannel装完后默认使用10.244.0.0/16网段和kubeadm.yml里的podSubnet吻合。等每个节点上的kube-flannel-dsPod变为Running基本上网络就通了。4.5 集群健康检查别急着跑业务装完之后先看一轮状态kubectl get nodes kubectl get pods -A然后跑到工作节点上看Pod网络情况kubectl run ngx --imagenginx:alpine --port80 kubectl get pod -o wide从工作节点或控制平面去curl这个Pod的IP如果能通说明Pod间网络正常。再尝试创建一个NodePort Service验证节点端口转发能力。这套检查流程虽然简单但能在业务部署前把底层网络问题暴露干净。5. 离线安装踩坑记录五个让我多花了一个周末的问题单看步骤很顺但中间我实际踩了不少坑。这些坑每个都能让部署暂停半小时以上一一列出来希望各位能绕开。5.1 Pod 卡在 ContainerCreating罪魁祸首是 pause 镜像现象kubeadm init完成后kube-system里的Pod全部卡在ContainerCreatingdescribe一看是sandbox创建失败。排查执行ctr -n k8s.io images list | grep pause结果里没找到pause镜像。原因是我在一台机器上导镜像时只导了控制面组件镜像忘了把pause单独拉出来。而containerd默认配置里的sandbox_image指向registry.k8s.io/pause:3.10离线环境下无法拉取Pod就永远建不起来。解决介质机补下pause镜像docker pull registry.k8s.io/pause:3.10导出再到内网导入重启containerd。另外我建议在containerd配置里显式写清楚sandbox_image不要依赖默认值——版本升级时默认值可能变显式配置能减少差异。5.2 join 失败却提示 timeout先查 /etc/hosts 和主机名现象工作节点执行kubeadm join报timeout waiting for the kubelet这类错误信息很泛让人无从下手。排查链路先看systemctl status kubelet发现kubelet在反复重启日志里报证书或节点注册相关的错误。再看/etc/hosts发现工作节点的主机名并没有解析到本机IP。Kubelet启动后要注册到apiserver它得能把自己地址报上去而hostname解析不畅时注册就卡住。解决在所有节点/etc/hosts里加上192.168.10.11 master01 192.168.10.21 node01 192.168.10.22 node02然后重启kubelet。这个问题在纯内网环境特别常见因为内网DNS不一定建得全。5.3 kube-proxy 引用 ipvs 模式系统却缺模块现象kube-system里kube-proxyPod反复CrashLoopBackOff日志中提示can not find ipvs或者ipset相关错误。原因1.32版本的kube-proxy默认或配置文件中可能使用ipvs转发模式但麒麟V10系统默认没有加载ip_vs内核模块也没有安装ipvsadm、ipset这些工具。解决yum install -y ipvsadm ipset cat /etc/modules-load.d/k8s.conf EOF ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack EOF modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack如果还是不行就把kube-proxy的mode改回iptables配置里把mode: iptables显式写好。生产环境用iptables虽然性能略低但胜在依赖少、好排查小规模集群完全够用。5.4 RPM 安装顺序带来的依赖连环坑现象直接执行rpm -ivh kubelet-1.32.11.rpm系统提示缺少kubernetes-cni或libseccomp依赖。原因RPM安装不像yum会自动解析依赖离线介质包里的RPM必须一次性安装或者按依赖顺序装。解决其实介质机在下载时就可以把依赖一起下全。内网安装时最好用yum localinstall -y ./rpm/*.rpm如果某些RPM依然被yum拒绝再手动指定依赖顺序先装libseccomp、conntrack-tools、socat、kubernetes-cni最后装kubelet、kubeadm、kubectl。依赖顺序这个事介质机上下载多花五分钟内网能省两小时。5.5 重启之后集群失联enable 从左往右都要配现象全部装完主备确认没问题结果晚上机房断电重启第二天集群连不上了。排查kubectl get nodes超时登录控制平面看systemctl status kubelet发现kubelet是inactive状态。再看containerd也是没开机自启。原因安装过程中我手动启动过服务但没有执行systemctl enable。重启后containerd和kubelet都没拉起来kubelet的enable是RPM包安装时配好的但我用的二进制解压方式没有systemd服务文件。解决用RPM方式安装的跑systemctl enable --now containerd systemctl enable --now kubelet用二进制方式安装的需要自己准备systemd service文件。检查一下/usr/lib/systemd/system/下有没有kubelet.service没有就从介质包里补一个。另外etcd是静态Pod由kubelet拉起所以kubelet没起来时etcd也会一起“消失”这也是重启后连不上集群的核心原因。6. 收尾这套离线方案后续怎么维护集群跑起来只是开始后续的维护才见真章。离线环境没有外网一切都要靠介质包和备份来兜底。6.1 介质包是“活化石”升级扩容都靠它离线部署的介质包必须留好它是你这个集群版本的“活化石”。以后要扩容节点直接拿同一套介质包按第4章的流程再走一遍要升级补丁版本则在介质机上重新拉新的二进制和镜像打包后再上传内网。我建议给介质包打上版本标签例如k8s-1.32.11-offline-2025xx.tar.gz哪怕K8s升级了新版本旧的介质包也别删。K8s补丁版本之间一般可以滚动升级但你总会遇到某台老节点需要回滚或重装的情况那时候旧介质包就是救命稻草。6.2 该备份的文件清单至少备份这几类东西/etc/kubernetes/kubeadm配置、证书、admin.conf/etc/containerd/config.tomlcontainerd配置/etc/sysctl.d/k8s.conf内核参数/var/lib/etcd/etcd数据如果有条件定期做etcd快照kubeadm.yml初始化配置文件建议写个cron脚本定期把etcd快照和kubeadm配置打包放到NFS或独立备份机。离线环境最怕的是“一切都在那台机器上”结果那台机器坏了。6.3 有条件的话再往前一步做内网镜像仓库纯离线介质包适合一两次部署和几十台以内的集群。如果这套环境要长期运营、节点会不断增长建议在条件允许时搭一个内网镜像仓库比如基于私有Registry或Harbor把镜像统一推送到内网仓库各节点从仓库拉取。介质包的导入是静态的镜像仓库是动态的。前者适合开局后者适合运维。两者可以并存介质包用于节点初始化镜像仓库用于日常调度和扩容。先问自己一个问题“这套集群会不会活过三个月”会的话镜像仓库值得提前准备。最后讲点实在的我在这套麒麟V10离线环境上最大的体会是离线部署的复杂度并不在K8s本身而在“能不能把所有变量前置控制住”。版本锁定、依赖齐全、镜像清单精确离线环境会比在线环境更容易复现结果——因为没有人会跑着跑着偷偷给你装个新版本。把介质包维护好、把备份做勤快这套集群就能安心跑很久。