☰
麒麟V11离线安装k8s 1.32.11并接入KubeSphere全流程
2026/10/2 3:40:34 网站建设 项目流程

干过信创项目的人都有体会:开发环境里随便敲几条命令就能把k8s拉起来,真到了隔离内网,所有依赖、镜像、组件包都得自己一车一车拉进去,少一个包就是一夜白忙。这篇文章聊的是我最近在麒麟V11(银河麒麟高级服务器操作系统V11)上,用containerd 2.1.5全离线安装k8s 1.32.11,再接上KubeSphere的完整过程。内容覆盖离线物料包的准备逻辑、麒麟V11的系统初始化、containerd落地配置、kubeadm init的完整执行链路,以及那个让很多人卡到怀疑人生的报错——the api server is not healthy after 4m0.00747357s——该怎么一层层排查。如果你同样是做信创环境交付、国产化适配,或者只是想在完全没有外网的情况下把k8s跑起来,这篇应该能帮你省掉好几个通宵。

1. 为什么锁死这套组合:麒麟V11、containerd 2.1.5和k8s 1.32.11的兼容性底牌

1.1 信创环境里选型,第一原则是"别给自己留暗雷"

信创项目有个典型约束:交付环境往往是隔离内网,或者只有一条极其有限的白名单出口,外部的软件源、镜像仓库基本不可用。这就意味着你在开工前定下来的版本组合,必须在一台能联网的机器上完整复现一遍,把所有依赖都验证过、物料都备齐,才能进场操作。我见过不少同事在进场第一天才发现k8s版本和容器运行时版本不兼容,结果在客户现场临时改方案,进度直接废掉。

选择这套组合的逻辑其实很朴素:麒麟V11是目前信创项目里占有率非常高的服务器系统,内核和用户态组件都比较新,对容器生态的支持远好于早期CentOS 7时代的国产系统;k8s 1.32.11属于当前主流稳定分支,安全修复和稳定性都在线;containerd 2.1.5则是最新的大版本容器运行时,直接对接CRI接口,不需要docker那一层的额外开销。三者放在一起,既符合"新装系统用新组件"的原则,又不会出现某方版本过老、官方已经不维护的尴尬。

1.2 containerd替代docker,不是选择题而是必答题

很多从旧项目过来的人还在习惯性问"要不要先装docker?"。这里必须说清楚:从k8s 1.24开始,dockershim这个中间层就被移除了,kubelet不再支持绕道docker来调用容器运行时。到了k8s 1.32这个版本,你实际可选的CRI实现基本就是containerd或者CRI-O,而containerd的生态成熟度和文档完整度明显更好。

另外,containerd 2.x版本的配置和1.x比也有不少变化,最直观的就是config.toml的格式更规范了,默认生成的配置就带CRI插件,不用像1.x那样自己手工拼一堆路径。我这次用的2.1.5,在麒麟V11上跑得很稳,和systemd的交互也正常,没有出现容器起不来、cgroup驱动对不上的问题。

1.3 版本矩阵,一进场就定死

我整理过一套自己用的版本对照,每次做信创环境都按这个思路来核对:

组件版本选择关键理由
操作系统银河麒麟高级服务器操作系统V11信创项目主流,内核较新
容器运行时containerd 2.1.5对接CRI,2.x配置简洁
容器网络CNI plugins v1.x + Calico离线可备,支持多种网络策略
Kubernetesv1.32.11稳定分支,持续有安全修复
管理平台KubeSphere 3.x / 4.x可视化运维,离线可交付

这套组合最关键的一点是:所有组件都在一台联网的机器上先跑通过,再打包进内网。后文所有操作步骤,都是在这个前提下展开的。

2. 离线物料包清单:在能联网的那台机器上一趟备齐

2.1 先列出"进场物资清单",别信记忆力

离线安装最大的敌人不是技术难度,而是"不知道缺什么"。我的习惯是先在联网机器上建一个清单文件,逐项确认:

  • 系统层工具:yum-utils、createrepo(用来做本地rpm源)
  • 容器运行时:containerd-2.1.5-linux-amd64.tar.gz
  • Kubernetes组件:kubeadm、kubelet、kubectl三个rpm包,版本都锁在1.32.11
  • CNI插件包:cni-plugins-linux-amd64-v1.x.tgz
  • 所有镜像tar包:控制平面镜像(pause、etcd、kube-apiserver、kube-controller-manager、kube-scheduler、coredns)、Calico组件镜像、KubeSphere相关镜像

