
干过信创项目的人都有体会开发环境里随便敲几条命令就能把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对接CRI2.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.gzKubernetes组件kubeadm、kubelet、kubectl三个rpm包版本都锁在1.32.11CNI插件包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] nameK8s Local Repo baseurlfile:///root/k8s-rpms enabled1 gpgcheck0这样之后安装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的系统初始化别跳过任何一个小操作进入内网前我会先在每台节点上做一遍系统底噪清理。这一步看起来零碎但少了任何一个后面都可能以诡异的方式炸出来。首先是关闭防火墙和SELinuxsystemctl stop firewalld systemctl disable firewalld sed -i s/SELINUXenforcing/SELINUXdisabled/ /etc/selinux/config setenforce 0然后关闭swapk8s对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 true2.x的配置结构比1.x清晰改完直接启动systemctl daemon-reload systemctl enable --now containerd3.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 imagescrictl 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.yamlapiVersion: 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 --configkubeadm-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/config4.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驱动不一致统一改为systemdapiserver容器一直ImagePullBackOff本地镜像tag和manifest不一致重新导入或改config.toml的sandbox_imageapiserver连接etcd超时etcd容器没起来或时间不同步检查etcd镜像和日志配置chronyapiserver容器反复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.yamlCalico 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.yamlks-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初始密码是Pssw0rd如果安装包不同可能会变离线包的文档里一般会写明。第一次登录系统会要求改密码这一步别偷懒生产环境尤其重要。如果登录后看到有些功能模块显示不可用先检查底层必然关联的存储类是不是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 PendingKubeSphere离线装完之后如果客户是全新环境大概率没有默认存储类。最典型的表现是创建企业空间、创建项目这些功能正常但一旦要部署带持久化的应用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上折腾这套组合的经验整理出来能帮同行少走几趟弯路。