信创项目里最难受的从来不是K8s本身,而是“内网+离线”这四个字。交到你手上的可能是一台刚装好银河麒麟服务器版V11的裸机,要求把K8s 1.32.11集群和KubeSphere全部搭起来,网线只通内网,所有安装包、镜像都得提前拷进去。这套流程我实际走过不止一遍,中间踩过不少坑,所以把整个过程重新整理了一遍,使用containerd 2.1.5作为容器运行时,没有继续沿用docker——K8s 1.32已不再维护dockershim,containerd作为原生CRI运行时更稳、更省资源。这篇内容适合正在做信创环境交付的运维、刚接触离线K8s部署的实施同学参考,照着做完,你会得到一套带KubeSphere可视化控制台的多节点集群。
1. 前期准备:版本选型与离线物料清单
1.1 为什么要用这套组合
先说结论:这套组合最核心的思路是“二进制文件安装,不碰系统软件源”。银河麒麟V11虽然兼容性做得不错,但不同版本自带的软件源、依赖库差异还是存在的,如果走yum install docker或者apt install kubeadm这条路,光依赖解析就能耗掉你半天。直接把官方二进制解压到/usr/local/bin,再写成systemd服务,这样不依赖麒麟自带的包管理器,离线环境里最省心。
K8s版本选了1.32.11,这是当前比较新也比较稳的版本,API兼容性、cgroup v2支持都没什么大问题。containerd则选了2.1.5,比docker更轻,配置项也简单,而且kubeadm从1.26之后就默认对接containerd,不需要再装docker-shim之类的东西。KubeSphere用在交付场景里很加分,客户看到可视化控制台,比单纯给一堆kubectl get pods输出更有说服力,而且它自带监控、日志、多集群管理入口,省得自己额外搭grafana和prometheus。
这套组合里有个容易踩坑的细节:所有组件都要留意CPU架构。信创环境里x86_64和ARM64都可能遇到,下载二进制和镜像时务必先uname -m确认架构,别拿着amd64的包去装ARM的机器。
1.2 离线物料清单
开工前先建一个目录,比如/data/k8s-offline,后面所有物料都放这里。我按如下清单准备:
| 类别 | 物料 | 说明 |
|---|---|---|
| 容器运行时 | containerd-2.1.5-linux-amd64.tar.gz | 解压后包含containerd二进制和依赖 |
| CRI调试工具 | crictl-v1.32.0-linux-amd64.tar.gz | 用于调试容器运行时状态 |
| CNI插件 | cni-plugins-linux-amd64-v1.6.0.tgz | Calico等插件运行的基础插件 |
| Kubernetes组件 | kubeadm、kubelet、kubectl v1.32.11 | 三节点都需要kubelet和kubeadm |
| 控制面镜像 | etcd、coredns、pause、kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy | 用kubeadm config images list确认完整列表 |
| 网络插件 | calico.yaml + calico镜像tar包 | 或用cilium,Calico离线部署最简单 |
| KubeSphere | ks-installer.yaml + cluster-configuration.yaml + 镜像tar包 | 必须有StorageClass支撑 |
镜像制作是在一台可以联网、且和机房服务器同CPU架构的机器上完成的。先执行kubeadm config images list --kubernetes-version=v1.32.11拿到官方镜像清单,再用skopeo copy docker://<镜像名> docker-archive:/data/images/<镜像名>.tar逐个导出。有人说用docker save也行,但docker save打出来的tar导入containerd时偶尔会出现tag信息不完整的情况,我还是推荐skopeo,干净直接。
所有物料拷进内网后,第一件事是校验sha256。比如sha256sum containerd-2.1.5-linux-amd64.tar.gz,生成一个清单文件随手放在目录里。离线环境没有外网,如果拷进来一个损坏的tar,导入时“archive/tar: invalid tar header”这类报错会把你折磨疯,校验这一步别省。
2. 系统初始化:麒麟V11上的环境配置
2.1 主机规划和基础网络
我这次用的是三节点结构:一台控制平面,两台工作节点。机器不多,没必要上VIP和负载均衡器,控制平面地址直接写master的IP就行。离线环境里越简单越不容易出错。
| 节点 | IP | 角色 | 配置建议 |
|---|---|---|---|
| k8s-master01 | 10.10.10.11 | control-plane | 4C8G起 |
| k8s-node01 | 10.10.10.12 | worker | 8C16G起 |
| k8s-node02 | 10.10.10.13 | worker | 8C16G起 |
每台机器都执行:
hostnamectl set-hostname k8s-master01然后修改/etc/hosts:
10.10.10.11 k8s-master01 10.10.10.12 k8s-node01 10.10.10.13 k8s-node02这里有个原则:所有节点的主机名、IP映射必须一致,不能只改master不改node。K8s集群内部的证书、节点注册都依赖主机名解析,最典型的问题就是kubectl get nodes后节点显示NotReady,因为kubelet的hostname解析到了localhost。
时间同步也要提前处理。内网没有外网NTP的话,至少保证所有节点手动同步一次:date -s "2025-01-20 10:00:00"。K8s对证书时间校验非常敏感,节点时间差超过五分钟,kubelet和apiserver之间的TLS握手就会失败,报错看起来是证书问题,实际是时间差。
2.2 防火墙、SELinux和Swap
麒麟V11默认开着firewalld,先关掉:
systemctl stop firewalld systemctl disable firewalld如果系统里启用了SELinux,也要处理:
setenforce 0 sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/configSwap必须关闭:
swapoff -a sed -i '/swap/s/^/#/' /etc/fstab关闭防火墙和SELinux的原因,不是为了省事,而是K8s的Pod网络、NodePort、kube-proxy都会动态创建iptables规则,防火墙策略很容易把这些规则拦掉。SELinux对容器文件系统的权限管理非常严格,后续挂载目录、Conntrack、网络插件都容易触发权限拒绝。生产环境如果要求开启防火墙,就需要梳理大量端口,至少要在每个节点放行6443、2379、2380、10250、30000-32767等端口,离线交付场景我建议先关掉,保证业务能跑起来,验收后再按安全基线加固。
2.3 内核模块、系统参数和CNI插件
加载两个关键内核模块:
modprobe overlay modprobe br_netfilter cat > /etc/modules-load.d/k8s.conf <<EOF overlay br_netfilter EOF写入系统参数:
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这几个参数不是随便抄的。net.bridge.bridge-nf-call-iptables必须设为1,原因是K8s的Service通过iptables做DNAT时,如果网桥流量不经过netfilter,Pod跨节点访问Service就会被绕过,表现就是同节点Pod正常、跨节点Pod不通。net.ipv4.ip_forward为1是为了让Pod网段的流量能在节点间转发,Calico或Flannel的IPIP/VXLAN模式都依赖它。
顺便把CNI插件装上,每个节点都要:
mkdir -p /opt/cni/bin tar -C /opt/cni/bin -xzf cni-plugins-linux-amd64-v1.6.0.tgz ls /opt/cni/bin | grep bridge这一步经常被忽略,Calico虽然会往宿主机拷自己的一套CNI插件,但portmap、loopback这些基础插件还是得有。缺了的话,Pod创建时会卡在SetUpPod,看了半天不知道原因。
3. 安装containerd 2.1.5并导入离线镜像
3.1 安装containerd与调优配置
containerd的安装很简单,解压就行:
tar -C /usr/local -xzf containerd-2.1.5-linux-amd64.tar.gz mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml生成的config.toml很长,别慌,只需要关心两个地方。
第一,把SystemdCgroup改成true。找到[plugins.'io.containerd.grpc.v1.cri'.containerd.runtimes.runc]段落,把[plugins.'io.containerd.grpc.v1.cri'.containerd.runtimes.runc.options]下面的SystemdCgroup = false改成true:
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml这个必须改。Kubelet默认的cgroup驱动是systemd,containerd如果还保持cgroupfs,两边看到的cgroup路径就对不上,kubelet会报failed to run Kubelet: failed to validate the kubelet's cgroup configuration against the runtime,节点直接NotReady。
第二,修改sandbox_image。这个就是pause镜像,K8s每个Pod启动都要靠它先拉起一个沙箱容器。默认值可能是registry.k8s.io/pause:3.10,我们离线环境里只要保证本地导入的镜像tag和这里一致就行,我统一改成了:
sed -i 's#sandbox_image = "registry.k8s.io/pause:3.8"#sandbox_image = "registry.k8s.io/pause:3.10"#' /etc/containerd/config.toml如果你离线包里的pause版本不是3.10,以你实际镜像为准,关键是config.toml里的名字要和后面导入到containerd里的tag一模一样。
然后创建systemd服务:
cat > /etc/systemd/system/containerd.service <<'EOF' [Unit] Description=containerd container runtime After=network.target local-fs.target [Service] Type=notify ExecStart=/usr/local/bin/containerd --config /etc/containerd/config.toml Restart=always RestartSec=5 LimitNOFILE=1048576 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now containerd systemctl status containerd确认containerd起来了再往下走。这里有个经验:Type=notify必须保留,containerd启动后通过sd_notify通知systemd自己就绪,kubelet如果等不到containerd的socket,会一直报“container runtime is down”。
3.2 把镜像导入k8s.io命名空间
镜像从离线目录拷贝到每个节点后,批量导入:
for img in /data/k8s-offline/images/*.tar; do ctr -n k8s.io images import --all-platforms "$img" done这个-n k8s.io是全文最关键的一个参数,请务必记住。ctr默认命名空间是default,但K8s通过CRI调用containerd时,使用的是k8s.io命名空间。如果你用ctr images import不带-n k8s.io,镜像会被导到default命名空间里,K8s那边永远看不见,随后kubelet开始疯狂拉镜像,离线环境下又拉不到,最终状态就是节点NotReady。
导入完成后,验证一下:
ctr -n k8s.io images list | grep -E 'pause|etcd|kube-apiserver'如果发现镜像tag和kubeadm config images list输出的不一致,用ctr -n k8s.io images tag补一个tag。例如:
ctr -n k8s.io images tag docker.io/library/pause:3.10 registry.k8s.io/pause:3.10为了方便调试CRI,再配置一个crictl客户端:
cat > /etc/crictl.yaml <<EOF runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF这样后面排查问题时,直接crictl ps、crictl images,比每次打一长串ctr -n k8s.io要省事得多。
4. 用kubeadm初始化Kubernetes 1.32.11
4.1 安装kubelet、kubeadm、kubectl
三个二进制文件都放到/usr/local/bin下,然后赋权:
chmod +x /usr/local/bin/kubeadm /usr/local/bin/kubelet /usr/local/bin/kubectl kubeadm version工作节点也需要这几个二进制,至少kubeadm和kubelet要一致。
给kubelet写一个systemd服务。有一个容易被误导的地方:kubeadm init之前,kubelet其实还没有正确的启动配置,所以服务启动失败是正常的,但systemd服务必须先存在,否则kubeadm初始化阶段无法帮你拉起kubelet。
cat > /etc/systemd/system/kubelet.service <<'EOF' [Unit] Description=Kubernetes Kubelet Documentation=https://github.com/kubernetes/kubernetes After=containerd.service Requires=containerd.service [Service] ExecStart=/usr/local/bin/kubelet Restart=always RestartSec=10 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now kubelet这里先不用纠结--kubeconfig这些参数,kubeadm init后会在/var/lib/kubelet下生成config.yaml、bootstrap-kubelet.conf,并通过drop-in文件把kubelet的实际启动参数补全。我们这里只给systemd一个最简启动入口。
4.2 初始化控制平面
在master节点执行:
kubeadm init \ --kubernetes-version=v1.32.11 \ --control-plane-endpoint=10.10.10.11:6443 \ --pod-network-cidr=192.168.0.0/16 \ --service-cidr=10.96.0.0/12 \ --image-repository=registry.k8s.io \ --image-pull-policy=IfNotPresent \ --cri-socket=unix:///run/containerd/containerd.sock参数含义过一遍:
--kubernetes-version:明确指定版本,防止kubeadm去外网探测最新版。--control-plane-endpoint:控制平面的统一入口,这里直接用master IP。如果不指定,它会默认当前节点的hostname,之后如果多控制平面会麻烦。--pod-network-cidr:Pod网段,必须和后面Calico配置保持一致。--service-cidr:Service网段,只要和物理网络不冲突就行。--image-repository:统一走registry.k8s.io前缀。所有镜像已经离线导入,这一步更多是确保名字标准。--image-pull-policy=IfNotPresent:这是离线环境的关键兜底。告诉kubeadm,本地已有镜像就不要再去远程拉取。少这个参数的话,有些版本即便本地有镜像也会尝试访问远程仓库,然后卡在waiting for control plane。
初始化成功后会输出一大段提示,记得保存最后面的kubeadm join命令。接着配置kubectl:
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config kubectl get nodes这时master节点大概率是NotReady,因为还没有安装CNI网络插件,这是正常的,继续往下走。
4.3 安装Calico网络插件
先把Calico的镜像导入,这里我不再演示导入命令,和前面一样的套路。关键是把calico.yaml里的Pod网段改成和初始化时一致。Calico默认是192.168.0.0/16,如果初始化时用的就是这个,可以直接apply:
kubectl apply -f calico.yaml kubectl get pods -n kube-system -w正常情况下,calico-node、coredns等Pod在一两分钟内变为Running。如果卡在Init/CrashLoopBackOff,优先看日志:
kubectl logs -n kube-system -l k8s-app=calico-node我最常遇到的是calico-node报Error: unknown flag: --cluster-type这类版本不匹配,多半是Calico版本和K8s版本兼容问题,换一个更新版本的Calico就好。
4.4 工作节点加入集群
在master节点生成加入命令:
kubeadm token create --print-join-command输出的命令类似:
kubeadm join 10.10.10.11:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx在每台工作节点上,先完成上面第2、3步的系统配置和containerd配置,导入同样的K8s镜像、Calico镜像,然后执行join命令。注意工作节点不需要执行kubeadm init,不要搞混。
节点加入后,在master上验证:
kubectl get nodes所有节点都应显示Ready。如果节点一直NotReady,查看kubelet日志:
journalctl -u kubelet -f有一种情况比较隐蔽:工作节点上hosts文件和master不一致,join时报could not find the requested Kubernetes endpoint,检查/etc/hosts里的10.10.10.11映射是否写了正确的master hostname。
5. 离线部署KubeSphere可视化平台
5.1 前置准备:StorageClass必须先解决
KubeSphere不是简单一个Deployment,它会有metrics-server、监控组件、审计组件等一大堆要用PVC的东西。如果没有默认StorageClass,大量Pod会在PVC那一步卡住,状态永远是Pending,安装日志里全是waiting for a volume to be created。
离线环境最省事的方案是装一个local-path-provisioner,给每个节点指定一个目录当作本地卷,把local-path设置为默认StorageClass。这个组件只有一个镜像,导出导入都很方便。部署完确认一下:
kubectl get sc看到local-path的PROVISIONER正常,且storageclass.kubernetes.io/is-default-class: true状态,再继续。
5.2 ks-installer与镜像导入
KubeSphere在已有K8s集群上的安装,通常走ks-installer这条路。提前在联网机器上下载好kubesphere-installer.yaml和cluster-configuration.yaml,这两个文件里的镜像引用很多,最好先提取镜像清单,再逐一把镜像做成tar包:
grep -rh "image:" kubesphere-installer.yaml cluster-configuration.yaml | awk '{print $2}' | sort -u > /data/kubesphere-images.txt然后在可以联网的机器上循环拉取并导出:
while read img; do skopeo copy docker://$img docker-archive:/data/kubesphere/images/$(basename $img).tar done < /data/kubesphere-images.txtKubeSphere版本和K8s的兼容性,以官方支持矩阵为准。我在1.32.11上用的是官方离线包里那一版,运行正常,如果你遇到组件版本冲突,优先看官方release说明。
将所有镜像tar包拷入内网后,导入操作还是那句老话:
for img in /data/kubesphere/images/*.tar; do ctr -n k8s.io images import --all-platforms "$img" done5.3 执行安装并验证
导入完成、StorageClass准备就绪后,依次apply两个yaml:
kubectl apply -f kubesphere-installer.yaml kubectl apply -f cluster-configuration.yaml安装过程比较长,直接看安装日志:
kubectl logs -n kubesphere-system $(kubectl get pod -n kubesphere-system -l app=ks-install -o jsonpath='{.items[0].metadata.name}') -f日志里能看到它在逐项创建CRD、依赖组件、监控组件。等全部安装完成后,访问控制台:
kubectl get svc -n kubesphere-system kubesphere-console默认端口是30880,访问形如http://10.10.10.11:30880。初始账号一般是admin/P@88w0rd,首次登录会强制改密。如果访问不了,先确认节点防火墙已关闭,再检查kubectl get svc是否绑定了所有IP,NodePort没变化就是端口问题。
KubeSphere安装完成后,看下所有相关Pod:
kubectl get pods -A | grep kubesphere我建议这一步多留出10分钟观察,不要因为控制台能打开就觉得完成。有些组件还在CrashLoop,比如ks-apiserver内存不足反复重启,就需要调大master节点内存或给系统组件设置requests。
6. 高频故障与排查实录
6.1 containerd和镜像导入问题
镜像导入最常见的错误是archive/tar: invalid tar header,基本是tar包损坏或不是标准docker-archive格式,重新用skopeo导出一次即可。另一个经典问题是K8s报了“Failed to pull image X”但镜像明明已导入过,这时先看ctr -n k8s.io images list | grep X,如果没有结果,说明导入命名空间不对;如果有结果但tag带docker.io/library/前缀,和yaml里写的registry.k8s.io/xxx对不上,用ctr -n k8s.io images tag补一个标准tag。
还有个问题要注意:相同镜像名但digest不同。K8s 1.32默认如果镜像带digest引用,即使tag匹配也可能触发重新拉取。离线场景下尽量确保导入的镜像digest与官方一致,所以制作tar包时用skopeo默认行为就行,不要手动打tag覆盖。
6.2 kubelet与CRI状态异常
节点NotReady时,我基本按这个顺序排查:
kubectl describe node <node-name> journalctl -u kubelet -n 50 -f crictl ps如果crictl ps报cannot connect to unix:///run/containerd/containerd.sock,多半是containerd崩了或没起来,systemctl status containerd看一眼,然后journalctl -u containerd -e。如果kubelet日志里有failed to pull pause image,回到镜像导入那一步。
cgroup驱动不一致的报错也要记一下:kubelet日志里会有failed to derive cgroup或用kubeadm init时直接提示kubelet cgroup driver=systemd与runtime的cgroup driver不匹配。解决办法就是我上面说的,把containerd的SystemdCgroup=true。这个坑我甚至在线上见过,重新检查完配置、重启containerd就好了。
6.3 网络和KubeSphere问题
Calico Pod起来了但节点仍NotReady,优先看kubectl get pods -n kube-system里calico-node是否Ready。如果Calico报IPv4 address is not configured,说明/etc/hosts没配好或主机名不对。如果跨节点Pod不通,检查sysctl net.ipv4.ip_forward是否为1,防火墙是否彻底关闭。
KubeSphere安装卡住时,先看PVC:
kubectl get pvc -A除非已经部署了StorageClass,否则这里一定有一堆Pending。另外KubeSphere的配置里可以把一些组件开启或关闭,如果只需要最小可用平台,在cluster-configuration.yaml里把监控、DevOps这些组件关掉,可以省很多资源。组件全开的话,光KubeSphere相关Pod就可能吃掉4G以上内存,节点内存紧张就会各种OOM,表现是ks-console反复重启。
节点token过期是另一个很常见的问题。kubeadm token create --print-join-command生成的新token默认有效期24小时。如果之前是控制平面节点需要加入,证书key也要重新上传:
kubeadm init phase upload-certs --upload-certs这条命令会打印新的certificate key,配合kubeadm join --control-plane --certificate-key一起用。
在实际操作中我更倾向于把离线部署当作一条“车间流水线”:所有节点先统一跑完系统初始化脚本,再统一安装containerd并导入镜像,最后master先init、节点再join。每一步都单独验证,不要等到最后再一起查问题。这套流程我复用过很多次,只要物料齐、tag对、cgroup一致,基本一遍就能通。装完KubeSphere后再做一次快照,后续重复交付就会非常轻松。