清单确认后,用脚本批量拉取。rpm包建议用yumdownloader --resolve把依赖一起拉下来,避免到了内网发现某个依赖库缺失。

2.2 用createrepo把rpm包整理成内网yum源

到了内网之后,如果每台机器都手工rpm -ivh发现缺依赖,那会非常痛苦。所以我习惯把rpm包丢到一个目录里,执行:

mkdir -p /root/k8s-rpms # 把从联网机器上下载的所有rpm放到这个目录 createrepo /root/k8s-rpms

然后在麒麟V11上写一个repo文件:

[k8s-local] name=K8s Local Repo baseurl=file:///root/k8s-rpms enabled=1 gpgcheck=0

这样之后安装kubeadm、kubelet、kubectl都可以直接用yum完成,依赖自动解析,干净利落。如果节点多,还可以把这个目录通过HTTP共享出去,所有节点统一配置。

2.3 镜像tar包:用ctr导出比docker save更稳

在联网机器上,如果你恰好装有containerd,可以直接用ctr导出镜像,比用docker save再倒腾到containerd少一层转换:

ctr -n k8s.io images export k8s-control-plane.tar registry.aliyuncs.com/google_containers/kube-apiserver:v1.32.11

把所有需要的内网镜像导出成一个或几个tar文件,进场后用同一套命令导入到每台节点的containerd里:

ctr -n k8s.io images import k8s-control-plane.tar

这里要特别提醒:ctr导入时指定命名空间为k8s.io很关键,因为kubelet的CRI插件默认就是从这个命名空间读镜像的。你导到别的命名空间,kubelet拉镜像时依然认为本地没有,会尝试去远程拉,离线环境就卡死了。

2.4 节点多的时候,建议内网起一个私有镜像仓库

如果只有两三台节点,每台手工导入镜像没问题。但如果我有几十台节点,我宁愿在内网先跑一个私有镜像仓库,所有节点把/etc/containerd/config.toml里的仓库地址指向内网仓库,kubelet拉镜像时走内网流量,比每台机器导入快得多也可靠得多。

私有仓库的准备也是在联网机器上完成的:导出registry的镜像tar,进场后先跑起来,再把所有需要的镜像push进去。这个动作看起来多了一步,但在节点规模稍大的信创项目里非常值得。

3. 麒麟V11系统底噪清理与containerd落地细节

3.1 麒麟V11的系统初始化,别跳过任何一个"小操作"

进入内网前,我会先在每台节点上做一遍系统底噪清理。这一步看起来零碎,但少了任何一个,后面都可能以诡异的方式炸出来。

首先是关闭防火墙和SELinux:

systemctl stop firewalld systemctl disable firewalld sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config setenforce 0

然后关闭swap,k8s对swap是零容忍的:

swapoff -a sed -i 's/.*swap.*/# &/' /etc/fstab

加载内核模块并设置sysctl参数:

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

还有一件事容易被忽略:检查是不是之前装过docker或者旧版本容器运行时。如果装过,docker0网桥和那一堆iptables链会留着,后面CNI插件起来的时候经常出现路由冲突。我的做法是确认干净环境再进场,或者进场后先清理残留网络设备。

3.2 containerd 2.1.5的安装与config.toml关键配置

containerd的二进制安装没什么花头,解压到/usr/local目录,之后写systemd服务。但在启动之前,第一件事是生成配置文件:

containerd config default > /etc/containerd/config.toml

打开这个文件,重点改两处。第一是CRI插件的sandbox镜像,默认值在离线环境里根本拉不到,你必须改成和你导入本地镜像的tag一致:

[plugins.'io.containerd.grpc.v1.cri'] sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.10"

第二是SystemdCgroup,必须设置为true,否则kubelet和containerd的cgroup驱动不一致,你会在kubelet日志里看到各种failed to run kubelet或者pod一直ContainerCreating:

[plugins.'io.containerd.grpc.v1.cri'.containerd.runtimes.runc.options] SystemdCgroup = true

2.x的配置结构比1.x清晰,改完直接启动:

systemctl daemon-reload systemctl enable --now containerd

3.3 用crictl验证运行时"真的能用"

配置好crictl的端点是很有必要的,它和kubelet一样通过CRI去操作containerd,比ctr更贴近k8s的视角:

runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false

保存为/etc/crictl.yaml后,跑几条命令确认状态:

crictl version crictl images

crictl images能看到你之前导入的镜像,说明CRI链路已经通了。这个动作别省,我看过太多人kubeadm init失败才发现containerd压根没起来,或者cri插件没加载,前面全白干。

3.4 CNI插件放对位置,网络插件才不闹脾气

containerd的CNI插件路径一般在/opt/cni/bin,如果你的cni-plugins包没解压到这里,后面Calico或者flannel创建网络接口时会直接报错。国产系统有个特点,某些目录权限和历史版本的路径习惯不太一样,最稳妥的做法是:

mkdir -p /opt/cni/bin tar -zxvf cni-plugins-linux-amd64-v1.x.tgz -C /opt/cni/bin

装完containerd和CNI插件,我会在每台节点上手动跑一个最小容器验证,确认网络和镜像导入都是正常的。这些都稳定了才进入kubeadm阶段。

4. kubeadm init全链路与api-server不健康报错的保姆式排查

4.1 用配置文件初始化,别直接敲一长串参数

连续执行节点多了之后,直接在命令行里敲kubeadm init --apiserver-advertise-address xxx --pod-network-cidr xxx太容易漏参数。我习惯把配置写成文件,每次进场只改IP和主机名。

创建kubeadm-config.yaml:

apiVersion: kubeadm.k8s.io/v1beta4 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.11 bindPort: 6443 nodeRegistration: criSocket: unix:///run/containerd/containerd.sock name: k8s-master-01 kubeletExtraArgs: cgroup-driver: systemd --- apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration kubernetesVersion: v1.32.11 imageRepository: registry.aliyuncs.com/google_containers networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd

需要注意:imageRepository这个值要和你导入的镜像前段路径对应,如果直接用默认的registry.k8s.io,离线环境肯定是拉不到的。podSubnet要和后面装的CNI插件保持一致,不然后患无穷。

然后执行初始化:

kubeadm init --config=kubeadm-config.yaml

成功后会看到kubeconfig的路径指引,按提示把admin.conf拷贝到当前用户下:

mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config

4.2 经典报错:the api server is not healthy after 4m0.00747357s

这个报错几乎每个离线装k8s的人都遇到过,字面意思是kubelet等了四分钟还没等到apiserver通过健康检查。但真正的原因通常不在"等待时间不够",而是apiserver压根就没正常起来。我遇到的情况基本都是下面几种。

第一步,看kubelet日志。kubelet是static pod的启动者,apiserver以static pod方式运行在kubelet之下。如果kubelet本身没起来,后面的所有事情都无从谈起:

journalctl -u kubelet --no-pager -n 100

如果这里有failed to run kubelet之类的错误,大概率是cgroup driver配置问题,回到上一章检查containerd的SystemdCgroup和kubelet的cgroup-driver是否都是systemd。

第二步,看容器运行时里有没有apiserver容器。kubelet正常运行时,会读取/etc/kubernetes/manifests/kube-apiserver.yaml并交给containerd创建容器。用crictl确认一下:

crictl ps -a | grep kube-apiserver

如果这里连容器都没有,说明manifest没被kubelet加载,或者kubelet的CRI连接有问题。还有种情况是你明明导入了镜像,但tag和manifest里的不完全一致,containerd本地找不到就去远程拉,结局就是ImagePullBackOff。

第三步,看apiserver容器的日志。如果apiserver容器存在但反复重启,直接看日志:

crictl logs <container-id>

我遇到过最典型的有三种:etcd没起来导致apiserver连不上存储后端;节点时间偏差过大导致证书校验和通信异常;内存不足导致容器OOMKilled。

我把常见根因整理成了一张排查表,每次进场都按这个顺序过:

现象可能根因处理方式
kubelet日志报cgroup错误containerd与kubelet的cgroup驱动不一致统一改为systemd
apiserver容器一直ImagePullBackOff本地镜像tag和manifest不一致重新导入或改config.toml的sandbox_image
apiserver连接etcd超时etcd容器没起来或时间不同步检查etcd镜像和日志,配置chrony
apiserver容器反复OOMKilled节点内存不足增加内存或减少不必要的组件

最后一步,清理现场重来。如果排查了还是起不来,别硬等,先把环境reset干净再重新init。直接跑reset会清掉当前集群状态,但不会动你导入的镜像,不用重新导包:

kubeadm reset -f rm -rf /etc/cni/net.d rm -rf $HOME/.kube

然后修复之前定位到的问题,再次初始化。这个过程我也经历过几次,关键是把每一步的日志看清楚,不要反复盲目重试。

4.3 控制节点就绪后,工作节点加入

控制节点正常后,生成join命令:

kubeadm token create --print-join-command

把输出的命令在工作节点上执行即可。如果工作节点之前也跑过init,记得先kubeadm reset清理。全部节点加入后,用kubectl get nodes看一下状态,刚加入的节点会处于NotReady,因为CNI网络插件还没装。

4.4 离线环境下的Calico落地

网络插件这一步离线环境容易出问题。先在每台节点上把Calico相关的镜像导入containerd,然后从准备好的manifest文件直接应用:

kubectl apply -f calico.yaml

Calico manifest里如果指定了镜像版本,请务必将对应镜像预先导入。这一点特别坑:很多人把公众号教程里的manifest直接拿来用,没注意到里面image tag和本地导入的不一致,结果所有calico-node Pod卡在ImagePullBackOff。我的习惯是进场前在联网机器上把最终要用的manifest和镜像版本核对一遍,锁死后再走离线流程。

5. KubeSphere离线落地:从安装器到控制台对外可达

5.1 先搞清楚你拿到的是3.x还是4.x的离线包

KubeSphere版本不同,离线安装的方式差别很大。3.x系列用的是ks-installer这个安装器,在已有k8s集群上apply几个yaml就能跑;4.x系列则改成了以ks-core为核心的chart安装方式,通常配合kk工具使用。拿到离线包的第一步,先确认包内的安装器形态,再决定后续命令,不要拿着3.x的教程去执行4.x的包,会到处碰壁。

我这次环境里用的离线包结构大概是这样的:kubesphere-installer.yaml、cluster-config.yaml,加上一个大的镜像tar包。这种结构是3.x的典型形态。

5.2 用ks-installer在已有k8s集群上部署

先把KubeSphere的镜像全部导入每台节点。注意这个镜像包通常比较大,导入时要确认磁盘空间足够,containerd的存储目录默认在/var/lib/containerd。

然后执行:

kubectl apply -f kubesphere-installer.yaml kubectl apply -f cluster-config.yaml

ks-installer会创建一个Job,在kubesphere-system命名空间里跑组件部署。查看进度:

kubectl get pods -n kubesphere-system -w

全离线环境下这个过程可能会比较久,耐心等。看到所有pod变成Running,基本就完成了一大半。中间如果有pod一直Pending,多半是镜像没导全或资源不足,对照kubectl describe pod里的事件去排查。

5.3 控制台访问:NodePort和externalIP到底怎么选

KubeSphere默认安装完成后,控制台服务是ks-console,默认映射的NodePort是30880。最直接的访问方式就是:

http://<节点IP>:30880

但在实际交付中,"客户端和k8s节点不在同一个网络里"的情况很常见。这时候我给两个思路。

思路一:直接用NodePort。所有节点都能访问30880,运维简单,但端口不够优雅,且暴露面比较大。

思路二:给控制台服务配置externalIP。这个方法在信创内网里很实用,给ks-console这个Service指定一个固定的内网IP,客户端直接访问这个IP即可:

kubectl -n kubesphere-system edit svc ks-console

在spec下面加上:

externalIPs: - 192.168.1.200

这里要提醒:externalIP的本质是让k8s把Service的流量路由到指定IP,它不是负载均衡器,没有健康检查和自动故障转移。所以你填的这个IP必须真实落在你的内网网段,并且能路由到集群节点,否则客户端照样访问不通。

如果客户环境里连NodePort这种端口映射也不方便用,可以考虑在集群里装MetalLB,用LoadBalancer类型把控制台暴露出来,但这需要额外准备MetalLB的镜像和配置,离线环境下就要预留这部分物料。

5.4 登录与初始账号

KubeSphere安装完成后,默认管理员是admin,初始密码是P@ssw0rd(如果安装包不同可能会变,离线包的文档里一般会写明)。第一次登录系统会要求改密码,这一步别偷懒,生产环境尤其重要。

如果登录后看到有些功能模块显示不可用,先检查底层必然关联的存储类是不是ready。KubeSphere的大部分组件都需要PVC,没有可用的存储类,所有依赖存储的Pod都会卡住。这个话题在下一章展开讲。

6. 跑了一周之后发现的几个坑和补救方案

6.1 节点重启后kubelet不自动起来

这个问题发生在一个很自然的场景:机房断电,服务器重启,整个集群就剩控制节点一个心眼。检查之后发现kubelet其实装了,但忘了enable。随手补上:

systemctl enable --now kubelet

这看起来是小事,但在信创项目里,客户那边可能没有专职的k8s运维,节点重启后没人会想到去手动启动kubelet。建议每台节点装完组件后,统一执行一遍enable操作,形成交付清单的一部分。

6.2 kubeadm证书一年有效期,提前告诉客户

k8s 1.32.11里kubeadm默认签发的证书有效期仍然是一年。KubeSphere登录正常、业务跑着跑着,某天apiserver突然拒绝连接,十有八九是证书到期。运维要记得提前执行:

kubeadm certs renew all

然后重启kubelet和apiserver相关组件。这件事最好在交付时就写进运维手册,或者挂个定时任务提醒。我一般会让客户把证书到期时间标注在维护日历上,别等炸了再排查。

6.3 存储类缺失导致的PVC Pending

KubeSphere离线装完之后,如果客户是全新环境,大概率没有默认存储类。最典型的表现是:创建企业空间、创建项目这些功能正常,但一旦要部署带持久化的应用,PVC永远Pending。

确认方式:

kubectl get storageclass

如果没有,就需要内网准备一个存储插件,比如local-path-provisioner或者NFS provisioner。离线环境下同样是导出镜像、导入、apply manifest三步走。很多人在准备离线物料时完全没把存储插件算进去,我建议把它纳入标准清单,因为KubeSphere本身对存储的依赖非常重。

6.4 镜像tag的"冰山":临时补包是最痛苦的

离线环境里最难受的莫过于:进场后执行某个组件时才发现少了一个镜像,或者tag不一致,然后要在内网里临时想尽办法补包。我吃过几次亏之后,养成了一个习惯:在联网机器上把最终要用的所有yaml文件先跑一遍kubectl prepare或者直接扫码所有image字段,逐条核对镜像清单,确认绝对完整了再封包。

另外要注意架构标签:信创环境里x86和ARM的机器都很常见,镜像tar包要按linux/amd64、linux/arm64分开准备,混着导入会出各种奇怪的问题。这个坑我在一套鲲鹏机器上踩过,镜像导进去显示存在,但Pod拉取时平台不匹配,一直调度失败。

6.5 时间同步是整个集群最容易忽略的暗雷

k8s组件对节点间的时间偏差非常敏感。证书校验、etcd选举、日志时间戳,全部依赖一致的时间。麒麟V11默认不一定配了NTP客户端,我建议在所有节点统一配置chrony,时间源指向内网时间服务器。这一步半小时就能搞定,但能避免很多"看起来毫无规律"的故障。

我在实际项目里最深的体会是:离线的k8s集群,真正考验的不是某个命令会不会敲,而是有没有把所有"看似不重要"的细节在进场前消化干净。一个tag不一致、一个cgroup选项没开、一份证书没提醒,都可能让整个交付延期。希望这篇把我在麒麟V11上折腾这套组合的经验整理出来,能帮同行少走几趟弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询